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, Rgba, 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;
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, IndentGuideBackgroundColoring,
52 IndentGuideColoring, InlayHintSettingsContent, ProjectSettingsContent, SearchSettingsContent,
53 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, MoveItemToPaneInDirection, NavigationEntry,
71 OpenOptions, ViewId,
72 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
73 register_project_item,
74};
75
76fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
77 editor
78 .selections
79 .display_ranges(&editor.display_snapshot(cx))
80}
81
82#[gpui::test]
83fn test_edit_events(cx: &mut TestAppContext) {
84 init_test(cx, |_| {});
85
86 let buffer = cx.new(|cx| {
87 let mut buffer = language::Buffer::local("123456", cx);
88 buffer.set_group_interval(Duration::from_secs(1));
89 buffer
90 });
91
92 let events = Rc::new(RefCell::new(Vec::new()));
93 let editor1 = cx.add_window({
94 let events = events.clone();
95 |window, cx| {
96 let entity = cx.entity();
97 cx.subscribe_in(
98 &entity,
99 window,
100 move |_, _, event: &EditorEvent, _, _| match event {
101 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
102 EditorEvent::BufferEdited => {
103 events.borrow_mut().push(("editor1", "buffer edited"))
104 }
105 _ => {}
106 },
107 )
108 .detach();
109 Editor::for_buffer(buffer.clone(), None, window, cx)
110 }
111 });
112
113 let editor2 = cx.add_window({
114 let events = events.clone();
115 |window, cx| {
116 cx.subscribe_in(
117 &cx.entity(),
118 window,
119 move |_, _, event: &EditorEvent, _, _| match event {
120 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
121 EditorEvent::BufferEdited => {
122 events.borrow_mut().push(("editor2", "buffer edited"))
123 }
124 _ => {}
125 },
126 )
127 .detach();
128 Editor::for_buffer(buffer.clone(), None, window, cx)
129 }
130 });
131
132 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
133
134 // Mutating editor 1 will emit an `Edited` event only for that editor.
135 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
136 assert_eq!(
137 mem::take(&mut *events.borrow_mut()),
138 [
139 ("editor1", "edited"),
140 ("editor1", "buffer edited"),
141 ("editor2", "buffer edited"),
142 ]
143 );
144
145 // Mutating editor 2 will emit an `Edited` event only for that editor.
146 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
147 assert_eq!(
148 mem::take(&mut *events.borrow_mut()),
149 [
150 ("editor2", "edited"),
151 ("editor1", "buffer edited"),
152 ("editor2", "buffer edited"),
153 ]
154 );
155
156 // Undoing on editor 1 will emit an `Edited` event only for that editor.
157 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
158 assert_eq!(
159 mem::take(&mut *events.borrow_mut()),
160 [
161 ("editor1", "edited"),
162 ("editor1", "buffer edited"),
163 ("editor2", "buffer edited"),
164 ]
165 );
166
167 // Redoing on editor 1 will emit an `Edited` event only for that editor.
168 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
169 assert_eq!(
170 mem::take(&mut *events.borrow_mut()),
171 [
172 ("editor1", "edited"),
173 ("editor1", "buffer edited"),
174 ("editor2", "buffer edited"),
175 ]
176 );
177
178 // Undoing on editor 2 will emit an `Edited` event only for that editor.
179 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
180 assert_eq!(
181 mem::take(&mut *events.borrow_mut()),
182 [
183 ("editor2", "edited"),
184 ("editor1", "buffer edited"),
185 ("editor2", "buffer edited"),
186 ]
187 );
188
189 // Redoing on editor 2 will emit an `Edited` event only for that editor.
190 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
191 assert_eq!(
192 mem::take(&mut *events.borrow_mut()),
193 [
194 ("editor2", "edited"),
195 ("editor1", "buffer edited"),
196 ("editor2", "buffer edited"),
197 ]
198 );
199
200 // No event is emitted when the mutation is a no-op.
201 _ = editor2.update(cx, |editor, window, cx| {
202 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
203 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
204 });
205
206 editor.backspace(&Backspace, window, cx);
207 });
208 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
209}
210
211#[gpui::test]
212fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
213 init_test(cx, |_| {});
214
215 let mut now = Instant::now();
216 let group_interval = Duration::from_millis(1);
217 let buffer = cx.new(|cx| {
218 let mut buf = language::Buffer::local("123456", cx);
219 buf.set_group_interval(group_interval);
220 buf
221 });
222 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
223 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
224
225 _ = editor.update(cx, |editor, window, cx| {
226 editor.start_transaction_at(now, window, cx);
227 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
228 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(4)])
229 });
230
231 editor.insert("cd", window, cx);
232 editor.end_transaction_at(now, cx);
233 assert_eq!(editor.text(cx), "12cd56");
234 assert_eq!(
235 editor.selections.ranges(&editor.display_snapshot(cx)),
236 vec![MultiBufferOffset(4)..MultiBufferOffset(4)]
237 );
238
239 editor.start_transaction_at(now, window, cx);
240 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
241 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(5)])
242 });
243 editor.insert("e", window, cx);
244 editor.end_transaction_at(now, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(
247 editor.selections.ranges(&editor.display_snapshot(cx)),
248 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
249 );
250
251 now += group_interval + Duration::from_millis(1);
252 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
253 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)])
254 });
255
256 // Simulate an edit in another editor
257 buffer.update(cx, |buffer, cx| {
258 buffer.start_transaction_at(now, cx);
259 buffer.edit(
260 [(MultiBufferOffset(0)..MultiBufferOffset(1), "a")],
261 None,
262 cx,
263 );
264 buffer.edit(
265 [(MultiBufferOffset(1)..MultiBufferOffset(1), "b")],
266 None,
267 cx,
268 );
269 buffer.end_transaction_at(now, cx);
270 });
271
272 assert_eq!(editor.text(cx), "ab2cde6");
273 assert_eq!(
274 editor.selections.ranges(&editor.display_snapshot(cx)),
275 vec![MultiBufferOffset(3)..MultiBufferOffset(3)]
276 );
277
278 // Last transaction happened past the group interval in a different editor.
279 // Undo it individually and don't restore selections.
280 editor.undo(&Undo, window, cx);
281 assert_eq!(editor.text(cx), "12cde6");
282 assert_eq!(
283 editor.selections.ranges(&editor.display_snapshot(cx)),
284 vec![MultiBufferOffset(2)..MultiBufferOffset(2)]
285 );
286
287 // First two transactions happened within the group interval in this editor.
288 // Undo them together and restore selections.
289 editor.undo(&Undo, window, cx);
290 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
291 assert_eq!(editor.text(cx), "123456");
292 assert_eq!(
293 editor.selections.ranges(&editor.display_snapshot(cx)),
294 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
295 );
296
297 // Redo the first two transactions together.
298 editor.redo(&Redo, window, cx);
299 assert_eq!(editor.text(cx), "12cde6");
300 assert_eq!(
301 editor.selections.ranges(&editor.display_snapshot(cx)),
302 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
303 );
304
305 // Redo the last transaction on its own.
306 editor.redo(&Redo, window, cx);
307 assert_eq!(editor.text(cx), "ab2cde6");
308 assert_eq!(
309 editor.selections.ranges(&editor.display_snapshot(cx)),
310 vec![MultiBufferOffset(6)..MultiBufferOffset(6)]
311 );
312
313 // Test empty transactions.
314 editor.start_transaction_at(now, window, cx);
315 editor.end_transaction_at(now, cx);
316 editor.undo(&Undo, window, cx);
317 assert_eq!(editor.text(cx), "12cde6");
318 });
319}
320
321#[gpui::test]
322fn test_ime_composition(cx: &mut TestAppContext) {
323 init_test(cx, |_| {});
324
325 let buffer = cx.new(|cx| {
326 let mut buffer = language::Buffer::local("abcde", cx);
327 // Ensure automatic grouping doesn't occur.
328 buffer.set_group_interval(Duration::ZERO);
329 buffer
330 });
331
332 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
333 cx.add_window(|window, cx| {
334 let mut editor = build_editor(buffer.clone(), window, cx);
335
336 // Start a new IME composition.
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 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
340 assert_eq!(editor.text(cx), "äbcde");
341 assert_eq!(
342 editor.marked_text_ranges(cx),
343 Some(vec![
344 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
345 ])
346 );
347
348 // Finalize IME composition.
349 editor.replace_text_in_range(None, "ā", window, cx);
350 assert_eq!(editor.text(cx), "ābcde");
351 assert_eq!(editor.marked_text_ranges(cx), None);
352
353 // IME composition edits are grouped and are undone/redone at once.
354 editor.undo(&Default::default(), window, cx);
355 assert_eq!(editor.text(cx), "abcde");
356 assert_eq!(editor.marked_text_ranges(cx), None);
357 editor.redo(&Default::default(), window, cx);
358 assert_eq!(editor.text(cx), "ābcde");
359 assert_eq!(editor.marked_text_ranges(cx), None);
360
361 // Start a new IME composition.
362 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
363 assert_eq!(
364 editor.marked_text_ranges(cx),
365 Some(vec![
366 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
367 ])
368 );
369
370 // Undoing during an IME composition cancels it.
371 editor.undo(&Default::default(), window, cx);
372 assert_eq!(editor.text(cx), "ābcde");
373 assert_eq!(editor.marked_text_ranges(cx), None);
374
375 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
376 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
377 assert_eq!(editor.text(cx), "ābcdè");
378 assert_eq!(
379 editor.marked_text_ranges(cx),
380 Some(vec![
381 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(5))
382 ])
383 );
384
385 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
386 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
387 assert_eq!(editor.text(cx), "ābcdę");
388 assert_eq!(editor.marked_text_ranges(cx), None);
389
390 // Start a new IME composition with multiple cursors.
391 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
392 s.select_ranges([
393 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(1)),
394 MultiBufferOffsetUtf16(OffsetUtf16(3))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
395 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(5)),
396 ])
397 });
398 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
399 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
400 assert_eq!(
401 editor.marked_text_ranges(cx),
402 Some(vec![
403 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
404 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(7)),
405 MultiBufferOffsetUtf16(OffsetUtf16(8))..MultiBufferOffsetUtf16(OffsetUtf16(11))
406 ])
407 );
408
409 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
410 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
411 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
412 assert_eq!(
413 editor.marked_text_ranges(cx),
414 Some(vec![
415 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(2)),
416 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(6)),
417 MultiBufferOffsetUtf16(OffsetUtf16(9))..MultiBufferOffsetUtf16(OffsetUtf16(10))
418 ])
419 );
420
421 // Finalize IME composition with multiple cursors.
422 editor.replace_text_in_range(Some(9..10), "2", window, cx);
423 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
424 assert_eq!(editor.marked_text_ranges(cx), None);
425
426 editor
427 });
428}
429
430#[gpui::test]
431fn test_selection_with_mouse(cx: &mut TestAppContext) {
432 init_test(cx, |_| {});
433
434 let editor = cx.add_window(|window, cx| {
435 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
436 build_editor(buffer, window, cx)
437 });
438
439 _ = editor.update(cx, |editor, window, cx| {
440 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
441 });
442 assert_eq!(
443 editor
444 .update(cx, |editor, _, cx| display_ranges(editor, cx))
445 .unwrap(),
446 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
447 );
448
449 _ = editor.update(cx, |editor, window, cx| {
450 editor.update_selection(
451 DisplayPoint::new(DisplayRow(3), 3),
452 0,
453 gpui::Point::<f32>::default(),
454 window,
455 cx,
456 );
457 });
458
459 assert_eq!(
460 editor
461 .update(cx, |editor, _, cx| display_ranges(editor, cx))
462 .unwrap(),
463 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
464 );
465
466 _ = editor.update(cx, |editor, window, cx| {
467 editor.update_selection(
468 DisplayPoint::new(DisplayRow(1), 1),
469 0,
470 gpui::Point::<f32>::default(),
471 window,
472 cx,
473 );
474 });
475
476 assert_eq!(
477 editor
478 .update(cx, |editor, _, cx| display_ranges(editor, cx))
479 .unwrap(),
480 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
481 );
482
483 _ = editor.update(cx, |editor, window, cx| {
484 editor.end_selection(window, cx);
485 editor.update_selection(
486 DisplayPoint::new(DisplayRow(3), 3),
487 0,
488 gpui::Point::<f32>::default(),
489 window,
490 cx,
491 );
492 });
493
494 assert_eq!(
495 editor
496 .update(cx, |editor, _, cx| display_ranges(editor, cx))
497 .unwrap(),
498 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
499 );
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
503 editor.update_selection(
504 DisplayPoint::new(DisplayRow(0), 0),
505 0,
506 gpui::Point::<f32>::default(),
507 window,
508 cx,
509 );
510 });
511
512 assert_eq!(
513 editor
514 .update(cx, |editor, _, cx| display_ranges(editor, cx))
515 .unwrap(),
516 [
517 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
518 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
519 ]
520 );
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.end_selection(window, cx);
524 });
525
526 assert_eq!(
527 editor
528 .update(cx, |editor, _, cx| display_ranges(editor, cx))
529 .unwrap(),
530 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
531 );
532}
533
534#[gpui::test]
535fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
536 init_test(cx, |_| {});
537
538 let editor = cx.add_window(|window, cx| {
539 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
540 build_editor(buffer, window, cx)
541 });
542
543 _ = editor.update(cx, |editor, window, cx| {
544 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
545 });
546
547 _ = editor.update(cx, |editor, window, cx| {
548 editor.end_selection(window, cx);
549 });
550
551 _ = editor.update(cx, |editor, window, cx| {
552 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
553 });
554
555 _ = editor.update(cx, |editor, window, cx| {
556 editor.end_selection(window, cx);
557 });
558
559 assert_eq!(
560 editor
561 .update(cx, |editor, _, cx| display_ranges(editor, cx))
562 .unwrap(),
563 [
564 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
565 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
566 ]
567 );
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
571 });
572
573 _ = editor.update(cx, |editor, window, cx| {
574 editor.end_selection(window, cx);
575 });
576
577 assert_eq!(
578 editor
579 .update(cx, |editor, _, cx| display_ranges(editor, cx))
580 .unwrap(),
581 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
582 );
583}
584
585#[gpui::test]
586fn test_canceling_pending_selection(cx: &mut TestAppContext) {
587 init_test(cx, |_| {});
588
589 let editor = cx.add_window(|window, cx| {
590 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
591 build_editor(buffer, window, cx)
592 });
593
594 _ = editor.update(cx, |editor, window, cx| {
595 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
596 assert_eq!(
597 display_ranges(editor, cx),
598 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
599 );
600 });
601
602 _ = editor.update(cx, |editor, window, cx| {
603 editor.update_selection(
604 DisplayPoint::new(DisplayRow(3), 3),
605 0,
606 gpui::Point::<f32>::default(),
607 window,
608 cx,
609 );
610 assert_eq!(
611 display_ranges(editor, cx),
612 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
613 );
614 });
615
616 _ = editor.update(cx, |editor, window, cx| {
617 editor.cancel(&Cancel, window, cx);
618 editor.update_selection(
619 DisplayPoint::new(DisplayRow(1), 1),
620 0,
621 gpui::Point::<f32>::default(),
622 window,
623 cx,
624 );
625 assert_eq!(
626 display_ranges(editor, cx),
627 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
628 );
629 });
630}
631
632#[gpui::test]
633fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
634 init_test(cx, |_| {});
635
636 let editor = cx.add_window(|window, cx| {
637 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
638 build_editor(buffer, window, cx)
639 });
640
641 _ = editor.update(cx, |editor, window, cx| {
642 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
643 assert_eq!(
644 display_ranges(editor, cx),
645 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
646 );
647
648 editor.move_down(&Default::default(), window, cx);
649 assert_eq!(
650 display_ranges(editor, cx),
651 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
652 );
653
654 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
655 assert_eq!(
656 display_ranges(editor, cx),
657 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
658 );
659
660 editor.move_up(&Default::default(), window, cx);
661 assert_eq!(
662 display_ranges(editor, cx),
663 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
664 );
665 });
666}
667
668#[gpui::test]
669fn test_extending_selection(cx: &mut TestAppContext) {
670 init_test(cx, |_| {});
671
672 let editor = cx.add_window(|window, cx| {
673 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
674 build_editor(buffer, window, cx)
675 });
676
677 _ = editor.update(cx, |editor, window, cx| {
678 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
679 editor.end_selection(window, cx);
680 assert_eq!(
681 display_ranges(editor, cx),
682 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
683 );
684
685 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
686 editor.end_selection(window, cx);
687 assert_eq!(
688 display_ranges(editor, cx),
689 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
690 );
691
692 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
693 editor.end_selection(window, cx);
694 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
695 assert_eq!(
696 display_ranges(editor, cx),
697 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
698 );
699
700 editor.update_selection(
701 DisplayPoint::new(DisplayRow(0), 1),
702 0,
703 gpui::Point::<f32>::default(),
704 window,
705 cx,
706 );
707 editor.end_selection(window, cx);
708 assert_eq!(
709 display_ranges(editor, cx),
710 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
711 );
712
713 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
714 editor.end_selection(window, cx);
715 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
716 editor.end_selection(window, cx);
717 assert_eq!(
718 display_ranges(editor, cx),
719 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
720 );
721
722 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
723 assert_eq!(
724 display_ranges(editor, cx),
725 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
726 );
727
728 editor.update_selection(
729 DisplayPoint::new(DisplayRow(0), 6),
730 0,
731 gpui::Point::<f32>::default(),
732 window,
733 cx,
734 );
735 assert_eq!(
736 display_ranges(editor, cx),
737 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
738 );
739
740 editor.update_selection(
741 DisplayPoint::new(DisplayRow(0), 1),
742 0,
743 gpui::Point::<f32>::default(),
744 window,
745 cx,
746 );
747 editor.end_selection(window, cx);
748 assert_eq!(
749 display_ranges(editor, cx),
750 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
751 );
752 });
753}
754
755#[gpui::test]
756fn test_clone(cx: &mut TestAppContext) {
757 init_test(cx, |_| {});
758
759 let (text, selection_ranges) = marked_text_ranges(
760 indoc! {"
761 one
762 two
763 threeˇ
764 four
765 fiveˇ
766 "},
767 true,
768 );
769
770 let editor = cx.add_window(|window, cx| {
771 let buffer = MultiBuffer::build_simple(&text, cx);
772 build_editor(buffer, window, cx)
773 });
774
775 _ = editor.update(cx, |editor, window, cx| {
776 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
777 s.select_ranges(
778 selection_ranges
779 .iter()
780 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
781 )
782 });
783 editor.fold_creases(
784 vec![
785 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
786 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
787 ],
788 true,
789 window,
790 cx,
791 );
792 });
793
794 let cloned_editor = editor
795 .update(cx, |editor, _, cx| {
796 cx.open_window(Default::default(), |window, cx| {
797 cx.new(|cx| editor.clone(window, cx))
798 })
799 })
800 .unwrap()
801 .unwrap();
802
803 let snapshot = editor
804 .update(cx, |e, window, cx| e.snapshot(window, cx))
805 .unwrap();
806 let cloned_snapshot = cloned_editor
807 .update(cx, |e, window, cx| e.snapshot(window, cx))
808 .unwrap();
809
810 assert_eq!(
811 cloned_editor
812 .update(cx, |e, _, cx| e.display_text(cx))
813 .unwrap(),
814 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
815 );
816 assert_eq!(
817 cloned_snapshot
818 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
819 .collect::<Vec<_>>(),
820 snapshot
821 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
822 .collect::<Vec<_>>(),
823 );
824 assert_set_eq!(
825 cloned_editor
826 .update(cx, |editor, _, cx| editor
827 .selections
828 .ranges::<Point>(&editor.display_snapshot(cx)))
829 .unwrap(),
830 editor
831 .update(cx, |editor, _, cx| editor
832 .selections
833 .ranges(&editor.display_snapshot(cx)))
834 .unwrap()
835 );
836 assert_set_eq!(
837 cloned_editor
838 .update(cx, |e, _window, cx| e
839 .selections
840 .display_ranges(&e.display_snapshot(cx)))
841 .unwrap(),
842 editor
843 .update(cx, |e, _, cx| e
844 .selections
845 .display_ranges(&e.display_snapshot(cx)))
846 .unwrap()
847 );
848}
849
850#[gpui::test]
851async fn test_navigation_history(cx: &mut TestAppContext) {
852 init_test(cx, |_| {});
853
854 use workspace::item::Item;
855
856 let fs = FakeFs::new(cx.executor());
857 let project = Project::test(fs, [], cx).await;
858 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
859 let pane = workspace
860 .update(cx, |workspace, _, _| workspace.active_pane().clone())
861 .unwrap();
862
863 _ = workspace.update(cx, |_v, window, cx| {
864 cx.new(|cx| {
865 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
866 let mut editor = build_editor(buffer, window, cx);
867 let handle = cx.entity();
868 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
869
870 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
871 editor.nav_history.as_mut().unwrap().pop_backward(cx)
872 }
873
874 // Move the cursor a small distance.
875 // Nothing is added to the navigation history.
876 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
877 s.select_display_ranges([
878 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
879 ])
880 });
881 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
882 s.select_display_ranges([
883 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
884 ])
885 });
886 assert!(pop_history(&mut editor, cx).is_none());
887
888 // Move the cursor a large distance.
889 // The history can jump back to the previous position.
890 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
891 s.select_display_ranges([
892 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
893 ])
894 });
895 let nav_entry = pop_history(&mut editor, cx).unwrap();
896 editor.navigate(nav_entry.data.unwrap(), window, cx);
897 assert_eq!(nav_entry.item.id(), cx.entity_id());
898 assert_eq!(
899 editor
900 .selections
901 .display_ranges(&editor.display_snapshot(cx)),
902 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
903 );
904 assert!(pop_history(&mut editor, cx).is_none());
905
906 // Move the cursor a small distance via the mouse.
907 // Nothing is added to the navigation history.
908 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
909 editor.end_selection(window, cx);
910 assert_eq!(
911 editor
912 .selections
913 .display_ranges(&editor.display_snapshot(cx)),
914 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
915 );
916 assert!(pop_history(&mut editor, cx).is_none());
917
918 // Move the cursor a large distance via the mouse.
919 // The history can jump back to the previous position.
920 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
921 editor.end_selection(window, cx);
922 assert_eq!(
923 editor
924 .selections
925 .display_ranges(&editor.display_snapshot(cx)),
926 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
927 );
928 let nav_entry = pop_history(&mut editor, cx).unwrap();
929 editor.navigate(nav_entry.data.unwrap(), window, cx);
930 assert_eq!(nav_entry.item.id(), cx.entity_id());
931 assert_eq!(
932 editor
933 .selections
934 .display_ranges(&editor.display_snapshot(cx)),
935 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
936 );
937 assert!(pop_history(&mut editor, cx).is_none());
938
939 // Set scroll position to check later
940 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
941 let original_scroll_position = editor.scroll_manager.anchor();
942
943 // Jump to the end of the document and adjust scroll
944 editor.move_to_end(&MoveToEnd, window, cx);
945 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
946 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
947
948 let nav_entry = pop_history(&mut editor, cx).unwrap();
949 editor.navigate(nav_entry.data.unwrap(), window, cx);
950 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
951
952 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
953 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
954 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
955 let invalid_point = Point::new(9999, 0);
956 editor.navigate(
957 Arc::new(NavigationData {
958 cursor_anchor: invalid_anchor,
959 cursor_position: invalid_point,
960 scroll_anchor: ScrollAnchor {
961 anchor: invalid_anchor,
962 offset: Default::default(),
963 },
964 scroll_top_row: invalid_point.row,
965 }),
966 window,
967 cx,
968 );
969 assert_eq!(
970 editor
971 .selections
972 .display_ranges(&editor.display_snapshot(cx)),
973 &[editor.max_point(cx)..editor.max_point(cx)]
974 );
975 assert_eq!(
976 editor.scroll_position(cx),
977 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
978 );
979
980 editor
981 })
982 });
983}
984
985#[gpui::test]
986fn test_cancel(cx: &mut TestAppContext) {
987 init_test(cx, |_| {});
988
989 let editor = cx.add_window(|window, cx| {
990 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
991 build_editor(buffer, window, cx)
992 });
993
994 _ = editor.update(cx, |editor, window, cx| {
995 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
996 editor.update_selection(
997 DisplayPoint::new(DisplayRow(1), 1),
998 0,
999 gpui::Point::<f32>::default(),
1000 window,
1001 cx,
1002 );
1003 editor.end_selection(window, cx);
1004
1005 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
1006 editor.update_selection(
1007 DisplayPoint::new(DisplayRow(0), 3),
1008 0,
1009 gpui::Point::<f32>::default(),
1010 window,
1011 cx,
1012 );
1013 editor.end_selection(window, cx);
1014 assert_eq!(
1015 display_ranges(editor, cx),
1016 [
1017 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
1018 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
1019 ]
1020 );
1021 });
1022
1023 _ = editor.update(cx, |editor, window, cx| {
1024 editor.cancel(&Cancel, window, cx);
1025 assert_eq!(
1026 display_ranges(editor, cx),
1027 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1028 );
1029 });
1030
1031 _ = editor.update(cx, |editor, window, cx| {
1032 editor.cancel(&Cancel, window, cx);
1033 assert_eq!(
1034 display_ranges(editor, cx),
1035 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1036 );
1037 });
1038}
1039
1040#[gpui::test]
1041fn test_fold_action(cx: &mut TestAppContext) {
1042 init_test(cx, |_| {});
1043
1044 let editor = cx.add_window(|window, cx| {
1045 let buffer = MultiBuffer::build_simple(
1046 &"
1047 impl Foo {
1048 // Hello!
1049
1050 fn a() {
1051 1
1052 }
1053
1054 fn b() {
1055 2
1056 }
1057
1058 fn c() {
1059 3
1060 }
1061 }
1062 "
1063 .unindent(),
1064 cx,
1065 );
1066 build_editor(buffer, window, cx)
1067 });
1068
1069 _ = editor.update(cx, |editor, window, cx| {
1070 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1071 s.select_display_ranges([
1072 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1073 ]);
1074 });
1075 editor.fold(&Fold, window, cx);
1076 assert_eq!(
1077 editor.display_text(cx),
1078 "
1079 impl Foo {
1080 // Hello!
1081
1082 fn a() {
1083 1
1084 }
1085
1086 fn b() {⋯
1087 }
1088
1089 fn c() {⋯
1090 }
1091 }
1092 "
1093 .unindent(),
1094 );
1095
1096 editor.fold(&Fold, window, cx);
1097 assert_eq!(
1098 editor.display_text(cx),
1099 "
1100 impl Foo {⋯
1101 }
1102 "
1103 .unindent(),
1104 );
1105
1106 editor.unfold_lines(&UnfoldLines, window, cx);
1107 assert_eq!(
1108 editor.display_text(cx),
1109 "
1110 impl Foo {
1111 // Hello!
1112
1113 fn a() {
1114 1
1115 }
1116
1117 fn b() {⋯
1118 }
1119
1120 fn c() {⋯
1121 }
1122 }
1123 "
1124 .unindent(),
1125 );
1126
1127 editor.unfold_lines(&UnfoldLines, window, cx);
1128 assert_eq!(
1129 editor.display_text(cx),
1130 editor.buffer.read(cx).read(cx).text()
1131 );
1132 });
1133}
1134
1135#[gpui::test]
1136fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1137 init_test(cx, |_| {});
1138
1139 let editor = cx.add_window(|window, cx| {
1140 let buffer = MultiBuffer::build_simple(
1141 &"
1142 class Foo:
1143 # Hello!
1144
1145 def a():
1146 print(1)
1147
1148 def b():
1149 print(2)
1150
1151 def c():
1152 print(3)
1153 "
1154 .unindent(),
1155 cx,
1156 );
1157 build_editor(buffer, window, cx)
1158 });
1159
1160 _ = editor.update(cx, |editor, window, cx| {
1161 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1162 s.select_display_ranges([
1163 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1164 ]);
1165 });
1166 editor.fold(&Fold, window, cx);
1167 assert_eq!(
1168 editor.display_text(cx),
1169 "
1170 class Foo:
1171 # Hello!
1172
1173 def a():
1174 print(1)
1175
1176 def b():⋯
1177
1178 def c():⋯
1179 "
1180 .unindent(),
1181 );
1182
1183 editor.fold(&Fold, window, cx);
1184 assert_eq!(
1185 editor.display_text(cx),
1186 "
1187 class Foo:⋯
1188 "
1189 .unindent(),
1190 );
1191
1192 editor.unfold_lines(&UnfoldLines, window, cx);
1193 assert_eq!(
1194 editor.display_text(cx),
1195 "
1196 class Foo:
1197 # Hello!
1198
1199 def a():
1200 print(1)
1201
1202 def b():⋯
1203
1204 def c():⋯
1205 "
1206 .unindent(),
1207 );
1208
1209 editor.unfold_lines(&UnfoldLines, window, cx);
1210 assert_eq!(
1211 editor.display_text(cx),
1212 editor.buffer.read(cx).read(cx).text()
1213 );
1214 });
1215}
1216
1217#[gpui::test]
1218fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1219 init_test(cx, |_| {});
1220
1221 let editor = cx.add_window(|window, cx| {
1222 let buffer = MultiBuffer::build_simple(
1223 &"
1224 class Foo:
1225 # Hello!
1226
1227 def a():
1228 print(1)
1229
1230 def b():
1231 print(2)
1232
1233
1234 def c():
1235 print(3)
1236
1237
1238 "
1239 .unindent(),
1240 cx,
1241 );
1242 build_editor(buffer, window, cx)
1243 });
1244
1245 _ = editor.update(cx, |editor, window, cx| {
1246 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1247 s.select_display_ranges([
1248 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1249 ]);
1250 });
1251 editor.fold(&Fold, window, cx);
1252 assert_eq!(
1253 editor.display_text(cx),
1254 "
1255 class Foo:
1256 # Hello!
1257
1258 def a():
1259 print(1)
1260
1261 def b():⋯
1262
1263
1264 def c():⋯
1265
1266
1267 "
1268 .unindent(),
1269 );
1270
1271 editor.fold(&Fold, window, cx);
1272 assert_eq!(
1273 editor.display_text(cx),
1274 "
1275 class Foo:⋯
1276
1277
1278 "
1279 .unindent(),
1280 );
1281
1282 editor.unfold_lines(&UnfoldLines, window, cx);
1283 assert_eq!(
1284 editor.display_text(cx),
1285 "
1286 class Foo:
1287 # Hello!
1288
1289 def a():
1290 print(1)
1291
1292 def b():⋯
1293
1294
1295 def c():⋯
1296
1297
1298 "
1299 .unindent(),
1300 );
1301
1302 editor.unfold_lines(&UnfoldLines, window, cx);
1303 assert_eq!(
1304 editor.display_text(cx),
1305 editor.buffer.read(cx).read(cx).text()
1306 );
1307 });
1308}
1309
1310#[gpui::test]
1311fn test_fold_at_level(cx: &mut TestAppContext) {
1312 init_test(cx, |_| {});
1313
1314 let editor = cx.add_window(|window, cx| {
1315 let buffer = MultiBuffer::build_simple(
1316 &"
1317 class Foo:
1318 # Hello!
1319
1320 def a():
1321 print(1)
1322
1323 def b():
1324 print(2)
1325
1326
1327 class Bar:
1328 # World!
1329
1330 def a():
1331 print(1)
1332
1333 def b():
1334 print(2)
1335
1336
1337 "
1338 .unindent(),
1339 cx,
1340 );
1341 build_editor(buffer, window, cx)
1342 });
1343
1344 _ = editor.update(cx, |editor, window, cx| {
1345 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1346 assert_eq!(
1347 editor.display_text(cx),
1348 "
1349 class Foo:
1350 # Hello!
1351
1352 def a():⋯
1353
1354 def b():⋯
1355
1356
1357 class Bar:
1358 # World!
1359
1360 def a():⋯
1361
1362 def b():⋯
1363
1364
1365 "
1366 .unindent(),
1367 );
1368
1369 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1370 assert_eq!(
1371 editor.display_text(cx),
1372 "
1373 class Foo:⋯
1374
1375
1376 class Bar:⋯
1377
1378
1379 "
1380 .unindent(),
1381 );
1382
1383 editor.unfold_all(&UnfoldAll, window, cx);
1384 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1385 assert_eq!(
1386 editor.display_text(cx),
1387 "
1388 class Foo:
1389 # Hello!
1390
1391 def a():
1392 print(1)
1393
1394 def b():
1395 print(2)
1396
1397
1398 class Bar:
1399 # World!
1400
1401 def a():
1402 print(1)
1403
1404 def b():
1405 print(2)
1406
1407
1408 "
1409 .unindent(),
1410 );
1411
1412 assert_eq!(
1413 editor.display_text(cx),
1414 editor.buffer.read(cx).read(cx).text()
1415 );
1416 let (_, positions) = marked_text_ranges(
1417 &"
1418 class Foo:
1419 # Hello!
1420
1421 def a():
1422 print(1)
1423
1424 def b():
1425 p«riˇ»nt(2)
1426
1427
1428 class Bar:
1429 # World!
1430
1431 def a():
1432 «ˇprint(1)
1433
1434 def b():
1435 print(2)»
1436
1437
1438 "
1439 .unindent(),
1440 true,
1441 );
1442
1443 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1444 s.select_ranges(
1445 positions
1446 .iter()
1447 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
1448 )
1449 });
1450
1451 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1452 assert_eq!(
1453 editor.display_text(cx),
1454 "
1455 class Foo:
1456 # Hello!
1457
1458 def a():⋯
1459
1460 def b():
1461 print(2)
1462
1463
1464 class Bar:
1465 # World!
1466
1467 def a():
1468 print(1)
1469
1470 def b():
1471 print(2)
1472
1473
1474 "
1475 .unindent(),
1476 );
1477 });
1478}
1479
1480#[gpui::test]
1481fn test_move_cursor(cx: &mut TestAppContext) {
1482 init_test(cx, |_| {});
1483
1484 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1485 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1486
1487 buffer.update(cx, |buffer, cx| {
1488 buffer.edit(
1489 vec![
1490 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1491 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1492 ],
1493 None,
1494 cx,
1495 );
1496 });
1497 _ = editor.update(cx, |editor, window, cx| {
1498 assert_eq!(
1499 display_ranges(editor, cx),
1500 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1501 );
1502
1503 editor.move_down(&MoveDown, window, cx);
1504 assert_eq!(
1505 display_ranges(editor, cx),
1506 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1507 );
1508
1509 editor.move_right(&MoveRight, window, cx);
1510 assert_eq!(
1511 display_ranges(editor, cx),
1512 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1513 );
1514
1515 editor.move_left(&MoveLeft, window, cx);
1516 assert_eq!(
1517 display_ranges(editor, cx),
1518 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1519 );
1520
1521 editor.move_up(&MoveUp, window, cx);
1522 assert_eq!(
1523 display_ranges(editor, cx),
1524 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1525 );
1526
1527 editor.move_to_end(&MoveToEnd, window, cx);
1528 assert_eq!(
1529 display_ranges(editor, cx),
1530 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1531 );
1532
1533 editor.move_to_beginning(&MoveToBeginning, window, cx);
1534 assert_eq!(
1535 display_ranges(editor, cx),
1536 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1537 );
1538
1539 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1540 s.select_display_ranges([
1541 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1542 ]);
1543 });
1544 editor.select_to_beginning(&SelectToBeginning, window, cx);
1545 assert_eq!(
1546 display_ranges(editor, cx),
1547 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1548 );
1549
1550 editor.select_to_end(&SelectToEnd, window, cx);
1551 assert_eq!(
1552 display_ranges(editor, cx),
1553 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1554 );
1555 });
1556}
1557
1558#[gpui::test]
1559fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1560 init_test(cx, |_| {});
1561
1562 let editor = cx.add_window(|window, cx| {
1563 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1564 build_editor(buffer, window, cx)
1565 });
1566
1567 assert_eq!('🟥'.len_utf8(), 4);
1568 assert_eq!('α'.len_utf8(), 2);
1569
1570 _ = editor.update(cx, |editor, window, cx| {
1571 editor.fold_creases(
1572 vec![
1573 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1574 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1575 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1576 ],
1577 true,
1578 window,
1579 cx,
1580 );
1581 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1582
1583 editor.move_right(&MoveRight, window, cx);
1584 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1585 editor.move_right(&MoveRight, window, cx);
1586 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1587 editor.move_right(&MoveRight, window, cx);
1588 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1589
1590 editor.move_down(&MoveDown, window, cx);
1591 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1592 editor.move_left(&MoveLeft, window, cx);
1593 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".len())]);
1594 editor.move_left(&MoveLeft, window, cx);
1595 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab".len())]);
1596 editor.move_left(&MoveLeft, window, cx);
1597 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1598
1599 editor.move_down(&MoveDown, window, cx);
1600 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1601 editor.move_right(&MoveRight, window, cx);
1602 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ".len())]);
1603 editor.move_right(&MoveRight, window, cx);
1604 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯".len())]);
1605 editor.move_right(&MoveRight, window, cx);
1606 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1607
1608 editor.move_up(&MoveUp, window, cx);
1609 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1610 editor.move_down(&MoveDown, window, cx);
1611 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1612 editor.move_up(&MoveUp, window, cx);
1613 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1614
1615 editor.move_up(&MoveUp, window, cx);
1616 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1617 editor.move_left(&MoveLeft, window, cx);
1618 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1619 editor.move_left(&MoveLeft, window, cx);
1620 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1621 });
1622}
1623
1624#[gpui::test]
1625fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1626 init_test(cx, |_| {});
1627
1628 let editor = cx.add_window(|window, cx| {
1629 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1630 build_editor(buffer, window, cx)
1631 });
1632 _ = editor.update(cx, |editor, window, cx| {
1633 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1634 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1635 });
1636
1637 // moving above start of document should move selection to start of document,
1638 // but the next move down should still be at the original goal_x
1639 editor.move_up(&MoveUp, window, cx);
1640 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1641
1642 editor.move_down(&MoveDown, window, cx);
1643 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1644
1645 editor.move_down(&MoveDown, window, cx);
1646 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1647
1648 editor.move_down(&MoveDown, window, cx);
1649 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1650
1651 editor.move_down(&MoveDown, window, cx);
1652 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1653
1654 // moving past end of document should not change goal_x
1655 editor.move_down(&MoveDown, window, cx);
1656 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1657
1658 editor.move_down(&MoveDown, window, cx);
1659 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1660
1661 editor.move_up(&MoveUp, window, cx);
1662 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1663
1664 editor.move_up(&MoveUp, window, cx);
1665 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1666
1667 editor.move_up(&MoveUp, window, cx);
1668 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1669 });
1670}
1671
1672#[gpui::test]
1673fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1674 init_test(cx, |_| {});
1675 let move_to_beg = MoveToBeginningOfLine {
1676 stop_at_soft_wraps: true,
1677 stop_at_indent: true,
1678 };
1679
1680 let delete_to_beg = DeleteToBeginningOfLine {
1681 stop_at_indent: false,
1682 };
1683
1684 let move_to_end = MoveToEndOfLine {
1685 stop_at_soft_wraps: true,
1686 };
1687
1688 let editor = cx.add_window(|window, cx| {
1689 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1690 build_editor(buffer, window, cx)
1691 });
1692 _ = editor.update(cx, |editor, window, cx| {
1693 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1694 s.select_display_ranges([
1695 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1696 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1697 ]);
1698 });
1699 });
1700
1701 _ = editor.update(cx, |editor, window, cx| {
1702 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1703 assert_eq!(
1704 display_ranges(editor, cx),
1705 &[
1706 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1707 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1708 ]
1709 );
1710 });
1711
1712 _ = editor.update(cx, |editor, window, cx| {
1713 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1714 assert_eq!(
1715 display_ranges(editor, cx),
1716 &[
1717 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1718 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1719 ]
1720 );
1721 });
1722
1723 _ = editor.update(cx, |editor, window, cx| {
1724 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1725 assert_eq!(
1726 display_ranges(editor, cx),
1727 &[
1728 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1729 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1730 ]
1731 );
1732 });
1733
1734 _ = editor.update(cx, |editor, window, cx| {
1735 editor.move_to_end_of_line(&move_to_end, window, cx);
1736 assert_eq!(
1737 display_ranges(editor, cx),
1738 &[
1739 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1740 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1741 ]
1742 );
1743 });
1744
1745 // Moving to the end of line again is a no-op.
1746 _ = editor.update(cx, |editor, window, cx| {
1747 editor.move_to_end_of_line(&move_to_end, window, cx);
1748 assert_eq!(
1749 display_ranges(editor, cx),
1750 &[
1751 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1752 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1753 ]
1754 );
1755 });
1756
1757 _ = editor.update(cx, |editor, window, cx| {
1758 editor.move_left(&MoveLeft, window, cx);
1759 editor.select_to_beginning_of_line(
1760 &SelectToBeginningOfLine {
1761 stop_at_soft_wraps: true,
1762 stop_at_indent: true,
1763 },
1764 window,
1765 cx,
1766 );
1767 assert_eq!(
1768 display_ranges(editor, cx),
1769 &[
1770 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1771 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1772 ]
1773 );
1774 });
1775
1776 _ = editor.update(cx, |editor, window, cx| {
1777 editor.select_to_beginning_of_line(
1778 &SelectToBeginningOfLine {
1779 stop_at_soft_wraps: true,
1780 stop_at_indent: true,
1781 },
1782 window,
1783 cx,
1784 );
1785 assert_eq!(
1786 display_ranges(editor, cx),
1787 &[
1788 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1789 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1790 ]
1791 );
1792 });
1793
1794 _ = editor.update(cx, |editor, window, cx| {
1795 editor.select_to_beginning_of_line(
1796 &SelectToBeginningOfLine {
1797 stop_at_soft_wraps: true,
1798 stop_at_indent: true,
1799 },
1800 window,
1801 cx,
1802 );
1803 assert_eq!(
1804 display_ranges(editor, cx),
1805 &[
1806 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1807 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1808 ]
1809 );
1810 });
1811
1812 _ = editor.update(cx, |editor, window, cx| {
1813 editor.select_to_end_of_line(
1814 &SelectToEndOfLine {
1815 stop_at_soft_wraps: true,
1816 },
1817 window,
1818 cx,
1819 );
1820 assert_eq!(
1821 display_ranges(editor, cx),
1822 &[
1823 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1824 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1825 ]
1826 );
1827 });
1828
1829 _ = editor.update(cx, |editor, window, cx| {
1830 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1831 assert_eq!(editor.display_text(cx), "ab\n de");
1832 assert_eq!(
1833 display_ranges(editor, cx),
1834 &[
1835 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1836 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1837 ]
1838 );
1839 });
1840
1841 _ = editor.update(cx, |editor, window, cx| {
1842 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1843 assert_eq!(editor.display_text(cx), "\n");
1844 assert_eq!(
1845 display_ranges(editor, cx),
1846 &[
1847 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1848 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1849 ]
1850 );
1851 });
1852}
1853
1854#[gpui::test]
1855fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1856 init_test(cx, |_| {});
1857 let move_to_beg = MoveToBeginningOfLine {
1858 stop_at_soft_wraps: false,
1859 stop_at_indent: false,
1860 };
1861
1862 let move_to_end = MoveToEndOfLine {
1863 stop_at_soft_wraps: false,
1864 };
1865
1866 let editor = cx.add_window(|window, cx| {
1867 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1868 build_editor(buffer, window, cx)
1869 });
1870
1871 _ = editor.update(cx, |editor, window, cx| {
1872 editor.set_wrap_width(Some(140.0.into()), cx);
1873
1874 // We expect the following lines after wrapping
1875 // ```
1876 // thequickbrownfox
1877 // jumpedoverthelazydo
1878 // gs
1879 // ```
1880 // The final `gs` was soft-wrapped onto a new line.
1881 assert_eq!(
1882 "thequickbrownfox\njumpedoverthelaz\nydogs",
1883 editor.display_text(cx),
1884 );
1885
1886 // First, let's assert behavior on the first line, that was not soft-wrapped.
1887 // Start the cursor at the `k` on the first line
1888 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1889 s.select_display_ranges([
1890 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1891 ]);
1892 });
1893
1894 // Moving to the beginning of the line should put us at the beginning of the line.
1895 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1896 assert_eq!(
1897 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1898 display_ranges(editor, cx)
1899 );
1900
1901 // Moving to the end of the line should put us at the end of the line.
1902 editor.move_to_end_of_line(&move_to_end, window, cx);
1903 assert_eq!(
1904 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1905 display_ranges(editor, cx)
1906 );
1907
1908 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1909 // Start the cursor at the last line (`y` that was wrapped to a new line)
1910 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1911 s.select_display_ranges([
1912 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1913 ]);
1914 });
1915
1916 // Moving to the beginning of the line should put us at the start of the second line of
1917 // display text, i.e., the `j`.
1918 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1919 assert_eq!(
1920 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1921 display_ranges(editor, cx)
1922 );
1923
1924 // Moving to the beginning of the line again should be a no-op.
1925 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1926 assert_eq!(
1927 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1928 display_ranges(editor, cx)
1929 );
1930
1931 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1932 // next display line.
1933 editor.move_to_end_of_line(&move_to_end, window, cx);
1934 assert_eq!(
1935 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1936 display_ranges(editor, cx)
1937 );
1938
1939 // Moving to the end of the line again should be a no-op.
1940 editor.move_to_end_of_line(&move_to_end, window, cx);
1941 assert_eq!(
1942 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1943 display_ranges(editor, cx)
1944 );
1945 });
1946}
1947
1948#[gpui::test]
1949fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1950 init_test(cx, |_| {});
1951
1952 let move_to_beg = MoveToBeginningOfLine {
1953 stop_at_soft_wraps: true,
1954 stop_at_indent: true,
1955 };
1956
1957 let select_to_beg = SelectToBeginningOfLine {
1958 stop_at_soft_wraps: true,
1959 stop_at_indent: true,
1960 };
1961
1962 let delete_to_beg = DeleteToBeginningOfLine {
1963 stop_at_indent: true,
1964 };
1965
1966 let move_to_end = MoveToEndOfLine {
1967 stop_at_soft_wraps: false,
1968 };
1969
1970 let editor = cx.add_window(|window, cx| {
1971 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1972 build_editor(buffer, window, cx)
1973 });
1974
1975 _ = editor.update(cx, |editor, window, cx| {
1976 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1977 s.select_display_ranges([
1978 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1979 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1980 ]);
1981 });
1982
1983 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1984 // and the second cursor at the first non-whitespace character in the line.
1985 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1986 assert_eq!(
1987 display_ranges(editor, cx),
1988 &[
1989 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1990 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1991 ]
1992 );
1993
1994 // Moving to the beginning of the line again should be a no-op for the first cursor,
1995 // and should move the second cursor to the beginning of the line.
1996 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1997 assert_eq!(
1998 display_ranges(editor, cx),
1999 &[
2000 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2001 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2002 ]
2003 );
2004
2005 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2006 // and should move the second cursor back to the first non-whitespace character in the line.
2007 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2008 assert_eq!(
2009 display_ranges(editor, cx),
2010 &[
2011 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2012 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2013 ]
2014 );
2015
2016 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2017 // and to the first non-whitespace character in the line for the second cursor.
2018 editor.move_to_end_of_line(&move_to_end, window, cx);
2019 editor.move_left(&MoveLeft, window, cx);
2020 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2021 assert_eq!(
2022 display_ranges(editor, cx),
2023 &[
2024 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2025 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2026 ]
2027 );
2028
2029 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2030 // and should select to the beginning of the line for the second cursor.
2031 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2032 assert_eq!(
2033 display_ranges(editor, cx),
2034 &[
2035 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2036 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2037 ]
2038 );
2039
2040 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2041 // and should delete to the first non-whitespace character in the line for the second cursor.
2042 editor.move_to_end_of_line(&move_to_end, window, cx);
2043 editor.move_left(&MoveLeft, window, cx);
2044 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2045 assert_eq!(editor.text(cx), "c\n f");
2046 });
2047}
2048
2049#[gpui::test]
2050fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2051 init_test(cx, |_| {});
2052
2053 let move_to_beg = MoveToBeginningOfLine {
2054 stop_at_soft_wraps: true,
2055 stop_at_indent: true,
2056 };
2057
2058 let editor = cx.add_window(|window, cx| {
2059 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2060 build_editor(buffer, window, cx)
2061 });
2062
2063 _ = editor.update(cx, |editor, window, cx| {
2064 // test cursor between line_start and indent_start
2065 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2066 s.select_display_ranges([
2067 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2068 ]);
2069 });
2070
2071 // cursor should move to line_start
2072 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2073 assert_eq!(
2074 display_ranges(editor, cx),
2075 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2076 );
2077
2078 // cursor should move to indent_start
2079 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2080 assert_eq!(
2081 display_ranges(editor, cx),
2082 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2083 );
2084
2085 // cursor should move to back 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}
2093
2094#[gpui::test]
2095fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2096 init_test(cx, |_| {});
2097
2098 let editor = cx.add_window(|window, cx| {
2099 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2100 build_editor(buffer, window, cx)
2101 });
2102 _ = editor.update(cx, |editor, window, cx| {
2103 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2104 s.select_display_ranges([
2105 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2106 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2107 ])
2108 });
2109 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2110 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2111
2112 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2113 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2114
2115 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2116 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2117
2118 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2119 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2120
2121 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2122 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2123
2124 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2125 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2126
2127 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2128 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2129
2130 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2131 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2132
2133 editor.move_right(&MoveRight, window, cx);
2134 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2135 assert_selection_ranges(
2136 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2137 editor,
2138 cx,
2139 );
2140
2141 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2142 assert_selection_ranges(
2143 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2144 editor,
2145 cx,
2146 );
2147
2148 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2149 assert_selection_ranges(
2150 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2151 editor,
2152 cx,
2153 );
2154 });
2155}
2156
2157#[gpui::test]
2158fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2159 init_test(cx, |_| {});
2160
2161 let editor = cx.add_window(|window, cx| {
2162 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2163 build_editor(buffer, window, cx)
2164 });
2165
2166 _ = editor.update(cx, |editor, window, cx| {
2167 editor.set_wrap_width(Some(140.0.into()), cx);
2168 assert_eq!(
2169 editor.display_text(cx),
2170 "use one::{\n two::three::\n four::five\n};"
2171 );
2172
2173 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2174 s.select_display_ranges([
2175 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2176 ]);
2177 });
2178
2179 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2180 assert_eq!(
2181 display_ranges(editor, cx),
2182 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2183 );
2184
2185 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2186 assert_eq!(
2187 display_ranges(editor, cx),
2188 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2189 );
2190
2191 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2192 assert_eq!(
2193 display_ranges(editor, cx),
2194 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2195 );
2196
2197 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2198 assert_eq!(
2199 display_ranges(editor, cx),
2200 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2201 );
2202
2203 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2204 assert_eq!(
2205 display_ranges(editor, cx),
2206 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2207 );
2208
2209 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2210 assert_eq!(
2211 display_ranges(editor, cx),
2212 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2213 );
2214 });
2215}
2216
2217#[gpui::test]
2218async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2219 init_test(cx, |_| {});
2220 let mut cx = EditorTestContext::new(cx).await;
2221
2222 let line_height = cx.update_editor(|editor, window, cx| {
2223 editor
2224 .style(cx)
2225 .text
2226 .line_height_in_pixels(window.rem_size())
2227 });
2228 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2229
2230 cx.set_state(
2231 &r#"ˇone
2232 two
2233
2234 three
2235 fourˇ
2236 five
2237
2238 six"#
2239 .unindent(),
2240 );
2241
2242 cx.update_editor(|editor, window, cx| {
2243 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2244 });
2245 cx.assert_editor_state(
2246 &r#"one
2247 two
2248 ˇ
2249 three
2250 four
2251 five
2252 ˇ
2253 six"#
2254 .unindent(),
2255 );
2256
2257 cx.update_editor(|editor, window, cx| {
2258 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2259 });
2260 cx.assert_editor_state(
2261 &r#"one
2262 two
2263
2264 three
2265 four
2266 five
2267 ˇ
2268 sixˇ"#
2269 .unindent(),
2270 );
2271
2272 cx.update_editor(|editor, window, cx| {
2273 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2274 });
2275 cx.assert_editor_state(
2276 &r#"one
2277 two
2278
2279 three
2280 four
2281 five
2282
2283 sixˇ"#
2284 .unindent(),
2285 );
2286
2287 cx.update_editor(|editor, window, cx| {
2288 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2289 });
2290 cx.assert_editor_state(
2291 &r#"one
2292 two
2293
2294 three
2295 four
2296 five
2297 ˇ
2298 six"#
2299 .unindent(),
2300 );
2301
2302 cx.update_editor(|editor, window, cx| {
2303 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2304 });
2305 cx.assert_editor_state(
2306 &r#"one
2307 two
2308 ˇ
2309 three
2310 four
2311 five
2312
2313 six"#
2314 .unindent(),
2315 );
2316
2317 cx.update_editor(|editor, window, cx| {
2318 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2319 });
2320 cx.assert_editor_state(
2321 &r#"ˇone
2322 two
2323
2324 three
2325 four
2326 five
2327
2328 six"#
2329 .unindent(),
2330 );
2331}
2332
2333#[gpui::test]
2334async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2335 init_test(cx, |_| {});
2336 let mut cx = EditorTestContext::new(cx).await;
2337 let line_height = cx.update_editor(|editor, window, cx| {
2338 editor
2339 .style(cx)
2340 .text
2341 .line_height_in_pixels(window.rem_size())
2342 });
2343 let window = cx.window;
2344 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2345
2346 cx.set_state(
2347 r#"ˇone
2348 two
2349 three
2350 four
2351 five
2352 six
2353 seven
2354 eight
2355 nine
2356 ten
2357 "#,
2358 );
2359
2360 cx.update_editor(|editor, window, cx| {
2361 assert_eq!(
2362 editor.snapshot(window, cx).scroll_position(),
2363 gpui::Point::new(0., 0.)
2364 );
2365 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2366 assert_eq!(
2367 editor.snapshot(window, cx).scroll_position(),
2368 gpui::Point::new(0., 3.)
2369 );
2370 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2371 assert_eq!(
2372 editor.snapshot(window, cx).scroll_position(),
2373 gpui::Point::new(0., 6.)
2374 );
2375 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2376 assert_eq!(
2377 editor.snapshot(window, cx).scroll_position(),
2378 gpui::Point::new(0., 3.)
2379 );
2380
2381 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2382 assert_eq!(
2383 editor.snapshot(window, cx).scroll_position(),
2384 gpui::Point::new(0., 1.)
2385 );
2386 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2387 assert_eq!(
2388 editor.snapshot(window, cx).scroll_position(),
2389 gpui::Point::new(0., 3.)
2390 );
2391 });
2392}
2393
2394#[gpui::test]
2395async fn test_autoscroll(cx: &mut TestAppContext) {
2396 init_test(cx, |_| {});
2397 let mut cx = EditorTestContext::new(cx).await;
2398
2399 let line_height = cx.update_editor(|editor, window, cx| {
2400 editor.set_vertical_scroll_margin(2, cx);
2401 editor
2402 .style(cx)
2403 .text
2404 .line_height_in_pixels(window.rem_size())
2405 });
2406 let window = cx.window;
2407 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2408
2409 cx.set_state(
2410 r#"ˇone
2411 two
2412 three
2413 four
2414 five
2415 six
2416 seven
2417 eight
2418 nine
2419 ten
2420 "#,
2421 );
2422 cx.update_editor(|editor, window, cx| {
2423 assert_eq!(
2424 editor.snapshot(window, cx).scroll_position(),
2425 gpui::Point::new(0., 0.0)
2426 );
2427 });
2428
2429 // Add a cursor below the visible area. Since both cursors cannot fit
2430 // on screen, the editor autoscrolls to reveal the newest cursor, and
2431 // allows the vertical scroll margin below that cursor.
2432 cx.update_editor(|editor, window, cx| {
2433 editor.change_selections(Default::default(), window, cx, |selections| {
2434 selections.select_ranges([
2435 Point::new(0, 0)..Point::new(0, 0),
2436 Point::new(6, 0)..Point::new(6, 0),
2437 ]);
2438 })
2439 });
2440 cx.update_editor(|editor, window, cx| {
2441 assert_eq!(
2442 editor.snapshot(window, cx).scroll_position(),
2443 gpui::Point::new(0., 3.0)
2444 );
2445 });
2446
2447 // Move down. The editor cursor scrolls down to track the newest cursor.
2448 cx.update_editor(|editor, window, cx| {
2449 editor.move_down(&Default::default(), window, cx);
2450 });
2451 cx.update_editor(|editor, window, cx| {
2452 assert_eq!(
2453 editor.snapshot(window, cx).scroll_position(),
2454 gpui::Point::new(0., 4.0)
2455 );
2456 });
2457
2458 // Add a cursor above the visible area. Since both cursors fit on screen,
2459 // the editor scrolls to show both.
2460 cx.update_editor(|editor, window, cx| {
2461 editor.change_selections(Default::default(), window, cx, |selections| {
2462 selections.select_ranges([
2463 Point::new(1, 0)..Point::new(1, 0),
2464 Point::new(6, 0)..Point::new(6, 0),
2465 ]);
2466 })
2467 });
2468 cx.update_editor(|editor, window, cx| {
2469 assert_eq!(
2470 editor.snapshot(window, cx).scroll_position(),
2471 gpui::Point::new(0., 1.0)
2472 );
2473 });
2474}
2475
2476#[gpui::test]
2477async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2478 init_test(cx, |_| {});
2479 let mut cx = EditorTestContext::new(cx).await;
2480
2481 let line_height = cx.update_editor(|editor, window, cx| {
2482 editor
2483 .style(cx)
2484 .text
2485 .line_height_in_pixels(window.rem_size())
2486 });
2487 let window = cx.window;
2488 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2489 cx.set_state(
2490 &r#"
2491 ˇone
2492 two
2493 threeˇ
2494 four
2495 five
2496 six
2497 seven
2498 eight
2499 nine
2500 ten
2501 "#
2502 .unindent(),
2503 );
2504
2505 cx.update_editor(|editor, window, cx| {
2506 editor.move_page_down(&MovePageDown::default(), window, cx)
2507 });
2508 cx.assert_editor_state(
2509 &r#"
2510 one
2511 two
2512 three
2513 ˇfour
2514 five
2515 sixˇ
2516 seven
2517 eight
2518 nine
2519 ten
2520 "#
2521 .unindent(),
2522 );
2523
2524 cx.update_editor(|editor, window, cx| {
2525 editor.move_page_down(&MovePageDown::default(), window, cx)
2526 });
2527 cx.assert_editor_state(
2528 &r#"
2529 one
2530 two
2531 three
2532 four
2533 five
2534 six
2535 ˇseven
2536 eight
2537 nineˇ
2538 ten
2539 "#
2540 .unindent(),
2541 );
2542
2543 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2544 cx.assert_editor_state(
2545 &r#"
2546 one
2547 two
2548 three
2549 ˇfour
2550 five
2551 sixˇ
2552 seven
2553 eight
2554 nine
2555 ten
2556 "#
2557 .unindent(),
2558 );
2559
2560 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2561 cx.assert_editor_state(
2562 &r#"
2563 ˇone
2564 two
2565 threeˇ
2566 four
2567 five
2568 six
2569 seven
2570 eight
2571 nine
2572 ten
2573 "#
2574 .unindent(),
2575 );
2576
2577 // Test select collapsing
2578 cx.update_editor(|editor, window, cx| {
2579 editor.move_page_down(&MovePageDown::default(), window, cx);
2580 editor.move_page_down(&MovePageDown::default(), window, cx);
2581 editor.move_page_down(&MovePageDown::default(), window, cx);
2582 });
2583 cx.assert_editor_state(
2584 &r#"
2585 one
2586 two
2587 three
2588 four
2589 five
2590 six
2591 seven
2592 eight
2593 nine
2594 ˇten
2595 ˇ"#
2596 .unindent(),
2597 );
2598}
2599
2600#[gpui::test]
2601async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2602 init_test(cx, |_| {});
2603 let mut cx = EditorTestContext::new(cx).await;
2604 cx.set_state("one «two threeˇ» four");
2605 cx.update_editor(|editor, window, cx| {
2606 editor.delete_to_beginning_of_line(
2607 &DeleteToBeginningOfLine {
2608 stop_at_indent: false,
2609 },
2610 window,
2611 cx,
2612 );
2613 assert_eq!(editor.text(cx), " four");
2614 });
2615}
2616
2617#[gpui::test]
2618async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2619 init_test(cx, |_| {});
2620
2621 let mut cx = EditorTestContext::new(cx).await;
2622
2623 // For an empty selection, the preceding word fragment is deleted.
2624 // For non-empty selections, only selected characters are deleted.
2625 cx.set_state("onˇe two t«hreˇ»e four");
2626 cx.update_editor(|editor, window, cx| {
2627 editor.delete_to_previous_word_start(
2628 &DeleteToPreviousWordStart {
2629 ignore_newlines: false,
2630 ignore_brackets: false,
2631 },
2632 window,
2633 cx,
2634 );
2635 });
2636 cx.assert_editor_state("ˇe two tˇe four");
2637
2638 cx.set_state("e tˇwo te «fˇ»our");
2639 cx.update_editor(|editor, window, cx| {
2640 editor.delete_to_next_word_end(
2641 &DeleteToNextWordEnd {
2642 ignore_newlines: false,
2643 ignore_brackets: false,
2644 },
2645 window,
2646 cx,
2647 );
2648 });
2649 cx.assert_editor_state("e tˇ te ˇour");
2650}
2651
2652#[gpui::test]
2653async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2654 init_test(cx, |_| {});
2655
2656 let mut cx = EditorTestContext::new(cx).await;
2657
2658 cx.set_state("here is some text ˇwith a space");
2659 cx.update_editor(|editor, window, cx| {
2660 editor.delete_to_previous_word_start(
2661 &DeleteToPreviousWordStart {
2662 ignore_newlines: false,
2663 ignore_brackets: true,
2664 },
2665 window,
2666 cx,
2667 );
2668 });
2669 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2670 cx.assert_editor_state("here is some textˇwith a space");
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: false,
2678 },
2679 window,
2680 cx,
2681 );
2682 });
2683 cx.assert_editor_state("here is some textˇwith a space");
2684
2685 cx.set_state("here is some textˇ with a space");
2686 cx.update_editor(|editor, window, cx| {
2687 editor.delete_to_next_word_end(
2688 &DeleteToNextWordEnd {
2689 ignore_newlines: false,
2690 ignore_brackets: true,
2691 },
2692 window,
2693 cx,
2694 );
2695 });
2696 // Same happens in the other direction.
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: false,
2705 },
2706 window,
2707 cx,
2708 );
2709 });
2710 cx.assert_editor_state("here is some textˇwith a space");
2711
2712 cx.set_state("here is some textˇ with a space");
2713 cx.update_editor(|editor, window, cx| {
2714 editor.delete_to_next_word_end(
2715 &DeleteToNextWordEnd {
2716 ignore_newlines: true,
2717 ignore_brackets: false,
2718 },
2719 window,
2720 cx,
2721 );
2722 });
2723 cx.assert_editor_state("here is some textˇwith a space");
2724 cx.update_editor(|editor, window, cx| {
2725 editor.delete_to_previous_word_start(
2726 &DeleteToPreviousWordStart {
2727 ignore_newlines: true,
2728 ignore_brackets: false,
2729 },
2730 window,
2731 cx,
2732 );
2733 });
2734 cx.assert_editor_state("here is some ˇwith a space");
2735 cx.update_editor(|editor, window, cx| {
2736 editor.delete_to_previous_word_start(
2737 &DeleteToPreviousWordStart {
2738 ignore_newlines: true,
2739 ignore_brackets: false,
2740 },
2741 window,
2742 cx,
2743 );
2744 });
2745 // Single whitespaces are removed with the word behind them.
2746 cx.assert_editor_state("here is ˇwith a space");
2747 cx.update_editor(|editor, window, cx| {
2748 editor.delete_to_previous_word_start(
2749 &DeleteToPreviousWordStart {
2750 ignore_newlines: true,
2751 ignore_brackets: false,
2752 },
2753 window,
2754 cx,
2755 );
2756 });
2757 cx.assert_editor_state("here ˇwith a space");
2758 cx.update_editor(|editor, window, cx| {
2759 editor.delete_to_previous_word_start(
2760 &DeleteToPreviousWordStart {
2761 ignore_newlines: true,
2762 ignore_brackets: false,
2763 },
2764 window,
2765 cx,
2766 );
2767 });
2768 cx.assert_editor_state("ˇwith a space");
2769 cx.update_editor(|editor, window, cx| {
2770 editor.delete_to_previous_word_start(
2771 &DeleteToPreviousWordStart {
2772 ignore_newlines: true,
2773 ignore_brackets: false,
2774 },
2775 window,
2776 cx,
2777 );
2778 });
2779 cx.assert_editor_state("ˇwith a space");
2780 cx.update_editor(|editor, window, cx| {
2781 editor.delete_to_next_word_end(
2782 &DeleteToNextWordEnd {
2783 ignore_newlines: true,
2784 ignore_brackets: false,
2785 },
2786 window,
2787 cx,
2788 );
2789 });
2790 // Same happens in the other direction.
2791 cx.assert_editor_state("ˇ a space");
2792 cx.update_editor(|editor, window, cx| {
2793 editor.delete_to_next_word_end(
2794 &DeleteToNextWordEnd {
2795 ignore_newlines: true,
2796 ignore_brackets: false,
2797 },
2798 window,
2799 cx,
2800 );
2801 });
2802 cx.assert_editor_state("ˇ space");
2803 cx.update_editor(|editor, window, cx| {
2804 editor.delete_to_next_word_end(
2805 &DeleteToNextWordEnd {
2806 ignore_newlines: true,
2807 ignore_brackets: false,
2808 },
2809 window,
2810 cx,
2811 );
2812 });
2813 cx.assert_editor_state("ˇ");
2814 cx.update_editor(|editor, window, cx| {
2815 editor.delete_to_next_word_end(
2816 &DeleteToNextWordEnd {
2817 ignore_newlines: true,
2818 ignore_brackets: false,
2819 },
2820 window,
2821 cx,
2822 );
2823 });
2824 cx.assert_editor_state("ˇ");
2825 cx.update_editor(|editor, window, cx| {
2826 editor.delete_to_previous_word_start(
2827 &DeleteToPreviousWordStart {
2828 ignore_newlines: true,
2829 ignore_brackets: false,
2830 },
2831 window,
2832 cx,
2833 );
2834 });
2835 cx.assert_editor_state("ˇ");
2836}
2837
2838#[gpui::test]
2839async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2840 init_test(cx, |_| {});
2841
2842 let language = Arc::new(
2843 Language::new(
2844 LanguageConfig {
2845 brackets: BracketPairConfig {
2846 pairs: vec![
2847 BracketPair {
2848 start: "\"".to_string(),
2849 end: "\"".to_string(),
2850 close: true,
2851 surround: true,
2852 newline: false,
2853 },
2854 BracketPair {
2855 start: "(".to_string(),
2856 end: ")".to_string(),
2857 close: true,
2858 surround: true,
2859 newline: true,
2860 },
2861 ],
2862 ..BracketPairConfig::default()
2863 },
2864 ..LanguageConfig::default()
2865 },
2866 Some(tree_sitter_rust::LANGUAGE.into()),
2867 )
2868 .with_brackets_query(
2869 r#"
2870 ("(" @open ")" @close)
2871 ("\"" @open "\"" @close)
2872 "#,
2873 )
2874 .unwrap(),
2875 );
2876
2877 let mut cx = EditorTestContext::new(cx).await;
2878 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2879
2880 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2881 cx.update_editor(|editor, window, cx| {
2882 editor.delete_to_previous_word_start(
2883 &DeleteToPreviousWordStart {
2884 ignore_newlines: true,
2885 ignore_brackets: false,
2886 },
2887 window,
2888 cx,
2889 );
2890 });
2891 // Deletion stops before brackets if asked to not ignore them.
2892 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2893 cx.update_editor(|editor, window, cx| {
2894 editor.delete_to_previous_word_start(
2895 &DeleteToPreviousWordStart {
2896 ignore_newlines: true,
2897 ignore_brackets: false,
2898 },
2899 window,
2900 cx,
2901 );
2902 });
2903 // Deletion has to remove a single bracket and then stop again.
2904 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2905
2906 cx.update_editor(|editor, window, cx| {
2907 editor.delete_to_previous_word_start(
2908 &DeleteToPreviousWordStart {
2909 ignore_newlines: true,
2910 ignore_brackets: false,
2911 },
2912 window,
2913 cx,
2914 );
2915 });
2916 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2917
2918 cx.update_editor(|editor, window, cx| {
2919 editor.delete_to_previous_word_start(
2920 &DeleteToPreviousWordStart {
2921 ignore_newlines: true,
2922 ignore_brackets: false,
2923 },
2924 window,
2925 cx,
2926 );
2927 });
2928 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2929
2930 cx.update_editor(|editor, window, cx| {
2931 editor.delete_to_previous_word_start(
2932 &DeleteToPreviousWordStart {
2933 ignore_newlines: true,
2934 ignore_brackets: false,
2935 },
2936 window,
2937 cx,
2938 );
2939 });
2940 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2941
2942 cx.update_editor(|editor, window, cx| {
2943 editor.delete_to_next_word_end(
2944 &DeleteToNextWordEnd {
2945 ignore_newlines: true,
2946 ignore_brackets: false,
2947 },
2948 window,
2949 cx,
2950 );
2951 });
2952 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2953 cx.assert_editor_state(r#"ˇ");"#);
2954
2955 cx.update_editor(|editor, window, cx| {
2956 editor.delete_to_next_word_end(
2957 &DeleteToNextWordEnd {
2958 ignore_newlines: true,
2959 ignore_brackets: false,
2960 },
2961 window,
2962 cx,
2963 );
2964 });
2965 cx.assert_editor_state(r#"ˇ"#);
2966
2967 cx.update_editor(|editor, window, cx| {
2968 editor.delete_to_next_word_end(
2969 &DeleteToNextWordEnd {
2970 ignore_newlines: true,
2971 ignore_brackets: false,
2972 },
2973 window,
2974 cx,
2975 );
2976 });
2977 cx.assert_editor_state(r#"ˇ"#);
2978
2979 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2980 cx.update_editor(|editor, window, cx| {
2981 editor.delete_to_previous_word_start(
2982 &DeleteToPreviousWordStart {
2983 ignore_newlines: true,
2984 ignore_brackets: true,
2985 },
2986 window,
2987 cx,
2988 );
2989 });
2990 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2991}
2992
2993#[gpui::test]
2994fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2995 init_test(cx, |_| {});
2996
2997 let editor = cx.add_window(|window, cx| {
2998 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2999 build_editor(buffer, window, cx)
3000 });
3001 let del_to_prev_word_start = DeleteToPreviousWordStart {
3002 ignore_newlines: false,
3003 ignore_brackets: false,
3004 };
3005 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3006 ignore_newlines: true,
3007 ignore_brackets: false,
3008 };
3009
3010 _ = editor.update(cx, |editor, window, cx| {
3011 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3012 s.select_display_ranges([
3013 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3014 ])
3015 });
3016 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3017 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3018 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3019 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3020 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3021 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3022 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3023 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3024 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3025 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3026 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3027 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3028 });
3029}
3030
3031#[gpui::test]
3032fn test_delete_to_previous_subword_start_or_newline(cx: &mut TestAppContext) {
3033 init_test(cx, |_| {});
3034
3035 let editor = cx.add_window(|window, cx| {
3036 let buffer = MultiBuffer::build_simple("fooBar\n\nbazQux", cx);
3037 build_editor(buffer, window, cx)
3038 });
3039 let del_to_prev_sub_word_start = DeleteToPreviousSubwordStart {
3040 ignore_newlines: false,
3041 ignore_brackets: false,
3042 };
3043 let del_to_prev_sub_word_start_ignore_newlines = DeleteToPreviousSubwordStart {
3044 ignore_newlines: true,
3045 ignore_brackets: false,
3046 };
3047
3048 _ = editor.update(cx, |editor, window, cx| {
3049 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3050 s.select_display_ranges([
3051 DisplayPoint::new(DisplayRow(2), 6)..DisplayPoint::new(DisplayRow(2), 6)
3052 ])
3053 });
3054 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3055 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n\nbaz");
3056 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3057 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n\n");
3058 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3059 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n");
3060 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3061 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar");
3062 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3063 assert_eq!(editor.buffer.read(cx).read(cx).text(), "foo");
3064 editor.delete_to_previous_subword_start(
3065 &del_to_prev_sub_word_start_ignore_newlines,
3066 window,
3067 cx,
3068 );
3069 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3070 });
3071}
3072
3073#[gpui::test]
3074fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3075 init_test(cx, |_| {});
3076
3077 let editor = cx.add_window(|window, cx| {
3078 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3079 build_editor(buffer, window, cx)
3080 });
3081 let del_to_next_word_end = DeleteToNextWordEnd {
3082 ignore_newlines: false,
3083 ignore_brackets: false,
3084 };
3085 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3086 ignore_newlines: true,
3087 ignore_brackets: false,
3088 };
3089
3090 _ = editor.update(cx, |editor, window, cx| {
3091 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3092 s.select_display_ranges([
3093 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3094 ])
3095 });
3096 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3097 assert_eq!(
3098 editor.buffer.read(cx).read(cx).text(),
3099 "one\n two\nthree\n four"
3100 );
3101 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3102 assert_eq!(
3103 editor.buffer.read(cx).read(cx).text(),
3104 "\n two\nthree\n four"
3105 );
3106 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3107 assert_eq!(
3108 editor.buffer.read(cx).read(cx).text(),
3109 "two\nthree\n four"
3110 );
3111 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3112 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3113 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3114 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3115 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3116 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3117 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3118 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3119 });
3120}
3121
3122#[gpui::test]
3123fn test_delete_to_next_subword_end_or_newline(cx: &mut TestAppContext) {
3124 init_test(cx, |_| {});
3125
3126 let editor = cx.add_window(|window, cx| {
3127 let buffer = MultiBuffer::build_simple("\nfooBar\n bazQux", cx);
3128 build_editor(buffer, window, cx)
3129 });
3130 let del_to_next_subword_end = DeleteToNextSubwordEnd {
3131 ignore_newlines: false,
3132 ignore_brackets: false,
3133 };
3134 let del_to_next_subword_end_ignore_newlines = DeleteToNextSubwordEnd {
3135 ignore_newlines: true,
3136 ignore_brackets: false,
3137 };
3138
3139 _ = editor.update(cx, |editor, window, cx| {
3140 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3141 s.select_display_ranges([
3142 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3143 ])
3144 });
3145 // Delete "\n" (empty line)
3146 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3147 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n bazQux");
3148 // Delete "foo" (subword boundary)
3149 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3150 assert_eq!(editor.buffer.read(cx).read(cx).text(), "Bar\n bazQux");
3151 // Delete "Bar"
3152 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3153 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n bazQux");
3154 // Delete "\n " (newline + leading whitespace)
3155 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3156 assert_eq!(editor.buffer.read(cx).read(cx).text(), "bazQux");
3157 // Delete "baz" (subword boundary)
3158 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3159 assert_eq!(editor.buffer.read(cx).read(cx).text(), "Qux");
3160 // With ignore_newlines, delete "Qux"
3161 editor.delete_to_next_subword_end(&del_to_next_subword_end_ignore_newlines, window, cx);
3162 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3163 });
3164}
3165
3166#[gpui::test]
3167fn test_newline(cx: &mut TestAppContext) {
3168 init_test(cx, |_| {});
3169
3170 let editor = cx.add_window(|window, cx| {
3171 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3172 build_editor(buffer, window, cx)
3173 });
3174
3175 _ = editor.update(cx, |editor, window, cx| {
3176 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3177 s.select_display_ranges([
3178 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3179 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3180 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3181 ])
3182 });
3183
3184 editor.newline(&Newline, window, cx);
3185 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3186 });
3187}
3188
3189#[gpui::test]
3190async fn test_newline_yaml(cx: &mut TestAppContext) {
3191 init_test(cx, |_| {});
3192
3193 let mut cx = EditorTestContext::new(cx).await;
3194 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3195 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3196
3197 // Object (between 2 fields)
3198 cx.set_state(indoc! {"
3199 test:ˇ
3200 hello: bye"});
3201 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3202 cx.assert_editor_state(indoc! {"
3203 test:
3204 ˇ
3205 hello: bye"});
3206
3207 // Object (first and single line)
3208 cx.set_state(indoc! {"
3209 test:ˇ"});
3210 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3211 cx.assert_editor_state(indoc! {"
3212 test:
3213 ˇ"});
3214
3215 // Array with objects (after first element)
3216 cx.set_state(indoc! {"
3217 test:
3218 - foo: barˇ"});
3219 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3220 cx.assert_editor_state(indoc! {"
3221 test:
3222 - foo: bar
3223 ˇ"});
3224
3225 // Array with objects and comment
3226 cx.set_state(indoc! {"
3227 test:
3228 - foo: bar
3229 - bar: # testˇ"});
3230 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3231 cx.assert_editor_state(indoc! {"
3232 test:
3233 - foo: bar
3234 - bar: # test
3235 ˇ"});
3236
3237 // Array with objects (after second element)
3238 cx.set_state(indoc! {"
3239 test:
3240 - foo: bar
3241 - bar: fooˇ"});
3242 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3243 cx.assert_editor_state(indoc! {"
3244 test:
3245 - foo: bar
3246 - bar: foo
3247 ˇ"});
3248
3249 // Array with strings (after first element)
3250 cx.set_state(indoc! {"
3251 test:
3252 - fooˇ"});
3253 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3254 cx.assert_editor_state(indoc! {"
3255 test:
3256 - foo
3257 ˇ"});
3258}
3259
3260#[gpui::test]
3261fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3262 init_test(cx, |_| {});
3263
3264 let editor = cx.add_window(|window, cx| {
3265 let buffer = MultiBuffer::build_simple(
3266 "
3267 a
3268 b(
3269 X
3270 )
3271 c(
3272 X
3273 )
3274 "
3275 .unindent()
3276 .as_str(),
3277 cx,
3278 );
3279 let mut editor = build_editor(buffer, window, cx);
3280 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3281 s.select_ranges([
3282 Point::new(2, 4)..Point::new(2, 5),
3283 Point::new(5, 4)..Point::new(5, 5),
3284 ])
3285 });
3286 editor
3287 });
3288
3289 _ = editor.update(cx, |editor, window, cx| {
3290 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3291 editor.buffer.update(cx, |buffer, cx| {
3292 buffer.edit(
3293 [
3294 (Point::new(1, 2)..Point::new(3, 0), ""),
3295 (Point::new(4, 2)..Point::new(6, 0), ""),
3296 ],
3297 None,
3298 cx,
3299 );
3300 assert_eq!(
3301 buffer.read(cx).text(),
3302 "
3303 a
3304 b()
3305 c()
3306 "
3307 .unindent()
3308 );
3309 });
3310 assert_eq!(
3311 editor.selections.ranges(&editor.display_snapshot(cx)),
3312 &[
3313 Point::new(1, 2)..Point::new(1, 2),
3314 Point::new(2, 2)..Point::new(2, 2),
3315 ],
3316 );
3317
3318 editor.newline(&Newline, window, cx);
3319 assert_eq!(
3320 editor.text(cx),
3321 "
3322 a
3323 b(
3324 )
3325 c(
3326 )
3327 "
3328 .unindent()
3329 );
3330
3331 // The selections are moved after the inserted newlines
3332 assert_eq!(
3333 editor.selections.ranges(&editor.display_snapshot(cx)),
3334 &[
3335 Point::new(2, 0)..Point::new(2, 0),
3336 Point::new(4, 0)..Point::new(4, 0),
3337 ],
3338 );
3339 });
3340}
3341
3342#[gpui::test]
3343async fn test_newline_above(cx: &mut TestAppContext) {
3344 init_test(cx, |settings| {
3345 settings.defaults.tab_size = NonZeroU32::new(4)
3346 });
3347
3348 let language = Arc::new(
3349 Language::new(
3350 LanguageConfig::default(),
3351 Some(tree_sitter_rust::LANGUAGE.into()),
3352 )
3353 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3354 .unwrap(),
3355 );
3356
3357 let mut cx = EditorTestContext::new(cx).await;
3358 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3359 cx.set_state(indoc! {"
3360 const a: ˇA = (
3361 (ˇ
3362 «const_functionˇ»(ˇ),
3363 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3364 )ˇ
3365 ˇ);ˇ
3366 "});
3367
3368 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3369 cx.assert_editor_state(indoc! {"
3370 ˇ
3371 const a: A = (
3372 ˇ
3373 (
3374 ˇ
3375 ˇ
3376 const_function(),
3377 ˇ
3378 ˇ
3379 ˇ
3380 ˇ
3381 something_else,
3382 ˇ
3383 )
3384 ˇ
3385 ˇ
3386 );
3387 "});
3388}
3389
3390#[gpui::test]
3391async fn test_newline_below(cx: &mut TestAppContext) {
3392 init_test(cx, |settings| {
3393 settings.defaults.tab_size = NonZeroU32::new(4)
3394 });
3395
3396 let language = Arc::new(
3397 Language::new(
3398 LanguageConfig::default(),
3399 Some(tree_sitter_rust::LANGUAGE.into()),
3400 )
3401 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3402 .unwrap(),
3403 );
3404
3405 let mut cx = EditorTestContext::new(cx).await;
3406 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3407 cx.set_state(indoc! {"
3408 const a: ˇA = (
3409 (ˇ
3410 «const_functionˇ»(ˇ),
3411 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3412 )ˇ
3413 ˇ);ˇ
3414 "});
3415
3416 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3417 cx.assert_editor_state(indoc! {"
3418 const a: A = (
3419 ˇ
3420 (
3421 ˇ
3422 const_function(),
3423 ˇ
3424 ˇ
3425 something_else,
3426 ˇ
3427 ˇ
3428 ˇ
3429 ˇ
3430 )
3431 ˇ
3432 );
3433 ˇ
3434 ˇ
3435 "});
3436}
3437
3438#[gpui::test]
3439async fn test_newline_comments(cx: &mut TestAppContext) {
3440 init_test(cx, |settings| {
3441 settings.defaults.tab_size = NonZeroU32::new(4)
3442 });
3443
3444 let language = Arc::new(Language::new(
3445 LanguageConfig {
3446 line_comments: vec!["// ".into()],
3447 ..LanguageConfig::default()
3448 },
3449 None,
3450 ));
3451 {
3452 let mut cx = EditorTestContext::new(cx).await;
3453 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3454 cx.set_state(indoc! {"
3455 // Fooˇ
3456 "});
3457
3458 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3459 cx.assert_editor_state(indoc! {"
3460 // Foo
3461 // ˇ
3462 "});
3463 // Ensure that we add comment prefix when existing line contains space
3464 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3465 cx.assert_editor_state(
3466 indoc! {"
3467 // Foo
3468 //s
3469 // ˇ
3470 "}
3471 .replace("s", " ") // s is used as space placeholder to prevent format on save
3472 .as_str(),
3473 );
3474 // Ensure that we add comment prefix when existing line does not contain space
3475 cx.set_state(indoc! {"
3476 // Foo
3477 //ˇ
3478 "});
3479 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3480 cx.assert_editor_state(indoc! {"
3481 // Foo
3482 //
3483 // ˇ
3484 "});
3485 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3486 cx.set_state(indoc! {"
3487 ˇ// Foo
3488 "});
3489 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3490 cx.assert_editor_state(indoc! {"
3491
3492 ˇ// Foo
3493 "});
3494 }
3495 // Ensure that comment continuations can be disabled.
3496 update_test_language_settings(cx, |settings| {
3497 settings.defaults.extend_comment_on_newline = Some(false);
3498 });
3499 let mut cx = EditorTestContext::new(cx).await;
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 // Foo
3506 ˇ
3507 "});
3508}
3509
3510#[gpui::test]
3511async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3512 init_test(cx, |settings| {
3513 settings.defaults.tab_size = NonZeroU32::new(4)
3514 });
3515
3516 let language = Arc::new(Language::new(
3517 LanguageConfig {
3518 line_comments: vec!["// ".into(), "/// ".into()],
3519 ..LanguageConfig::default()
3520 },
3521 None,
3522 ));
3523 {
3524 let mut cx = EditorTestContext::new(cx).await;
3525 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3526 cx.set_state(indoc! {"
3527 //ˇ
3528 "});
3529 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3530 cx.assert_editor_state(indoc! {"
3531 //
3532 // ˇ
3533 "});
3534
3535 cx.set_state(indoc! {"
3536 ///ˇ
3537 "});
3538 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3539 cx.assert_editor_state(indoc! {"
3540 ///
3541 /// ˇ
3542 "});
3543 }
3544}
3545
3546#[gpui::test]
3547async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3548 init_test(cx, |settings| {
3549 settings.defaults.tab_size = NonZeroU32::new(4)
3550 });
3551
3552 let language = Arc::new(
3553 Language::new(
3554 LanguageConfig {
3555 documentation_comment: Some(language::BlockCommentConfig {
3556 start: "/**".into(),
3557 end: "*/".into(),
3558 prefix: "* ".into(),
3559 tab_size: 1,
3560 }),
3561
3562 ..LanguageConfig::default()
3563 },
3564 Some(tree_sitter_rust::LANGUAGE.into()),
3565 )
3566 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3567 .unwrap(),
3568 );
3569
3570 {
3571 let mut cx = EditorTestContext::new(cx).await;
3572 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3573 cx.set_state(indoc! {"
3574 /**ˇ
3575 "});
3576
3577 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3578 cx.assert_editor_state(indoc! {"
3579 /**
3580 * ˇ
3581 "});
3582 // Ensure that if cursor is before the comment start,
3583 // we do not actually insert a comment prefix.
3584 cx.set_state(indoc! {"
3585 ˇ/**
3586 "});
3587 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3588 cx.assert_editor_state(indoc! {"
3589
3590 ˇ/**
3591 "});
3592 // Ensure that if cursor is between it doesn't add comment prefix.
3593 cx.set_state(indoc! {"
3594 /*ˇ*
3595 "});
3596 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3597 cx.assert_editor_state(indoc! {"
3598 /*
3599 ˇ*
3600 "});
3601 // Ensure that if suffix exists on same line after cursor it adds new line.
3602 cx.set_state(indoc! {"
3603 /**ˇ*/
3604 "});
3605 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3606 cx.assert_editor_state(indoc! {"
3607 /**
3608 * ˇ
3609 */
3610 "});
3611 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3612 cx.set_state(indoc! {"
3613 /**ˇ */
3614 "});
3615 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3616 cx.assert_editor_state(indoc! {"
3617 /**
3618 * ˇ
3619 */
3620 "});
3621 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3622 cx.set_state(indoc! {"
3623 /** ˇ*/
3624 "});
3625 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3626 cx.assert_editor_state(
3627 indoc! {"
3628 /**s
3629 * ˇ
3630 */
3631 "}
3632 .replace("s", " ") // s is used as space placeholder to prevent format on save
3633 .as_str(),
3634 );
3635 // Ensure that delimiter space is preserved when newline on already
3636 // spaced delimiter.
3637 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3638 cx.assert_editor_state(
3639 indoc! {"
3640 /**s
3641 *s
3642 * ˇ
3643 */
3644 "}
3645 .replace("s", " ") // s is used as space placeholder to prevent format on save
3646 .as_str(),
3647 );
3648 // Ensure that delimiter space is preserved when space is not
3649 // on existing delimiter.
3650 cx.set_state(indoc! {"
3651 /**
3652 *ˇ
3653 */
3654 "});
3655 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3656 cx.assert_editor_state(indoc! {"
3657 /**
3658 *
3659 * ˇ
3660 */
3661 "});
3662 // Ensure that if suffix exists on same line after cursor it
3663 // doesn't add extra new line if prefix is not on same line.
3664 cx.set_state(indoc! {"
3665 /**
3666 ˇ*/
3667 "});
3668 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3669 cx.assert_editor_state(indoc! {"
3670 /**
3671
3672 ˇ*/
3673 "});
3674 // Ensure that it detects suffix after existing prefix.
3675 cx.set_state(indoc! {"
3676 /**ˇ/
3677 "});
3678 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3679 cx.assert_editor_state(indoc! {"
3680 /**
3681 ˇ/
3682 "});
3683 // Ensure that if suffix exists on same line before
3684 // cursor it does not add comment prefix.
3685 cx.set_state(indoc! {"
3686 /** */ˇ
3687 "});
3688 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3689 cx.assert_editor_state(indoc! {"
3690 /** */
3691 ˇ
3692 "});
3693 // Ensure that if suffix exists on same line before
3694 // cursor it does not add comment prefix.
3695 cx.set_state(indoc! {"
3696 /**
3697 *
3698 */ˇ
3699 "});
3700 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3701 cx.assert_editor_state(indoc! {"
3702 /**
3703 *
3704 */
3705 ˇ
3706 "});
3707
3708 // Ensure that inline comment followed by code
3709 // doesn't add comment prefix on newline
3710 cx.set_state(indoc! {"
3711 /** */ textˇ
3712 "});
3713 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3714 cx.assert_editor_state(indoc! {"
3715 /** */ text
3716 ˇ
3717 "});
3718
3719 // Ensure that text after comment end tag
3720 // doesn't add comment prefix on newline
3721 cx.set_state(indoc! {"
3722 /**
3723 *
3724 */ˇtext
3725 "});
3726 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3727 cx.assert_editor_state(indoc! {"
3728 /**
3729 *
3730 */
3731 ˇtext
3732 "});
3733
3734 // Ensure if not comment block it doesn't
3735 // add comment prefix on newline
3736 cx.set_state(indoc! {"
3737 * textˇ
3738 "});
3739 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3740 cx.assert_editor_state(indoc! {"
3741 * text
3742 ˇ
3743 "});
3744 }
3745 // Ensure that comment continuations can be disabled.
3746 update_test_language_settings(cx, |settings| {
3747 settings.defaults.extend_comment_on_newline = Some(false);
3748 });
3749 let mut cx = EditorTestContext::new(cx).await;
3750 cx.set_state(indoc! {"
3751 /**ˇ
3752 "});
3753 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3754 cx.assert_editor_state(indoc! {"
3755 /**
3756 ˇ
3757 "});
3758}
3759
3760#[gpui::test]
3761async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3762 init_test(cx, |settings| {
3763 settings.defaults.tab_size = NonZeroU32::new(4)
3764 });
3765
3766 let lua_language = Arc::new(Language::new(
3767 LanguageConfig {
3768 line_comments: vec!["--".into()],
3769 block_comment: Some(language::BlockCommentConfig {
3770 start: "--[[".into(),
3771 prefix: "".into(),
3772 end: "]]".into(),
3773 tab_size: 0,
3774 }),
3775 ..LanguageConfig::default()
3776 },
3777 None,
3778 ));
3779
3780 let mut cx = EditorTestContext::new(cx).await;
3781 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3782
3783 // Line with line comment should extend
3784 cx.set_state(indoc! {"
3785 --ˇ
3786 "});
3787 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3788 cx.assert_editor_state(indoc! {"
3789 --
3790 --ˇ
3791 "});
3792
3793 // Line with block comment that matches line comment should not extend
3794 cx.set_state(indoc! {"
3795 --[[ˇ
3796 "});
3797 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3798 cx.assert_editor_state(indoc! {"
3799 --[[
3800 ˇ
3801 "});
3802}
3803
3804#[gpui::test]
3805fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3806 init_test(cx, |_| {});
3807
3808 let editor = cx.add_window(|window, cx| {
3809 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3810 let mut editor = build_editor(buffer, window, cx);
3811 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3812 s.select_ranges([
3813 MultiBufferOffset(3)..MultiBufferOffset(4),
3814 MultiBufferOffset(11)..MultiBufferOffset(12),
3815 MultiBufferOffset(19)..MultiBufferOffset(20),
3816 ])
3817 });
3818 editor
3819 });
3820
3821 _ = editor.update(cx, |editor, window, cx| {
3822 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3823 editor.buffer.update(cx, |buffer, cx| {
3824 buffer.edit(
3825 [
3826 (MultiBufferOffset(2)..MultiBufferOffset(5), ""),
3827 (MultiBufferOffset(10)..MultiBufferOffset(13), ""),
3828 (MultiBufferOffset(18)..MultiBufferOffset(21), ""),
3829 ],
3830 None,
3831 cx,
3832 );
3833 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3834 });
3835 assert_eq!(
3836 editor.selections.ranges(&editor.display_snapshot(cx)),
3837 &[
3838 MultiBufferOffset(2)..MultiBufferOffset(2),
3839 MultiBufferOffset(7)..MultiBufferOffset(7),
3840 MultiBufferOffset(12)..MultiBufferOffset(12)
3841 ],
3842 );
3843
3844 editor.insert("Z", window, cx);
3845 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3846
3847 // The selections are moved after the inserted characters
3848 assert_eq!(
3849 editor.selections.ranges(&editor.display_snapshot(cx)),
3850 &[
3851 MultiBufferOffset(3)..MultiBufferOffset(3),
3852 MultiBufferOffset(9)..MultiBufferOffset(9),
3853 MultiBufferOffset(15)..MultiBufferOffset(15)
3854 ],
3855 );
3856 });
3857}
3858
3859#[gpui::test]
3860async fn test_tab(cx: &mut TestAppContext) {
3861 init_test(cx, |settings| {
3862 settings.defaults.tab_size = NonZeroU32::new(3)
3863 });
3864
3865 let mut cx = EditorTestContext::new(cx).await;
3866 cx.set_state(indoc! {"
3867 ˇabˇc
3868 ˇ🏀ˇ🏀ˇefg
3869 dˇ
3870 "});
3871 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3872 cx.assert_editor_state(indoc! {"
3873 ˇab ˇc
3874 ˇ🏀 ˇ🏀 ˇefg
3875 d ˇ
3876 "});
3877
3878 cx.set_state(indoc! {"
3879 a
3880 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3881 "});
3882 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3883 cx.assert_editor_state(indoc! {"
3884 a
3885 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3886 "});
3887}
3888
3889#[gpui::test]
3890async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3891 init_test(cx, |_| {});
3892
3893 let mut cx = EditorTestContext::new(cx).await;
3894 let language = Arc::new(
3895 Language::new(
3896 LanguageConfig::default(),
3897 Some(tree_sitter_rust::LANGUAGE.into()),
3898 )
3899 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3900 .unwrap(),
3901 );
3902 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3903
3904 // test when all cursors are not at suggested indent
3905 // then simply move to their suggested indent location
3906 cx.set_state(indoc! {"
3907 const a: B = (
3908 c(
3909 ˇ
3910 ˇ )
3911 );
3912 "});
3913 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3914 cx.assert_editor_state(indoc! {"
3915 const a: B = (
3916 c(
3917 ˇ
3918 ˇ)
3919 );
3920 "});
3921
3922 // test cursor already at suggested indent not moving when
3923 // other cursors are yet to reach their suggested indents
3924 cx.set_state(indoc! {"
3925 ˇ
3926 const a: B = (
3927 c(
3928 d(
3929 ˇ
3930 )
3931 ˇ
3932 ˇ )
3933 );
3934 "});
3935 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3936 cx.assert_editor_state(indoc! {"
3937 ˇ
3938 const a: B = (
3939 c(
3940 d(
3941 ˇ
3942 )
3943 ˇ
3944 ˇ)
3945 );
3946 "});
3947 // test when all cursors are at suggested indent then tab is inserted
3948 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3949 cx.assert_editor_state(indoc! {"
3950 ˇ
3951 const a: B = (
3952 c(
3953 d(
3954 ˇ
3955 )
3956 ˇ
3957 ˇ)
3958 );
3959 "});
3960
3961 // test when current indent is less than suggested indent,
3962 // we adjust line to match suggested indent and move cursor to it
3963 //
3964 // when no other cursor is at word boundary, all of them should move
3965 cx.set_state(indoc! {"
3966 const a: B = (
3967 c(
3968 d(
3969 ˇ
3970 ˇ )
3971 ˇ )
3972 );
3973 "});
3974 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3975 cx.assert_editor_state(indoc! {"
3976 const a: B = (
3977 c(
3978 d(
3979 ˇ
3980 ˇ)
3981 ˇ)
3982 );
3983 "});
3984
3985 // test when current indent is less than suggested indent,
3986 // we adjust line to match suggested indent and move cursor to it
3987 //
3988 // when some other cursor is at word boundary, it should not move
3989 cx.set_state(indoc! {"
3990 const a: B = (
3991 c(
3992 d(
3993 ˇ
3994 ˇ )
3995 ˇ)
3996 );
3997 "});
3998 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3999 cx.assert_editor_state(indoc! {"
4000 const a: B = (
4001 c(
4002 d(
4003 ˇ
4004 ˇ)
4005 ˇ)
4006 );
4007 "});
4008
4009 // test when current indent is more than suggested indent,
4010 // we just move cursor to current indent instead of suggested indent
4011 //
4012 // when no other cursor is at word boundary, all of them should move
4013 cx.set_state(indoc! {"
4014 const a: B = (
4015 c(
4016 d(
4017 ˇ
4018 ˇ )
4019 ˇ )
4020 );
4021 "});
4022 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4023 cx.assert_editor_state(indoc! {"
4024 const a: B = (
4025 c(
4026 d(
4027 ˇ
4028 ˇ)
4029 ˇ)
4030 );
4031 "});
4032 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4033 cx.assert_editor_state(indoc! {"
4034 const a: B = (
4035 c(
4036 d(
4037 ˇ
4038 ˇ)
4039 ˇ)
4040 );
4041 "});
4042
4043 // test when current indent is more than suggested indent,
4044 // we just move cursor to current indent instead of suggested indent
4045 //
4046 // when some other cursor is at word boundary, it doesn't move
4047 cx.set_state(indoc! {"
4048 const a: B = (
4049 c(
4050 d(
4051 ˇ
4052 ˇ )
4053 ˇ)
4054 );
4055 "});
4056 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4057 cx.assert_editor_state(indoc! {"
4058 const a: B = (
4059 c(
4060 d(
4061 ˇ
4062 ˇ)
4063 ˇ)
4064 );
4065 "});
4066
4067 // handle auto-indent when there are multiple cursors on the same line
4068 cx.set_state(indoc! {"
4069 const a: B = (
4070 c(
4071 ˇ ˇ
4072 ˇ )
4073 );
4074 "});
4075 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4076 cx.assert_editor_state(indoc! {"
4077 const a: B = (
4078 c(
4079 ˇ
4080 ˇ)
4081 );
4082 "});
4083}
4084
4085#[gpui::test]
4086async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
4087 init_test(cx, |settings| {
4088 settings.defaults.tab_size = NonZeroU32::new(3)
4089 });
4090
4091 let mut cx = EditorTestContext::new(cx).await;
4092 cx.set_state(indoc! {"
4093 ˇ
4094 \t ˇ
4095 \t ˇ
4096 \t ˇ
4097 \t \t\t \t \t\t \t\t \t \t ˇ
4098 "});
4099
4100 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4101 cx.assert_editor_state(indoc! {"
4102 ˇ
4103 \t ˇ
4104 \t ˇ
4105 \t ˇ
4106 \t \t\t \t \t\t \t\t \t \t ˇ
4107 "});
4108}
4109
4110#[gpui::test]
4111async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
4112 init_test(cx, |settings| {
4113 settings.defaults.tab_size = NonZeroU32::new(4)
4114 });
4115
4116 let language = Arc::new(
4117 Language::new(
4118 LanguageConfig::default(),
4119 Some(tree_sitter_rust::LANGUAGE.into()),
4120 )
4121 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
4122 .unwrap(),
4123 );
4124
4125 let mut cx = EditorTestContext::new(cx).await;
4126 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4127 cx.set_state(indoc! {"
4128 fn a() {
4129 if b {
4130 \t ˇc
4131 }
4132 }
4133 "});
4134
4135 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4136 cx.assert_editor_state(indoc! {"
4137 fn a() {
4138 if b {
4139 ˇc
4140 }
4141 }
4142 "});
4143}
4144
4145#[gpui::test]
4146async fn test_indent_outdent(cx: &mut TestAppContext) {
4147 init_test(cx, |settings| {
4148 settings.defaults.tab_size = NonZeroU32::new(4);
4149 });
4150
4151 let mut cx = EditorTestContext::new(cx).await;
4152
4153 cx.set_state(indoc! {"
4154 «oneˇ» «twoˇ»
4155 three
4156 four
4157 "});
4158 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4159 cx.assert_editor_state(indoc! {"
4160 «oneˇ» «twoˇ»
4161 three
4162 four
4163 "});
4164
4165 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4166 cx.assert_editor_state(indoc! {"
4167 «oneˇ» «twoˇ»
4168 three
4169 four
4170 "});
4171
4172 // select across line ending
4173 cx.set_state(indoc! {"
4174 one two
4175 t«hree
4176 ˇ» four
4177 "});
4178 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4179 cx.assert_editor_state(indoc! {"
4180 one two
4181 t«hree
4182 ˇ» four
4183 "});
4184
4185 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4186 cx.assert_editor_state(indoc! {"
4187 one two
4188 t«hree
4189 ˇ» four
4190 "});
4191
4192 // Ensure that indenting/outdenting works when the cursor is at column 0.
4193 cx.set_state(indoc! {"
4194 one two
4195 ˇthree
4196 four
4197 "});
4198 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4199 cx.assert_editor_state(indoc! {"
4200 one two
4201 ˇthree
4202 four
4203 "});
4204
4205 cx.set_state(indoc! {"
4206 one two
4207 ˇ three
4208 four
4209 "});
4210 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4211 cx.assert_editor_state(indoc! {"
4212 one two
4213 ˇthree
4214 four
4215 "});
4216}
4217
4218#[gpui::test]
4219async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4220 // This is a regression test for issue #33761
4221 init_test(cx, |_| {});
4222
4223 let mut cx = EditorTestContext::new(cx).await;
4224 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4225 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4226
4227 cx.set_state(
4228 r#"ˇ# ingress:
4229ˇ# api:
4230ˇ# enabled: false
4231ˇ# pathType: Prefix
4232ˇ# console:
4233ˇ# enabled: false
4234ˇ# pathType: Prefix
4235"#,
4236 );
4237
4238 // Press tab to indent all lines
4239 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4240
4241 cx.assert_editor_state(
4242 r#" ˇ# ingress:
4243 ˇ# api:
4244 ˇ# enabled: false
4245 ˇ# pathType: Prefix
4246 ˇ# console:
4247 ˇ# enabled: false
4248 ˇ# pathType: Prefix
4249"#,
4250 );
4251}
4252
4253#[gpui::test]
4254async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4255 // This is a test to make sure our fix for issue #33761 didn't break anything
4256 init_test(cx, |_| {});
4257
4258 let mut cx = EditorTestContext::new(cx).await;
4259 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4260 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4261
4262 cx.set_state(
4263 r#"ˇingress:
4264ˇ api:
4265ˇ enabled: false
4266ˇ pathType: Prefix
4267"#,
4268 );
4269
4270 // Press tab to indent all lines
4271 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4272
4273 cx.assert_editor_state(
4274 r#"ˇingress:
4275 ˇapi:
4276 ˇenabled: false
4277 ˇpathType: Prefix
4278"#,
4279 );
4280}
4281
4282#[gpui::test]
4283async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4284 init_test(cx, |settings| {
4285 settings.defaults.hard_tabs = Some(true);
4286 });
4287
4288 let mut cx = EditorTestContext::new(cx).await;
4289
4290 // select two ranges on one line
4291 cx.set_state(indoc! {"
4292 «oneˇ» «twoˇ»
4293 three
4294 four
4295 "});
4296 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4297 cx.assert_editor_state(indoc! {"
4298 \t«oneˇ» «twoˇ»
4299 three
4300 four
4301 "});
4302 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4303 cx.assert_editor_state(indoc! {"
4304 \t\t«oneˇ» «twoˇ»
4305 three
4306 four
4307 "});
4308 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4309 cx.assert_editor_state(indoc! {"
4310 \t«oneˇ» «twoˇ»
4311 three
4312 four
4313 "});
4314 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4315 cx.assert_editor_state(indoc! {"
4316 «oneˇ» «twoˇ»
4317 three
4318 four
4319 "});
4320
4321 // select across a line ending
4322 cx.set_state(indoc! {"
4323 one two
4324 t«hree
4325 ˇ»four
4326 "});
4327 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4328 cx.assert_editor_state(indoc! {"
4329 one two
4330 \tt«hree
4331 ˇ»four
4332 "});
4333 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4334 cx.assert_editor_state(indoc! {"
4335 one two
4336 \t\tt«hree
4337 ˇ»four
4338 "});
4339 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4340 cx.assert_editor_state(indoc! {"
4341 one two
4342 \tt«hree
4343 ˇ»four
4344 "});
4345 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4346 cx.assert_editor_state(indoc! {"
4347 one two
4348 t«hree
4349 ˇ»four
4350 "});
4351
4352 // Ensure that indenting/outdenting works when the cursor is at column 0.
4353 cx.set_state(indoc! {"
4354 one two
4355 ˇthree
4356 four
4357 "});
4358 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4359 cx.assert_editor_state(indoc! {"
4360 one two
4361 ˇthree
4362 four
4363 "});
4364 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4365 cx.assert_editor_state(indoc! {"
4366 one two
4367 \tˇthree
4368 four
4369 "});
4370 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4371 cx.assert_editor_state(indoc! {"
4372 one two
4373 ˇthree
4374 four
4375 "});
4376}
4377
4378#[gpui::test]
4379fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4380 init_test(cx, |settings| {
4381 settings.languages.0.extend([
4382 (
4383 "TOML".into(),
4384 LanguageSettingsContent {
4385 tab_size: NonZeroU32::new(2),
4386 ..Default::default()
4387 },
4388 ),
4389 (
4390 "Rust".into(),
4391 LanguageSettingsContent {
4392 tab_size: NonZeroU32::new(4),
4393 ..Default::default()
4394 },
4395 ),
4396 ]);
4397 });
4398
4399 let toml_language = Arc::new(Language::new(
4400 LanguageConfig {
4401 name: "TOML".into(),
4402 ..Default::default()
4403 },
4404 None,
4405 ));
4406 let rust_language = Arc::new(Language::new(
4407 LanguageConfig {
4408 name: "Rust".into(),
4409 ..Default::default()
4410 },
4411 None,
4412 ));
4413
4414 let toml_buffer =
4415 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4416 let rust_buffer =
4417 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4418 let multibuffer = cx.new(|cx| {
4419 let mut multibuffer = MultiBuffer::new(ReadWrite);
4420 multibuffer.push_excerpts(
4421 toml_buffer.clone(),
4422 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4423 cx,
4424 );
4425 multibuffer.push_excerpts(
4426 rust_buffer.clone(),
4427 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4428 cx,
4429 );
4430 multibuffer
4431 });
4432
4433 cx.add_window(|window, cx| {
4434 let mut editor = build_editor(multibuffer, window, cx);
4435
4436 assert_eq!(
4437 editor.text(cx),
4438 indoc! {"
4439 a = 1
4440 b = 2
4441
4442 const c: usize = 3;
4443 "}
4444 );
4445
4446 select_ranges(
4447 &mut editor,
4448 indoc! {"
4449 «aˇ» = 1
4450 b = 2
4451
4452 «const c:ˇ» usize = 3;
4453 "},
4454 window,
4455 cx,
4456 );
4457
4458 editor.tab(&Tab, window, cx);
4459 assert_text_with_selections(
4460 &mut editor,
4461 indoc! {"
4462 «aˇ» = 1
4463 b = 2
4464
4465 «const c:ˇ» usize = 3;
4466 "},
4467 cx,
4468 );
4469 editor.backtab(&Backtab, window, cx);
4470 assert_text_with_selections(
4471 &mut editor,
4472 indoc! {"
4473 «aˇ» = 1
4474 b = 2
4475
4476 «const c:ˇ» usize = 3;
4477 "},
4478 cx,
4479 );
4480
4481 editor
4482 });
4483}
4484
4485#[gpui::test]
4486async fn test_backspace(cx: &mut TestAppContext) {
4487 init_test(cx, |_| {});
4488
4489 let mut cx = EditorTestContext::new(cx).await;
4490
4491 // Basic backspace
4492 cx.set_state(indoc! {"
4493 onˇe two three
4494 fou«rˇ» five six
4495 seven «ˇeight nine
4496 »ten
4497 "});
4498 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4499 cx.assert_editor_state(indoc! {"
4500 oˇe two three
4501 fouˇ five six
4502 seven ˇten
4503 "});
4504
4505 // Test backspace inside and around indents
4506 cx.set_state(indoc! {"
4507 zero
4508 ˇone
4509 ˇtwo
4510 ˇ ˇ ˇ three
4511 ˇ ˇ four
4512 "});
4513 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4514 cx.assert_editor_state(indoc! {"
4515 zero
4516 ˇone
4517 ˇtwo
4518 ˇ threeˇ four
4519 "});
4520}
4521
4522#[gpui::test]
4523async fn test_delete(cx: &mut TestAppContext) {
4524 init_test(cx, |_| {});
4525
4526 let mut cx = EditorTestContext::new(cx).await;
4527 cx.set_state(indoc! {"
4528 onˇe two three
4529 fou«rˇ» five six
4530 seven «ˇeight nine
4531 »ten
4532 "});
4533 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4534 cx.assert_editor_state(indoc! {"
4535 onˇ two three
4536 fouˇ five six
4537 seven ˇten
4538 "});
4539}
4540
4541#[gpui::test]
4542fn test_delete_line(cx: &mut TestAppContext) {
4543 init_test(cx, |_| {});
4544
4545 let editor = cx.add_window(|window, cx| {
4546 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4547 build_editor(buffer, window, cx)
4548 });
4549 _ = editor.update(cx, |editor, window, cx| {
4550 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4551 s.select_display_ranges([
4552 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4553 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4554 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4555 ])
4556 });
4557 editor.delete_line(&DeleteLine, window, cx);
4558 assert_eq!(editor.display_text(cx), "ghi");
4559 assert_eq!(
4560 display_ranges(editor, cx),
4561 vec![
4562 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4563 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4564 ]
4565 );
4566 });
4567
4568 let editor = cx.add_window(|window, cx| {
4569 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4570 build_editor(buffer, window, cx)
4571 });
4572 _ = editor.update(cx, |editor, window, cx| {
4573 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4574 s.select_display_ranges([
4575 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4576 ])
4577 });
4578 editor.delete_line(&DeleteLine, window, cx);
4579 assert_eq!(editor.display_text(cx), "ghi\n");
4580 assert_eq!(
4581 display_ranges(editor, cx),
4582 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4583 );
4584 });
4585
4586 let editor = cx.add_window(|window, cx| {
4587 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4588 build_editor(buffer, window, cx)
4589 });
4590 _ = editor.update(cx, |editor, window, cx| {
4591 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4592 s.select_display_ranges([
4593 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4594 ])
4595 });
4596 editor.delete_line(&DeleteLine, window, cx);
4597 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4598 assert_eq!(
4599 display_ranges(editor, cx),
4600 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4601 );
4602 });
4603}
4604
4605#[gpui::test]
4606fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4607 init_test(cx, |_| {});
4608
4609 cx.add_window(|window, cx| {
4610 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4611 let mut editor = build_editor(buffer.clone(), window, cx);
4612 let buffer = buffer.read(cx).as_singleton().unwrap();
4613
4614 assert_eq!(
4615 editor
4616 .selections
4617 .ranges::<Point>(&editor.display_snapshot(cx)),
4618 &[Point::new(0, 0)..Point::new(0, 0)]
4619 );
4620
4621 // When on single line, replace newline at end by space
4622 editor.join_lines(&JoinLines, window, cx);
4623 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4624 assert_eq!(
4625 editor
4626 .selections
4627 .ranges::<Point>(&editor.display_snapshot(cx)),
4628 &[Point::new(0, 3)..Point::new(0, 3)]
4629 );
4630
4631 // When multiple lines are selected, remove newlines that are spanned by the selection
4632 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4633 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4634 });
4635 editor.join_lines(&JoinLines, window, cx);
4636 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4637 assert_eq!(
4638 editor
4639 .selections
4640 .ranges::<Point>(&editor.display_snapshot(cx)),
4641 &[Point::new(0, 11)..Point::new(0, 11)]
4642 );
4643
4644 // Undo should be transactional
4645 editor.undo(&Undo, window, cx);
4646 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4647 assert_eq!(
4648 editor
4649 .selections
4650 .ranges::<Point>(&editor.display_snapshot(cx)),
4651 &[Point::new(0, 5)..Point::new(2, 2)]
4652 );
4653
4654 // When joining an empty line don't insert a space
4655 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4656 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4657 });
4658 editor.join_lines(&JoinLines, window, cx);
4659 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4660 assert_eq!(
4661 editor
4662 .selections
4663 .ranges::<Point>(&editor.display_snapshot(cx)),
4664 [Point::new(2, 3)..Point::new(2, 3)]
4665 );
4666
4667 // We can remove trailing newlines
4668 editor.join_lines(&JoinLines, window, cx);
4669 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4670 assert_eq!(
4671 editor
4672 .selections
4673 .ranges::<Point>(&editor.display_snapshot(cx)),
4674 [Point::new(2, 3)..Point::new(2, 3)]
4675 );
4676
4677 // We don't blow up on the last line
4678 editor.join_lines(&JoinLines, window, cx);
4679 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4680 assert_eq!(
4681 editor
4682 .selections
4683 .ranges::<Point>(&editor.display_snapshot(cx)),
4684 [Point::new(2, 3)..Point::new(2, 3)]
4685 );
4686
4687 // reset to test indentation
4688 editor.buffer.update(cx, |buffer, cx| {
4689 buffer.edit(
4690 [
4691 (Point::new(1, 0)..Point::new(1, 2), " "),
4692 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4693 ],
4694 None,
4695 cx,
4696 )
4697 });
4698
4699 // We remove any leading spaces
4700 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4701 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4702 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4703 });
4704 editor.join_lines(&JoinLines, window, cx);
4705 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4706
4707 // We don't insert a space for a line containing only spaces
4708 editor.join_lines(&JoinLines, window, cx);
4709 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4710
4711 // We ignore any leading tabs
4712 editor.join_lines(&JoinLines, window, cx);
4713 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4714
4715 editor
4716 });
4717}
4718
4719#[gpui::test]
4720fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4721 init_test(cx, |_| {});
4722
4723 cx.add_window(|window, cx| {
4724 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4725 let mut editor = build_editor(buffer.clone(), window, cx);
4726 let buffer = buffer.read(cx).as_singleton().unwrap();
4727
4728 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4729 s.select_ranges([
4730 Point::new(0, 2)..Point::new(1, 1),
4731 Point::new(1, 2)..Point::new(1, 2),
4732 Point::new(3, 1)..Point::new(3, 2),
4733 ])
4734 });
4735
4736 editor.join_lines(&JoinLines, window, cx);
4737 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4738
4739 assert_eq!(
4740 editor
4741 .selections
4742 .ranges::<Point>(&editor.display_snapshot(cx)),
4743 [
4744 Point::new(0, 7)..Point::new(0, 7),
4745 Point::new(1, 3)..Point::new(1, 3)
4746 ]
4747 );
4748 editor
4749 });
4750}
4751
4752#[gpui::test]
4753async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4754 init_test(cx, |_| {});
4755
4756 let mut cx = EditorTestContext::new(cx).await;
4757
4758 let diff_base = r#"
4759 Line 0
4760 Line 1
4761 Line 2
4762 Line 3
4763 "#
4764 .unindent();
4765
4766 cx.set_state(
4767 &r#"
4768 ˇLine 0
4769 Line 1
4770 Line 2
4771 Line 3
4772 "#
4773 .unindent(),
4774 );
4775
4776 cx.set_head_text(&diff_base);
4777 executor.run_until_parked();
4778
4779 // Join lines
4780 cx.update_editor(|editor, window, cx| {
4781 editor.join_lines(&JoinLines, window, cx);
4782 });
4783 executor.run_until_parked();
4784
4785 cx.assert_editor_state(
4786 &r#"
4787 Line 0ˇ Line 1
4788 Line 2
4789 Line 3
4790 "#
4791 .unindent(),
4792 );
4793 // Join again
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ˇ Line 2
4802 Line 3
4803 "#
4804 .unindent(),
4805 );
4806}
4807
4808#[gpui::test]
4809async fn test_custom_newlines_cause_no_false_positive_diffs(
4810 executor: BackgroundExecutor,
4811 cx: &mut TestAppContext,
4812) {
4813 init_test(cx, |_| {});
4814 let mut cx = EditorTestContext::new(cx).await;
4815 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4816 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4817 executor.run_until_parked();
4818
4819 cx.update_editor(|editor, window, cx| {
4820 let snapshot = editor.snapshot(window, cx);
4821 assert_eq!(
4822 snapshot
4823 .buffer_snapshot()
4824 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
4825 .collect::<Vec<_>>(),
4826 Vec::new(),
4827 "Should not have any diffs for files with custom newlines"
4828 );
4829 });
4830}
4831
4832#[gpui::test]
4833async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4834 init_test(cx, |_| {});
4835
4836 let mut cx = EditorTestContext::new(cx).await;
4837
4838 // Test sort_lines_case_insensitive()
4839 cx.set_state(indoc! {"
4840 «z
4841 y
4842 x
4843 Z
4844 Y
4845 Xˇ»
4846 "});
4847 cx.update_editor(|e, window, cx| {
4848 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4849 });
4850 cx.assert_editor_state(indoc! {"
4851 «x
4852 X
4853 y
4854 Y
4855 z
4856 Zˇ»
4857 "});
4858
4859 // Test sort_lines_by_length()
4860 //
4861 // Demonstrates:
4862 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4863 // - sort is stable
4864 cx.set_state(indoc! {"
4865 «123
4866 æ
4867 12
4868 ∞
4869 1
4870 æˇ»
4871 "});
4872 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4873 cx.assert_editor_state(indoc! {"
4874 «æ
4875 ∞
4876 1
4877 æ
4878 12
4879 123ˇ»
4880 "});
4881
4882 // Test reverse_lines()
4883 cx.set_state(indoc! {"
4884 «5
4885 4
4886 3
4887 2
4888 1ˇ»
4889 "});
4890 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4891 cx.assert_editor_state(indoc! {"
4892 «1
4893 2
4894 3
4895 4
4896 5ˇ»
4897 "});
4898
4899 // Skip testing shuffle_line()
4900
4901 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4902 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4903
4904 // Don't manipulate when cursor is on single line, but expand the selection
4905 cx.set_state(indoc! {"
4906 ddˇdd
4907 ccc
4908 bb
4909 a
4910 "});
4911 cx.update_editor(|e, window, cx| {
4912 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4913 });
4914 cx.assert_editor_state(indoc! {"
4915 «ddddˇ»
4916 ccc
4917 bb
4918 a
4919 "});
4920
4921 // Basic manipulate case
4922 // Start selection moves to column 0
4923 // End of selection shrinks to fit shorter line
4924 cx.set_state(indoc! {"
4925 dd«d
4926 ccc
4927 bb
4928 aaaaaˇ»
4929 "});
4930 cx.update_editor(|e, window, cx| {
4931 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4932 });
4933 cx.assert_editor_state(indoc! {"
4934 «aaaaa
4935 bb
4936 ccc
4937 dddˇ»
4938 "});
4939
4940 // Manipulate case with newlines
4941 cx.set_state(indoc! {"
4942 dd«d
4943 ccc
4944
4945 bb
4946 aaaaa
4947
4948 ˇ»
4949 "});
4950 cx.update_editor(|e, window, cx| {
4951 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4952 });
4953 cx.assert_editor_state(indoc! {"
4954 «
4955
4956 aaaaa
4957 bb
4958 ccc
4959 dddˇ»
4960
4961 "});
4962
4963 // Adding new line
4964 cx.set_state(indoc! {"
4965 aa«a
4966 bbˇ»b
4967 "});
4968 cx.update_editor(|e, window, cx| {
4969 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4970 });
4971 cx.assert_editor_state(indoc! {"
4972 «aaa
4973 bbb
4974 added_lineˇ»
4975 "});
4976
4977 // Removing line
4978 cx.set_state(indoc! {"
4979 aa«a
4980 bbbˇ»
4981 "});
4982 cx.update_editor(|e, window, cx| {
4983 e.manipulate_immutable_lines(window, cx, |lines| {
4984 lines.pop();
4985 })
4986 });
4987 cx.assert_editor_state(indoc! {"
4988 «aaaˇ»
4989 "});
4990
4991 // Removing all lines
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.drain(..);
4999 })
5000 });
5001 cx.assert_editor_state(indoc! {"
5002 ˇ
5003 "});
5004}
5005
5006#[gpui::test]
5007async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
5008 init_test(cx, |_| {});
5009
5010 let mut cx = EditorTestContext::new(cx).await;
5011
5012 // Consider continuous selection as single selection
5013 cx.set_state(indoc! {"
5014 Aaa«aa
5015 cˇ»c«c
5016 bb
5017 aaaˇ»aa
5018 "});
5019 cx.update_editor(|e, window, cx| {
5020 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5021 });
5022 cx.assert_editor_state(indoc! {"
5023 «Aaaaa
5024 ccc
5025 bb
5026 aaaaaˇ»
5027 "});
5028
5029 cx.set_state(indoc! {"
5030 Aaa«aa
5031 cˇ»c«c
5032 bb
5033 aaaˇ»aa
5034 "});
5035 cx.update_editor(|e, window, cx| {
5036 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5037 });
5038 cx.assert_editor_state(indoc! {"
5039 «Aaaaa
5040 ccc
5041 bbˇ»
5042 "});
5043
5044 // Consider non continuous selection as distinct dedup operations
5045 cx.set_state(indoc! {"
5046 «aaaaa
5047 bb
5048 aaaaa
5049 aaaaaˇ»
5050
5051 aaa«aaˇ»
5052 "});
5053 cx.update_editor(|e, window, cx| {
5054 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5055 });
5056 cx.assert_editor_state(indoc! {"
5057 «aaaaa
5058 bbˇ»
5059
5060 «aaaaaˇ»
5061 "});
5062}
5063
5064#[gpui::test]
5065async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
5066 init_test(cx, |_| {});
5067
5068 let mut cx = EditorTestContext::new(cx).await;
5069
5070 cx.set_state(indoc! {"
5071 «Aaa
5072 aAa
5073 Aaaˇ»
5074 "});
5075 cx.update_editor(|e, window, cx| {
5076 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5077 });
5078 cx.assert_editor_state(indoc! {"
5079 «Aaa
5080 aAaˇ»
5081 "});
5082
5083 cx.set_state(indoc! {"
5084 «Aaa
5085 aAa
5086 aaAˇ»
5087 "});
5088 cx.update_editor(|e, window, cx| {
5089 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5090 });
5091 cx.assert_editor_state(indoc! {"
5092 «Aaaˇ»
5093 "});
5094}
5095
5096#[gpui::test]
5097async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
5098 init_test(cx, |_| {});
5099
5100 let mut cx = EditorTestContext::new(cx).await;
5101
5102 let js_language = Arc::new(Language::new(
5103 LanguageConfig {
5104 name: "JavaScript".into(),
5105 wrap_characters: Some(language::WrapCharactersConfig {
5106 start_prefix: "<".into(),
5107 start_suffix: ">".into(),
5108 end_prefix: "</".into(),
5109 end_suffix: ">".into(),
5110 }),
5111 ..LanguageConfig::default()
5112 },
5113 None,
5114 ));
5115
5116 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5117
5118 cx.set_state(indoc! {"
5119 «testˇ»
5120 "});
5121 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5122 cx.assert_editor_state(indoc! {"
5123 <«ˇ»>test</«ˇ»>
5124 "});
5125
5126 cx.set_state(indoc! {"
5127 «test
5128 testˇ»
5129 "});
5130 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5131 cx.assert_editor_state(indoc! {"
5132 <«ˇ»>test
5133 test</«ˇ»>
5134 "});
5135
5136 cx.set_state(indoc! {"
5137 teˇst
5138 "});
5139 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5140 cx.assert_editor_state(indoc! {"
5141 te<«ˇ»></«ˇ»>st
5142 "});
5143}
5144
5145#[gpui::test]
5146async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5147 init_test(cx, |_| {});
5148
5149 let mut cx = EditorTestContext::new(cx).await;
5150
5151 let js_language = Arc::new(Language::new(
5152 LanguageConfig {
5153 name: "JavaScript".into(),
5154 wrap_characters: Some(language::WrapCharactersConfig {
5155 start_prefix: "<".into(),
5156 start_suffix: ">".into(),
5157 end_prefix: "</".into(),
5158 end_suffix: ">".into(),
5159 }),
5160 ..LanguageConfig::default()
5161 },
5162 None,
5163 ));
5164
5165 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5166
5167 cx.set_state(indoc! {"
5168 «testˇ»
5169 «testˇ» «testˇ»
5170 «testˇ»
5171 "});
5172 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5173 cx.assert_editor_state(indoc! {"
5174 <«ˇ»>test</«ˇ»>
5175 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5176 <«ˇ»>test</«ˇ»>
5177 "});
5178
5179 cx.set_state(indoc! {"
5180 «test
5181 testˇ»
5182 «test
5183 testˇ»
5184 "});
5185 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5186 cx.assert_editor_state(indoc! {"
5187 <«ˇ»>test
5188 test</«ˇ»>
5189 <«ˇ»>test
5190 test</«ˇ»>
5191 "});
5192}
5193
5194#[gpui::test]
5195async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5196 init_test(cx, |_| {});
5197
5198 let mut cx = EditorTestContext::new(cx).await;
5199
5200 let plaintext_language = Arc::new(Language::new(
5201 LanguageConfig {
5202 name: "Plain Text".into(),
5203 ..LanguageConfig::default()
5204 },
5205 None,
5206 ));
5207
5208 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5209
5210 cx.set_state(indoc! {"
5211 «testˇ»
5212 "});
5213 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5214 cx.assert_editor_state(indoc! {"
5215 «testˇ»
5216 "});
5217}
5218
5219#[gpui::test]
5220async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5221 init_test(cx, |_| {});
5222
5223 let mut cx = EditorTestContext::new(cx).await;
5224
5225 // Manipulate with multiple selections on a single line
5226 cx.set_state(indoc! {"
5227 dd«dd
5228 cˇ»c«c
5229 bb
5230 aaaˇ»aa
5231 "});
5232 cx.update_editor(|e, window, cx| {
5233 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5234 });
5235 cx.assert_editor_state(indoc! {"
5236 «aaaaa
5237 bb
5238 ccc
5239 ddddˇ»
5240 "});
5241
5242 // Manipulate with multiple disjoin selections
5243 cx.set_state(indoc! {"
5244 5«
5245 4
5246 3
5247 2
5248 1ˇ»
5249
5250 dd«dd
5251 ccc
5252 bb
5253 aaaˇ»aa
5254 "});
5255 cx.update_editor(|e, window, cx| {
5256 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5257 });
5258 cx.assert_editor_state(indoc! {"
5259 «1
5260 2
5261 3
5262 4
5263 5ˇ»
5264
5265 «aaaaa
5266 bb
5267 ccc
5268 ddddˇ»
5269 "});
5270
5271 // Adding lines on each selection
5272 cx.set_state(indoc! {"
5273 2«
5274 1ˇ»
5275
5276 bb«bb
5277 aaaˇ»aa
5278 "});
5279 cx.update_editor(|e, window, cx| {
5280 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5281 });
5282 cx.assert_editor_state(indoc! {"
5283 «2
5284 1
5285 added lineˇ»
5286
5287 «bbbb
5288 aaaaa
5289 added lineˇ»
5290 "});
5291
5292 // Removing lines on each selection
5293 cx.set_state(indoc! {"
5294 2«
5295 1ˇ»
5296
5297 bb«bb
5298 aaaˇ»aa
5299 "});
5300 cx.update_editor(|e, window, cx| {
5301 e.manipulate_immutable_lines(window, cx, |lines| {
5302 lines.pop();
5303 })
5304 });
5305 cx.assert_editor_state(indoc! {"
5306 «2ˇ»
5307
5308 «bbbbˇ»
5309 "});
5310}
5311
5312#[gpui::test]
5313async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5314 init_test(cx, |settings| {
5315 settings.defaults.tab_size = NonZeroU32::new(3)
5316 });
5317
5318 let mut cx = EditorTestContext::new(cx).await;
5319
5320 // MULTI SELECTION
5321 // Ln.1 "«" tests empty lines
5322 // Ln.9 tests just leading whitespace
5323 cx.set_state(indoc! {"
5324 «
5325 abc // No indentationˇ»
5326 «\tabc // 1 tabˇ»
5327 \t\tabc « ˇ» // 2 tabs
5328 \t ab«c // Tab followed by space
5329 \tabc // Space followed by tab (3 spaces should be the result)
5330 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5331 abˇ»ˇc ˇ ˇ // Already space indented«
5332 \t
5333 \tabc\tdef // Only the leading tab is manipulatedˇ»
5334 "});
5335 cx.update_editor(|e, window, cx| {
5336 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5337 });
5338 cx.assert_editor_state(
5339 indoc! {"
5340 «
5341 abc // No indentation
5342 abc // 1 tab
5343 abc // 2 tabs
5344 abc // Tab followed by space
5345 abc // Space followed by tab (3 spaces should be the result)
5346 abc // Mixed indentation (tab conversion depends on the column)
5347 abc // Already space indented
5348 ·
5349 abc\tdef // Only the leading tab is manipulatedˇ»
5350 "}
5351 .replace("·", "")
5352 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5353 );
5354
5355 // Test on just a few lines, the others should remain unchanged
5356 // Only lines (3, 5, 10, 11) should change
5357 cx.set_state(
5358 indoc! {"
5359 ·
5360 abc // No indentation
5361 \tabcˇ // 1 tab
5362 \t\tabc // 2 tabs
5363 \t abcˇ // Tab followed by space
5364 \tabc // Space followed by tab (3 spaces should be the result)
5365 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5366 abc // Already space indented
5367 «\t
5368 \tabc\tdef // Only the leading tab is manipulatedˇ»
5369 "}
5370 .replace("·", "")
5371 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5372 );
5373 cx.update_editor(|e, window, cx| {
5374 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5375 });
5376 cx.assert_editor_state(
5377 indoc! {"
5378 ·
5379 abc // No indentation
5380 « abc // 1 tabˇ»
5381 \t\tabc // 2 tabs
5382 « abc // Tab followed by spaceˇ»
5383 \tabc // Space followed by tab (3 spaces should be the result)
5384 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5385 abc // Already space indented
5386 « ·
5387 abc\tdef // Only the leading tab is manipulatedˇ»
5388 "}
5389 .replace("·", "")
5390 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5391 );
5392
5393 // SINGLE SELECTION
5394 // Ln.1 "«" tests empty lines
5395 // Ln.9 tests just leading whitespace
5396 cx.set_state(indoc! {"
5397 «
5398 abc // No indentation
5399 \tabc // 1 tab
5400 \t\tabc // 2 tabs
5401 \t abc // Tab followed by space
5402 \tabc // Space followed by tab (3 spaces should be the result)
5403 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5404 abc // Already space indented
5405 \t
5406 \tabc\tdef // Only the leading tab is manipulatedˇ»
5407 "});
5408 cx.update_editor(|e, window, cx| {
5409 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5410 });
5411 cx.assert_editor_state(
5412 indoc! {"
5413 «
5414 abc // No indentation
5415 abc // 1 tab
5416 abc // 2 tabs
5417 abc // Tab followed by space
5418 abc // Space followed by tab (3 spaces should be the result)
5419 abc // Mixed indentation (tab conversion depends on the column)
5420 abc // Already space indented
5421 ·
5422 abc\tdef // Only the leading tab is manipulatedˇ»
5423 "}
5424 .replace("·", "")
5425 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5426 );
5427}
5428
5429#[gpui::test]
5430async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5431 init_test(cx, |settings| {
5432 settings.defaults.tab_size = NonZeroU32::new(3)
5433 });
5434
5435 let mut cx = EditorTestContext::new(cx).await;
5436
5437 // MULTI SELECTION
5438 // Ln.1 "«" tests empty lines
5439 // Ln.11 tests just leading whitespace
5440 cx.set_state(indoc! {"
5441 «
5442 abˇ»ˇc // No indentation
5443 abc ˇ ˇ // 1 space (< 3 so dont convert)
5444 abc « // 2 spaces (< 3 so dont convert)
5445 abc // 3 spaces (convert)
5446 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5447 «\tˇ»\t«\tˇ»abc // Already tab indented
5448 «\t abc // Tab followed by space
5449 \tabc // Space followed by tab (should be consumed due to tab)
5450 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5451 \tˇ» «\t
5452 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5453 "});
5454 cx.update_editor(|e, window, cx| {
5455 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5456 });
5457 cx.assert_editor_state(indoc! {"
5458 «
5459 abc // No indentation
5460 abc // 1 space (< 3 so dont convert)
5461 abc // 2 spaces (< 3 so dont convert)
5462 \tabc // 3 spaces (convert)
5463 \t abc // 5 spaces (1 tab + 2 spaces)
5464 \t\t\tabc // Already tab indented
5465 \t abc // Tab followed by space
5466 \tabc // Space followed by tab (should be consumed due to tab)
5467 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5468 \t\t\t
5469 \tabc \t // Only the leading spaces should be convertedˇ»
5470 "});
5471
5472 // Test on just a few lines, the other should remain unchanged
5473 // Only lines (4, 8, 11, 12) should change
5474 cx.set_state(
5475 indoc! {"
5476 ·
5477 abc // No indentation
5478 abc // 1 space (< 3 so dont convert)
5479 abc // 2 spaces (< 3 so dont convert)
5480 « abc // 3 spaces (convert)ˇ»
5481 abc // 5 spaces (1 tab + 2 spaces)
5482 \t\t\tabc // Already tab indented
5483 \t abc // Tab followed by space
5484 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5485 \t\t \tabc // Mixed indentation
5486 \t \t \t \tabc // Mixed indentation
5487 \t \tˇ
5488 « abc \t // Only the leading spaces should be convertedˇ»
5489 "}
5490 .replace("·", "")
5491 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5492 );
5493 cx.update_editor(|e, window, cx| {
5494 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5495 });
5496 cx.assert_editor_state(
5497 indoc! {"
5498 ·
5499 abc // No indentation
5500 abc // 1 space (< 3 so dont convert)
5501 abc // 2 spaces (< 3 so dont convert)
5502 «\tabc // 3 spaces (convert)ˇ»
5503 abc // 5 spaces (1 tab + 2 spaces)
5504 \t\t\tabc // Already tab indented
5505 \t abc // Tab followed by space
5506 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5507 \t\t \tabc // Mixed indentation
5508 \t \t \t \tabc // Mixed indentation
5509 «\t\t\t
5510 \tabc \t // Only the leading spaces should be convertedˇ»
5511 "}
5512 .replace("·", "")
5513 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5514 );
5515
5516 // SINGLE SELECTION
5517 // Ln.1 "«" tests empty lines
5518 // Ln.11 tests just leading whitespace
5519 cx.set_state(indoc! {"
5520 «
5521 abc // No indentation
5522 abc // 1 space (< 3 so dont convert)
5523 abc // 2 spaces (< 3 so dont convert)
5524 abc // 3 spaces (convert)
5525 abc // 5 spaces (1 tab + 2 spaces)
5526 \t\t\tabc // Already tab indented
5527 \t abc // Tab followed by space
5528 \tabc // Space followed by tab (should be consumed due to tab)
5529 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5530 \t \t
5531 abc \t // Only the leading spaces should be convertedˇ»
5532 "});
5533 cx.update_editor(|e, window, cx| {
5534 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5535 });
5536 cx.assert_editor_state(indoc! {"
5537 «
5538 abc // No indentation
5539 abc // 1 space (< 3 so dont convert)
5540 abc // 2 spaces (< 3 so dont convert)
5541 \tabc // 3 spaces (convert)
5542 \t abc // 5 spaces (1 tab + 2 spaces)
5543 \t\t\tabc // Already tab indented
5544 \t abc // Tab followed by space
5545 \tabc // Space followed by tab (should be consumed due to tab)
5546 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5547 \t\t\t
5548 \tabc \t // Only the leading spaces should be convertedˇ»
5549 "});
5550}
5551
5552#[gpui::test]
5553async fn test_toggle_case(cx: &mut TestAppContext) {
5554 init_test(cx, |_| {});
5555
5556 let mut cx = EditorTestContext::new(cx).await;
5557
5558 // If all lower case -> upper case
5559 cx.set_state(indoc! {"
5560 «hello worldˇ»
5561 "});
5562 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5563 cx.assert_editor_state(indoc! {"
5564 «HELLO WORLDˇ»
5565 "});
5566
5567 // If all upper case -> lower case
5568 cx.set_state(indoc! {"
5569 «HELLO WORLDˇ»
5570 "});
5571 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5572 cx.assert_editor_state(indoc! {"
5573 «hello worldˇ»
5574 "});
5575
5576 // If any upper case characters are identified -> lower case
5577 // This matches JetBrains IDEs
5578 cx.set_state(indoc! {"
5579 «hEllo worldˇ»
5580 "});
5581 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5582 cx.assert_editor_state(indoc! {"
5583 «hello worldˇ»
5584 "});
5585}
5586
5587#[gpui::test]
5588async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5589 init_test(cx, |_| {});
5590
5591 let mut cx = EditorTestContext::new(cx).await;
5592
5593 cx.set_state(indoc! {"
5594 «implement-windows-supportˇ»
5595 "});
5596 cx.update_editor(|e, window, cx| {
5597 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5598 });
5599 cx.assert_editor_state(indoc! {"
5600 «Implement windows supportˇ»
5601 "});
5602}
5603
5604#[gpui::test]
5605async fn test_manipulate_text(cx: &mut TestAppContext) {
5606 init_test(cx, |_| {});
5607
5608 let mut cx = EditorTestContext::new(cx).await;
5609
5610 // Test convert_to_upper_case()
5611 cx.set_state(indoc! {"
5612 «hello worldˇ»
5613 "});
5614 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5615 cx.assert_editor_state(indoc! {"
5616 «HELLO WORLDˇ»
5617 "});
5618
5619 // Test convert_to_lower_case()
5620 cx.set_state(indoc! {"
5621 «HELLO WORLDˇ»
5622 "});
5623 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5624 cx.assert_editor_state(indoc! {"
5625 «hello worldˇ»
5626 "});
5627
5628 // Test multiple line, single selection case
5629 cx.set_state(indoc! {"
5630 «The quick brown
5631 fox jumps over
5632 the lazy dogˇ»
5633 "});
5634 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5635 cx.assert_editor_state(indoc! {"
5636 «The Quick Brown
5637 Fox Jumps Over
5638 The Lazy Dogˇ»
5639 "});
5640
5641 // Test multiple line, single selection case
5642 cx.set_state(indoc! {"
5643 «The quick brown
5644 fox jumps over
5645 the lazy dogˇ»
5646 "});
5647 cx.update_editor(|e, window, cx| {
5648 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5649 });
5650 cx.assert_editor_state(indoc! {"
5651 «TheQuickBrown
5652 FoxJumpsOver
5653 TheLazyDogˇ»
5654 "});
5655
5656 // From here on out, test more complex cases of manipulate_text()
5657
5658 // Test no selection case - should affect words cursors are in
5659 // Cursor at beginning, middle, and end of word
5660 cx.set_state(indoc! {"
5661 ˇhello big beauˇtiful worldˇ
5662 "});
5663 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5664 cx.assert_editor_state(indoc! {"
5665 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5666 "});
5667
5668 // Test multiple selections on a single line and across multiple lines
5669 cx.set_state(indoc! {"
5670 «Theˇ» quick «brown
5671 foxˇ» jumps «overˇ»
5672 the «lazyˇ» dog
5673 "});
5674 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5675 cx.assert_editor_state(indoc! {"
5676 «THEˇ» quick «BROWN
5677 FOXˇ» jumps «OVERˇ»
5678 the «LAZYˇ» dog
5679 "});
5680
5681 // Test case where text length grows
5682 cx.set_state(indoc! {"
5683 «tschüߡ»
5684 "});
5685 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5686 cx.assert_editor_state(indoc! {"
5687 «TSCHÜSSˇ»
5688 "});
5689
5690 // Test to make sure we don't crash when text shrinks
5691 cx.set_state(indoc! {"
5692 aaa_bbbˇ
5693 "});
5694 cx.update_editor(|e, window, cx| {
5695 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5696 });
5697 cx.assert_editor_state(indoc! {"
5698 «aaaBbbˇ»
5699 "});
5700
5701 // Test to make sure we all aware of the fact that each word can grow and shrink
5702 // Final selections should be aware of this fact
5703 cx.set_state(indoc! {"
5704 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5705 "});
5706 cx.update_editor(|e, window, cx| {
5707 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5708 });
5709 cx.assert_editor_state(indoc! {"
5710 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5711 "});
5712
5713 cx.set_state(indoc! {"
5714 «hElLo, WoRld!ˇ»
5715 "});
5716 cx.update_editor(|e, window, cx| {
5717 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5718 });
5719 cx.assert_editor_state(indoc! {"
5720 «HeLlO, wOrLD!ˇ»
5721 "});
5722
5723 // Test selections with `line_mode() = true`.
5724 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5725 cx.set_state(indoc! {"
5726 «The quick brown
5727 fox jumps over
5728 tˇ»he lazy dog
5729 "});
5730 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5731 cx.assert_editor_state(indoc! {"
5732 «THE QUICK BROWN
5733 FOX JUMPS OVER
5734 THE LAZY DOGˇ»
5735 "});
5736}
5737
5738#[gpui::test]
5739fn test_duplicate_line(cx: &mut TestAppContext) {
5740 init_test(cx, |_| {});
5741
5742 let editor = cx.add_window(|window, cx| {
5743 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5744 build_editor(buffer, window, cx)
5745 });
5746 _ = editor.update(cx, |editor, window, cx| {
5747 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5748 s.select_display_ranges([
5749 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5750 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5751 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5752 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5753 ])
5754 });
5755 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5756 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5757 assert_eq!(
5758 display_ranges(editor, cx),
5759 vec![
5760 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5761 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5762 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5763 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5764 ]
5765 );
5766 });
5767
5768 let editor = cx.add_window(|window, cx| {
5769 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5770 build_editor(buffer, window, cx)
5771 });
5772 _ = editor.update(cx, |editor, window, cx| {
5773 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5774 s.select_display_ranges([
5775 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5776 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5777 ])
5778 });
5779 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5780 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5781 assert_eq!(
5782 display_ranges(editor, cx),
5783 vec![
5784 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5785 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5786 ]
5787 );
5788 });
5789
5790 // With `duplicate_line_up` the selections move to the duplicated lines,
5791 // which are inserted above the original lines
5792 let editor = cx.add_window(|window, cx| {
5793 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5794 build_editor(buffer, window, cx)
5795 });
5796 _ = editor.update(cx, |editor, window, cx| {
5797 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5798 s.select_display_ranges([
5799 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5800 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5801 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5802 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5803 ])
5804 });
5805 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5806 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5807 assert_eq!(
5808 display_ranges(editor, cx),
5809 vec![
5810 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5811 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5812 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5813 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5814 ]
5815 );
5816 });
5817
5818 let editor = cx.add_window(|window, cx| {
5819 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5820 build_editor(buffer, window, cx)
5821 });
5822 _ = editor.update(cx, |editor, window, cx| {
5823 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5824 s.select_display_ranges([
5825 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5826 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5827 ])
5828 });
5829 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5830 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5831 assert_eq!(
5832 display_ranges(editor, cx),
5833 vec![
5834 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5835 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5836 ]
5837 );
5838 });
5839
5840 let editor = cx.add_window(|window, cx| {
5841 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5842 build_editor(buffer, window, cx)
5843 });
5844 _ = editor.update(cx, |editor, window, cx| {
5845 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5846 s.select_display_ranges([
5847 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5848 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5849 ])
5850 });
5851 editor.duplicate_selection(&DuplicateSelection, window, cx);
5852 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5853 assert_eq!(
5854 display_ranges(editor, cx),
5855 vec![
5856 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5857 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5858 ]
5859 );
5860 });
5861}
5862
5863#[gpui::test]
5864async fn test_rotate_selections(cx: &mut TestAppContext) {
5865 init_test(cx, |_| {});
5866
5867 let mut cx = EditorTestContext::new(cx).await;
5868
5869 // Rotate text selections (horizontal)
5870 cx.set_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
5871 cx.update_editor(|e, window, cx| {
5872 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5873 });
5874 cx.assert_editor_state("x=«3ˇ», y=«1ˇ», z=«2ˇ»");
5875 cx.update_editor(|e, window, cx| {
5876 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5877 });
5878 cx.assert_editor_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
5879
5880 // Rotate text selections (vertical)
5881 cx.set_state(indoc! {"
5882 x=«1ˇ»
5883 y=«2ˇ»
5884 z=«3ˇ»
5885 "});
5886 cx.update_editor(|e, window, cx| {
5887 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5888 });
5889 cx.assert_editor_state(indoc! {"
5890 x=«3ˇ»
5891 y=«1ˇ»
5892 z=«2ˇ»
5893 "});
5894 cx.update_editor(|e, window, cx| {
5895 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5896 });
5897 cx.assert_editor_state(indoc! {"
5898 x=«1ˇ»
5899 y=«2ˇ»
5900 z=«3ˇ»
5901 "});
5902
5903 // Rotate text selections (vertical, different lengths)
5904 cx.set_state(indoc! {"
5905 x=\"«ˇ»\"
5906 y=\"«aˇ»\"
5907 z=\"«aaˇ»\"
5908 "});
5909 cx.update_editor(|e, window, cx| {
5910 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5911 });
5912 cx.assert_editor_state(indoc! {"
5913 x=\"«aaˇ»\"
5914 y=\"«ˇ»\"
5915 z=\"«aˇ»\"
5916 "});
5917 cx.update_editor(|e, window, cx| {
5918 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5919 });
5920 cx.assert_editor_state(indoc! {"
5921 x=\"«ˇ»\"
5922 y=\"«aˇ»\"
5923 z=\"«aaˇ»\"
5924 "});
5925
5926 // Rotate whole lines (cursor positions preserved)
5927 cx.set_state(indoc! {"
5928 ˇline123
5929 liˇne23
5930 line3ˇ
5931 "});
5932 cx.update_editor(|e, window, cx| {
5933 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5934 });
5935 cx.assert_editor_state(indoc! {"
5936 line3ˇ
5937 ˇline123
5938 liˇne23
5939 "});
5940 cx.update_editor(|e, window, cx| {
5941 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5942 });
5943 cx.assert_editor_state(indoc! {"
5944 ˇline123
5945 liˇne23
5946 line3ˇ
5947 "});
5948
5949 // Rotate whole lines, multiple cursors per line (positions preserved)
5950 cx.set_state(indoc! {"
5951 ˇliˇne123
5952 ˇline23
5953 ˇline3
5954 "});
5955 cx.update_editor(|e, window, cx| {
5956 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5957 });
5958 cx.assert_editor_state(indoc! {"
5959 ˇline3
5960 ˇliˇne123
5961 ˇline23
5962 "});
5963 cx.update_editor(|e, window, cx| {
5964 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5965 });
5966 cx.assert_editor_state(indoc! {"
5967 ˇliˇne123
5968 ˇline23
5969 ˇline3
5970 "});
5971}
5972
5973#[gpui::test]
5974fn test_move_line_up_down(cx: &mut TestAppContext) {
5975 init_test(cx, |_| {});
5976
5977 let editor = cx.add_window(|window, cx| {
5978 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5979 build_editor(buffer, window, cx)
5980 });
5981 _ = editor.update(cx, |editor, window, cx| {
5982 editor.fold_creases(
5983 vec![
5984 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5985 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5986 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5987 ],
5988 true,
5989 window,
5990 cx,
5991 );
5992 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5993 s.select_display_ranges([
5994 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5995 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5996 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5997 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5998 ])
5999 });
6000 assert_eq!(
6001 editor.display_text(cx),
6002 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
6003 );
6004
6005 editor.move_line_up(&MoveLineUp, window, cx);
6006 assert_eq!(
6007 editor.display_text(cx),
6008 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
6009 );
6010 assert_eq!(
6011 display_ranges(editor, cx),
6012 vec![
6013 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6014 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6015 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
6016 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
6017 ]
6018 );
6019 });
6020
6021 _ = editor.update(cx, |editor, window, cx| {
6022 editor.move_line_down(&MoveLineDown, window, cx);
6023 assert_eq!(
6024 editor.display_text(cx),
6025 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
6026 );
6027 assert_eq!(
6028 display_ranges(editor, cx),
6029 vec![
6030 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6031 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
6032 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
6033 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
6034 ]
6035 );
6036 });
6037
6038 _ = editor.update(cx, |editor, window, cx| {
6039 editor.move_line_down(&MoveLineDown, window, cx);
6040 assert_eq!(
6041 editor.display_text(cx),
6042 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
6043 );
6044 assert_eq!(
6045 display_ranges(editor, cx),
6046 vec![
6047 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6048 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
6049 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
6050 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
6051 ]
6052 );
6053 });
6054
6055 _ = editor.update(cx, |editor, window, cx| {
6056 editor.move_line_up(&MoveLineUp, window, cx);
6057 assert_eq!(
6058 editor.display_text(cx),
6059 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
6060 );
6061 assert_eq!(
6062 display_ranges(editor, cx),
6063 vec![
6064 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6065 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6066 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
6067 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
6068 ]
6069 );
6070 });
6071}
6072
6073#[gpui::test]
6074fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
6075 init_test(cx, |_| {});
6076 let editor = cx.add_window(|window, cx| {
6077 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
6078 build_editor(buffer, window, cx)
6079 });
6080 _ = editor.update(cx, |editor, window, cx| {
6081 editor.fold_creases(
6082 vec![Crease::simple(
6083 Point::new(6, 4)..Point::new(7, 4),
6084 FoldPlaceholder::test(),
6085 )],
6086 true,
6087 window,
6088 cx,
6089 );
6090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6091 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
6092 });
6093 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
6094 editor.move_line_up(&MoveLineUp, window, cx);
6095 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
6096 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
6097 });
6098}
6099
6100#[gpui::test]
6101fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
6102 init_test(cx, |_| {});
6103
6104 let editor = cx.add_window(|window, cx| {
6105 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
6106 build_editor(buffer, window, cx)
6107 });
6108 _ = editor.update(cx, |editor, window, cx| {
6109 let snapshot = editor.buffer.read(cx).snapshot(cx);
6110 editor.insert_blocks(
6111 [BlockProperties {
6112 style: BlockStyle::Fixed,
6113 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
6114 height: Some(1),
6115 render: Arc::new(|_| div().into_any()),
6116 priority: 0,
6117 }],
6118 Some(Autoscroll::fit()),
6119 cx,
6120 );
6121 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6122 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
6123 });
6124 editor.move_line_down(&MoveLineDown, window, cx);
6125 });
6126}
6127
6128#[gpui::test]
6129async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
6130 init_test(cx, |_| {});
6131
6132 let mut cx = EditorTestContext::new(cx).await;
6133 cx.set_state(
6134 &"
6135 ˇzero
6136 one
6137 two
6138 three
6139 four
6140 five
6141 "
6142 .unindent(),
6143 );
6144
6145 // Create a four-line block that replaces three lines of text.
6146 cx.update_editor(|editor, window, cx| {
6147 let snapshot = editor.snapshot(window, cx);
6148 let snapshot = &snapshot.buffer_snapshot();
6149 let placement = BlockPlacement::Replace(
6150 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
6151 );
6152 editor.insert_blocks(
6153 [BlockProperties {
6154 placement,
6155 height: Some(4),
6156 style: BlockStyle::Sticky,
6157 render: Arc::new(|_| gpui::div().into_any_element()),
6158 priority: 0,
6159 }],
6160 None,
6161 cx,
6162 );
6163 });
6164
6165 // Move down so that the cursor touches the block.
6166 cx.update_editor(|editor, window, cx| {
6167 editor.move_down(&Default::default(), window, cx);
6168 });
6169 cx.assert_editor_state(
6170 &"
6171 zero
6172 «one
6173 two
6174 threeˇ»
6175 four
6176 five
6177 "
6178 .unindent(),
6179 );
6180
6181 // Move down past the block.
6182 cx.update_editor(|editor, window, cx| {
6183 editor.move_down(&Default::default(), window, cx);
6184 });
6185 cx.assert_editor_state(
6186 &"
6187 zero
6188 one
6189 two
6190 three
6191 ˇfour
6192 five
6193 "
6194 .unindent(),
6195 );
6196}
6197
6198#[gpui::test]
6199fn test_transpose(cx: &mut TestAppContext) {
6200 init_test(cx, |_| {});
6201
6202 _ = cx.add_window(|window, cx| {
6203 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
6204 editor.set_style(EditorStyle::default(), window, cx);
6205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6206 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
6207 });
6208 editor.transpose(&Default::default(), window, cx);
6209 assert_eq!(editor.text(cx), "bac");
6210 assert_eq!(
6211 editor.selections.ranges(&editor.display_snapshot(cx)),
6212 [MultiBufferOffset(2)..MultiBufferOffset(2)]
6213 );
6214
6215 editor.transpose(&Default::default(), window, cx);
6216 assert_eq!(editor.text(cx), "bca");
6217 assert_eq!(
6218 editor.selections.ranges(&editor.display_snapshot(cx)),
6219 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6220 );
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(3)..MultiBufferOffset(3)]
6227 );
6228
6229 editor
6230 });
6231
6232 _ = cx.add_window(|window, cx| {
6233 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6234 editor.set_style(EditorStyle::default(), window, cx);
6235 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6236 s.select_ranges([MultiBufferOffset(3)..MultiBufferOffset(3)])
6237 });
6238 editor.transpose(&Default::default(), window, cx);
6239 assert_eq!(editor.text(cx), "acb\nde");
6240 assert_eq!(
6241 editor.selections.ranges(&editor.display_snapshot(cx)),
6242 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6243 );
6244
6245 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6246 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6247 });
6248 editor.transpose(&Default::default(), window, cx);
6249 assert_eq!(editor.text(cx), "acbd\ne");
6250 assert_eq!(
6251 editor.selections.ranges(&editor.display_snapshot(cx)),
6252 [MultiBufferOffset(5)..MultiBufferOffset(5)]
6253 );
6254
6255 editor.transpose(&Default::default(), window, cx);
6256 assert_eq!(editor.text(cx), "acbde\n");
6257 assert_eq!(
6258 editor.selections.ranges(&editor.display_snapshot(cx)),
6259 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6260 );
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(6)..MultiBufferOffset(6)]
6267 );
6268
6269 editor
6270 });
6271
6272 _ = cx.add_window(|window, cx| {
6273 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6274 editor.set_style(EditorStyle::default(), window, cx);
6275 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6276 s.select_ranges([
6277 MultiBufferOffset(1)..MultiBufferOffset(1),
6278 MultiBufferOffset(2)..MultiBufferOffset(2),
6279 MultiBufferOffset(4)..MultiBufferOffset(4),
6280 ])
6281 });
6282 editor.transpose(&Default::default(), window, cx);
6283 assert_eq!(editor.text(cx), "bacd\ne");
6284 assert_eq!(
6285 editor.selections.ranges(&editor.display_snapshot(cx)),
6286 [
6287 MultiBufferOffset(2)..MultiBufferOffset(2),
6288 MultiBufferOffset(3)..MultiBufferOffset(3),
6289 MultiBufferOffset(5)..MultiBufferOffset(5)
6290 ]
6291 );
6292
6293 editor.transpose(&Default::default(), window, cx);
6294 assert_eq!(editor.text(cx), "bcade\n");
6295 assert_eq!(
6296 editor.selections.ranges(&editor.display_snapshot(cx)),
6297 [
6298 MultiBufferOffset(3)..MultiBufferOffset(3),
6299 MultiBufferOffset(4)..MultiBufferOffset(4),
6300 MultiBufferOffset(6)..MultiBufferOffset(6)
6301 ]
6302 );
6303
6304 editor.transpose(&Default::default(), window, cx);
6305 assert_eq!(editor.text(cx), "bcda\ne");
6306 assert_eq!(
6307 editor.selections.ranges(&editor.display_snapshot(cx)),
6308 [
6309 MultiBufferOffset(4)..MultiBufferOffset(4),
6310 MultiBufferOffset(6)..MultiBufferOffset(6)
6311 ]
6312 );
6313
6314 editor.transpose(&Default::default(), window, cx);
6315 assert_eq!(editor.text(cx), "bcade\n");
6316 assert_eq!(
6317 editor.selections.ranges(&editor.display_snapshot(cx)),
6318 [
6319 MultiBufferOffset(4)..MultiBufferOffset(4),
6320 MultiBufferOffset(6)..MultiBufferOffset(6)
6321 ]
6322 );
6323
6324 editor.transpose(&Default::default(), window, cx);
6325 assert_eq!(editor.text(cx), "bcaed\n");
6326 assert_eq!(
6327 editor.selections.ranges(&editor.display_snapshot(cx)),
6328 [
6329 MultiBufferOffset(5)..MultiBufferOffset(5),
6330 MultiBufferOffset(6)..MultiBufferOffset(6)
6331 ]
6332 );
6333
6334 editor
6335 });
6336
6337 _ = cx.add_window(|window, cx| {
6338 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6339 editor.set_style(EditorStyle::default(), window, cx);
6340 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6341 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6342 });
6343 editor.transpose(&Default::default(), window, cx);
6344 assert_eq!(editor.text(cx), "🏀🍐✋");
6345 assert_eq!(
6346 editor.selections.ranges(&editor.display_snapshot(cx)),
6347 [MultiBufferOffset(8)..MultiBufferOffset(8)]
6348 );
6349
6350 editor.transpose(&Default::default(), window, cx);
6351 assert_eq!(editor.text(cx), "🏀✋🍐");
6352 assert_eq!(
6353 editor.selections.ranges(&editor.display_snapshot(cx)),
6354 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6355 );
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(11)..MultiBufferOffset(11)]
6362 );
6363
6364 editor
6365 });
6366}
6367
6368#[gpui::test]
6369async fn test_rewrap(cx: &mut TestAppContext) {
6370 init_test(cx, |settings| {
6371 settings.languages.0.extend([
6372 (
6373 "Markdown".into(),
6374 LanguageSettingsContent {
6375 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6376 preferred_line_length: Some(40),
6377 ..Default::default()
6378 },
6379 ),
6380 (
6381 "Plain Text".into(),
6382 LanguageSettingsContent {
6383 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6384 preferred_line_length: Some(40),
6385 ..Default::default()
6386 },
6387 ),
6388 (
6389 "C++".into(),
6390 LanguageSettingsContent {
6391 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6392 preferred_line_length: Some(40),
6393 ..Default::default()
6394 },
6395 ),
6396 (
6397 "Python".into(),
6398 LanguageSettingsContent {
6399 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6400 preferred_line_length: Some(40),
6401 ..Default::default()
6402 },
6403 ),
6404 (
6405 "Rust".into(),
6406 LanguageSettingsContent {
6407 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6408 preferred_line_length: Some(40),
6409 ..Default::default()
6410 },
6411 ),
6412 ])
6413 });
6414
6415 let mut cx = EditorTestContext::new(cx).await;
6416
6417 let cpp_language = Arc::new(Language::new(
6418 LanguageConfig {
6419 name: "C++".into(),
6420 line_comments: vec!["// ".into()],
6421 ..LanguageConfig::default()
6422 },
6423 None,
6424 ));
6425 let python_language = Arc::new(Language::new(
6426 LanguageConfig {
6427 name: "Python".into(),
6428 line_comments: vec!["# ".into()],
6429 ..LanguageConfig::default()
6430 },
6431 None,
6432 ));
6433 let markdown_language = Arc::new(Language::new(
6434 LanguageConfig {
6435 name: "Markdown".into(),
6436 rewrap_prefixes: vec![
6437 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6438 regex::Regex::new("[-*+]\\s+").unwrap(),
6439 ],
6440 ..LanguageConfig::default()
6441 },
6442 None,
6443 ));
6444 let rust_language = Arc::new(
6445 Language::new(
6446 LanguageConfig {
6447 name: "Rust".into(),
6448 line_comments: vec!["// ".into(), "/// ".into()],
6449 ..LanguageConfig::default()
6450 },
6451 Some(tree_sitter_rust::LANGUAGE.into()),
6452 )
6453 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6454 .unwrap(),
6455 );
6456
6457 let plaintext_language = Arc::new(Language::new(
6458 LanguageConfig {
6459 name: "Plain Text".into(),
6460 ..LanguageConfig::default()
6461 },
6462 None,
6463 ));
6464
6465 // Test basic rewrapping of a long line with a cursor
6466 assert_rewrap(
6467 indoc! {"
6468 // ˇThis is a long comment that needs to be wrapped.
6469 "},
6470 indoc! {"
6471 // ˇThis is a long comment that needs to
6472 // be wrapped.
6473 "},
6474 cpp_language.clone(),
6475 &mut cx,
6476 );
6477
6478 // Test rewrapping a full selection
6479 assert_rewrap(
6480 indoc! {"
6481 «// This selected long comment needs to be wrapped.ˇ»"
6482 },
6483 indoc! {"
6484 «// This selected long comment needs to
6485 // be wrapped.ˇ»"
6486 },
6487 cpp_language.clone(),
6488 &mut cx,
6489 );
6490
6491 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6492 assert_rewrap(
6493 indoc! {"
6494 // ˇThis is the first line.
6495 // Thisˇ is the second line.
6496 // This is the thirdˇ line, all part of one paragraph.
6497 "},
6498 indoc! {"
6499 // ˇThis is the first line. Thisˇ is the
6500 // second line. This is the thirdˇ line,
6501 // all part of one paragraph.
6502 "},
6503 cpp_language.clone(),
6504 &mut cx,
6505 );
6506
6507 // Test multiple cursors in different paragraphs trigger separate rewraps
6508 assert_rewrap(
6509 indoc! {"
6510 // ˇThis is the first paragraph, first line.
6511 // ˇThis is the first paragraph, second line.
6512
6513 // ˇThis is the second paragraph, first line.
6514 // ˇThis is the second paragraph, second line.
6515 "},
6516 indoc! {"
6517 // ˇThis is the first paragraph, first
6518 // line. ˇThis is the first paragraph,
6519 // second line.
6520
6521 // ˇThis is the second paragraph, first
6522 // line. ˇThis is the second paragraph,
6523 // second line.
6524 "},
6525 cpp_language.clone(),
6526 &mut cx,
6527 );
6528
6529 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6530 assert_rewrap(
6531 indoc! {"
6532 «// A regular long long comment to be wrapped.
6533 /// A documentation long comment to be wrapped.ˇ»
6534 "},
6535 indoc! {"
6536 «// A regular long long comment to be
6537 // wrapped.
6538 /// A documentation long comment to be
6539 /// wrapped.ˇ»
6540 "},
6541 rust_language.clone(),
6542 &mut cx,
6543 );
6544
6545 // Test that change in indentation level trigger seperate rewraps
6546 assert_rewrap(
6547 indoc! {"
6548 fn foo() {
6549 «// This is a long comment at the base indent.
6550 // This is a long comment at the next indent.ˇ»
6551 }
6552 "},
6553 indoc! {"
6554 fn foo() {
6555 «// This is a long comment at the
6556 // base indent.
6557 // This is a long comment at the
6558 // next indent.ˇ»
6559 }
6560 "},
6561 rust_language.clone(),
6562 &mut cx,
6563 );
6564
6565 // Test that different comment prefix characters (e.g., '#') are handled correctly
6566 assert_rewrap(
6567 indoc! {"
6568 # ˇThis is a long comment using a pound sign.
6569 "},
6570 indoc! {"
6571 # ˇThis is a long comment using a pound
6572 # sign.
6573 "},
6574 python_language,
6575 &mut cx,
6576 );
6577
6578 // Test rewrapping only affects comments, not code even when selected
6579 assert_rewrap(
6580 indoc! {"
6581 «/// This doc comment is long and should be wrapped.
6582 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6583 "},
6584 indoc! {"
6585 «/// This doc comment is long and should
6586 /// be wrapped.
6587 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6588 "},
6589 rust_language.clone(),
6590 &mut cx,
6591 );
6592
6593 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6594 assert_rewrap(
6595 indoc! {"
6596 # Header
6597
6598 A long long long line of markdown text to wrap.ˇ
6599 "},
6600 indoc! {"
6601 # Header
6602
6603 A long long long line of markdown text
6604 to wrap.ˇ
6605 "},
6606 markdown_language.clone(),
6607 &mut cx,
6608 );
6609
6610 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6611 assert_rewrap(
6612 indoc! {"
6613 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6614 2. This is a numbered list item that is very long and needs to be wrapped properly.
6615 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6616 "},
6617 indoc! {"
6618 «1. This is a numbered list item that is
6619 very long and needs to be wrapped
6620 properly.
6621 2. This is a numbered list item that is
6622 very long and needs to be wrapped
6623 properly.
6624 - This is an unordered list item that is
6625 also very long and should not merge
6626 with the numbered item.ˇ»
6627 "},
6628 markdown_language.clone(),
6629 &mut cx,
6630 );
6631
6632 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6633 assert_rewrap(
6634 indoc! {"
6635 «1. This is a numbered list item that is
6636 very long and needs to be wrapped
6637 properly.
6638 2. This is a numbered list item that is
6639 very long and needs to be wrapped
6640 properly.
6641 - This is an unordered list item that is
6642 also very long and should not merge with
6643 the numbered item.ˇ»
6644 "},
6645 indoc! {"
6646 «1. This is a numbered list item that is
6647 very long and needs to be wrapped
6648 properly.
6649 2. This is a numbered list item that is
6650 very long and needs to be wrapped
6651 properly.
6652 - This is an unordered list item that is
6653 also very long and should not merge
6654 with the numbered item.ˇ»
6655 "},
6656 markdown_language.clone(),
6657 &mut cx,
6658 );
6659
6660 // Test that rewrapping maintain indents even when they already exists.
6661 assert_rewrap(
6662 indoc! {"
6663 «1. This is a numbered list
6664 item that is very long and needs to be wrapped properly.
6665 2. This is a numbered list
6666 item that is very long and needs to be wrapped properly.
6667 - This is an unordered list item that is also very long and
6668 should not merge with the numbered item.ˇ»
6669 "},
6670 indoc! {"
6671 «1. This is a numbered list item that is
6672 very long and needs to be wrapped
6673 properly.
6674 2. This is a numbered list item that is
6675 very long and needs to be wrapped
6676 properly.
6677 - This is an unordered list item that is
6678 also very long and should not merge
6679 with the numbered item.ˇ»
6680 "},
6681 markdown_language,
6682 &mut cx,
6683 );
6684
6685 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6686 assert_rewrap(
6687 indoc! {"
6688 ˇThis is a very long line of plain text that will be wrapped.
6689 "},
6690 indoc! {"
6691 ˇThis is a very long line of plain text
6692 that will be wrapped.
6693 "},
6694 plaintext_language.clone(),
6695 &mut cx,
6696 );
6697
6698 // Test that non-commented code acts as a paragraph boundary within a selection
6699 assert_rewrap(
6700 indoc! {"
6701 «// This is the first long comment block to be wrapped.
6702 fn my_func(a: u32);
6703 // This is the second long comment block to be wrapped.ˇ»
6704 "},
6705 indoc! {"
6706 «// This is the first long comment block
6707 // to be wrapped.
6708 fn my_func(a: u32);
6709 // This is the second long comment block
6710 // to be wrapped.ˇ»
6711 "},
6712 rust_language,
6713 &mut cx,
6714 );
6715
6716 // Test rewrapping multiple selections, including ones with blank lines or tabs
6717 assert_rewrap(
6718 indoc! {"
6719 «ˇThis is a very long line that will be wrapped.
6720
6721 This is another paragraph in the same selection.»
6722
6723 «\tThis is a very long indented line that will be wrapped.ˇ»
6724 "},
6725 indoc! {"
6726 «ˇThis is a very long line that will be
6727 wrapped.
6728
6729 This is another paragraph in the same
6730 selection.»
6731
6732 «\tThis is a very long indented line
6733 \tthat will be wrapped.ˇ»
6734 "},
6735 plaintext_language,
6736 &mut cx,
6737 );
6738
6739 // Test that an empty comment line acts as a paragraph boundary
6740 assert_rewrap(
6741 indoc! {"
6742 // ˇThis is a long comment that will be wrapped.
6743 //
6744 // And this is another long comment that will also be wrapped.ˇ
6745 "},
6746 indoc! {"
6747 // ˇThis is a long comment that will be
6748 // wrapped.
6749 //
6750 // And this is another long comment that
6751 // will also be wrapped.ˇ
6752 "},
6753 cpp_language,
6754 &mut cx,
6755 );
6756
6757 #[track_caller]
6758 fn assert_rewrap(
6759 unwrapped_text: &str,
6760 wrapped_text: &str,
6761 language: Arc<Language>,
6762 cx: &mut EditorTestContext,
6763 ) {
6764 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6765 cx.set_state(unwrapped_text);
6766 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6767 cx.assert_editor_state(wrapped_text);
6768 }
6769}
6770
6771#[gpui::test]
6772async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6773 init_test(cx, |settings| {
6774 settings.languages.0.extend([(
6775 "Rust".into(),
6776 LanguageSettingsContent {
6777 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6778 preferred_line_length: Some(40),
6779 ..Default::default()
6780 },
6781 )])
6782 });
6783
6784 let mut cx = EditorTestContext::new(cx).await;
6785
6786 let rust_lang = Arc::new(
6787 Language::new(
6788 LanguageConfig {
6789 name: "Rust".into(),
6790 line_comments: vec!["// ".into()],
6791 block_comment: Some(BlockCommentConfig {
6792 start: "/*".into(),
6793 end: "*/".into(),
6794 prefix: "* ".into(),
6795 tab_size: 1,
6796 }),
6797 documentation_comment: Some(BlockCommentConfig {
6798 start: "/**".into(),
6799 end: "*/".into(),
6800 prefix: "* ".into(),
6801 tab_size: 1,
6802 }),
6803
6804 ..LanguageConfig::default()
6805 },
6806 Some(tree_sitter_rust::LANGUAGE.into()),
6807 )
6808 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6809 .unwrap(),
6810 );
6811
6812 // regular block comment
6813 assert_rewrap(
6814 indoc! {"
6815 /*
6816 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6817 */
6818 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6819 "},
6820 indoc! {"
6821 /*
6822 *ˇ Lorem ipsum dolor sit amet,
6823 * consectetur adipiscing elit.
6824 */
6825 /*
6826 *ˇ Lorem ipsum dolor sit amet,
6827 * consectetur adipiscing elit.
6828 */
6829 "},
6830 rust_lang.clone(),
6831 &mut cx,
6832 );
6833
6834 // indent is respected
6835 assert_rewrap(
6836 indoc! {"
6837 {}
6838 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6839 "},
6840 indoc! {"
6841 {}
6842 /*
6843 *ˇ Lorem ipsum dolor sit amet,
6844 * consectetur adipiscing elit.
6845 */
6846 "},
6847 rust_lang.clone(),
6848 &mut cx,
6849 );
6850
6851 // short block comments with inline delimiters
6852 assert_rewrap(
6853 indoc! {"
6854 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6855 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6856 */
6857 /*
6858 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6859 "},
6860 indoc! {"
6861 /*
6862 *ˇ Lorem ipsum dolor sit amet,
6863 * consectetur adipiscing elit.
6864 */
6865 /*
6866 *ˇ Lorem ipsum dolor sit amet,
6867 * consectetur adipiscing elit.
6868 */
6869 /*
6870 *ˇ Lorem ipsum dolor sit amet,
6871 * consectetur adipiscing elit.
6872 */
6873 "},
6874 rust_lang.clone(),
6875 &mut cx,
6876 );
6877
6878 // multiline block comment with inline start/end delimiters
6879 assert_rewrap(
6880 indoc! {"
6881 /*ˇ Lorem ipsum dolor sit amet,
6882 * consectetur adipiscing elit. */
6883 "},
6884 indoc! {"
6885 /*
6886 *ˇ Lorem ipsum dolor sit amet,
6887 * consectetur adipiscing elit.
6888 */
6889 "},
6890 rust_lang.clone(),
6891 &mut cx,
6892 );
6893
6894 // block comment rewrap still respects paragraph bounds
6895 assert_rewrap(
6896 indoc! {"
6897 /*
6898 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6899 *
6900 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6901 */
6902 "},
6903 indoc! {"
6904 /*
6905 *ˇ Lorem ipsum dolor sit amet,
6906 * consectetur adipiscing elit.
6907 *
6908 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6909 */
6910 "},
6911 rust_lang.clone(),
6912 &mut cx,
6913 );
6914
6915 // documentation comments
6916 assert_rewrap(
6917 indoc! {"
6918 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6919 /**
6920 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6921 */
6922 "},
6923 indoc! {"
6924 /**
6925 *ˇ Lorem ipsum dolor sit amet,
6926 * consectetur adipiscing elit.
6927 */
6928 /**
6929 *ˇ Lorem ipsum dolor sit amet,
6930 * consectetur adipiscing elit.
6931 */
6932 "},
6933 rust_lang.clone(),
6934 &mut cx,
6935 );
6936
6937 // different, adjacent comments
6938 assert_rewrap(
6939 indoc! {"
6940 /**
6941 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6942 */
6943 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6944 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6945 "},
6946 indoc! {"
6947 /**
6948 *ˇ Lorem ipsum dolor sit amet,
6949 * consectetur adipiscing elit.
6950 */
6951 /*
6952 *ˇ Lorem ipsum dolor sit amet,
6953 * consectetur adipiscing elit.
6954 */
6955 //ˇ Lorem ipsum dolor sit amet,
6956 // consectetur adipiscing elit.
6957 "},
6958 rust_lang.clone(),
6959 &mut cx,
6960 );
6961
6962 // selection w/ single short block comment
6963 assert_rewrap(
6964 indoc! {"
6965 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6966 "},
6967 indoc! {"
6968 «/*
6969 * Lorem ipsum dolor sit amet,
6970 * consectetur adipiscing elit.
6971 */ˇ»
6972 "},
6973 rust_lang.clone(),
6974 &mut cx,
6975 );
6976
6977 // rewrapping a single comment w/ abutting comments
6978 assert_rewrap(
6979 indoc! {"
6980 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6981 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6982 "},
6983 indoc! {"
6984 /*
6985 * ˇLorem ipsum dolor sit amet,
6986 * consectetur adipiscing elit.
6987 */
6988 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6989 "},
6990 rust_lang.clone(),
6991 &mut cx,
6992 );
6993
6994 // selection w/ non-abutting short block comments
6995 assert_rewrap(
6996 indoc! {"
6997 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6998
6999 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7000 "},
7001 indoc! {"
7002 «/*
7003 * Lorem ipsum dolor sit amet,
7004 * consectetur adipiscing elit.
7005 */
7006
7007 /*
7008 * Lorem ipsum dolor sit amet,
7009 * consectetur adipiscing elit.
7010 */ˇ»
7011 "},
7012 rust_lang.clone(),
7013 &mut cx,
7014 );
7015
7016 // selection of multiline block comments
7017 assert_rewrap(
7018 indoc! {"
7019 «/* Lorem ipsum dolor sit amet,
7020 * consectetur adipiscing elit. */ˇ»
7021 "},
7022 indoc! {"
7023 «/*
7024 * Lorem ipsum dolor sit amet,
7025 * consectetur adipiscing elit.
7026 */ˇ»
7027 "},
7028 rust_lang.clone(),
7029 &mut cx,
7030 );
7031
7032 // partial selection of multiline block comments
7033 assert_rewrap(
7034 indoc! {"
7035 «/* Lorem ipsum dolor sit amet,ˇ»
7036 * consectetur adipiscing elit. */
7037 /* Lorem ipsum dolor sit amet,
7038 «* consectetur adipiscing elit. */ˇ»
7039 "},
7040 indoc! {"
7041 «/*
7042 * Lorem ipsum dolor sit amet,ˇ»
7043 * consectetur adipiscing elit. */
7044 /* Lorem ipsum dolor sit amet,
7045 «* consectetur adipiscing elit.
7046 */ˇ»
7047 "},
7048 rust_lang.clone(),
7049 &mut cx,
7050 );
7051
7052 // selection w/ abutting short block comments
7053 // TODO: should not be combined; should rewrap as 2 comments
7054 assert_rewrap(
7055 indoc! {"
7056 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7057 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7058 "},
7059 // desired behavior:
7060 // indoc! {"
7061 // «/*
7062 // * Lorem ipsum dolor sit amet,
7063 // * consectetur adipiscing elit.
7064 // */
7065 // /*
7066 // * Lorem ipsum dolor sit amet,
7067 // * consectetur adipiscing elit.
7068 // */ˇ»
7069 // "},
7070 // actual behaviour:
7071 indoc! {"
7072 «/*
7073 * Lorem ipsum dolor sit amet,
7074 * consectetur adipiscing elit. Lorem
7075 * ipsum dolor sit amet, consectetur
7076 * adipiscing elit.
7077 */ˇ»
7078 "},
7079 rust_lang.clone(),
7080 &mut cx,
7081 );
7082
7083 // TODO: same as above, but with delimiters on separate line
7084 // assert_rewrap(
7085 // indoc! {"
7086 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7087 // */
7088 // /*
7089 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7090 // "},
7091 // // desired:
7092 // // indoc! {"
7093 // // «/*
7094 // // * Lorem ipsum dolor sit amet,
7095 // // * consectetur adipiscing elit.
7096 // // */
7097 // // /*
7098 // // * Lorem ipsum dolor sit amet,
7099 // // * consectetur adipiscing elit.
7100 // // */ˇ»
7101 // // "},
7102 // // actual: (but with trailing w/s on the empty lines)
7103 // indoc! {"
7104 // «/*
7105 // * Lorem ipsum dolor sit amet,
7106 // * consectetur adipiscing elit.
7107 // *
7108 // */
7109 // /*
7110 // *
7111 // * Lorem ipsum dolor sit amet,
7112 // * consectetur adipiscing elit.
7113 // */ˇ»
7114 // "},
7115 // rust_lang.clone(),
7116 // &mut cx,
7117 // );
7118
7119 // TODO these are unhandled edge cases; not correct, just documenting known issues
7120 assert_rewrap(
7121 indoc! {"
7122 /*
7123 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7124 */
7125 /*
7126 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7127 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
7128 "},
7129 // desired:
7130 // indoc! {"
7131 // /*
7132 // *ˇ Lorem ipsum dolor sit amet,
7133 // * consectetur adipiscing elit.
7134 // */
7135 // /*
7136 // *ˇ Lorem ipsum dolor sit amet,
7137 // * consectetur adipiscing elit.
7138 // */
7139 // /*
7140 // *ˇ Lorem ipsum dolor sit amet
7141 // */ /* consectetur adipiscing elit. */
7142 // "},
7143 // actual:
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 "},
7158 rust_lang,
7159 &mut cx,
7160 );
7161
7162 #[track_caller]
7163 fn assert_rewrap(
7164 unwrapped_text: &str,
7165 wrapped_text: &str,
7166 language: Arc<Language>,
7167 cx: &mut EditorTestContext,
7168 ) {
7169 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
7170 cx.set_state(unwrapped_text);
7171 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
7172 cx.assert_editor_state(wrapped_text);
7173 }
7174}
7175
7176#[gpui::test]
7177async fn test_hard_wrap(cx: &mut TestAppContext) {
7178 init_test(cx, |_| {});
7179 let mut cx = EditorTestContext::new(cx).await;
7180
7181 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
7182 cx.update_editor(|editor, _, cx| {
7183 editor.set_hard_wrap(Some(14), cx);
7184 });
7185
7186 cx.set_state(indoc!(
7187 "
7188 one two three ˇ
7189 "
7190 ));
7191 cx.simulate_input("four");
7192 cx.run_until_parked();
7193
7194 cx.assert_editor_state(indoc!(
7195 "
7196 one two three
7197 fourˇ
7198 "
7199 ));
7200
7201 cx.update_editor(|editor, window, cx| {
7202 editor.newline(&Default::default(), window, cx);
7203 });
7204 cx.run_until_parked();
7205 cx.assert_editor_state(indoc!(
7206 "
7207 one two three
7208 four
7209 ˇ
7210 "
7211 ));
7212
7213 cx.simulate_input("five");
7214 cx.run_until_parked();
7215 cx.assert_editor_state(indoc!(
7216 "
7217 one two three
7218 four
7219 fiveˇ
7220 "
7221 ));
7222
7223 cx.update_editor(|editor, window, cx| {
7224 editor.newline(&Default::default(), window, cx);
7225 });
7226 cx.run_until_parked();
7227 cx.simulate_input("# ");
7228 cx.run_until_parked();
7229 cx.assert_editor_state(indoc!(
7230 "
7231 one two three
7232 four
7233 five
7234 # ˇ
7235 "
7236 ));
7237
7238 cx.update_editor(|editor, window, cx| {
7239 editor.newline(&Default::default(), window, cx);
7240 });
7241 cx.run_until_parked();
7242 cx.assert_editor_state(indoc!(
7243 "
7244 one two three
7245 four
7246 five
7247 #\x20
7248 #ˇ
7249 "
7250 ));
7251
7252 cx.simulate_input(" 6");
7253 cx.run_until_parked();
7254 cx.assert_editor_state(indoc!(
7255 "
7256 one two three
7257 four
7258 five
7259 #
7260 # 6ˇ
7261 "
7262 ));
7263}
7264
7265#[gpui::test]
7266async fn test_cut_line_ends(cx: &mut TestAppContext) {
7267 init_test(cx, |_| {});
7268
7269 let mut cx = EditorTestContext::new(cx).await;
7270
7271 cx.set_state(indoc! {"The quick brownˇ"});
7272 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7273 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7274
7275 cx.set_state(indoc! {"The emacs foxˇ"});
7276 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7277 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7278
7279 cx.set_state(indoc! {"
7280 The quick« brownˇ»
7281 fox jumps overˇ
7282 the lazy dog"});
7283 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7284 cx.assert_editor_state(indoc! {"
7285 The quickˇ
7286 ˇthe lazy dog"});
7287
7288 cx.set_state(indoc! {"
7289 The quick« brownˇ»
7290 fox jumps overˇ
7291 the lazy dog"});
7292 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7293 cx.assert_editor_state(indoc! {"
7294 The quickˇ
7295 fox jumps overˇthe lazy dog"});
7296
7297 cx.set_state(indoc! {"
7298 The quick« brownˇ»
7299 fox jumps overˇ
7300 the lazy dog"});
7301 cx.update_editor(|e, window, cx| {
7302 e.cut_to_end_of_line(
7303 &CutToEndOfLine {
7304 stop_at_newlines: true,
7305 },
7306 window,
7307 cx,
7308 )
7309 });
7310 cx.assert_editor_state(indoc! {"
7311 The quickˇ
7312 fox jumps overˇ
7313 the lazy dog"});
7314
7315 cx.set_state(indoc! {"
7316 The quick« brownˇ»
7317 fox jumps overˇ
7318 the lazy dog"});
7319 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7320 cx.assert_editor_state(indoc! {"
7321 The quickˇ
7322 fox jumps overˇthe lazy dog"});
7323}
7324
7325#[gpui::test]
7326async fn test_clipboard(cx: &mut TestAppContext) {
7327 init_test(cx, |_| {});
7328
7329 let mut cx = EditorTestContext::new(cx).await;
7330
7331 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7332 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7333 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7334
7335 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7336 cx.set_state("two ˇfour ˇsix ˇ");
7337 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7338 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7339
7340 // Paste again but with only two cursors. Since the number of cursors doesn't
7341 // match the number of slices in the clipboard, the entire clipboard text
7342 // is pasted at each cursor.
7343 cx.set_state("ˇtwo one✅ four three six five ˇ");
7344 cx.update_editor(|e, window, cx| {
7345 e.handle_input("( ", window, cx);
7346 e.paste(&Paste, window, cx);
7347 e.handle_input(") ", window, cx);
7348 });
7349 cx.assert_editor_state(
7350 &([
7351 "( one✅ ",
7352 "three ",
7353 "five ) ˇtwo one✅ four three six five ( one✅ ",
7354 "three ",
7355 "five ) ˇ",
7356 ]
7357 .join("\n")),
7358 );
7359
7360 // Cut with three selections, one of which is full-line.
7361 cx.set_state(indoc! {"
7362 1«2ˇ»3
7363 4ˇ567
7364 «8ˇ»9"});
7365 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7366 cx.assert_editor_state(indoc! {"
7367 1ˇ3
7368 ˇ9"});
7369
7370 // Paste with three selections, noticing how the copied selection that was full-line
7371 // gets inserted before the second cursor.
7372 cx.set_state(indoc! {"
7373 1ˇ3
7374 9ˇ
7375 «oˇ»ne"});
7376 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7377 cx.assert_editor_state(indoc! {"
7378 12ˇ3
7379 4567
7380 9ˇ
7381 8ˇne"});
7382
7383 // Copy with a single cursor only, which writes the whole line into the clipboard.
7384 cx.set_state(indoc! {"
7385 The quick brown
7386 fox juˇmps over
7387 the lazy dog"});
7388 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7389 assert_eq!(
7390 cx.read_from_clipboard()
7391 .and_then(|item| item.text().as_deref().map(str::to_string)),
7392 Some("fox jumps over\n".to_string())
7393 );
7394
7395 // Paste with three selections, noticing how the copied full-line selection is inserted
7396 // before the empty selections but replaces the selection that is non-empty.
7397 cx.set_state(indoc! {"
7398 Tˇhe quick brown
7399 «foˇ»x jumps over
7400 tˇhe lazy dog"});
7401 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7402 cx.assert_editor_state(indoc! {"
7403 fox jumps over
7404 Tˇhe quick brown
7405 fox jumps over
7406 ˇx jumps over
7407 fox jumps over
7408 tˇhe lazy dog"});
7409}
7410
7411#[gpui::test]
7412async fn test_copy_trim(cx: &mut TestAppContext) {
7413 init_test(cx, |_| {});
7414
7415 let mut cx = EditorTestContext::new(cx).await;
7416 cx.set_state(
7417 r#" «for selection in selections.iter() {
7418 let mut start = selection.start;
7419 let mut end = selection.end;
7420 let is_entire_line = selection.is_empty();
7421 if is_entire_line {
7422 start = Point::new(start.row, 0);ˇ»
7423 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7424 }
7425 "#,
7426 );
7427 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7428 assert_eq!(
7429 cx.read_from_clipboard()
7430 .and_then(|item| item.text().as_deref().map(str::to_string)),
7431 Some(
7432 "for selection in selections.iter() {
7433 let mut start = selection.start;
7434 let mut end = selection.end;
7435 let is_entire_line = selection.is_empty();
7436 if is_entire_line {
7437 start = Point::new(start.row, 0);"
7438 .to_string()
7439 ),
7440 "Regular copying preserves all indentation selected",
7441 );
7442 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7443 assert_eq!(
7444 cx.read_from_clipboard()
7445 .and_then(|item| item.text().as_deref().map(str::to_string)),
7446 Some(
7447 "for selection in selections.iter() {
7448let mut start = selection.start;
7449let mut end = selection.end;
7450let is_entire_line = selection.is_empty();
7451if is_entire_line {
7452 start = Point::new(start.row, 0);"
7453 .to_string()
7454 ),
7455 "Copying with stripping should strip all leading whitespaces"
7456 );
7457
7458 cx.set_state(
7459 r#" « for selection in selections.iter() {
7460 let mut start = selection.start;
7461 let mut end = selection.end;
7462 let is_entire_line = selection.is_empty();
7463 if is_entire_line {
7464 start = Point::new(start.row, 0);ˇ»
7465 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7466 }
7467 "#,
7468 );
7469 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7470 assert_eq!(
7471 cx.read_from_clipboard()
7472 .and_then(|item| item.text().as_deref().map(str::to_string)),
7473 Some(
7474 " for selection in selections.iter() {
7475 let mut start = selection.start;
7476 let mut end = selection.end;
7477 let is_entire_line = selection.is_empty();
7478 if is_entire_line {
7479 start = Point::new(start.row, 0);"
7480 .to_string()
7481 ),
7482 "Regular copying preserves all indentation selected",
7483 );
7484 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7485 assert_eq!(
7486 cx.read_from_clipboard()
7487 .and_then(|item| item.text().as_deref().map(str::to_string)),
7488 Some(
7489 "for selection in selections.iter() {
7490let mut start = selection.start;
7491let mut end = selection.end;
7492let is_entire_line = selection.is_empty();
7493if is_entire_line {
7494 start = Point::new(start.row, 0);"
7495 .to_string()
7496 ),
7497 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7498 );
7499
7500 cx.set_state(
7501 r#" «ˇ for selection in selections.iter() {
7502 let mut start = selection.start;
7503 let mut end = selection.end;
7504 let is_entire_line = selection.is_empty();
7505 if is_entire_line {
7506 start = Point::new(start.row, 0);»
7507 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7508 }
7509 "#,
7510 );
7511 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7512 assert_eq!(
7513 cx.read_from_clipboard()
7514 .and_then(|item| item.text().as_deref().map(str::to_string)),
7515 Some(
7516 " for selection in selections.iter() {
7517 let mut start = selection.start;
7518 let mut end = selection.end;
7519 let is_entire_line = selection.is_empty();
7520 if is_entire_line {
7521 start = Point::new(start.row, 0);"
7522 .to_string()
7523 ),
7524 "Regular copying for reverse selection works the same",
7525 );
7526 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7527 assert_eq!(
7528 cx.read_from_clipboard()
7529 .and_then(|item| item.text().as_deref().map(str::to_string)),
7530 Some(
7531 "for selection in selections.iter() {
7532let mut start = selection.start;
7533let mut end = selection.end;
7534let is_entire_line = selection.is_empty();
7535if is_entire_line {
7536 start = Point::new(start.row, 0);"
7537 .to_string()
7538 ),
7539 "Copying with stripping for reverse selection works the same"
7540 );
7541
7542 cx.set_state(
7543 r#" for selection «in selections.iter() {
7544 let mut start = selection.start;
7545 let mut end = selection.end;
7546 let is_entire_line = selection.is_empty();
7547 if is_entire_line {
7548 start = Point::new(start.row, 0);ˇ»
7549 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7550 }
7551 "#,
7552 );
7553 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7554 assert_eq!(
7555 cx.read_from_clipboard()
7556 .and_then(|item| item.text().as_deref().map(str::to_string)),
7557 Some(
7558 "in selections.iter() {
7559 let mut start = selection.start;
7560 let mut end = selection.end;
7561 let is_entire_line = selection.is_empty();
7562 if is_entire_line {
7563 start = Point::new(start.row, 0);"
7564 .to_string()
7565 ),
7566 "When selecting past the indent, the copying works as usual",
7567 );
7568 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7569 assert_eq!(
7570 cx.read_from_clipboard()
7571 .and_then(|item| item.text().as_deref().map(str::to_string)),
7572 Some(
7573 "in selections.iter() {
7574 let mut start = selection.start;
7575 let mut end = selection.end;
7576 let is_entire_line = selection.is_empty();
7577 if is_entire_line {
7578 start = Point::new(start.row, 0);"
7579 .to_string()
7580 ),
7581 "When selecting past the indent, nothing is trimmed"
7582 );
7583
7584 cx.set_state(
7585 r#" «for selection in selections.iter() {
7586 let mut start = selection.start;
7587
7588 let mut end = selection.end;
7589 let is_entire_line = selection.is_empty();
7590 if is_entire_line {
7591 start = Point::new(start.row, 0);
7592ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7593 }
7594 "#,
7595 );
7596 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7597 assert_eq!(
7598 cx.read_from_clipboard()
7599 .and_then(|item| item.text().as_deref().map(str::to_string)),
7600 Some(
7601 "for selection in selections.iter() {
7602let mut start = selection.start;
7603
7604let mut end = selection.end;
7605let is_entire_line = selection.is_empty();
7606if is_entire_line {
7607 start = Point::new(start.row, 0);
7608"
7609 .to_string()
7610 ),
7611 "Copying with stripping should ignore empty lines"
7612 );
7613}
7614
7615#[gpui::test]
7616async fn test_copy_trim_line_mode(cx: &mut TestAppContext) {
7617 init_test(cx, |_| {});
7618
7619 let mut cx = EditorTestContext::new(cx).await;
7620
7621 cx.set_state(indoc! {"
7622 « a
7623 bˇ»
7624 "});
7625 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
7626 cx.update_editor(|editor, window, cx| editor.copy_and_trim(&CopyAndTrim, window, cx));
7627
7628 assert_eq!(
7629 cx.read_from_clipboard().and_then(|item| item.text()),
7630 Some("a\nb\n".to_string())
7631 );
7632}
7633
7634#[gpui::test]
7635async fn test_clipboard_line_numbers_from_multibuffer(cx: &mut TestAppContext) {
7636 init_test(cx, |_| {});
7637
7638 let fs = FakeFs::new(cx.executor());
7639 fs.insert_file(
7640 path!("/file.txt"),
7641 "first line\nsecond line\nthird line\nfourth line\nfifth line\n".into(),
7642 )
7643 .await;
7644
7645 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
7646
7647 let buffer = project
7648 .update(cx, |project, cx| {
7649 project.open_local_buffer(path!("/file.txt"), cx)
7650 })
7651 .await
7652 .unwrap();
7653
7654 let multibuffer = cx.new(|cx| {
7655 let mut multibuffer = MultiBuffer::new(ReadWrite);
7656 multibuffer.push_excerpts(
7657 buffer.clone(),
7658 [ExcerptRange::new(Point::new(2, 0)..Point::new(5, 0))],
7659 cx,
7660 );
7661 multibuffer
7662 });
7663
7664 let (editor, cx) = cx.add_window_view(|window, cx| {
7665 build_editor_with_project(project.clone(), multibuffer, window, cx)
7666 });
7667
7668 editor.update_in(cx, |editor, window, cx| {
7669 assert_eq!(editor.text(cx), "third line\nfourth line\nfifth line\n");
7670
7671 editor.select_all(&SelectAll, window, cx);
7672 editor.copy(&Copy, window, cx);
7673 });
7674
7675 let clipboard_selections: Option<Vec<ClipboardSelection>> = cx
7676 .read_from_clipboard()
7677 .and_then(|item| item.entries().first().cloned())
7678 .and_then(|entry| match entry {
7679 gpui::ClipboardEntry::String(text) => text.metadata_json(),
7680 _ => None,
7681 });
7682
7683 let selections = clipboard_selections.expect("should have clipboard selections");
7684 assert_eq!(selections.len(), 1);
7685 let selection = &selections[0];
7686 assert_eq!(
7687 selection.line_range,
7688 Some(2..=5),
7689 "line range should be from original file (rows 2-5), not multibuffer rows (0-2)"
7690 );
7691}
7692
7693#[gpui::test]
7694async fn test_paste_multiline(cx: &mut TestAppContext) {
7695 init_test(cx, |_| {});
7696
7697 let mut cx = EditorTestContext::new(cx).await;
7698 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7699
7700 // Cut an indented block, without the leading whitespace.
7701 cx.set_state(indoc! {"
7702 const a: B = (
7703 c(),
7704 «d(
7705 e,
7706 f
7707 )ˇ»
7708 );
7709 "});
7710 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7711 cx.assert_editor_state(indoc! {"
7712 const a: B = (
7713 c(),
7714 ˇ
7715 );
7716 "});
7717
7718 // Paste it at the same position.
7719 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7720 cx.assert_editor_state(indoc! {"
7721 const a: B = (
7722 c(),
7723 d(
7724 e,
7725 f
7726 )ˇ
7727 );
7728 "});
7729
7730 // Paste it at a line with a lower indent level.
7731 cx.set_state(indoc! {"
7732 ˇ
7733 const a: B = (
7734 c(),
7735 );
7736 "});
7737 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7738 cx.assert_editor_state(indoc! {"
7739 d(
7740 e,
7741 f
7742 )ˇ
7743 const a: B = (
7744 c(),
7745 );
7746 "});
7747
7748 // Cut an indented block, with the leading whitespace.
7749 cx.set_state(indoc! {"
7750 const a: B = (
7751 c(),
7752 « d(
7753 e,
7754 f
7755 )
7756 ˇ»);
7757 "});
7758 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7759 cx.assert_editor_state(indoc! {"
7760 const a: B = (
7761 c(),
7762 ˇ);
7763 "});
7764
7765 // Paste it at the same position.
7766 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7767 cx.assert_editor_state(indoc! {"
7768 const a: B = (
7769 c(),
7770 d(
7771 e,
7772 f
7773 )
7774 ˇ);
7775 "});
7776
7777 // Paste it at a line with a higher indent level.
7778 cx.set_state(indoc! {"
7779 const a: B = (
7780 c(),
7781 d(
7782 e,
7783 fˇ
7784 )
7785 );
7786 "});
7787 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7788 cx.assert_editor_state(indoc! {"
7789 const a: B = (
7790 c(),
7791 d(
7792 e,
7793 f d(
7794 e,
7795 f
7796 )
7797 ˇ
7798 )
7799 );
7800 "});
7801
7802 // Copy an indented block, starting mid-line
7803 cx.set_state(indoc! {"
7804 const a: B = (
7805 c(),
7806 somethin«g(
7807 e,
7808 f
7809 )ˇ»
7810 );
7811 "});
7812 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7813
7814 // Paste it on a line with a lower indent level
7815 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7816 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7817 cx.assert_editor_state(indoc! {"
7818 const a: B = (
7819 c(),
7820 something(
7821 e,
7822 f
7823 )
7824 );
7825 g(
7826 e,
7827 f
7828 )ˇ"});
7829}
7830
7831#[gpui::test]
7832async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7833 init_test(cx, |_| {});
7834
7835 cx.write_to_clipboard(ClipboardItem::new_string(
7836 " d(\n e\n );\n".into(),
7837 ));
7838
7839 let mut cx = EditorTestContext::new(cx).await;
7840 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7841
7842 cx.set_state(indoc! {"
7843 fn a() {
7844 b();
7845 if c() {
7846 ˇ
7847 }
7848 }
7849 "});
7850
7851 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7852 cx.assert_editor_state(indoc! {"
7853 fn a() {
7854 b();
7855 if c() {
7856 d(
7857 e
7858 );
7859 ˇ
7860 }
7861 }
7862 "});
7863
7864 cx.set_state(indoc! {"
7865 fn a() {
7866 b();
7867 ˇ
7868 }
7869 "});
7870
7871 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7872 cx.assert_editor_state(indoc! {"
7873 fn a() {
7874 b();
7875 d(
7876 e
7877 );
7878 ˇ
7879 }
7880 "});
7881}
7882
7883#[gpui::test]
7884fn test_select_all(cx: &mut TestAppContext) {
7885 init_test(cx, |_| {});
7886
7887 let editor = cx.add_window(|window, cx| {
7888 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7889 build_editor(buffer, window, cx)
7890 });
7891 _ = editor.update(cx, |editor, window, cx| {
7892 editor.select_all(&SelectAll, window, cx);
7893 assert_eq!(
7894 display_ranges(editor, cx),
7895 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7896 );
7897 });
7898}
7899
7900#[gpui::test]
7901fn test_select_line(cx: &mut TestAppContext) {
7902 init_test(cx, |_| {});
7903
7904 let editor = cx.add_window(|window, cx| {
7905 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7906 build_editor(buffer, window, cx)
7907 });
7908 _ = editor.update(cx, |editor, window, cx| {
7909 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7910 s.select_display_ranges([
7911 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7912 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7913 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7914 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7915 ])
7916 });
7917 editor.select_line(&SelectLine, window, cx);
7918 // Adjacent line selections should NOT merge (only overlapping ones do)
7919 assert_eq!(
7920 display_ranges(editor, cx),
7921 vec![
7922 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0),
7923 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(2), 0),
7924 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7925 ]
7926 );
7927 });
7928
7929 _ = editor.update(cx, |editor, window, cx| {
7930 editor.select_line(&SelectLine, window, cx);
7931 assert_eq!(
7932 display_ranges(editor, cx),
7933 vec![
7934 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7935 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7936 ]
7937 );
7938 });
7939
7940 _ = editor.update(cx, |editor, window, cx| {
7941 editor.select_line(&SelectLine, window, cx);
7942 // Adjacent but not overlapping, so they stay separate
7943 assert_eq!(
7944 display_ranges(editor, cx),
7945 vec![
7946 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(4), 0),
7947 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7948 ]
7949 );
7950 });
7951}
7952
7953#[gpui::test]
7954async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7955 init_test(cx, |_| {});
7956 let mut cx = EditorTestContext::new(cx).await;
7957
7958 #[track_caller]
7959 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7960 cx.set_state(initial_state);
7961 cx.update_editor(|e, window, cx| {
7962 e.split_selection_into_lines(&Default::default(), window, cx)
7963 });
7964 cx.assert_editor_state(expected_state);
7965 }
7966
7967 // Selection starts and ends at the middle of lines, left-to-right
7968 test(
7969 &mut cx,
7970 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7971 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7972 );
7973 // Same thing, right-to-left
7974 test(
7975 &mut cx,
7976 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7977 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7978 );
7979
7980 // Whole buffer, left-to-right, last line *doesn't* end with newline
7981 test(
7982 &mut cx,
7983 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7984 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7985 );
7986 // Same thing, right-to-left
7987 test(
7988 &mut cx,
7989 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7990 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7991 );
7992
7993 // Whole buffer, left-to-right, last line ends with newline
7994 test(
7995 &mut cx,
7996 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7997 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7998 );
7999 // Same thing, right-to-left
8000 test(
8001 &mut cx,
8002 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
8003 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
8004 );
8005
8006 // Starts at the end of a line, ends at the start of another
8007 test(
8008 &mut cx,
8009 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
8010 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
8011 );
8012}
8013
8014#[gpui::test]
8015async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
8016 init_test(cx, |_| {});
8017
8018 let editor = cx.add_window(|window, cx| {
8019 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
8020 build_editor(buffer, window, cx)
8021 });
8022
8023 // setup
8024 _ = editor.update(cx, |editor, window, cx| {
8025 editor.fold_creases(
8026 vec![
8027 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
8028 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
8029 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
8030 ],
8031 true,
8032 window,
8033 cx,
8034 );
8035 assert_eq!(
8036 editor.display_text(cx),
8037 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
8038 );
8039 });
8040
8041 _ = editor.update(cx, |editor, window, cx| {
8042 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8043 s.select_display_ranges([
8044 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8045 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
8046 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
8047 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
8048 ])
8049 });
8050 editor.split_selection_into_lines(&Default::default(), window, cx);
8051 assert_eq!(
8052 editor.display_text(cx),
8053 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
8054 );
8055 });
8056 EditorTestContext::for_editor(editor, cx)
8057 .await
8058 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
8059
8060 _ = editor.update(cx, |editor, window, cx| {
8061 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8062 s.select_display_ranges([
8063 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
8064 ])
8065 });
8066 editor.split_selection_into_lines(&Default::default(), window, cx);
8067 assert_eq!(
8068 editor.display_text(cx),
8069 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
8070 );
8071 assert_eq!(
8072 display_ranges(editor, cx),
8073 [
8074 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
8075 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
8076 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
8077 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
8078 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
8079 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
8080 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
8081 ]
8082 );
8083 });
8084 EditorTestContext::for_editor(editor, cx)
8085 .await
8086 .assert_editor_state(
8087 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
8088 );
8089}
8090
8091#[gpui::test]
8092async fn test_add_selection_above_below(cx: &mut TestAppContext) {
8093 init_test(cx, |_| {});
8094
8095 let mut cx = EditorTestContext::new(cx).await;
8096
8097 cx.set_state(indoc!(
8098 r#"abc
8099 defˇghi
8100
8101 jk
8102 nlmo
8103 "#
8104 ));
8105
8106 cx.update_editor(|editor, window, cx| {
8107 editor.add_selection_above(&Default::default(), window, cx);
8108 });
8109
8110 cx.assert_editor_state(indoc!(
8111 r#"abcˇ
8112 defˇghi
8113
8114 jk
8115 nlmo
8116 "#
8117 ));
8118
8119 cx.update_editor(|editor, window, cx| {
8120 editor.add_selection_above(&Default::default(), window, cx);
8121 });
8122
8123 cx.assert_editor_state(indoc!(
8124 r#"abcˇ
8125 defˇghi
8126
8127 jk
8128 nlmo
8129 "#
8130 ));
8131
8132 cx.update_editor(|editor, window, cx| {
8133 editor.add_selection_below(&Default::default(), window, cx);
8134 });
8135
8136 cx.assert_editor_state(indoc!(
8137 r#"abc
8138 defˇghi
8139
8140 jk
8141 nlmo
8142 "#
8143 ));
8144
8145 cx.update_editor(|editor, window, cx| {
8146 editor.undo_selection(&Default::default(), window, cx);
8147 });
8148
8149 cx.assert_editor_state(indoc!(
8150 r#"abcˇ
8151 defˇghi
8152
8153 jk
8154 nlmo
8155 "#
8156 ));
8157
8158 cx.update_editor(|editor, window, cx| {
8159 editor.redo_selection(&Default::default(), window, cx);
8160 });
8161
8162 cx.assert_editor_state(indoc!(
8163 r#"abc
8164 defˇghi
8165
8166 jk
8167 nlmo
8168 "#
8169 ));
8170
8171 cx.update_editor(|editor, window, cx| {
8172 editor.add_selection_below(&Default::default(), window, cx);
8173 });
8174
8175 cx.assert_editor_state(indoc!(
8176 r#"abc
8177 defˇghi
8178 ˇ
8179 jk
8180 nlmo
8181 "#
8182 ));
8183
8184 cx.update_editor(|editor, window, cx| {
8185 editor.add_selection_below(&Default::default(), window, cx);
8186 });
8187
8188 cx.assert_editor_state(indoc!(
8189 r#"abc
8190 defˇghi
8191 ˇ
8192 jkˇ
8193 nlmo
8194 "#
8195 ));
8196
8197 cx.update_editor(|editor, window, cx| {
8198 editor.add_selection_below(&Default::default(), window, cx);
8199 });
8200
8201 cx.assert_editor_state(indoc!(
8202 r#"abc
8203 defˇghi
8204 ˇ
8205 jkˇ
8206 nlmˇo
8207 "#
8208 ));
8209
8210 cx.update_editor(|editor, window, cx| {
8211 editor.add_selection_below(&Default::default(), window, cx);
8212 });
8213
8214 cx.assert_editor_state(indoc!(
8215 r#"abc
8216 defˇghi
8217 ˇ
8218 jkˇ
8219 nlmˇo
8220 ˇ"#
8221 ));
8222
8223 // change selections
8224 cx.set_state(indoc!(
8225 r#"abc
8226 def«ˇg»hi
8227
8228 jk
8229 nlmo
8230 "#
8231 ));
8232
8233 cx.update_editor(|editor, window, cx| {
8234 editor.add_selection_below(&Default::default(), window, cx);
8235 });
8236
8237 cx.assert_editor_state(indoc!(
8238 r#"abc
8239 def«ˇg»hi
8240
8241 jk
8242 nlm«ˇo»
8243 "#
8244 ));
8245
8246 cx.update_editor(|editor, window, cx| {
8247 editor.add_selection_below(&Default::default(), window, cx);
8248 });
8249
8250 cx.assert_editor_state(indoc!(
8251 r#"abc
8252 def«ˇg»hi
8253
8254 jk
8255 nlm«ˇo»
8256 "#
8257 ));
8258
8259 cx.update_editor(|editor, window, cx| {
8260 editor.add_selection_above(&Default::default(), window, cx);
8261 });
8262
8263 cx.assert_editor_state(indoc!(
8264 r#"abc
8265 def«ˇg»hi
8266
8267 jk
8268 nlmo
8269 "#
8270 ));
8271
8272 cx.update_editor(|editor, window, cx| {
8273 editor.add_selection_above(&Default::default(), window, cx);
8274 });
8275
8276 cx.assert_editor_state(indoc!(
8277 r#"abc
8278 def«ˇg»hi
8279
8280 jk
8281 nlmo
8282 "#
8283 ));
8284
8285 // Change selections again
8286 cx.set_state(indoc!(
8287 r#"a«bc
8288 defgˇ»hi
8289
8290 jk
8291 nlmo
8292 "#
8293 ));
8294
8295 cx.update_editor(|editor, window, cx| {
8296 editor.add_selection_below(&Default::default(), window, cx);
8297 });
8298
8299 cx.assert_editor_state(indoc!(
8300 r#"a«bcˇ»
8301 d«efgˇ»hi
8302
8303 j«kˇ»
8304 nlmo
8305 "#
8306 ));
8307
8308 cx.update_editor(|editor, window, cx| {
8309 editor.add_selection_below(&Default::default(), window, cx);
8310 });
8311 cx.assert_editor_state(indoc!(
8312 r#"a«bcˇ»
8313 d«efgˇ»hi
8314
8315 j«kˇ»
8316 n«lmoˇ»
8317 "#
8318 ));
8319 cx.update_editor(|editor, window, cx| {
8320 editor.add_selection_above(&Default::default(), window, cx);
8321 });
8322
8323 cx.assert_editor_state(indoc!(
8324 r#"a«bcˇ»
8325 d«efgˇ»hi
8326
8327 j«kˇ»
8328 nlmo
8329 "#
8330 ));
8331
8332 // Change selections again
8333 cx.set_state(indoc!(
8334 r#"abc
8335 d«ˇefghi
8336
8337 jk
8338 nlm»o
8339 "#
8340 ));
8341
8342 cx.update_editor(|editor, window, cx| {
8343 editor.add_selection_above(&Default::default(), window, cx);
8344 });
8345
8346 cx.assert_editor_state(indoc!(
8347 r#"a«ˇbc»
8348 d«ˇef»ghi
8349
8350 j«ˇk»
8351 n«ˇlm»o
8352 "#
8353 ));
8354
8355 cx.update_editor(|editor, window, cx| {
8356 editor.add_selection_below(&Default::default(), window, cx);
8357 });
8358
8359 cx.assert_editor_state(indoc!(
8360 r#"abc
8361 d«ˇef»ghi
8362
8363 j«ˇk»
8364 n«ˇlm»o
8365 "#
8366 ));
8367
8368 // Assert that the oldest selection's goal column is used when adding more
8369 // selections, not the most recently added selection's actual column.
8370 cx.set_state(indoc! {"
8371 foo bar bazˇ
8372 foo
8373 foo bar
8374 "});
8375
8376 cx.update_editor(|editor, window, cx| {
8377 editor.add_selection_below(
8378 &AddSelectionBelow {
8379 skip_soft_wrap: true,
8380 },
8381 window,
8382 cx,
8383 );
8384 });
8385
8386 cx.assert_editor_state(indoc! {"
8387 foo bar bazˇ
8388 fooˇ
8389 foo bar
8390 "});
8391
8392 cx.update_editor(|editor, window, cx| {
8393 editor.add_selection_below(
8394 &AddSelectionBelow {
8395 skip_soft_wrap: true,
8396 },
8397 window,
8398 cx,
8399 );
8400 });
8401
8402 cx.assert_editor_state(indoc! {"
8403 foo bar bazˇ
8404 fooˇ
8405 foo barˇ
8406 "});
8407
8408 cx.set_state(indoc! {"
8409 foo bar baz
8410 foo
8411 foo barˇ
8412 "});
8413
8414 cx.update_editor(|editor, window, cx| {
8415 editor.add_selection_above(
8416 &AddSelectionAbove {
8417 skip_soft_wrap: true,
8418 },
8419 window,
8420 cx,
8421 );
8422 });
8423
8424 cx.assert_editor_state(indoc! {"
8425 foo bar baz
8426 fooˇ
8427 foo barˇ
8428 "});
8429
8430 cx.update_editor(|editor, window, cx| {
8431 editor.add_selection_above(
8432 &AddSelectionAbove {
8433 skip_soft_wrap: true,
8434 },
8435 window,
8436 cx,
8437 );
8438 });
8439
8440 cx.assert_editor_state(indoc! {"
8441 foo barˇ baz
8442 fooˇ
8443 foo barˇ
8444 "});
8445}
8446
8447#[gpui::test]
8448async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8449 init_test(cx, |_| {});
8450 let mut cx = EditorTestContext::new(cx).await;
8451
8452 cx.set_state(indoc!(
8453 r#"line onˇe
8454 liˇne two
8455 line three
8456 line four"#
8457 ));
8458
8459 cx.update_editor(|editor, window, cx| {
8460 editor.add_selection_below(&Default::default(), window, cx);
8461 });
8462
8463 // test multiple cursors expand in the same direction
8464 cx.assert_editor_state(indoc!(
8465 r#"line onˇe
8466 liˇne twˇo
8467 liˇne three
8468 line four"#
8469 ));
8470
8471 cx.update_editor(|editor, window, cx| {
8472 editor.add_selection_below(&Default::default(), window, cx);
8473 });
8474
8475 cx.update_editor(|editor, window, cx| {
8476 editor.add_selection_below(&Default::default(), window, cx);
8477 });
8478
8479 // test multiple cursors expand below overflow
8480 cx.assert_editor_state(indoc!(
8481 r#"line onˇe
8482 liˇne twˇo
8483 liˇne thˇree
8484 liˇne foˇur"#
8485 ));
8486
8487 cx.update_editor(|editor, window, cx| {
8488 editor.add_selection_above(&Default::default(), window, cx);
8489 });
8490
8491 // test multiple cursors retrieves back correctly
8492 cx.assert_editor_state(indoc!(
8493 r#"line onˇe
8494 liˇne twˇo
8495 liˇne thˇree
8496 line four"#
8497 ));
8498
8499 cx.update_editor(|editor, window, cx| {
8500 editor.add_selection_above(&Default::default(), window, cx);
8501 });
8502
8503 cx.update_editor(|editor, window, cx| {
8504 editor.add_selection_above(&Default::default(), window, cx);
8505 });
8506
8507 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8508 cx.assert_editor_state(indoc!(
8509 r#"liˇne onˇe
8510 liˇne two
8511 line three
8512 line four"#
8513 ));
8514
8515 cx.update_editor(|editor, window, cx| {
8516 editor.undo_selection(&Default::default(), window, cx);
8517 });
8518
8519 // test undo
8520 cx.assert_editor_state(indoc!(
8521 r#"line onˇe
8522 liˇne twˇo
8523 line three
8524 line four"#
8525 ));
8526
8527 cx.update_editor(|editor, window, cx| {
8528 editor.redo_selection(&Default::default(), window, cx);
8529 });
8530
8531 // test redo
8532 cx.assert_editor_state(indoc!(
8533 r#"liˇne onˇe
8534 liˇne two
8535 line three
8536 line four"#
8537 ));
8538
8539 cx.set_state(indoc!(
8540 r#"abcd
8541 ef«ghˇ»
8542 ijkl
8543 «mˇ»nop"#
8544 ));
8545
8546 cx.update_editor(|editor, window, cx| {
8547 editor.add_selection_above(&Default::default(), window, cx);
8548 });
8549
8550 // test multiple selections expand in the same direction
8551 cx.assert_editor_state(indoc!(
8552 r#"ab«cdˇ»
8553 ef«ghˇ»
8554 «iˇ»jkl
8555 «mˇ»nop"#
8556 ));
8557
8558 cx.update_editor(|editor, window, cx| {
8559 editor.add_selection_above(&Default::default(), window, cx);
8560 });
8561
8562 // test multiple selection upward overflow
8563 cx.assert_editor_state(indoc!(
8564 r#"ab«cdˇ»
8565 «eˇ»f«ghˇ»
8566 «iˇ»jkl
8567 «mˇ»nop"#
8568 ));
8569
8570 cx.update_editor(|editor, window, cx| {
8571 editor.add_selection_below(&Default::default(), window, cx);
8572 });
8573
8574 // test multiple selection retrieves back correctly
8575 cx.assert_editor_state(indoc!(
8576 r#"abcd
8577 ef«ghˇ»
8578 «iˇ»jkl
8579 «mˇ»nop"#
8580 ));
8581
8582 cx.update_editor(|editor, window, cx| {
8583 editor.add_selection_below(&Default::default(), window, cx);
8584 });
8585
8586 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8587 cx.assert_editor_state(indoc!(
8588 r#"abcd
8589 ef«ghˇ»
8590 ij«klˇ»
8591 «mˇ»nop"#
8592 ));
8593
8594 cx.update_editor(|editor, window, cx| {
8595 editor.undo_selection(&Default::default(), window, cx);
8596 });
8597
8598 // test undo
8599 cx.assert_editor_state(indoc!(
8600 r#"abcd
8601 ef«ghˇ»
8602 «iˇ»jkl
8603 «mˇ»nop"#
8604 ));
8605
8606 cx.update_editor(|editor, window, cx| {
8607 editor.redo_selection(&Default::default(), window, cx);
8608 });
8609
8610 // test redo
8611 cx.assert_editor_state(indoc!(
8612 r#"abcd
8613 ef«ghˇ»
8614 ij«klˇ»
8615 «mˇ»nop"#
8616 ));
8617}
8618
8619#[gpui::test]
8620async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8621 init_test(cx, |_| {});
8622 let mut cx = EditorTestContext::new(cx).await;
8623
8624 cx.set_state(indoc!(
8625 r#"line onˇe
8626 liˇne two
8627 line three
8628 line four"#
8629 ));
8630
8631 cx.update_editor(|editor, window, cx| {
8632 editor.add_selection_below(&Default::default(), window, cx);
8633 editor.add_selection_below(&Default::default(), window, cx);
8634 editor.add_selection_below(&Default::default(), window, cx);
8635 });
8636
8637 // initial state with two multi cursor groups
8638 cx.assert_editor_state(indoc!(
8639 r#"line onˇe
8640 liˇne twˇo
8641 liˇne thˇree
8642 liˇne foˇur"#
8643 ));
8644
8645 // add single cursor in middle - simulate opt click
8646 cx.update_editor(|editor, window, cx| {
8647 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8648 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8649 editor.end_selection(window, cx);
8650 });
8651
8652 cx.assert_editor_state(indoc!(
8653 r#"line onˇe
8654 liˇne twˇo
8655 liˇneˇ thˇree
8656 liˇne foˇur"#
8657 ));
8658
8659 cx.update_editor(|editor, window, cx| {
8660 editor.add_selection_above(&Default::default(), window, cx);
8661 });
8662
8663 // test new added selection expands above and existing selection shrinks
8664 cx.assert_editor_state(indoc!(
8665 r#"line onˇe
8666 liˇneˇ twˇo
8667 liˇneˇ thˇree
8668 line four"#
8669 ));
8670
8671 cx.update_editor(|editor, window, cx| {
8672 editor.add_selection_above(&Default::default(), window, cx);
8673 });
8674
8675 // test new added selection expands above and existing selection shrinks
8676 cx.assert_editor_state(indoc!(
8677 r#"lineˇ onˇe
8678 liˇneˇ twˇo
8679 lineˇ three
8680 line four"#
8681 ));
8682
8683 // intial state with two selection groups
8684 cx.set_state(indoc!(
8685 r#"abcd
8686 ef«ghˇ»
8687 ijkl
8688 «mˇ»nop"#
8689 ));
8690
8691 cx.update_editor(|editor, window, cx| {
8692 editor.add_selection_above(&Default::default(), window, cx);
8693 editor.add_selection_above(&Default::default(), window, cx);
8694 });
8695
8696 cx.assert_editor_state(indoc!(
8697 r#"ab«cdˇ»
8698 «eˇ»f«ghˇ»
8699 «iˇ»jkl
8700 «mˇ»nop"#
8701 ));
8702
8703 // add single selection in middle - simulate opt drag
8704 cx.update_editor(|editor, window, cx| {
8705 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8706 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8707 editor.update_selection(
8708 DisplayPoint::new(DisplayRow(2), 4),
8709 0,
8710 gpui::Point::<f32>::default(),
8711 window,
8712 cx,
8713 );
8714 editor.end_selection(window, cx);
8715 });
8716
8717 cx.assert_editor_state(indoc!(
8718 r#"ab«cdˇ»
8719 «eˇ»f«ghˇ»
8720 «iˇ»jk«lˇ»
8721 «mˇ»nop"#
8722 ));
8723
8724 cx.update_editor(|editor, window, cx| {
8725 editor.add_selection_below(&Default::default(), window, cx);
8726 });
8727
8728 // test new added selection expands below, others shrinks from above
8729 cx.assert_editor_state(indoc!(
8730 r#"abcd
8731 ef«ghˇ»
8732 «iˇ»jk«lˇ»
8733 «mˇ»no«pˇ»"#
8734 ));
8735}
8736
8737#[gpui::test]
8738async fn test_select_next(cx: &mut TestAppContext) {
8739 init_test(cx, |_| {});
8740 let mut cx = EditorTestContext::new(cx).await;
8741
8742 // Enable case sensitive search.
8743 update_test_editor_settings(&mut cx, |settings| {
8744 let mut search_settings = SearchSettingsContent::default();
8745 search_settings.case_sensitive = Some(true);
8746 settings.search = Some(search_settings);
8747 });
8748
8749 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8750
8751 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8752 .unwrap();
8753 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8754
8755 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8756 .unwrap();
8757 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8758
8759 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8760 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8761
8762 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8763 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8764
8765 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8766 .unwrap();
8767 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8768
8769 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8770 .unwrap();
8771 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8772
8773 // Test selection direction should be preserved
8774 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8775
8776 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8777 .unwrap();
8778 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8779
8780 // Test case sensitivity
8781 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8782 cx.update_editor(|e, window, cx| {
8783 e.select_next(&SelectNext::default(), window, cx).unwrap();
8784 });
8785 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8786
8787 // Disable case sensitive search.
8788 update_test_editor_settings(&mut cx, |settings| {
8789 let mut search_settings = SearchSettingsContent::default();
8790 search_settings.case_sensitive = Some(false);
8791 settings.search = Some(search_settings);
8792 });
8793
8794 cx.set_state("«ˇfoo»\nFOO\nFoo");
8795 cx.update_editor(|e, window, cx| {
8796 e.select_next(&SelectNext::default(), window, cx).unwrap();
8797 e.select_next(&SelectNext::default(), window, cx).unwrap();
8798 });
8799 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8800}
8801
8802#[gpui::test]
8803async fn test_select_all_matches(cx: &mut TestAppContext) {
8804 init_test(cx, |_| {});
8805 let mut cx = EditorTestContext::new(cx).await;
8806
8807 // Enable case sensitive search.
8808 update_test_editor_settings(&mut cx, |settings| {
8809 let mut search_settings = SearchSettingsContent::default();
8810 search_settings.case_sensitive = Some(true);
8811 settings.search = Some(search_settings);
8812 });
8813
8814 // Test caret-only selections
8815 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8816 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8817 .unwrap();
8818 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8819
8820 // Test left-to-right selections
8821 cx.set_state("abc\n«abcˇ»\nabc");
8822 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8823 .unwrap();
8824 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8825
8826 // Test right-to-left selections
8827 cx.set_state("abc\n«ˇabc»\nabc");
8828 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8829 .unwrap();
8830 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8831
8832 // Test selecting whitespace with caret selection
8833 cx.set_state("abc\nˇ abc\nabc");
8834 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8835 .unwrap();
8836 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8837
8838 // Test selecting whitespace with left-to-right selection
8839 cx.set_state("abc\n«ˇ »abc\nabc");
8840 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8841 .unwrap();
8842 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8843
8844 // Test no matches with right-to-left selection
8845 cx.set_state("abc\n« ˇ»abc\nabc");
8846 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8847 .unwrap();
8848 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8849
8850 // Test with a single word and clip_at_line_ends=true (#29823)
8851 cx.set_state("aˇbc");
8852 cx.update_editor(|e, window, cx| {
8853 e.set_clip_at_line_ends(true, cx);
8854 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8855 e.set_clip_at_line_ends(false, cx);
8856 });
8857 cx.assert_editor_state("«abcˇ»");
8858
8859 // Test case sensitivity
8860 cx.set_state("fˇoo\nFOO\nFoo");
8861 cx.update_editor(|e, window, cx| {
8862 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8863 });
8864 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8865
8866 // Disable case sensitive search.
8867 update_test_editor_settings(&mut cx, |settings| {
8868 let mut search_settings = SearchSettingsContent::default();
8869 search_settings.case_sensitive = Some(false);
8870 settings.search = Some(search_settings);
8871 });
8872
8873 cx.set_state("fˇoo\nFOO\nFoo");
8874 cx.update_editor(|e, window, cx| {
8875 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8876 });
8877 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
8878}
8879
8880#[gpui::test]
8881async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8882 init_test(cx, |_| {});
8883
8884 let mut cx = EditorTestContext::new(cx).await;
8885
8886 let large_body_1 = "\nd".repeat(200);
8887 let large_body_2 = "\ne".repeat(200);
8888
8889 cx.set_state(&format!(
8890 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8891 ));
8892 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8893 let scroll_position = editor.scroll_position(cx);
8894 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8895 scroll_position
8896 });
8897
8898 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8899 .unwrap();
8900 cx.assert_editor_state(&format!(
8901 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8902 ));
8903 let scroll_position_after_selection =
8904 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8905 assert_eq!(
8906 initial_scroll_position, scroll_position_after_selection,
8907 "Scroll position should not change after selecting all matches"
8908 );
8909}
8910
8911#[gpui::test]
8912async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8913 init_test(cx, |_| {});
8914
8915 let mut cx = EditorLspTestContext::new_rust(
8916 lsp::ServerCapabilities {
8917 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8918 ..Default::default()
8919 },
8920 cx,
8921 )
8922 .await;
8923
8924 cx.set_state(indoc! {"
8925 line 1
8926 line 2
8927 linˇe 3
8928 line 4
8929 line 5
8930 "});
8931
8932 // Make an edit
8933 cx.update_editor(|editor, window, cx| {
8934 editor.handle_input("X", window, cx);
8935 });
8936
8937 // Move cursor to a different position
8938 cx.update_editor(|editor, window, cx| {
8939 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8940 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8941 });
8942 });
8943
8944 cx.assert_editor_state(indoc! {"
8945 line 1
8946 line 2
8947 linXe 3
8948 line 4
8949 liˇne 5
8950 "});
8951
8952 cx.lsp
8953 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8954 Ok(Some(vec![lsp::TextEdit::new(
8955 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8956 "PREFIX ".to_string(),
8957 )]))
8958 });
8959
8960 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8961 .unwrap()
8962 .await
8963 .unwrap();
8964
8965 cx.assert_editor_state(indoc! {"
8966 PREFIX line 1
8967 line 2
8968 linXe 3
8969 line 4
8970 liˇne 5
8971 "});
8972
8973 // Undo formatting
8974 cx.update_editor(|editor, window, cx| {
8975 editor.undo(&Default::default(), window, cx);
8976 });
8977
8978 // Verify cursor moved back to position after edit
8979 cx.assert_editor_state(indoc! {"
8980 line 1
8981 line 2
8982 linXˇe 3
8983 line 4
8984 line 5
8985 "});
8986}
8987
8988#[gpui::test]
8989async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8990 init_test(cx, |_| {});
8991
8992 let mut cx = EditorTestContext::new(cx).await;
8993
8994 let provider = cx.new(|_| FakeEditPredictionDelegate::default());
8995 cx.update_editor(|editor, window, cx| {
8996 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8997 });
8998
8999 cx.set_state(indoc! {"
9000 line 1
9001 line 2
9002 linˇe 3
9003 line 4
9004 line 5
9005 line 6
9006 line 7
9007 line 8
9008 line 9
9009 line 10
9010 "});
9011
9012 let snapshot = cx.buffer_snapshot();
9013 let edit_position = snapshot.anchor_after(Point::new(2, 4));
9014
9015 cx.update(|_, cx| {
9016 provider.update(cx, |provider, _| {
9017 provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
9018 id: None,
9019 edits: vec![(edit_position..edit_position, "X".into())],
9020 edit_preview: None,
9021 }))
9022 })
9023 });
9024
9025 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
9026 cx.update_editor(|editor, window, cx| {
9027 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
9028 });
9029
9030 cx.assert_editor_state(indoc! {"
9031 line 1
9032 line 2
9033 lineXˇ 3
9034 line 4
9035 line 5
9036 line 6
9037 line 7
9038 line 8
9039 line 9
9040 line 10
9041 "});
9042
9043 cx.update_editor(|editor, window, cx| {
9044 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9045 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
9046 });
9047 });
9048
9049 cx.assert_editor_state(indoc! {"
9050 line 1
9051 line 2
9052 lineX 3
9053 line 4
9054 line 5
9055 line 6
9056 line 7
9057 line 8
9058 line 9
9059 liˇne 10
9060 "});
9061
9062 cx.update_editor(|editor, window, cx| {
9063 editor.undo(&Default::default(), window, cx);
9064 });
9065
9066 cx.assert_editor_state(indoc! {"
9067 line 1
9068 line 2
9069 lineˇ 3
9070 line 4
9071 line 5
9072 line 6
9073 line 7
9074 line 8
9075 line 9
9076 line 10
9077 "});
9078}
9079
9080#[gpui::test]
9081async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
9082 init_test(cx, |_| {});
9083
9084 let mut cx = EditorTestContext::new(cx).await;
9085 cx.set_state(
9086 r#"let foo = 2;
9087lˇet foo = 2;
9088let fooˇ = 2;
9089let foo = 2;
9090let foo = ˇ2;"#,
9091 );
9092
9093 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9094 .unwrap();
9095 cx.assert_editor_state(
9096 r#"let foo = 2;
9097«letˇ» foo = 2;
9098let «fooˇ» = 2;
9099let foo = 2;
9100let foo = «2ˇ»;"#,
9101 );
9102
9103 // noop for multiple selections with different contents
9104 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9105 .unwrap();
9106 cx.assert_editor_state(
9107 r#"let foo = 2;
9108«letˇ» foo = 2;
9109let «fooˇ» = 2;
9110let foo = 2;
9111let foo = «2ˇ»;"#,
9112 );
9113
9114 // Test last selection direction should be preserved
9115 cx.set_state(
9116 r#"let foo = 2;
9117let foo = 2;
9118let «fooˇ» = 2;
9119let «ˇfoo» = 2;
9120let foo = 2;"#,
9121 );
9122
9123 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9124 .unwrap();
9125 cx.assert_editor_state(
9126 r#"let foo = 2;
9127let foo = 2;
9128let «fooˇ» = 2;
9129let «ˇfoo» = 2;
9130let «ˇfoo» = 2;"#,
9131 );
9132}
9133
9134#[gpui::test]
9135async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
9136 init_test(cx, |_| {});
9137
9138 let mut cx =
9139 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
9140
9141 cx.assert_editor_state(indoc! {"
9142 ˇbbb
9143 ccc
9144
9145 bbb
9146 ccc
9147 "});
9148 cx.dispatch_action(SelectPrevious::default());
9149 cx.assert_editor_state(indoc! {"
9150 «bbbˇ»
9151 ccc
9152
9153 bbb
9154 ccc
9155 "});
9156 cx.dispatch_action(SelectPrevious::default());
9157 cx.assert_editor_state(indoc! {"
9158 «bbbˇ»
9159 ccc
9160
9161 «bbbˇ»
9162 ccc
9163 "});
9164}
9165
9166#[gpui::test]
9167async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
9168 init_test(cx, |_| {});
9169
9170 let mut cx = EditorTestContext::new(cx).await;
9171 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
9172
9173 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9174 .unwrap();
9175 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9176
9177 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9178 .unwrap();
9179 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
9180
9181 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9182 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9183
9184 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9185 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
9186
9187 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9188 .unwrap();
9189 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
9190
9191 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9192 .unwrap();
9193 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
9194}
9195
9196#[gpui::test]
9197async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
9198 init_test(cx, |_| {});
9199
9200 let mut cx = EditorTestContext::new(cx).await;
9201 cx.set_state("aˇ");
9202
9203 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9204 .unwrap();
9205 cx.assert_editor_state("«aˇ»");
9206 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9207 .unwrap();
9208 cx.assert_editor_state("«aˇ»");
9209}
9210
9211#[gpui::test]
9212async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
9213 init_test(cx, |_| {});
9214
9215 let mut cx = EditorTestContext::new(cx).await;
9216 cx.set_state(
9217 r#"let foo = 2;
9218lˇet foo = 2;
9219let fooˇ = 2;
9220let foo = 2;
9221let foo = ˇ2;"#,
9222 );
9223
9224 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9225 .unwrap();
9226 cx.assert_editor_state(
9227 r#"let foo = 2;
9228«letˇ» foo = 2;
9229let «fooˇ» = 2;
9230let foo = 2;
9231let foo = «2ˇ»;"#,
9232 );
9233
9234 // noop for multiple selections with different contents
9235 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9236 .unwrap();
9237 cx.assert_editor_state(
9238 r#"let foo = 2;
9239«letˇ» foo = 2;
9240let «fooˇ» = 2;
9241let foo = 2;
9242let foo = «2ˇ»;"#,
9243 );
9244}
9245
9246#[gpui::test]
9247async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
9248 init_test(cx, |_| {});
9249 let mut cx = EditorTestContext::new(cx).await;
9250
9251 // Enable case sensitive search.
9252 update_test_editor_settings(&mut cx, |settings| {
9253 let mut search_settings = SearchSettingsContent::default();
9254 search_settings.case_sensitive = Some(true);
9255 settings.search = Some(search_settings);
9256 });
9257
9258 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
9259
9260 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9261 .unwrap();
9262 // selection direction is preserved
9263 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9264
9265 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9266 .unwrap();
9267 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9268
9269 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9270 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9271
9272 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9273 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9274
9275 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9276 .unwrap();
9277 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
9278
9279 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9280 .unwrap();
9281 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
9282
9283 // Test case sensitivity
9284 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
9285 cx.update_editor(|e, window, cx| {
9286 e.select_previous(&SelectPrevious::default(), window, cx)
9287 .unwrap();
9288 e.select_previous(&SelectPrevious::default(), window, cx)
9289 .unwrap();
9290 });
9291 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
9292
9293 // Disable case sensitive search.
9294 update_test_editor_settings(&mut cx, |settings| {
9295 let mut search_settings = SearchSettingsContent::default();
9296 search_settings.case_sensitive = Some(false);
9297 settings.search = Some(search_settings);
9298 });
9299
9300 cx.set_state("foo\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»\n«ˇFOO»\n«ˇFoo»");
9308}
9309
9310#[gpui::test]
9311async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
9312 init_test(cx, |_| {});
9313
9314 let language = Arc::new(Language::new(
9315 LanguageConfig::default(),
9316 Some(tree_sitter_rust::LANGUAGE.into()),
9317 ));
9318
9319 let text = r#"
9320 use mod1::mod2::{mod3, mod4};
9321
9322 fn fn_1(param1: bool, param2: &str) {
9323 let var1 = "text";
9324 }
9325 "#
9326 .unindent();
9327
9328 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9329 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9330 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9331
9332 editor
9333 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9334 .await;
9335
9336 editor.update_in(cx, |editor, window, cx| {
9337 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9338 s.select_display_ranges([
9339 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
9340 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
9341 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
9342 ]);
9343 });
9344 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9345 });
9346 editor.update(cx, |editor, cx| {
9347 assert_text_with_selections(
9348 editor,
9349 indoc! {r#"
9350 use mod1::mod2::{mod3, «mod4ˇ»};
9351
9352 fn fn_1«ˇ(param1: bool, param2: &str)» {
9353 let var1 = "«ˇtext»";
9354 }
9355 "#},
9356 cx,
9357 );
9358 });
9359
9360 editor.update_in(cx, |editor, window, cx| {
9361 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9362 });
9363 editor.update(cx, |editor, cx| {
9364 assert_text_with_selections(
9365 editor,
9366 indoc! {r#"
9367 use mod1::mod2::«{mod3, mod4}ˇ»;
9368
9369 «ˇfn fn_1(param1: bool, param2: &str) {
9370 let var1 = "text";
9371 }»
9372 "#},
9373 cx,
9374 );
9375 });
9376
9377 editor.update_in(cx, |editor, window, cx| {
9378 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9379 });
9380 assert_eq!(
9381 editor.update(cx, |editor, cx| editor
9382 .selections
9383 .display_ranges(&editor.display_snapshot(cx))),
9384 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9385 );
9386
9387 // Trying to expand the selected syntax node one more time has no effect.
9388 editor.update_in(cx, |editor, window, cx| {
9389 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9390 });
9391 assert_eq!(
9392 editor.update(cx, |editor, cx| editor
9393 .selections
9394 .display_ranges(&editor.display_snapshot(cx))),
9395 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9396 );
9397
9398 editor.update_in(cx, |editor, window, cx| {
9399 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9400 });
9401 editor.update(cx, |editor, cx| {
9402 assert_text_with_selections(
9403 editor,
9404 indoc! {r#"
9405 use mod1::mod2::«{mod3, mod4}ˇ»;
9406
9407 «ˇfn fn_1(param1: bool, param2: &str) {
9408 let var1 = "text";
9409 }»
9410 "#},
9411 cx,
9412 );
9413 });
9414
9415 editor.update_in(cx, |editor, window, cx| {
9416 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9417 });
9418 editor.update(cx, |editor, cx| {
9419 assert_text_with_selections(
9420 editor,
9421 indoc! {r#"
9422 use mod1::mod2::{mod3, «mod4ˇ»};
9423
9424 fn fn_1«ˇ(param1: bool, param2: &str)» {
9425 let var1 = "«ˇtext»";
9426 }
9427 "#},
9428 cx,
9429 );
9430 });
9431
9432 editor.update_in(cx, |editor, window, cx| {
9433 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9434 });
9435 editor.update(cx, |editor, cx| {
9436 assert_text_with_selections(
9437 editor,
9438 indoc! {r#"
9439 use mod1::mod2::{mod3, moˇd4};
9440
9441 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9442 let var1 = "teˇxt";
9443 }
9444 "#},
9445 cx,
9446 );
9447 });
9448
9449 // Trying to shrink the selected syntax node one more time has no effect.
9450 editor.update_in(cx, |editor, window, cx| {
9451 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9452 });
9453 editor.update_in(cx, |editor, _, cx| {
9454 assert_text_with_selections(
9455 editor,
9456 indoc! {r#"
9457 use mod1::mod2::{mod3, moˇd4};
9458
9459 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9460 let var1 = "teˇxt";
9461 }
9462 "#},
9463 cx,
9464 );
9465 });
9466
9467 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9468 // a fold.
9469 editor.update_in(cx, |editor, window, cx| {
9470 editor.fold_creases(
9471 vec![
9472 Crease::simple(
9473 Point::new(0, 21)..Point::new(0, 24),
9474 FoldPlaceholder::test(),
9475 ),
9476 Crease::simple(
9477 Point::new(3, 20)..Point::new(3, 22),
9478 FoldPlaceholder::test(),
9479 ),
9480 ],
9481 true,
9482 window,
9483 cx,
9484 );
9485 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9486 });
9487 editor.update(cx, |editor, cx| {
9488 assert_text_with_selections(
9489 editor,
9490 indoc! {r#"
9491 use mod1::mod2::«{mod3, mod4}ˇ»;
9492
9493 fn fn_1«ˇ(param1: bool, param2: &str)» {
9494 let var1 = "«ˇtext»";
9495 }
9496 "#},
9497 cx,
9498 );
9499 });
9500}
9501
9502#[gpui::test]
9503async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9504 init_test(cx, |_| {});
9505
9506 let language = Arc::new(Language::new(
9507 LanguageConfig::default(),
9508 Some(tree_sitter_rust::LANGUAGE.into()),
9509 ));
9510
9511 let text = "let a = 2;";
9512
9513 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9514 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9515 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9516
9517 editor
9518 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9519 .await;
9520
9521 // Test case 1: Cursor at end of word
9522 editor.update_in(cx, |editor, window, cx| {
9523 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9524 s.select_display_ranges([
9525 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9526 ]);
9527 });
9528 });
9529 editor.update(cx, |editor, cx| {
9530 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9531 });
9532 editor.update_in(cx, |editor, window, cx| {
9533 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9534 });
9535 editor.update(cx, |editor, cx| {
9536 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9537 });
9538 editor.update_in(cx, |editor, window, cx| {
9539 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9540 });
9541 editor.update(cx, |editor, cx| {
9542 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9543 });
9544
9545 // Test case 2: Cursor at end of statement
9546 editor.update_in(cx, |editor, window, cx| {
9547 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9548 s.select_display_ranges([
9549 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9550 ]);
9551 });
9552 });
9553 editor.update(cx, |editor, cx| {
9554 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9555 });
9556 editor.update_in(cx, |editor, window, cx| {
9557 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9558 });
9559 editor.update(cx, |editor, cx| {
9560 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9561 });
9562}
9563
9564#[gpui::test]
9565async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9566 init_test(cx, |_| {});
9567
9568 let language = Arc::new(Language::new(
9569 LanguageConfig {
9570 name: "JavaScript".into(),
9571 ..Default::default()
9572 },
9573 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9574 ));
9575
9576 let text = r#"
9577 let a = {
9578 key: "value",
9579 };
9580 "#
9581 .unindent();
9582
9583 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9584 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9585 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9586
9587 editor
9588 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9589 .await;
9590
9591 // Test case 1: Cursor after '{'
9592 editor.update_in(cx, |editor, window, cx| {
9593 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9594 s.select_display_ranges([
9595 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9596 ]);
9597 });
9598 });
9599 editor.update(cx, |editor, cx| {
9600 assert_text_with_selections(
9601 editor,
9602 indoc! {r#"
9603 let a = {ˇ
9604 key: "value",
9605 };
9606 "#},
9607 cx,
9608 );
9609 });
9610 editor.update_in(cx, |editor, window, cx| {
9611 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9612 });
9613 editor.update(cx, |editor, cx| {
9614 assert_text_with_selections(
9615 editor,
9616 indoc! {r#"
9617 let a = «ˇ{
9618 key: "value",
9619 }»;
9620 "#},
9621 cx,
9622 );
9623 });
9624
9625 // Test case 2: Cursor after ':'
9626 editor.update_in(cx, |editor, window, cx| {
9627 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9628 s.select_display_ranges([
9629 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9630 ]);
9631 });
9632 });
9633 editor.update(cx, |editor, cx| {
9634 assert_text_with_selections(
9635 editor,
9636 indoc! {r#"
9637 let a = {
9638 key:ˇ "value",
9639 };
9640 "#},
9641 cx,
9642 );
9643 });
9644 editor.update_in(cx, |editor, window, cx| {
9645 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9646 });
9647 editor.update(cx, |editor, cx| {
9648 assert_text_with_selections(
9649 editor,
9650 indoc! {r#"
9651 let a = {
9652 «ˇkey: "value"»,
9653 };
9654 "#},
9655 cx,
9656 );
9657 });
9658 editor.update_in(cx, |editor, window, cx| {
9659 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9660 });
9661 editor.update(cx, |editor, cx| {
9662 assert_text_with_selections(
9663 editor,
9664 indoc! {r#"
9665 let a = «ˇ{
9666 key: "value",
9667 }»;
9668 "#},
9669 cx,
9670 );
9671 });
9672
9673 // Test case 3: Cursor after ','
9674 editor.update_in(cx, |editor, window, cx| {
9675 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9676 s.select_display_ranges([
9677 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9678 ]);
9679 });
9680 });
9681 editor.update(cx, |editor, cx| {
9682 assert_text_with_selections(
9683 editor,
9684 indoc! {r#"
9685 let a = {
9686 key: "value",ˇ
9687 };
9688 "#},
9689 cx,
9690 );
9691 });
9692 editor.update_in(cx, |editor, window, cx| {
9693 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9694 });
9695 editor.update(cx, |editor, cx| {
9696 assert_text_with_selections(
9697 editor,
9698 indoc! {r#"
9699 let a = «ˇ{
9700 key: "value",
9701 }»;
9702 "#},
9703 cx,
9704 );
9705 });
9706
9707 // Test case 4: Cursor after ';'
9708 editor.update_in(cx, |editor, window, cx| {
9709 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9710 s.select_display_ranges([
9711 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9712 ]);
9713 });
9714 });
9715 editor.update(cx, |editor, cx| {
9716 assert_text_with_selections(
9717 editor,
9718 indoc! {r#"
9719 let a = {
9720 key: "value",
9721 };ˇ
9722 "#},
9723 cx,
9724 );
9725 });
9726 editor.update_in(cx, |editor, window, cx| {
9727 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9728 });
9729 editor.update(cx, |editor, cx| {
9730 assert_text_with_selections(
9731 editor,
9732 indoc! {r#"
9733 «ˇlet a = {
9734 key: "value",
9735 };
9736 »"#},
9737 cx,
9738 );
9739 });
9740}
9741
9742#[gpui::test]
9743async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9744 init_test(cx, |_| {});
9745
9746 let language = Arc::new(Language::new(
9747 LanguageConfig::default(),
9748 Some(tree_sitter_rust::LANGUAGE.into()),
9749 ));
9750
9751 let text = r#"
9752 use mod1::mod2::{mod3, mod4};
9753
9754 fn fn_1(param1: bool, param2: &str) {
9755 let var1 = "hello world";
9756 }
9757 "#
9758 .unindent();
9759
9760 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9761 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9762 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9763
9764 editor
9765 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9766 .await;
9767
9768 // Test 1: Cursor on a letter of a string word
9769 editor.update_in(cx, |editor, window, cx| {
9770 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9771 s.select_display_ranges([
9772 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9773 ]);
9774 });
9775 });
9776 editor.update_in(cx, |editor, window, cx| {
9777 assert_text_with_selections(
9778 editor,
9779 indoc! {r#"
9780 use mod1::mod2::{mod3, mod4};
9781
9782 fn fn_1(param1: bool, param2: &str) {
9783 let var1 = "hˇello world";
9784 }
9785 "#},
9786 cx,
9787 );
9788 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9789 assert_text_with_selections(
9790 editor,
9791 indoc! {r#"
9792 use mod1::mod2::{mod3, mod4};
9793
9794 fn fn_1(param1: bool, param2: &str) {
9795 let var1 = "«ˇhello» world";
9796 }
9797 "#},
9798 cx,
9799 );
9800 });
9801
9802 // Test 2: Partial selection within a word
9803 editor.update_in(cx, |editor, window, cx| {
9804 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9805 s.select_display_ranges([
9806 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9807 ]);
9808 });
9809 });
9810 editor.update_in(cx, |editor, window, cx| {
9811 assert_text_with_selections(
9812 editor,
9813 indoc! {r#"
9814 use mod1::mod2::{mod3, mod4};
9815
9816 fn fn_1(param1: bool, param2: &str) {
9817 let var1 = "h«elˇ»lo world";
9818 }
9819 "#},
9820 cx,
9821 );
9822 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9823 assert_text_with_selections(
9824 editor,
9825 indoc! {r#"
9826 use mod1::mod2::{mod3, mod4};
9827
9828 fn fn_1(param1: bool, param2: &str) {
9829 let var1 = "«ˇhello» world";
9830 }
9831 "#},
9832 cx,
9833 );
9834 });
9835
9836 // Test 3: Complete word already selected
9837 editor.update_in(cx, |editor, window, cx| {
9838 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9839 s.select_display_ranges([
9840 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9841 ]);
9842 });
9843 });
9844 editor.update_in(cx, |editor, window, cx| {
9845 assert_text_with_selections(
9846 editor,
9847 indoc! {r#"
9848 use mod1::mod2::{mod3, mod4};
9849
9850 fn fn_1(param1: bool, param2: &str) {
9851 let var1 = "«helloˇ» world";
9852 }
9853 "#},
9854 cx,
9855 );
9856 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9857 assert_text_with_selections(
9858 editor,
9859 indoc! {r#"
9860 use mod1::mod2::{mod3, mod4};
9861
9862 fn fn_1(param1: bool, param2: &str) {
9863 let var1 = "«hello worldˇ»";
9864 }
9865 "#},
9866 cx,
9867 );
9868 });
9869
9870 // Test 4: Selection spanning across words
9871 editor.update_in(cx, |editor, window, cx| {
9872 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9873 s.select_display_ranges([
9874 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9875 ]);
9876 });
9877 });
9878 editor.update_in(cx, |editor, window, cx| {
9879 assert_text_with_selections(
9880 editor,
9881 indoc! {r#"
9882 use mod1::mod2::{mod3, mod4};
9883
9884 fn fn_1(param1: bool, param2: &str) {
9885 let var1 = "hel«lo woˇ»rld";
9886 }
9887 "#},
9888 cx,
9889 );
9890 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9891 assert_text_with_selections(
9892 editor,
9893 indoc! {r#"
9894 use mod1::mod2::{mod3, mod4};
9895
9896 fn fn_1(param1: bool, param2: &str) {
9897 let var1 = "«ˇhello world»";
9898 }
9899 "#},
9900 cx,
9901 );
9902 });
9903
9904 // Test 5: Expansion beyond string
9905 editor.update_in(cx, |editor, window, cx| {
9906 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9907 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9908 assert_text_with_selections(
9909 editor,
9910 indoc! {r#"
9911 use mod1::mod2::{mod3, mod4};
9912
9913 fn fn_1(param1: bool, param2: &str) {
9914 «ˇlet var1 = "hello world";»
9915 }
9916 "#},
9917 cx,
9918 );
9919 });
9920}
9921
9922#[gpui::test]
9923async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9924 init_test(cx, |_| {});
9925
9926 let mut cx = EditorTestContext::new(cx).await;
9927
9928 let language = Arc::new(Language::new(
9929 LanguageConfig::default(),
9930 Some(tree_sitter_rust::LANGUAGE.into()),
9931 ));
9932
9933 cx.update_buffer(|buffer, cx| {
9934 buffer.set_language(Some(language), cx);
9935 });
9936
9937 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9938 cx.update_editor(|editor, window, cx| {
9939 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9940 });
9941
9942 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9943
9944 cx.set_state(indoc! { r#"fn a() {
9945 // what
9946 // a
9947 // ˇlong
9948 // method
9949 // I
9950 // sure
9951 // hope
9952 // it
9953 // works
9954 }"# });
9955
9956 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9957 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9958 cx.update(|_, cx| {
9959 multi_buffer.update(cx, |multi_buffer, cx| {
9960 multi_buffer.set_excerpts_for_path(
9961 PathKey::for_buffer(&buffer, cx),
9962 buffer,
9963 [Point::new(1, 0)..Point::new(1, 0)],
9964 3,
9965 cx,
9966 );
9967 });
9968 });
9969
9970 let editor2 = cx.new_window_entity(|window, cx| {
9971 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9972 });
9973
9974 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9975 cx.update_editor(|editor, window, cx| {
9976 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9977 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9978 })
9979 });
9980
9981 cx.assert_editor_state(indoc! { "
9982 fn a() {
9983 // what
9984 // a
9985 ˇ // long
9986 // method"});
9987
9988 cx.update_editor(|editor, window, cx| {
9989 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9990 });
9991
9992 // Although we could potentially make the action work when the syntax node
9993 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9994 // did. Maybe we could also expand the excerpt to contain the range?
9995 cx.assert_editor_state(indoc! { "
9996 fn a() {
9997 // what
9998 // a
9999 ˇ // long
10000 // method"});
10001}
10002
10003#[gpui::test]
10004async fn test_fold_function_bodies(cx: &mut TestAppContext) {
10005 init_test(cx, |_| {});
10006
10007 let base_text = r#"
10008 impl A {
10009 // this is an uncommitted comment
10010
10011 fn b() {
10012 c();
10013 }
10014
10015 // this is another uncommitted comment
10016
10017 fn d() {
10018 // e
10019 // f
10020 }
10021 }
10022
10023 fn g() {
10024 // h
10025 }
10026 "#
10027 .unindent();
10028
10029 let text = r#"
10030 ˇimpl A {
10031
10032 fn b() {
10033 c();
10034 }
10035
10036 fn d() {
10037 // e
10038 // f
10039 }
10040 }
10041
10042 fn g() {
10043 // h
10044 }
10045 "#
10046 .unindent();
10047
10048 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10049 cx.set_state(&text);
10050 cx.set_head_text(&base_text);
10051 cx.update_editor(|editor, window, cx| {
10052 editor.expand_all_diff_hunks(&Default::default(), window, cx);
10053 });
10054
10055 cx.assert_state_with_diff(
10056 "
10057 ˇimpl A {
10058 - // this is an uncommitted comment
10059
10060 fn b() {
10061 c();
10062 }
10063
10064 - // this is another uncommitted comment
10065 -
10066 fn d() {
10067 // e
10068 // f
10069 }
10070 }
10071
10072 fn g() {
10073 // h
10074 }
10075 "
10076 .unindent(),
10077 );
10078
10079 let expected_display_text = "
10080 impl A {
10081 // this is an uncommitted comment
10082
10083 fn b() {
10084 ⋯
10085 }
10086
10087 // this is another uncommitted comment
10088
10089 fn d() {
10090 ⋯
10091 }
10092 }
10093
10094 fn g() {
10095 ⋯
10096 }
10097 "
10098 .unindent();
10099
10100 cx.update_editor(|editor, window, cx| {
10101 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
10102 assert_eq!(editor.display_text(cx), expected_display_text);
10103 });
10104}
10105
10106#[gpui::test]
10107async fn test_autoindent(cx: &mut TestAppContext) {
10108 init_test(cx, |_| {});
10109
10110 let language = Arc::new(
10111 Language::new(
10112 LanguageConfig {
10113 brackets: BracketPairConfig {
10114 pairs: vec![
10115 BracketPair {
10116 start: "{".to_string(),
10117 end: "}".to_string(),
10118 close: false,
10119 surround: false,
10120 newline: true,
10121 },
10122 BracketPair {
10123 start: "(".to_string(),
10124 end: ")".to_string(),
10125 close: false,
10126 surround: false,
10127 newline: true,
10128 },
10129 ],
10130 ..Default::default()
10131 },
10132 ..Default::default()
10133 },
10134 Some(tree_sitter_rust::LANGUAGE.into()),
10135 )
10136 .with_indents_query(
10137 r#"
10138 (_ "(" ")" @end) @indent
10139 (_ "{" "}" @end) @indent
10140 "#,
10141 )
10142 .unwrap(),
10143 );
10144
10145 let text = "fn a() {}";
10146
10147 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10148 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10149 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10150 editor
10151 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10152 .await;
10153
10154 editor.update_in(cx, |editor, window, cx| {
10155 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10156 s.select_ranges([
10157 MultiBufferOffset(5)..MultiBufferOffset(5),
10158 MultiBufferOffset(8)..MultiBufferOffset(8),
10159 MultiBufferOffset(9)..MultiBufferOffset(9),
10160 ])
10161 });
10162 editor.newline(&Newline, window, cx);
10163 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
10164 assert_eq!(
10165 editor.selections.ranges(&editor.display_snapshot(cx)),
10166 &[
10167 Point::new(1, 4)..Point::new(1, 4),
10168 Point::new(3, 4)..Point::new(3, 4),
10169 Point::new(5, 0)..Point::new(5, 0)
10170 ]
10171 );
10172 });
10173}
10174
10175#[gpui::test]
10176async fn test_autoindent_disabled(cx: &mut TestAppContext) {
10177 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
10178
10179 let language = Arc::new(
10180 Language::new(
10181 LanguageConfig {
10182 brackets: BracketPairConfig {
10183 pairs: vec![
10184 BracketPair {
10185 start: "{".to_string(),
10186 end: "}".to_string(),
10187 close: false,
10188 surround: false,
10189 newline: true,
10190 },
10191 BracketPair {
10192 start: "(".to_string(),
10193 end: ")".to_string(),
10194 close: false,
10195 surround: false,
10196 newline: true,
10197 },
10198 ],
10199 ..Default::default()
10200 },
10201 ..Default::default()
10202 },
10203 Some(tree_sitter_rust::LANGUAGE.into()),
10204 )
10205 .with_indents_query(
10206 r#"
10207 (_ "(" ")" @end) @indent
10208 (_ "{" "}" @end) @indent
10209 "#,
10210 )
10211 .unwrap(),
10212 );
10213
10214 let text = "fn a() {}";
10215
10216 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10217 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10218 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10219 editor
10220 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10221 .await;
10222
10223 editor.update_in(cx, |editor, window, cx| {
10224 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10225 s.select_ranges([
10226 MultiBufferOffset(5)..MultiBufferOffset(5),
10227 MultiBufferOffset(8)..MultiBufferOffset(8),
10228 MultiBufferOffset(9)..MultiBufferOffset(9),
10229 ])
10230 });
10231 editor.newline(&Newline, window, cx);
10232 assert_eq!(
10233 editor.text(cx),
10234 indoc!(
10235 "
10236 fn a(
10237
10238 ) {
10239
10240 }
10241 "
10242 )
10243 );
10244 assert_eq!(
10245 editor.selections.ranges(&editor.display_snapshot(cx)),
10246 &[
10247 Point::new(1, 0)..Point::new(1, 0),
10248 Point::new(3, 0)..Point::new(3, 0),
10249 Point::new(5, 0)..Point::new(5, 0)
10250 ]
10251 );
10252 });
10253}
10254
10255#[gpui::test]
10256async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
10257 init_test(cx, |settings| {
10258 settings.defaults.auto_indent = Some(true);
10259 settings.languages.0.insert(
10260 "python".into(),
10261 LanguageSettingsContent {
10262 auto_indent: Some(false),
10263 ..Default::default()
10264 },
10265 );
10266 });
10267
10268 let mut cx = EditorTestContext::new(cx).await;
10269
10270 let injected_language = Arc::new(
10271 Language::new(
10272 LanguageConfig {
10273 brackets: BracketPairConfig {
10274 pairs: vec![
10275 BracketPair {
10276 start: "{".to_string(),
10277 end: "}".to_string(),
10278 close: false,
10279 surround: false,
10280 newline: true,
10281 },
10282 BracketPair {
10283 start: "(".to_string(),
10284 end: ")".to_string(),
10285 close: true,
10286 surround: false,
10287 newline: true,
10288 },
10289 ],
10290 ..Default::default()
10291 },
10292 name: "python".into(),
10293 ..Default::default()
10294 },
10295 Some(tree_sitter_python::LANGUAGE.into()),
10296 )
10297 .with_indents_query(
10298 r#"
10299 (_ "(" ")" @end) @indent
10300 (_ "{" "}" @end) @indent
10301 "#,
10302 )
10303 .unwrap(),
10304 );
10305
10306 let language = Arc::new(
10307 Language::new(
10308 LanguageConfig {
10309 brackets: BracketPairConfig {
10310 pairs: vec![
10311 BracketPair {
10312 start: "{".to_string(),
10313 end: "}".to_string(),
10314 close: false,
10315 surround: false,
10316 newline: true,
10317 },
10318 BracketPair {
10319 start: "(".to_string(),
10320 end: ")".to_string(),
10321 close: true,
10322 surround: false,
10323 newline: true,
10324 },
10325 ],
10326 ..Default::default()
10327 },
10328 name: LanguageName::new_static("rust"),
10329 ..Default::default()
10330 },
10331 Some(tree_sitter_rust::LANGUAGE.into()),
10332 )
10333 .with_indents_query(
10334 r#"
10335 (_ "(" ")" @end) @indent
10336 (_ "{" "}" @end) @indent
10337 "#,
10338 )
10339 .unwrap()
10340 .with_injection_query(
10341 r#"
10342 (macro_invocation
10343 macro: (identifier) @_macro_name
10344 (token_tree) @injection.content
10345 (#set! injection.language "python"))
10346 "#,
10347 )
10348 .unwrap(),
10349 );
10350
10351 cx.language_registry().add(injected_language);
10352 cx.language_registry().add(language.clone());
10353
10354 cx.update_buffer(|buffer, cx| {
10355 buffer.set_language(Some(language), cx);
10356 });
10357
10358 cx.set_state(r#"struct A {ˇ}"#);
10359
10360 cx.update_editor(|editor, window, cx| {
10361 editor.newline(&Default::default(), window, cx);
10362 });
10363
10364 cx.assert_editor_state(indoc!(
10365 "struct A {
10366 ˇ
10367 }"
10368 ));
10369
10370 cx.set_state(r#"select_biased!(ˇ)"#);
10371
10372 cx.update_editor(|editor, window, cx| {
10373 editor.newline(&Default::default(), window, cx);
10374 editor.handle_input("def ", window, cx);
10375 editor.handle_input("(", window, cx);
10376 editor.newline(&Default::default(), window, cx);
10377 editor.handle_input("a", window, cx);
10378 });
10379
10380 cx.assert_editor_state(indoc!(
10381 "select_biased!(
10382 def (
10383 aˇ
10384 )
10385 )"
10386 ));
10387}
10388
10389#[gpui::test]
10390async fn test_autoindent_selections(cx: &mut TestAppContext) {
10391 init_test(cx, |_| {});
10392
10393 {
10394 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10395 cx.set_state(indoc! {"
10396 impl A {
10397
10398 fn b() {}
10399
10400 «fn c() {
10401
10402 }ˇ»
10403 }
10404 "});
10405
10406 cx.update_editor(|editor, window, cx| {
10407 editor.autoindent(&Default::default(), window, cx);
10408 });
10409 cx.wait_for_autoindent_applied().await;
10410
10411 cx.assert_editor_state(indoc! {"
10412 impl A {
10413
10414 fn b() {}
10415
10416 «fn c() {
10417
10418 }ˇ»
10419 }
10420 "});
10421 }
10422
10423 {
10424 let mut cx = EditorTestContext::new_multibuffer(
10425 cx,
10426 [indoc! { "
10427 impl A {
10428 «
10429 // a
10430 fn b(){}
10431 »
10432 «
10433 }
10434 fn c(){}
10435 »
10436 "}],
10437 );
10438
10439 let buffer = cx.update_editor(|editor, _, cx| {
10440 let buffer = editor.buffer().update(cx, |buffer, _| {
10441 buffer.all_buffers().iter().next().unwrap().clone()
10442 });
10443 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10444 buffer
10445 });
10446
10447 cx.run_until_parked();
10448 cx.update_editor(|editor, window, cx| {
10449 editor.select_all(&Default::default(), window, cx);
10450 editor.autoindent(&Default::default(), window, cx)
10451 });
10452 cx.run_until_parked();
10453
10454 cx.update(|_, cx| {
10455 assert_eq!(
10456 buffer.read(cx).text(),
10457 indoc! { "
10458 impl A {
10459
10460 // a
10461 fn b(){}
10462
10463
10464 }
10465 fn c(){}
10466
10467 " }
10468 )
10469 });
10470 }
10471}
10472
10473#[gpui::test]
10474async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10475 init_test(cx, |_| {});
10476
10477 let mut cx = EditorTestContext::new(cx).await;
10478
10479 let language = Arc::new(Language::new(
10480 LanguageConfig {
10481 brackets: BracketPairConfig {
10482 pairs: vec![
10483 BracketPair {
10484 start: "{".to_string(),
10485 end: "}".to_string(),
10486 close: true,
10487 surround: true,
10488 newline: true,
10489 },
10490 BracketPair {
10491 start: "(".to_string(),
10492 end: ")".to_string(),
10493 close: true,
10494 surround: true,
10495 newline: true,
10496 },
10497 BracketPair {
10498 start: "/*".to_string(),
10499 end: " */".to_string(),
10500 close: true,
10501 surround: true,
10502 newline: true,
10503 },
10504 BracketPair {
10505 start: "[".to_string(),
10506 end: "]".to_string(),
10507 close: false,
10508 surround: false,
10509 newline: true,
10510 },
10511 BracketPair {
10512 start: "\"".to_string(),
10513 end: "\"".to_string(),
10514 close: true,
10515 surround: true,
10516 newline: false,
10517 },
10518 BracketPair {
10519 start: "<".to_string(),
10520 end: ">".to_string(),
10521 close: false,
10522 surround: true,
10523 newline: true,
10524 },
10525 ],
10526 ..Default::default()
10527 },
10528 autoclose_before: "})]".to_string(),
10529 ..Default::default()
10530 },
10531 Some(tree_sitter_rust::LANGUAGE.into()),
10532 ));
10533
10534 cx.language_registry().add(language.clone());
10535 cx.update_buffer(|buffer, cx| {
10536 buffer.set_language(Some(language), cx);
10537 });
10538
10539 cx.set_state(
10540 &r#"
10541 🏀ˇ
10542 εˇ
10543 ❤️ˇ
10544 "#
10545 .unindent(),
10546 );
10547
10548 // autoclose multiple nested brackets at multiple cursors
10549 cx.update_editor(|editor, window, cx| {
10550 editor.handle_input("{", window, cx);
10551 editor.handle_input("{", window, cx);
10552 editor.handle_input("{", window, cx);
10553 });
10554 cx.assert_editor_state(
10555 &"
10556 🏀{{{ˇ}}}
10557 ε{{{ˇ}}}
10558 ❤️{{{ˇ}}}
10559 "
10560 .unindent(),
10561 );
10562
10563 // insert a different closing bracket
10564 cx.update_editor(|editor, window, cx| {
10565 editor.handle_input(")", window, cx);
10566 });
10567 cx.assert_editor_state(
10568 &"
10569 🏀{{{)ˇ}}}
10570 ε{{{)ˇ}}}
10571 ❤️{{{)ˇ}}}
10572 "
10573 .unindent(),
10574 );
10575
10576 // skip over the auto-closed brackets when typing a closing bracket
10577 cx.update_editor(|editor, window, cx| {
10578 editor.move_right(&MoveRight, window, cx);
10579 editor.handle_input("}", window, cx);
10580 editor.handle_input("}", window, cx);
10581 editor.handle_input("}", window, cx);
10582 });
10583 cx.assert_editor_state(
10584 &"
10585 🏀{{{)}}}}ˇ
10586 ε{{{)}}}}ˇ
10587 ❤️{{{)}}}}ˇ
10588 "
10589 .unindent(),
10590 );
10591
10592 // autoclose multi-character pairs
10593 cx.set_state(
10594 &"
10595 ˇ
10596 ˇ
10597 "
10598 .unindent(),
10599 );
10600 cx.update_editor(|editor, window, cx| {
10601 editor.handle_input("/", window, cx);
10602 editor.handle_input("*", window, cx);
10603 });
10604 cx.assert_editor_state(
10605 &"
10606 /*ˇ */
10607 /*ˇ */
10608 "
10609 .unindent(),
10610 );
10611
10612 // one cursor autocloses a multi-character pair, one cursor
10613 // does not autoclose.
10614 cx.set_state(
10615 &"
10616 /ˇ
10617 ˇ
10618 "
10619 .unindent(),
10620 );
10621 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10622 cx.assert_editor_state(
10623 &"
10624 /*ˇ */
10625 *ˇ
10626 "
10627 .unindent(),
10628 );
10629
10630 // Don't autoclose if the next character isn't whitespace and isn't
10631 // listed in the language's "autoclose_before" section.
10632 cx.set_state("ˇa b");
10633 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10634 cx.assert_editor_state("{ˇa b");
10635
10636 // Don't autoclose if `close` is false for the bracket pair
10637 cx.set_state("ˇ");
10638 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10639 cx.assert_editor_state("[ˇ");
10640
10641 // Surround with brackets if text is selected
10642 cx.set_state("«aˇ» b");
10643 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10644 cx.assert_editor_state("{«aˇ»} b");
10645
10646 // Autoclose when not immediately after a word character
10647 cx.set_state("a ˇ");
10648 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10649 cx.assert_editor_state("a \"ˇ\"");
10650
10651 // Autoclose pair where the start and end characters are the same
10652 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10653 cx.assert_editor_state("a \"\"ˇ");
10654
10655 // Don't autoclose when immediately after a word character
10656 cx.set_state("aˇ");
10657 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10658 cx.assert_editor_state("a\"ˇ");
10659
10660 // Do autoclose when after a non-word character
10661 cx.set_state("{ˇ");
10662 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10663 cx.assert_editor_state("{\"ˇ\"");
10664
10665 // Non identical pairs autoclose regardless of preceding character
10666 cx.set_state("aˇ");
10667 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10668 cx.assert_editor_state("a{ˇ}");
10669
10670 // Don't autoclose pair if autoclose is disabled
10671 cx.set_state("ˇ");
10672 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10673 cx.assert_editor_state("<ˇ");
10674
10675 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10676 cx.set_state("«aˇ» b");
10677 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10678 cx.assert_editor_state("<«aˇ»> b");
10679}
10680
10681#[gpui::test]
10682async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10683 init_test(cx, |settings| {
10684 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10685 });
10686
10687 let mut cx = EditorTestContext::new(cx).await;
10688
10689 let language = Arc::new(Language::new(
10690 LanguageConfig {
10691 brackets: BracketPairConfig {
10692 pairs: vec![
10693 BracketPair {
10694 start: "{".to_string(),
10695 end: "}".to_string(),
10696 close: true,
10697 surround: true,
10698 newline: true,
10699 },
10700 BracketPair {
10701 start: "(".to_string(),
10702 end: ")".to_string(),
10703 close: true,
10704 surround: true,
10705 newline: true,
10706 },
10707 BracketPair {
10708 start: "[".to_string(),
10709 end: "]".to_string(),
10710 close: false,
10711 surround: false,
10712 newline: true,
10713 },
10714 ],
10715 ..Default::default()
10716 },
10717 autoclose_before: "})]".to_string(),
10718 ..Default::default()
10719 },
10720 Some(tree_sitter_rust::LANGUAGE.into()),
10721 ));
10722
10723 cx.language_registry().add(language.clone());
10724 cx.update_buffer(|buffer, cx| {
10725 buffer.set_language(Some(language), cx);
10726 });
10727
10728 cx.set_state(
10729 &"
10730 ˇ
10731 ˇ
10732 ˇ
10733 "
10734 .unindent(),
10735 );
10736
10737 // ensure only matching closing brackets are skipped over
10738 cx.update_editor(|editor, window, cx| {
10739 editor.handle_input("}", window, cx);
10740 editor.move_left(&MoveLeft, window, cx);
10741 editor.handle_input(")", window, cx);
10742 editor.move_left(&MoveLeft, window, cx);
10743 });
10744 cx.assert_editor_state(
10745 &"
10746 ˇ)}
10747 ˇ)}
10748 ˇ)}
10749 "
10750 .unindent(),
10751 );
10752
10753 // skip-over closing brackets at multiple cursors
10754 cx.update_editor(|editor, window, cx| {
10755 editor.handle_input(")", window, cx);
10756 editor.handle_input("}", window, cx);
10757 });
10758 cx.assert_editor_state(
10759 &"
10760 )}ˇ
10761 )}ˇ
10762 )}ˇ
10763 "
10764 .unindent(),
10765 );
10766
10767 // ignore non-close brackets
10768 cx.update_editor(|editor, window, cx| {
10769 editor.handle_input("]", window, cx);
10770 editor.move_left(&MoveLeft, window, cx);
10771 editor.handle_input("]", window, cx);
10772 });
10773 cx.assert_editor_state(
10774 &"
10775 )}]ˇ]
10776 )}]ˇ]
10777 )}]ˇ]
10778 "
10779 .unindent(),
10780 );
10781}
10782
10783#[gpui::test]
10784async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10785 init_test(cx, |_| {});
10786
10787 let mut cx = EditorTestContext::new(cx).await;
10788
10789 let html_language = Arc::new(
10790 Language::new(
10791 LanguageConfig {
10792 name: "HTML".into(),
10793 brackets: BracketPairConfig {
10794 pairs: vec![
10795 BracketPair {
10796 start: "<".into(),
10797 end: ">".into(),
10798 close: true,
10799 ..Default::default()
10800 },
10801 BracketPair {
10802 start: "{".into(),
10803 end: "}".into(),
10804 close: true,
10805 ..Default::default()
10806 },
10807 BracketPair {
10808 start: "(".into(),
10809 end: ")".into(),
10810 close: true,
10811 ..Default::default()
10812 },
10813 ],
10814 ..Default::default()
10815 },
10816 autoclose_before: "})]>".into(),
10817 ..Default::default()
10818 },
10819 Some(tree_sitter_html::LANGUAGE.into()),
10820 )
10821 .with_injection_query(
10822 r#"
10823 (script_element
10824 (raw_text) @injection.content
10825 (#set! injection.language "javascript"))
10826 "#,
10827 )
10828 .unwrap(),
10829 );
10830
10831 let javascript_language = Arc::new(Language::new(
10832 LanguageConfig {
10833 name: "JavaScript".into(),
10834 brackets: BracketPairConfig {
10835 pairs: vec![
10836 BracketPair {
10837 start: "/*".into(),
10838 end: " */".into(),
10839 close: true,
10840 ..Default::default()
10841 },
10842 BracketPair {
10843 start: "{".into(),
10844 end: "}".into(),
10845 close: true,
10846 ..Default::default()
10847 },
10848 BracketPair {
10849 start: "(".into(),
10850 end: ")".into(),
10851 close: true,
10852 ..Default::default()
10853 },
10854 ],
10855 ..Default::default()
10856 },
10857 autoclose_before: "})]>".into(),
10858 ..Default::default()
10859 },
10860 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10861 ));
10862
10863 cx.language_registry().add(html_language.clone());
10864 cx.language_registry().add(javascript_language);
10865 cx.executor().run_until_parked();
10866
10867 cx.update_buffer(|buffer, cx| {
10868 buffer.set_language(Some(html_language), cx);
10869 });
10870
10871 cx.set_state(
10872 &r#"
10873 <body>ˇ
10874 <script>
10875 var x = 1;ˇ
10876 </script>
10877 </body>ˇ
10878 "#
10879 .unindent(),
10880 );
10881
10882 // Precondition: different languages are active at different locations.
10883 cx.update_editor(|editor, window, cx| {
10884 let snapshot = editor.snapshot(window, cx);
10885 let cursors = editor
10886 .selections
10887 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
10888 let languages = cursors
10889 .iter()
10890 .map(|c| snapshot.language_at(c.start).unwrap().name())
10891 .collect::<Vec<_>>();
10892 assert_eq!(
10893 languages,
10894 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10895 );
10896 });
10897
10898 // Angle brackets autoclose in HTML, but not JavaScript.
10899 cx.update_editor(|editor, window, cx| {
10900 editor.handle_input("<", window, cx);
10901 editor.handle_input("a", window, cx);
10902 });
10903 cx.assert_editor_state(
10904 &r#"
10905 <body><aˇ>
10906 <script>
10907 var x = 1;<aˇ
10908 </script>
10909 </body><aˇ>
10910 "#
10911 .unindent(),
10912 );
10913
10914 // Curly braces and parens autoclose in both HTML and JavaScript.
10915 cx.update_editor(|editor, window, cx| {
10916 editor.handle_input(" b=", window, cx);
10917 editor.handle_input("{", window, cx);
10918 editor.handle_input("c", window, cx);
10919 editor.handle_input("(", window, cx);
10920 });
10921 cx.assert_editor_state(
10922 &r#"
10923 <body><a b={c(ˇ)}>
10924 <script>
10925 var x = 1;<a b={c(ˇ)}
10926 </script>
10927 </body><a b={c(ˇ)}>
10928 "#
10929 .unindent(),
10930 );
10931
10932 // Brackets that were already autoclosed are skipped.
10933 cx.update_editor(|editor, window, cx| {
10934 editor.handle_input(")", window, cx);
10935 editor.handle_input("d", window, cx);
10936 editor.handle_input("}", window, cx);
10937 });
10938 cx.assert_editor_state(
10939 &r#"
10940 <body><a b={c()d}ˇ>
10941 <script>
10942 var x = 1;<a b={c()d}ˇ
10943 </script>
10944 </body><a b={c()d}ˇ>
10945 "#
10946 .unindent(),
10947 );
10948 cx.update_editor(|editor, window, cx| {
10949 editor.handle_input(">", window, cx);
10950 });
10951 cx.assert_editor_state(
10952 &r#"
10953 <body><a b={c()d}>ˇ
10954 <script>
10955 var x = 1;<a b={c()d}>ˇ
10956 </script>
10957 </body><a b={c()d}>ˇ
10958 "#
10959 .unindent(),
10960 );
10961
10962 // Reset
10963 cx.set_state(
10964 &r#"
10965 <body>ˇ
10966 <script>
10967 var x = 1;ˇ
10968 </script>
10969 </body>ˇ
10970 "#
10971 .unindent(),
10972 );
10973
10974 cx.update_editor(|editor, window, cx| {
10975 editor.handle_input("<", window, cx);
10976 });
10977 cx.assert_editor_state(
10978 &r#"
10979 <body><ˇ>
10980 <script>
10981 var x = 1;<ˇ
10982 </script>
10983 </body><ˇ>
10984 "#
10985 .unindent(),
10986 );
10987
10988 // When backspacing, the closing angle brackets are removed.
10989 cx.update_editor(|editor, window, cx| {
10990 editor.backspace(&Backspace, window, cx);
10991 });
10992 cx.assert_editor_state(
10993 &r#"
10994 <body>ˇ
10995 <script>
10996 var x = 1;ˇ
10997 </script>
10998 </body>ˇ
10999 "#
11000 .unindent(),
11001 );
11002
11003 // Block comments autoclose in JavaScript, but not HTML.
11004 cx.update_editor(|editor, window, cx| {
11005 editor.handle_input("/", window, cx);
11006 editor.handle_input("*", 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
11020#[gpui::test]
11021async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
11022 init_test(cx, |_| {});
11023
11024 let mut cx = EditorTestContext::new(cx).await;
11025
11026 let rust_language = Arc::new(
11027 Language::new(
11028 LanguageConfig {
11029 name: "Rust".into(),
11030 brackets: serde_json::from_value(json!([
11031 { "start": "{", "end": "}", "close": true, "newline": true },
11032 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
11033 ]))
11034 .unwrap(),
11035 autoclose_before: "})]>".into(),
11036 ..Default::default()
11037 },
11038 Some(tree_sitter_rust::LANGUAGE.into()),
11039 )
11040 .with_override_query("(string_literal) @string")
11041 .unwrap(),
11042 );
11043
11044 cx.language_registry().add(rust_language.clone());
11045 cx.update_buffer(|buffer, cx| {
11046 buffer.set_language(Some(rust_language), cx);
11047 });
11048
11049 cx.set_state(
11050 &r#"
11051 let x = ˇ
11052 "#
11053 .unindent(),
11054 );
11055
11056 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
11057 cx.update_editor(|editor, window, cx| {
11058 editor.handle_input("\"", window, cx);
11059 });
11060 cx.assert_editor_state(
11061 &r#"
11062 let x = "ˇ"
11063 "#
11064 .unindent(),
11065 );
11066
11067 // Inserting another quotation mark. The cursor moves across the existing
11068 // automatically-inserted quotation mark.
11069 cx.update_editor(|editor, window, cx| {
11070 editor.handle_input("\"", window, cx);
11071 });
11072 cx.assert_editor_state(
11073 &r#"
11074 let x = ""ˇ
11075 "#
11076 .unindent(),
11077 );
11078
11079 // Reset
11080 cx.set_state(
11081 &r#"
11082 let x = ˇ
11083 "#
11084 .unindent(),
11085 );
11086
11087 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
11088 cx.update_editor(|editor, window, cx| {
11089 editor.handle_input("\"", window, cx);
11090 editor.handle_input(" ", window, cx);
11091 editor.move_left(&Default::default(), window, cx);
11092 editor.handle_input("\\", window, cx);
11093 editor.handle_input("\"", window, cx);
11094 });
11095 cx.assert_editor_state(
11096 &r#"
11097 let x = "\"ˇ "
11098 "#
11099 .unindent(),
11100 );
11101
11102 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
11103 // mark. Nothing is inserted.
11104 cx.update_editor(|editor, window, cx| {
11105 editor.move_right(&Default::default(), window, cx);
11106 editor.handle_input("\"", window, cx);
11107 });
11108 cx.assert_editor_state(
11109 &r#"
11110 let x = "\" "ˇ
11111 "#
11112 .unindent(),
11113 );
11114}
11115
11116#[gpui::test]
11117async fn test_autoclose_quotes_with_scope_awareness(cx: &mut TestAppContext) {
11118 init_test(cx, |_| {});
11119
11120 let mut cx = EditorTestContext::new(cx).await;
11121 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
11122
11123 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11124
11125 // Double quote inside single-quoted string
11126 cx.set_state(indoc! {r#"
11127 def main():
11128 items = ['"', ˇ]
11129 "#});
11130 cx.update_editor(|editor, window, cx| {
11131 editor.handle_input("\"", window, cx);
11132 });
11133 cx.assert_editor_state(indoc! {r#"
11134 def main():
11135 items = ['"', "ˇ"]
11136 "#});
11137
11138 // Two double quotes inside single-quoted string
11139 cx.set_state(indoc! {r#"
11140 def main():
11141 items = ['""', ˇ]
11142 "#});
11143 cx.update_editor(|editor, window, cx| {
11144 editor.handle_input("\"", window, cx);
11145 });
11146 cx.assert_editor_state(indoc! {r#"
11147 def main():
11148 items = ['""', "ˇ"]
11149 "#});
11150
11151 // Single quote inside double-quoted string
11152 cx.set_state(indoc! {r#"
11153 def main():
11154 items = ["'", ˇ]
11155 "#});
11156 cx.update_editor(|editor, window, cx| {
11157 editor.handle_input("'", window, cx);
11158 });
11159 cx.assert_editor_state(indoc! {r#"
11160 def main():
11161 items = ["'", 'ˇ']
11162 "#});
11163
11164 // Two single quotes inside double-quoted string
11165 cx.set_state(indoc! {r#"
11166 def main():
11167 items = ["''", ˇ]
11168 "#});
11169 cx.update_editor(|editor, window, cx| {
11170 editor.handle_input("'", window, cx);
11171 });
11172 cx.assert_editor_state(indoc! {r#"
11173 def main():
11174 items = ["''", 'ˇ']
11175 "#});
11176
11177 // Mixed quotes on same line
11178 cx.set_state(indoc! {r#"
11179 def main():
11180 items = ['"""', "'''''", ˇ]
11181 "#});
11182 cx.update_editor(|editor, window, cx| {
11183 editor.handle_input("\"", window, cx);
11184 });
11185 cx.assert_editor_state(indoc! {r#"
11186 def main():
11187 items = ['"""', "'''''", "ˇ"]
11188 "#});
11189 cx.update_editor(|editor, window, cx| {
11190 editor.move_right(&MoveRight, window, cx);
11191 });
11192 cx.update_editor(|editor, window, cx| {
11193 editor.handle_input(", ", window, cx);
11194 });
11195 cx.update_editor(|editor, window, cx| {
11196 editor.handle_input("'", window, cx);
11197 });
11198 cx.assert_editor_state(indoc! {r#"
11199 def main():
11200 items = ['"""', "'''''", "", 'ˇ']
11201 "#});
11202}
11203
11204#[gpui::test]
11205async fn test_autoclose_quotes_with_multibyte_characters(cx: &mut TestAppContext) {
11206 init_test(cx, |_| {});
11207
11208 let mut cx = EditorTestContext::new(cx).await;
11209 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
11210 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11211
11212 cx.set_state(indoc! {r#"
11213 def main():
11214 items = ["🎉", ˇ]
11215 "#});
11216 cx.update_editor(|editor, window, cx| {
11217 editor.handle_input("\"", window, cx);
11218 });
11219 cx.assert_editor_state(indoc! {r#"
11220 def main():
11221 items = ["🎉", "ˇ"]
11222 "#});
11223}
11224
11225#[gpui::test]
11226async fn test_surround_with_pair(cx: &mut TestAppContext) {
11227 init_test(cx, |_| {});
11228
11229 let language = Arc::new(Language::new(
11230 LanguageConfig {
11231 brackets: BracketPairConfig {
11232 pairs: vec![
11233 BracketPair {
11234 start: "{".to_string(),
11235 end: "}".to_string(),
11236 close: true,
11237 surround: true,
11238 newline: true,
11239 },
11240 BracketPair {
11241 start: "/* ".to_string(),
11242 end: "*/".to_string(),
11243 close: true,
11244 surround: true,
11245 ..Default::default()
11246 },
11247 ],
11248 ..Default::default()
11249 },
11250 ..Default::default()
11251 },
11252 Some(tree_sitter_rust::LANGUAGE.into()),
11253 ));
11254
11255 let text = r#"
11256 a
11257 b
11258 c
11259 "#
11260 .unindent();
11261
11262 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11263 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11264 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11265 editor
11266 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11267 .await;
11268
11269 editor.update_in(cx, |editor, window, cx| {
11270 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11271 s.select_display_ranges([
11272 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11273 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11274 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
11275 ])
11276 });
11277
11278 editor.handle_input("{", window, cx);
11279 editor.handle_input("{", window, cx);
11280 editor.handle_input("{", window, cx);
11281 assert_eq!(
11282 editor.text(cx),
11283 "
11284 {{{a}}}
11285 {{{b}}}
11286 {{{c}}}
11287 "
11288 .unindent()
11289 );
11290 assert_eq!(
11291 display_ranges(editor, cx),
11292 [
11293 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
11294 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
11295 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
11296 ]
11297 );
11298
11299 editor.undo(&Undo, window, cx);
11300 editor.undo(&Undo, window, cx);
11301 editor.undo(&Undo, window, cx);
11302 assert_eq!(
11303 editor.text(cx),
11304 "
11305 a
11306 b
11307 c
11308 "
11309 .unindent()
11310 );
11311 assert_eq!(
11312 display_ranges(editor, cx),
11313 [
11314 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11315 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11316 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11317 ]
11318 );
11319
11320 // Ensure inserting the first character of a multi-byte bracket pair
11321 // doesn't surround the selections with the bracket.
11322 editor.handle_input("/", window, cx);
11323 assert_eq!(
11324 editor.text(cx),
11325 "
11326 /
11327 /
11328 /
11329 "
11330 .unindent()
11331 );
11332 assert_eq!(
11333 display_ranges(editor, cx),
11334 [
11335 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11336 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11337 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11338 ]
11339 );
11340
11341 editor.undo(&Undo, window, cx);
11342 assert_eq!(
11343 editor.text(cx),
11344 "
11345 a
11346 b
11347 c
11348 "
11349 .unindent()
11350 );
11351 assert_eq!(
11352 display_ranges(editor, cx),
11353 [
11354 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11355 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11356 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11357 ]
11358 );
11359
11360 // Ensure inserting the last character of a multi-byte bracket pair
11361 // doesn't surround the selections with the bracket.
11362 editor.handle_input("*", window, cx);
11363 assert_eq!(
11364 editor.text(cx),
11365 "
11366 *
11367 *
11368 *
11369 "
11370 .unindent()
11371 );
11372 assert_eq!(
11373 display_ranges(editor, cx),
11374 [
11375 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11376 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11377 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11378 ]
11379 );
11380 });
11381}
11382
11383#[gpui::test]
11384async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
11385 init_test(cx, |_| {});
11386
11387 let language = Arc::new(Language::new(
11388 LanguageConfig {
11389 brackets: BracketPairConfig {
11390 pairs: vec![BracketPair {
11391 start: "{".to_string(),
11392 end: "}".to_string(),
11393 close: true,
11394 surround: true,
11395 newline: true,
11396 }],
11397 ..Default::default()
11398 },
11399 autoclose_before: "}".to_string(),
11400 ..Default::default()
11401 },
11402 Some(tree_sitter_rust::LANGUAGE.into()),
11403 ));
11404
11405 let text = r#"
11406 a
11407 b
11408 c
11409 "#
11410 .unindent();
11411
11412 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11413 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11414 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11415 editor
11416 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11417 .await;
11418
11419 editor.update_in(cx, |editor, window, cx| {
11420 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11421 s.select_ranges([
11422 Point::new(0, 1)..Point::new(0, 1),
11423 Point::new(1, 1)..Point::new(1, 1),
11424 Point::new(2, 1)..Point::new(2, 1),
11425 ])
11426 });
11427
11428 editor.handle_input("{", window, cx);
11429 editor.handle_input("{", window, cx);
11430 editor.handle_input("_", window, cx);
11431 assert_eq!(
11432 editor.text(cx),
11433 "
11434 a{{_}}
11435 b{{_}}
11436 c{{_}}
11437 "
11438 .unindent()
11439 );
11440 assert_eq!(
11441 editor
11442 .selections
11443 .ranges::<Point>(&editor.display_snapshot(cx)),
11444 [
11445 Point::new(0, 4)..Point::new(0, 4),
11446 Point::new(1, 4)..Point::new(1, 4),
11447 Point::new(2, 4)..Point::new(2, 4)
11448 ]
11449 );
11450
11451 editor.backspace(&Default::default(), window, cx);
11452 editor.backspace(&Default::default(), window, cx);
11453 assert_eq!(
11454 editor.text(cx),
11455 "
11456 a{}
11457 b{}
11458 c{}
11459 "
11460 .unindent()
11461 );
11462 assert_eq!(
11463 editor
11464 .selections
11465 .ranges::<Point>(&editor.display_snapshot(cx)),
11466 [
11467 Point::new(0, 2)..Point::new(0, 2),
11468 Point::new(1, 2)..Point::new(1, 2),
11469 Point::new(2, 2)..Point::new(2, 2)
11470 ]
11471 );
11472
11473 editor.delete_to_previous_word_start(&Default::default(), window, cx);
11474 assert_eq!(
11475 editor.text(cx),
11476 "
11477 a
11478 b
11479 c
11480 "
11481 .unindent()
11482 );
11483 assert_eq!(
11484 editor
11485 .selections
11486 .ranges::<Point>(&editor.display_snapshot(cx)),
11487 [
11488 Point::new(0, 1)..Point::new(0, 1),
11489 Point::new(1, 1)..Point::new(1, 1),
11490 Point::new(2, 1)..Point::new(2, 1)
11491 ]
11492 );
11493 });
11494}
11495
11496#[gpui::test]
11497async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
11498 init_test(cx, |settings| {
11499 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11500 });
11501
11502 let mut cx = EditorTestContext::new(cx).await;
11503
11504 let language = Arc::new(Language::new(
11505 LanguageConfig {
11506 brackets: BracketPairConfig {
11507 pairs: vec![
11508 BracketPair {
11509 start: "{".to_string(),
11510 end: "}".to_string(),
11511 close: true,
11512 surround: true,
11513 newline: true,
11514 },
11515 BracketPair {
11516 start: "(".to_string(),
11517 end: ")".to_string(),
11518 close: true,
11519 surround: true,
11520 newline: true,
11521 },
11522 BracketPair {
11523 start: "[".to_string(),
11524 end: "]".to_string(),
11525 close: false,
11526 surround: true,
11527 newline: true,
11528 },
11529 ],
11530 ..Default::default()
11531 },
11532 autoclose_before: "})]".to_string(),
11533 ..Default::default()
11534 },
11535 Some(tree_sitter_rust::LANGUAGE.into()),
11536 ));
11537
11538 cx.language_registry().add(language.clone());
11539 cx.update_buffer(|buffer, cx| {
11540 buffer.set_language(Some(language), cx);
11541 });
11542
11543 cx.set_state(
11544 &"
11545 {(ˇ)}
11546 [[ˇ]]
11547 {(ˇ)}
11548 "
11549 .unindent(),
11550 );
11551
11552 cx.update_editor(|editor, window, cx| {
11553 editor.backspace(&Default::default(), window, cx);
11554 editor.backspace(&Default::default(), window, cx);
11555 });
11556
11557 cx.assert_editor_state(
11558 &"
11559 ˇ
11560 ˇ]]
11561 ˇ
11562 "
11563 .unindent(),
11564 );
11565
11566 cx.update_editor(|editor, window, cx| {
11567 editor.handle_input("{", window, cx);
11568 editor.handle_input("{", window, cx);
11569 editor.move_right(&MoveRight, window, cx);
11570 editor.move_right(&MoveRight, window, cx);
11571 editor.move_left(&MoveLeft, window, cx);
11572 editor.move_left(&MoveLeft, window, cx);
11573 editor.backspace(&Default::default(), window, cx);
11574 });
11575
11576 cx.assert_editor_state(
11577 &"
11578 {ˇ}
11579 {ˇ}]]
11580 {ˇ}
11581 "
11582 .unindent(),
11583 );
11584
11585 cx.update_editor(|editor, window, cx| {
11586 editor.backspace(&Default::default(), window, cx);
11587 });
11588
11589 cx.assert_editor_state(
11590 &"
11591 ˇ
11592 ˇ]]
11593 ˇ
11594 "
11595 .unindent(),
11596 );
11597}
11598
11599#[gpui::test]
11600async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11601 init_test(cx, |_| {});
11602
11603 let language = Arc::new(Language::new(
11604 LanguageConfig::default(),
11605 Some(tree_sitter_rust::LANGUAGE.into()),
11606 ));
11607
11608 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11609 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11610 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11611 editor
11612 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11613 .await;
11614
11615 editor.update_in(cx, |editor, window, cx| {
11616 editor.set_auto_replace_emoji_shortcode(true);
11617
11618 editor.handle_input("Hello ", window, cx);
11619 editor.handle_input(":wave", window, cx);
11620 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11621
11622 editor.handle_input(":", window, cx);
11623 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11624
11625 editor.handle_input(" :smile", window, cx);
11626 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11627
11628 editor.handle_input(":", window, cx);
11629 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11630
11631 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11632 editor.handle_input(":wave", window, cx);
11633 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11634
11635 editor.handle_input(":", window, cx);
11636 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11637
11638 editor.handle_input(":1", window, cx);
11639 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11640
11641 editor.handle_input(":", window, cx);
11642 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11643
11644 // Ensure shortcode does not get replaced when it is part of a word
11645 editor.handle_input(" Test:wave", window, cx);
11646 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11647
11648 editor.handle_input(":", window, cx);
11649 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11650
11651 editor.set_auto_replace_emoji_shortcode(false);
11652
11653 // Ensure shortcode does not get replaced when auto replace is off
11654 editor.handle_input(" :wave", window, cx);
11655 assert_eq!(
11656 editor.text(cx),
11657 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11658 );
11659
11660 editor.handle_input(":", window, cx);
11661 assert_eq!(
11662 editor.text(cx),
11663 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11664 );
11665 });
11666}
11667
11668#[gpui::test]
11669async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11670 init_test(cx, |_| {});
11671
11672 let (text, insertion_ranges) = marked_text_ranges(
11673 indoc! {"
11674 ˇ
11675 "},
11676 false,
11677 );
11678
11679 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11680 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11681
11682 _ = editor.update_in(cx, |editor, window, cx| {
11683 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11684
11685 editor
11686 .insert_snippet(
11687 &insertion_ranges
11688 .iter()
11689 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11690 .collect::<Vec<_>>(),
11691 snippet,
11692 window,
11693 cx,
11694 )
11695 .unwrap();
11696
11697 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11698 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11699 assert_eq!(editor.text(cx), expected_text);
11700 assert_eq!(
11701 editor.selections.ranges(&editor.display_snapshot(cx)),
11702 selection_ranges
11703 .iter()
11704 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11705 .collect::<Vec<_>>()
11706 );
11707 }
11708
11709 assert(
11710 editor,
11711 cx,
11712 indoc! {"
11713 type «» =•
11714 "},
11715 );
11716
11717 assert!(editor.context_menu_visible(), "There should be a matches");
11718 });
11719}
11720
11721#[gpui::test]
11722async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11723 init_test(cx, |_| {});
11724
11725 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11726 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11727 assert_eq!(editor.text(cx), expected_text);
11728 assert_eq!(
11729 editor.selections.ranges(&editor.display_snapshot(cx)),
11730 selection_ranges
11731 .iter()
11732 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11733 .collect::<Vec<_>>()
11734 );
11735 }
11736
11737 let (text, insertion_ranges) = marked_text_ranges(
11738 indoc! {"
11739 ˇ
11740 "},
11741 false,
11742 );
11743
11744 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11745 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11746
11747 _ = editor.update_in(cx, |editor, window, cx| {
11748 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11749
11750 editor
11751 .insert_snippet(
11752 &insertion_ranges
11753 .iter()
11754 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11755 .collect::<Vec<_>>(),
11756 snippet,
11757 window,
11758 cx,
11759 )
11760 .unwrap();
11761
11762 assert_state(
11763 editor,
11764 cx,
11765 indoc! {"
11766 type «» = ;•
11767 "},
11768 );
11769
11770 assert!(
11771 editor.context_menu_visible(),
11772 "Context menu should be visible for placeholder choices"
11773 );
11774
11775 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11776
11777 assert_state(
11778 editor,
11779 cx,
11780 indoc! {"
11781 type = «»;•
11782 "},
11783 );
11784
11785 assert!(
11786 !editor.context_menu_visible(),
11787 "Context menu should be hidden after moving to next tabstop"
11788 );
11789
11790 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11791
11792 assert_state(
11793 editor,
11794 cx,
11795 indoc! {"
11796 type = ; ˇ
11797 "},
11798 );
11799
11800 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11801
11802 assert_state(
11803 editor,
11804 cx,
11805 indoc! {"
11806 type = ; ˇ
11807 "},
11808 );
11809 });
11810
11811 _ = editor.update_in(cx, |editor, window, cx| {
11812 editor.select_all(&SelectAll, window, cx);
11813 editor.backspace(&Backspace, window, cx);
11814
11815 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11816 let insertion_ranges = editor
11817 .selections
11818 .all(&editor.display_snapshot(cx))
11819 .iter()
11820 .map(|s| s.range())
11821 .collect::<Vec<_>>();
11822
11823 editor
11824 .insert_snippet(&insertion_ranges, snippet, window, cx)
11825 .unwrap();
11826
11827 assert_state(editor, cx, "fn «» = value;•");
11828
11829 assert!(
11830 editor.context_menu_visible(),
11831 "Context menu should be visible for placeholder choices"
11832 );
11833
11834 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11835
11836 assert_state(editor, cx, "fn = «valueˇ»;•");
11837
11838 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11839
11840 assert_state(editor, cx, "fn «» = value;•");
11841
11842 assert!(
11843 editor.context_menu_visible(),
11844 "Context menu should be visible again after returning to first tabstop"
11845 );
11846
11847 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11848
11849 assert_state(editor, cx, "fn «» = value;•");
11850 });
11851}
11852
11853#[gpui::test]
11854async fn test_snippets(cx: &mut TestAppContext) {
11855 init_test(cx, |_| {});
11856
11857 let mut cx = EditorTestContext::new(cx).await;
11858
11859 cx.set_state(indoc! {"
11860 a.ˇ b
11861 a.ˇ b
11862 a.ˇ b
11863 "});
11864
11865 cx.update_editor(|editor, window, cx| {
11866 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11867 let insertion_ranges = editor
11868 .selections
11869 .all(&editor.display_snapshot(cx))
11870 .iter()
11871 .map(|s| s.range())
11872 .collect::<Vec<_>>();
11873 editor
11874 .insert_snippet(&insertion_ranges, snippet, window, cx)
11875 .unwrap();
11876 });
11877
11878 cx.assert_editor_state(indoc! {"
11879 a.f(«oneˇ», two, «threeˇ») b
11880 a.f(«oneˇ», two, «threeˇ») b
11881 a.f(«oneˇ», two, «threeˇ») b
11882 "});
11883
11884 // Can't move earlier than the first tab stop
11885 cx.update_editor(|editor, window, cx| {
11886 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11887 });
11888 cx.assert_editor_state(indoc! {"
11889 a.f(«oneˇ», two, «threeˇ») b
11890 a.f(«oneˇ», two, «threeˇ») b
11891 a.f(«oneˇ», two, «threeˇ») b
11892 "});
11893
11894 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11895 cx.assert_editor_state(indoc! {"
11896 a.f(one, «twoˇ», three) b
11897 a.f(one, «twoˇ», three) b
11898 a.f(one, «twoˇ», three) b
11899 "});
11900
11901 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11902 cx.assert_editor_state(indoc! {"
11903 a.f(«oneˇ», two, «threeˇ») b
11904 a.f(«oneˇ», two, «threeˇ») b
11905 a.f(«oneˇ», two, «threeˇ») b
11906 "});
11907
11908 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11909 cx.assert_editor_state(indoc! {"
11910 a.f(one, «twoˇ», three) b
11911 a.f(one, «twoˇ», three) b
11912 a.f(one, «twoˇ», three) b
11913 "});
11914 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11915 cx.assert_editor_state(indoc! {"
11916 a.f(one, two, three)ˇ b
11917 a.f(one, two, three)ˇ b
11918 a.f(one, two, three)ˇ b
11919 "});
11920
11921 // As soon as the last tab stop is reached, snippet state is gone
11922 cx.update_editor(|editor, window, cx| {
11923 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11924 });
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}
11931
11932#[gpui::test]
11933async fn test_snippet_indentation(cx: &mut TestAppContext) {
11934 init_test(cx, |_| {});
11935
11936 let mut cx = EditorTestContext::new(cx).await;
11937
11938 cx.update_editor(|editor, window, cx| {
11939 let snippet = Snippet::parse(indoc! {"
11940 /*
11941 * Multiline comment with leading indentation
11942 *
11943 * $1
11944 */
11945 $0"})
11946 .unwrap();
11947 let insertion_ranges = editor
11948 .selections
11949 .all(&editor.display_snapshot(cx))
11950 .iter()
11951 .map(|s| s.range())
11952 .collect::<Vec<_>>();
11953 editor
11954 .insert_snippet(&insertion_ranges, snippet, window, cx)
11955 .unwrap();
11956 });
11957
11958 cx.assert_editor_state(indoc! {"
11959 /*
11960 * Multiline comment with leading indentation
11961 *
11962 * ˇ
11963 */
11964 "});
11965
11966 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11967 cx.assert_editor_state(indoc! {"
11968 /*
11969 * Multiline comment with leading indentation
11970 *
11971 *•
11972 */
11973 ˇ"});
11974}
11975
11976#[gpui::test]
11977async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11978 init_test(cx, |_| {});
11979
11980 let mut cx = EditorTestContext::new(cx).await;
11981 cx.update_editor(|editor, _, cx| {
11982 editor.project().unwrap().update(cx, |project, cx| {
11983 project.snippets().update(cx, |snippets, _cx| {
11984 let snippet = project::snippet_provider::Snippet {
11985 prefix: vec!["multi word".to_string()],
11986 body: "this is many words".to_string(),
11987 description: Some("description".to_string()),
11988 name: "multi-word snippet test".to_string(),
11989 };
11990 snippets.add_snippet_for_test(
11991 None,
11992 PathBuf::from("test_snippets.json"),
11993 vec![Arc::new(snippet)],
11994 );
11995 });
11996 })
11997 });
11998
11999 for (input_to_simulate, should_match_snippet) in [
12000 ("m", true),
12001 ("m ", true),
12002 ("m w", true),
12003 ("aa m w", true),
12004 ("aa m g", false),
12005 ] {
12006 cx.set_state("ˇ");
12007 cx.simulate_input(input_to_simulate); // fails correctly
12008
12009 cx.update_editor(|editor, _, _| {
12010 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
12011 else {
12012 assert!(!should_match_snippet); // no completions! don't even show the menu
12013 return;
12014 };
12015 assert!(context_menu.visible());
12016 let completions = context_menu.completions.borrow();
12017
12018 assert_eq!(!completions.is_empty(), should_match_snippet);
12019 });
12020 }
12021}
12022
12023#[gpui::test]
12024async fn test_document_format_during_save(cx: &mut TestAppContext) {
12025 init_test(cx, |_| {});
12026
12027 let fs = FakeFs::new(cx.executor());
12028 fs.insert_file(path!("/file.rs"), Default::default()).await;
12029
12030 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
12031
12032 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12033 language_registry.add(rust_lang());
12034 let mut fake_servers = language_registry.register_fake_lsp(
12035 "Rust",
12036 FakeLspAdapter {
12037 capabilities: lsp::ServerCapabilities {
12038 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12039 ..Default::default()
12040 },
12041 ..Default::default()
12042 },
12043 );
12044
12045 let buffer = project
12046 .update(cx, |project, cx| {
12047 project.open_local_buffer(path!("/file.rs"), cx)
12048 })
12049 .await
12050 .unwrap();
12051
12052 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12053 let (editor, cx) = cx.add_window_view(|window, cx| {
12054 build_editor_with_project(project.clone(), buffer, window, cx)
12055 });
12056 editor.update_in(cx, |editor, window, cx| {
12057 editor.set_text("one\ntwo\nthree\n", window, cx)
12058 });
12059 assert!(cx.read(|cx| editor.is_dirty(cx)));
12060
12061 let fake_server = fake_servers.next().await.unwrap();
12062
12063 {
12064 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12065 move |params, _| async move {
12066 assert_eq!(
12067 params.text_document.uri,
12068 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12069 );
12070 assert_eq!(params.options.tab_size, 4);
12071 Ok(Some(vec![lsp::TextEdit::new(
12072 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12073 ", ".to_string(),
12074 )]))
12075 },
12076 );
12077 let save = editor
12078 .update_in(cx, |editor, window, cx| {
12079 editor.save(
12080 SaveOptions {
12081 format: true,
12082 autosave: false,
12083 },
12084 project.clone(),
12085 window,
12086 cx,
12087 )
12088 })
12089 .unwrap();
12090 save.await;
12091
12092 assert_eq!(
12093 editor.update(cx, |editor, cx| editor.text(cx)),
12094 "one, two\nthree\n"
12095 );
12096 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12097 }
12098
12099 {
12100 editor.update_in(cx, |editor, window, cx| {
12101 editor.set_text("one\ntwo\nthree\n", window, cx)
12102 });
12103 assert!(cx.read(|cx| editor.is_dirty(cx)));
12104
12105 // Ensure we can still save even if formatting hangs.
12106 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12107 move |params, _| async move {
12108 assert_eq!(
12109 params.text_document.uri,
12110 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12111 );
12112 futures::future::pending::<()>().await;
12113 unreachable!()
12114 },
12115 );
12116 let save = editor
12117 .update_in(cx, |editor, window, cx| {
12118 editor.save(
12119 SaveOptions {
12120 format: true,
12121 autosave: false,
12122 },
12123 project.clone(),
12124 window,
12125 cx,
12126 )
12127 })
12128 .unwrap();
12129 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12130 save.await;
12131 assert_eq!(
12132 editor.update(cx, |editor, cx| editor.text(cx)),
12133 "one\ntwo\nthree\n"
12134 );
12135 }
12136
12137 // Set rust language override and assert overridden tabsize is sent to language server
12138 update_test_language_settings(cx, |settings| {
12139 settings.languages.0.insert(
12140 "Rust".into(),
12141 LanguageSettingsContent {
12142 tab_size: NonZeroU32::new(8),
12143 ..Default::default()
12144 },
12145 );
12146 });
12147
12148 {
12149 editor.update_in(cx, |editor, window, cx| {
12150 editor.set_text("somehting_new\n", window, cx)
12151 });
12152 assert!(cx.read(|cx| editor.is_dirty(cx)));
12153 let _formatting_request_signal = fake_server
12154 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12155 assert_eq!(
12156 params.text_document.uri,
12157 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12158 );
12159 assert_eq!(params.options.tab_size, 8);
12160 Ok(Some(vec![]))
12161 });
12162 let save = editor
12163 .update_in(cx, |editor, window, cx| {
12164 editor.save(
12165 SaveOptions {
12166 format: true,
12167 autosave: false,
12168 },
12169 project.clone(),
12170 window,
12171 cx,
12172 )
12173 })
12174 .unwrap();
12175 save.await;
12176 }
12177}
12178
12179#[gpui::test]
12180async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
12181 init_test(cx, |settings| {
12182 settings.defaults.ensure_final_newline_on_save = Some(false);
12183 });
12184
12185 let fs = FakeFs::new(cx.executor());
12186 fs.insert_file(path!("/file.txt"), "foo".into()).await;
12187
12188 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
12189
12190 let buffer = project
12191 .update(cx, |project, cx| {
12192 project.open_local_buffer(path!("/file.txt"), cx)
12193 })
12194 .await
12195 .unwrap();
12196
12197 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12198 let (editor, cx) = cx.add_window_view(|window, cx| {
12199 build_editor_with_project(project.clone(), buffer, window, cx)
12200 });
12201 editor.update_in(cx, |editor, window, cx| {
12202 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
12203 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
12204 });
12205 });
12206 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12207
12208 editor.update_in(cx, |editor, window, cx| {
12209 editor.handle_input("\n", window, cx)
12210 });
12211 cx.run_until_parked();
12212 save(&editor, &project, cx).await;
12213 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
12214
12215 editor.update_in(cx, |editor, window, cx| {
12216 editor.undo(&Default::default(), window, cx);
12217 });
12218 save(&editor, &project, cx).await;
12219 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
12220
12221 editor.update_in(cx, |editor, window, cx| {
12222 editor.redo(&Default::default(), window, cx);
12223 });
12224 cx.run_until_parked();
12225 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
12226
12227 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
12228 let save = editor
12229 .update_in(cx, |editor, window, cx| {
12230 editor.save(
12231 SaveOptions {
12232 format: true,
12233 autosave: false,
12234 },
12235 project.clone(),
12236 window,
12237 cx,
12238 )
12239 })
12240 .unwrap();
12241 save.await;
12242 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12243 }
12244}
12245
12246#[gpui::test]
12247async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
12248 init_test(cx, |_| {});
12249
12250 let cols = 4;
12251 let rows = 10;
12252 let sample_text_1 = sample_text(rows, cols, 'a');
12253 assert_eq!(
12254 sample_text_1,
12255 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12256 );
12257 let sample_text_2 = sample_text(rows, cols, 'l');
12258 assert_eq!(
12259 sample_text_2,
12260 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12261 );
12262 let sample_text_3 = sample_text(rows, cols, 'v');
12263 assert_eq!(
12264 sample_text_3,
12265 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12266 );
12267
12268 let fs = FakeFs::new(cx.executor());
12269 fs.insert_tree(
12270 path!("/a"),
12271 json!({
12272 "main.rs": sample_text_1,
12273 "other.rs": sample_text_2,
12274 "lib.rs": sample_text_3,
12275 }),
12276 )
12277 .await;
12278
12279 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12280 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12281 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12282
12283 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12284 language_registry.add(rust_lang());
12285 let mut fake_servers = language_registry.register_fake_lsp(
12286 "Rust",
12287 FakeLspAdapter {
12288 capabilities: lsp::ServerCapabilities {
12289 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12290 ..Default::default()
12291 },
12292 ..Default::default()
12293 },
12294 );
12295
12296 let worktree = project.update(cx, |project, cx| {
12297 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
12298 assert_eq!(worktrees.len(), 1);
12299 worktrees.pop().unwrap()
12300 });
12301 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12302
12303 let buffer_1 = project
12304 .update(cx, |project, cx| {
12305 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
12306 })
12307 .await
12308 .unwrap();
12309 let buffer_2 = project
12310 .update(cx, |project, cx| {
12311 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
12312 })
12313 .await
12314 .unwrap();
12315 let buffer_3 = project
12316 .update(cx, |project, cx| {
12317 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
12318 })
12319 .await
12320 .unwrap();
12321
12322 let multi_buffer = cx.new(|cx| {
12323 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12324 multi_buffer.push_excerpts(
12325 buffer_1.clone(),
12326 [
12327 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12328 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12329 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12330 ],
12331 cx,
12332 );
12333 multi_buffer.push_excerpts(
12334 buffer_2.clone(),
12335 [
12336 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12337 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12338 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12339 ],
12340 cx,
12341 );
12342 multi_buffer.push_excerpts(
12343 buffer_3.clone(),
12344 [
12345 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12346 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12347 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12348 ],
12349 cx,
12350 );
12351 multi_buffer
12352 });
12353 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12354 Editor::new(
12355 EditorMode::full(),
12356 multi_buffer,
12357 Some(project.clone()),
12358 window,
12359 cx,
12360 )
12361 });
12362
12363 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12364 editor.change_selections(
12365 SelectionEffects::scroll(Autoscroll::Next),
12366 window,
12367 cx,
12368 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
12369 );
12370 editor.insert("|one|two|three|", window, cx);
12371 });
12372 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12373 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12374 editor.change_selections(
12375 SelectionEffects::scroll(Autoscroll::Next),
12376 window,
12377 cx,
12378 |s| s.select_ranges(Some(MultiBufferOffset(60)..MultiBufferOffset(70))),
12379 );
12380 editor.insert("|four|five|six|", window, cx);
12381 });
12382 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12383
12384 // First two buffers should be edited, but not the third one.
12385 assert_eq!(
12386 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12387 "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}",
12388 );
12389 buffer_1.update(cx, |buffer, _| {
12390 assert!(buffer.is_dirty());
12391 assert_eq!(
12392 buffer.text(),
12393 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
12394 )
12395 });
12396 buffer_2.update(cx, |buffer, _| {
12397 assert!(buffer.is_dirty());
12398 assert_eq!(
12399 buffer.text(),
12400 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
12401 )
12402 });
12403 buffer_3.update(cx, |buffer, _| {
12404 assert!(!buffer.is_dirty());
12405 assert_eq!(buffer.text(), sample_text_3,)
12406 });
12407 cx.executor().run_until_parked();
12408
12409 let save = multi_buffer_editor
12410 .update_in(cx, |editor, window, cx| {
12411 editor.save(
12412 SaveOptions {
12413 format: true,
12414 autosave: false,
12415 },
12416 project.clone(),
12417 window,
12418 cx,
12419 )
12420 })
12421 .unwrap();
12422
12423 let fake_server = fake_servers.next().await.unwrap();
12424 fake_server
12425 .server
12426 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
12427 Ok(Some(vec![lsp::TextEdit::new(
12428 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12429 format!("[{} formatted]", params.text_document.uri),
12430 )]))
12431 })
12432 .detach();
12433 save.await;
12434
12435 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
12436 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
12437 assert_eq!(
12438 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12439 uri!(
12440 "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}"
12441 ),
12442 );
12443 buffer_1.update(cx, |buffer, _| {
12444 assert!(!buffer.is_dirty());
12445 assert_eq!(
12446 buffer.text(),
12447 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
12448 )
12449 });
12450 buffer_2.update(cx, |buffer, _| {
12451 assert!(!buffer.is_dirty());
12452 assert_eq!(
12453 buffer.text(),
12454 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
12455 )
12456 });
12457 buffer_3.update(cx, |buffer, _| {
12458 assert!(!buffer.is_dirty());
12459 assert_eq!(buffer.text(), sample_text_3,)
12460 });
12461}
12462
12463#[gpui::test]
12464async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
12465 init_test(cx, |_| {});
12466
12467 let fs = FakeFs::new(cx.executor());
12468 fs.insert_tree(
12469 path!("/dir"),
12470 json!({
12471 "file1.rs": "fn main() { println!(\"hello\"); }",
12472 "file2.rs": "fn test() { println!(\"test\"); }",
12473 "file3.rs": "fn other() { println!(\"other\"); }\n",
12474 }),
12475 )
12476 .await;
12477
12478 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
12479 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12480 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12481
12482 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12483 language_registry.add(rust_lang());
12484
12485 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
12486 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12487
12488 // Open three buffers
12489 let buffer_1 = project
12490 .update(cx, |project, cx| {
12491 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
12492 })
12493 .await
12494 .unwrap();
12495 let buffer_2 = project
12496 .update(cx, |project, cx| {
12497 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
12498 })
12499 .await
12500 .unwrap();
12501 let buffer_3 = project
12502 .update(cx, |project, cx| {
12503 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
12504 })
12505 .await
12506 .unwrap();
12507
12508 // Create a multi-buffer with all three buffers
12509 let multi_buffer = cx.new(|cx| {
12510 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12511 multi_buffer.push_excerpts(
12512 buffer_1.clone(),
12513 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12514 cx,
12515 );
12516 multi_buffer.push_excerpts(
12517 buffer_2.clone(),
12518 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12519 cx,
12520 );
12521 multi_buffer.push_excerpts(
12522 buffer_3.clone(),
12523 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12524 cx,
12525 );
12526 multi_buffer
12527 });
12528
12529 let editor = cx.new_window_entity(|window, cx| {
12530 Editor::new(
12531 EditorMode::full(),
12532 multi_buffer,
12533 Some(project.clone()),
12534 window,
12535 cx,
12536 )
12537 });
12538
12539 // Edit only the first buffer
12540 editor.update_in(cx, |editor, window, cx| {
12541 editor.change_selections(
12542 SelectionEffects::scroll(Autoscroll::Next),
12543 window,
12544 cx,
12545 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
12546 );
12547 editor.insert("// edited", window, cx);
12548 });
12549
12550 // Verify that only buffer 1 is dirty
12551 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
12552 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12553 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12554
12555 // Get write counts after file creation (files were created with initial content)
12556 // We expect each file to have been written once during creation
12557 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12558 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12559 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12560
12561 // Perform autosave
12562 let save_task = editor.update_in(cx, |editor, window, cx| {
12563 editor.save(
12564 SaveOptions {
12565 format: true,
12566 autosave: true,
12567 },
12568 project.clone(),
12569 window,
12570 cx,
12571 )
12572 });
12573 save_task.await.unwrap();
12574
12575 // Only the dirty buffer should have been saved
12576 assert_eq!(
12577 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12578 1,
12579 "Buffer 1 was dirty, so it should have been written once during autosave"
12580 );
12581 assert_eq!(
12582 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12583 0,
12584 "Buffer 2 was clean, so it should not have been written during autosave"
12585 );
12586 assert_eq!(
12587 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12588 0,
12589 "Buffer 3 was clean, so it should not have been written during autosave"
12590 );
12591
12592 // Verify buffer states after autosave
12593 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12594 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12595 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12596
12597 // Now perform a manual save (format = true)
12598 let save_task = editor.update_in(cx, |editor, window, cx| {
12599 editor.save(
12600 SaveOptions {
12601 format: true,
12602 autosave: false,
12603 },
12604 project.clone(),
12605 window,
12606 cx,
12607 )
12608 });
12609 save_task.await.unwrap();
12610
12611 // During manual save, clean buffers don't get written to disk
12612 // They just get did_save called for language server notifications
12613 assert_eq!(
12614 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12615 1,
12616 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12617 );
12618 assert_eq!(
12619 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12620 0,
12621 "Buffer 2 should not have been written at all"
12622 );
12623 assert_eq!(
12624 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12625 0,
12626 "Buffer 3 should not have been written at all"
12627 );
12628}
12629
12630async fn setup_range_format_test(
12631 cx: &mut TestAppContext,
12632) -> (
12633 Entity<Project>,
12634 Entity<Editor>,
12635 &mut gpui::VisualTestContext,
12636 lsp::FakeLanguageServer,
12637) {
12638 init_test(cx, |_| {});
12639
12640 let fs = FakeFs::new(cx.executor());
12641 fs.insert_file(path!("/file.rs"), Default::default()).await;
12642
12643 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12644
12645 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12646 language_registry.add(rust_lang());
12647 let mut fake_servers = language_registry.register_fake_lsp(
12648 "Rust",
12649 FakeLspAdapter {
12650 capabilities: lsp::ServerCapabilities {
12651 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12652 ..lsp::ServerCapabilities::default()
12653 },
12654 ..FakeLspAdapter::default()
12655 },
12656 );
12657
12658 let buffer = project
12659 .update(cx, |project, cx| {
12660 project.open_local_buffer(path!("/file.rs"), cx)
12661 })
12662 .await
12663 .unwrap();
12664
12665 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12666 let (editor, cx) = cx.add_window_view(|window, cx| {
12667 build_editor_with_project(project.clone(), buffer, window, cx)
12668 });
12669
12670 let fake_server = fake_servers.next().await.unwrap();
12671
12672 (project, editor, cx, fake_server)
12673}
12674
12675#[gpui::test]
12676async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12677 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12678
12679 editor.update_in(cx, |editor, window, cx| {
12680 editor.set_text("one\ntwo\nthree\n", window, cx)
12681 });
12682 assert!(cx.read(|cx| editor.is_dirty(cx)));
12683
12684 let save = editor
12685 .update_in(cx, |editor, window, cx| {
12686 editor.save(
12687 SaveOptions {
12688 format: true,
12689 autosave: false,
12690 },
12691 project.clone(),
12692 window,
12693 cx,
12694 )
12695 })
12696 .unwrap();
12697 fake_server
12698 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12699 assert_eq!(
12700 params.text_document.uri,
12701 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12702 );
12703 assert_eq!(params.options.tab_size, 4);
12704 Ok(Some(vec![lsp::TextEdit::new(
12705 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12706 ", ".to_string(),
12707 )]))
12708 })
12709 .next()
12710 .await;
12711 save.await;
12712 assert_eq!(
12713 editor.update(cx, |editor, cx| editor.text(cx)),
12714 "one, two\nthree\n"
12715 );
12716 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12717}
12718
12719#[gpui::test]
12720async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12721 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12722
12723 editor.update_in(cx, |editor, window, cx| {
12724 editor.set_text("one\ntwo\nthree\n", window, cx)
12725 });
12726 assert!(cx.read(|cx| editor.is_dirty(cx)));
12727
12728 // Test that save still works when formatting hangs
12729 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12730 move |params, _| async move {
12731 assert_eq!(
12732 params.text_document.uri,
12733 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12734 );
12735 futures::future::pending::<()>().await;
12736 unreachable!()
12737 },
12738 );
12739 let save = editor
12740 .update_in(cx, |editor, window, cx| {
12741 editor.save(
12742 SaveOptions {
12743 format: true,
12744 autosave: false,
12745 },
12746 project.clone(),
12747 window,
12748 cx,
12749 )
12750 })
12751 .unwrap();
12752 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12753 save.await;
12754 assert_eq!(
12755 editor.update(cx, |editor, cx| editor.text(cx)),
12756 "one\ntwo\nthree\n"
12757 );
12758 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12759}
12760
12761#[gpui::test]
12762async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12763 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12764
12765 // Buffer starts clean, no formatting should be requested
12766 let save = editor
12767 .update_in(cx, |editor, window, cx| {
12768 editor.save(
12769 SaveOptions {
12770 format: false,
12771 autosave: false,
12772 },
12773 project.clone(),
12774 window,
12775 cx,
12776 )
12777 })
12778 .unwrap();
12779 let _pending_format_request = fake_server
12780 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12781 panic!("Should not be invoked");
12782 })
12783 .next();
12784 save.await;
12785 cx.run_until_parked();
12786}
12787
12788#[gpui::test]
12789async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12790 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12791
12792 // Set Rust language override and assert overridden tabsize is sent to language server
12793 update_test_language_settings(cx, |settings| {
12794 settings.languages.0.insert(
12795 "Rust".into(),
12796 LanguageSettingsContent {
12797 tab_size: NonZeroU32::new(8),
12798 ..Default::default()
12799 },
12800 );
12801 });
12802
12803 editor.update_in(cx, |editor, window, cx| {
12804 editor.set_text("something_new\n", window, cx)
12805 });
12806 assert!(cx.read(|cx| editor.is_dirty(cx)));
12807 let save = editor
12808 .update_in(cx, |editor, window, cx| {
12809 editor.save(
12810 SaveOptions {
12811 format: true,
12812 autosave: false,
12813 },
12814 project.clone(),
12815 window,
12816 cx,
12817 )
12818 })
12819 .unwrap();
12820 fake_server
12821 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12822 assert_eq!(
12823 params.text_document.uri,
12824 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12825 );
12826 assert_eq!(params.options.tab_size, 8);
12827 Ok(Some(Vec::new()))
12828 })
12829 .next()
12830 .await;
12831 save.await;
12832}
12833
12834#[gpui::test]
12835async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12836 init_test(cx, |settings| {
12837 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12838 settings::LanguageServerFormatterSpecifier::Current,
12839 )))
12840 });
12841
12842 let fs = FakeFs::new(cx.executor());
12843 fs.insert_file(path!("/file.rs"), Default::default()).await;
12844
12845 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12846
12847 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12848 language_registry.add(Arc::new(Language::new(
12849 LanguageConfig {
12850 name: "Rust".into(),
12851 matcher: LanguageMatcher {
12852 path_suffixes: vec!["rs".to_string()],
12853 ..Default::default()
12854 },
12855 ..LanguageConfig::default()
12856 },
12857 Some(tree_sitter_rust::LANGUAGE.into()),
12858 )));
12859 update_test_language_settings(cx, |settings| {
12860 // Enable Prettier formatting for the same buffer, and ensure
12861 // LSP is called instead of Prettier.
12862 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12863 });
12864 let mut fake_servers = language_registry.register_fake_lsp(
12865 "Rust",
12866 FakeLspAdapter {
12867 capabilities: lsp::ServerCapabilities {
12868 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12869 ..Default::default()
12870 },
12871 ..Default::default()
12872 },
12873 );
12874
12875 let buffer = project
12876 .update(cx, |project, cx| {
12877 project.open_local_buffer(path!("/file.rs"), cx)
12878 })
12879 .await
12880 .unwrap();
12881
12882 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12883 let (editor, cx) = cx.add_window_view(|window, cx| {
12884 build_editor_with_project(project.clone(), buffer, window, cx)
12885 });
12886 editor.update_in(cx, |editor, window, cx| {
12887 editor.set_text("one\ntwo\nthree\n", window, cx)
12888 });
12889
12890 let fake_server = fake_servers.next().await.unwrap();
12891
12892 let format = editor
12893 .update_in(cx, |editor, window, cx| {
12894 editor.perform_format(
12895 project.clone(),
12896 FormatTrigger::Manual,
12897 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12898 window,
12899 cx,
12900 )
12901 })
12902 .unwrap();
12903 fake_server
12904 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12905 assert_eq!(
12906 params.text_document.uri,
12907 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12908 );
12909 assert_eq!(params.options.tab_size, 4);
12910 Ok(Some(vec![lsp::TextEdit::new(
12911 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12912 ", ".to_string(),
12913 )]))
12914 })
12915 .next()
12916 .await;
12917 format.await;
12918 assert_eq!(
12919 editor.update(cx, |editor, cx| editor.text(cx)),
12920 "one, two\nthree\n"
12921 );
12922
12923 editor.update_in(cx, |editor, window, cx| {
12924 editor.set_text("one\ntwo\nthree\n", window, cx)
12925 });
12926 // Ensure we don't lock if formatting hangs.
12927 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12928 move |params, _| async move {
12929 assert_eq!(
12930 params.text_document.uri,
12931 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12932 );
12933 futures::future::pending::<()>().await;
12934 unreachable!()
12935 },
12936 );
12937 let format = editor
12938 .update_in(cx, |editor, window, cx| {
12939 editor.perform_format(
12940 project,
12941 FormatTrigger::Manual,
12942 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12943 window,
12944 cx,
12945 )
12946 })
12947 .unwrap();
12948 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12949 format.await;
12950 assert_eq!(
12951 editor.update(cx, |editor, cx| editor.text(cx)),
12952 "one\ntwo\nthree\n"
12953 );
12954}
12955
12956#[gpui::test]
12957async fn test_multiple_formatters(cx: &mut TestAppContext) {
12958 init_test(cx, |settings| {
12959 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12960 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12961 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12962 Formatter::CodeAction("code-action-1".into()),
12963 Formatter::CodeAction("code-action-2".into()),
12964 ]))
12965 });
12966
12967 let fs = FakeFs::new(cx.executor());
12968 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12969 .await;
12970
12971 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12972 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12973 language_registry.add(rust_lang());
12974
12975 let mut fake_servers = language_registry.register_fake_lsp(
12976 "Rust",
12977 FakeLspAdapter {
12978 capabilities: lsp::ServerCapabilities {
12979 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12980 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12981 commands: vec!["the-command-for-code-action-1".into()],
12982 ..Default::default()
12983 }),
12984 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12985 ..Default::default()
12986 },
12987 ..Default::default()
12988 },
12989 );
12990
12991 let buffer = project
12992 .update(cx, |project, cx| {
12993 project.open_local_buffer(path!("/file.rs"), cx)
12994 })
12995 .await
12996 .unwrap();
12997
12998 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12999 let (editor, cx) = cx.add_window_view(|window, cx| {
13000 build_editor_with_project(project.clone(), buffer, window, cx)
13001 });
13002
13003 let fake_server = fake_servers.next().await.unwrap();
13004 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
13005 move |_params, _| async move {
13006 Ok(Some(vec![lsp::TextEdit::new(
13007 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
13008 "applied-formatting\n".to_string(),
13009 )]))
13010 },
13011 );
13012 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
13013 move |params, _| async move {
13014 let requested_code_actions = params.context.only.expect("Expected code action request");
13015 assert_eq!(requested_code_actions.len(), 1);
13016
13017 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
13018 let code_action = match requested_code_actions[0].as_str() {
13019 "code-action-1" => lsp::CodeAction {
13020 kind: Some("code-action-1".into()),
13021 edit: Some(lsp::WorkspaceEdit::new(
13022 [(
13023 uri,
13024 vec![lsp::TextEdit::new(
13025 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
13026 "applied-code-action-1-edit\n".to_string(),
13027 )],
13028 )]
13029 .into_iter()
13030 .collect(),
13031 )),
13032 command: Some(lsp::Command {
13033 command: "the-command-for-code-action-1".into(),
13034 ..Default::default()
13035 }),
13036 ..Default::default()
13037 },
13038 "code-action-2" => lsp::CodeAction {
13039 kind: Some("code-action-2".into()),
13040 edit: Some(lsp::WorkspaceEdit::new(
13041 [(
13042 uri,
13043 vec![lsp::TextEdit::new(
13044 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
13045 "applied-code-action-2-edit\n".to_string(),
13046 )],
13047 )]
13048 .into_iter()
13049 .collect(),
13050 )),
13051 ..Default::default()
13052 },
13053 req => panic!("Unexpected code action request: {:?}", req),
13054 };
13055 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
13056 code_action,
13057 )]))
13058 },
13059 );
13060
13061 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
13062 move |params, _| async move { Ok(params) }
13063 });
13064
13065 let command_lock = Arc::new(futures::lock::Mutex::new(()));
13066 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
13067 let fake = fake_server.clone();
13068 let lock = command_lock.clone();
13069 move |params, _| {
13070 assert_eq!(params.command, "the-command-for-code-action-1");
13071 let fake = fake.clone();
13072 let lock = lock.clone();
13073 async move {
13074 lock.lock().await;
13075 fake.server
13076 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
13077 label: None,
13078 edit: lsp::WorkspaceEdit {
13079 changes: Some(
13080 [(
13081 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
13082 vec![lsp::TextEdit {
13083 range: lsp::Range::new(
13084 lsp::Position::new(0, 0),
13085 lsp::Position::new(0, 0),
13086 ),
13087 new_text: "applied-code-action-1-command\n".into(),
13088 }],
13089 )]
13090 .into_iter()
13091 .collect(),
13092 ),
13093 ..Default::default()
13094 },
13095 })
13096 .await
13097 .into_response()
13098 .unwrap();
13099 Ok(Some(json!(null)))
13100 }
13101 }
13102 });
13103
13104 editor
13105 .update_in(cx, |editor, window, cx| {
13106 editor.perform_format(
13107 project.clone(),
13108 FormatTrigger::Manual,
13109 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
13110 window,
13111 cx,
13112 )
13113 })
13114 .unwrap()
13115 .await;
13116 editor.update(cx, |editor, cx| {
13117 assert_eq!(
13118 editor.text(cx),
13119 r#"
13120 applied-code-action-2-edit
13121 applied-code-action-1-command
13122 applied-code-action-1-edit
13123 applied-formatting
13124 one
13125 two
13126 three
13127 "#
13128 .unindent()
13129 );
13130 });
13131
13132 editor.update_in(cx, |editor, window, cx| {
13133 editor.undo(&Default::default(), window, cx);
13134 assert_eq!(editor.text(cx), "one \ntwo \nthree");
13135 });
13136
13137 // Perform a manual edit while waiting for an LSP command
13138 // that's being run as part of a formatting code action.
13139 let lock_guard = command_lock.lock().await;
13140 let format = editor
13141 .update_in(cx, |editor, window, cx| {
13142 editor.perform_format(
13143 project.clone(),
13144 FormatTrigger::Manual,
13145 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
13146 window,
13147 cx,
13148 )
13149 })
13150 .unwrap();
13151 cx.run_until_parked();
13152 editor.update(cx, |editor, cx| {
13153 assert_eq!(
13154 editor.text(cx),
13155 r#"
13156 applied-code-action-1-edit
13157 applied-formatting
13158 one
13159 two
13160 three
13161 "#
13162 .unindent()
13163 );
13164
13165 editor.buffer.update(cx, |buffer, cx| {
13166 let ix = buffer.len(cx);
13167 buffer.edit([(ix..ix, "edited\n")], None, cx);
13168 });
13169 });
13170
13171 // Allow the LSP command to proceed. Because the buffer was edited,
13172 // the second code action will not be run.
13173 drop(lock_guard);
13174 format.await;
13175 editor.update_in(cx, |editor, window, cx| {
13176 assert_eq!(
13177 editor.text(cx),
13178 r#"
13179 applied-code-action-1-command
13180 applied-code-action-1-edit
13181 applied-formatting
13182 one
13183 two
13184 three
13185 edited
13186 "#
13187 .unindent()
13188 );
13189
13190 // The manual edit is undone first, because it is the last thing the user did
13191 // (even though the command completed afterwards).
13192 editor.undo(&Default::default(), window, cx);
13193 assert_eq!(
13194 editor.text(cx),
13195 r#"
13196 applied-code-action-1-command
13197 applied-code-action-1-edit
13198 applied-formatting
13199 one
13200 two
13201 three
13202 "#
13203 .unindent()
13204 );
13205
13206 // All the formatting (including the command, which completed after the manual edit)
13207 // is undone together.
13208 editor.undo(&Default::default(), window, cx);
13209 assert_eq!(editor.text(cx), "one \ntwo \nthree");
13210 });
13211}
13212
13213#[gpui::test]
13214async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
13215 init_test(cx, |settings| {
13216 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
13217 settings::LanguageServerFormatterSpecifier::Current,
13218 )]))
13219 });
13220
13221 let fs = FakeFs::new(cx.executor());
13222 fs.insert_file(path!("/file.ts"), Default::default()).await;
13223
13224 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13225
13226 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13227 language_registry.add(Arc::new(Language::new(
13228 LanguageConfig {
13229 name: "TypeScript".into(),
13230 matcher: LanguageMatcher {
13231 path_suffixes: vec!["ts".to_string()],
13232 ..Default::default()
13233 },
13234 ..LanguageConfig::default()
13235 },
13236 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13237 )));
13238 update_test_language_settings(cx, |settings| {
13239 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
13240 });
13241 let mut fake_servers = language_registry.register_fake_lsp(
13242 "TypeScript",
13243 FakeLspAdapter {
13244 capabilities: lsp::ServerCapabilities {
13245 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
13246 ..Default::default()
13247 },
13248 ..Default::default()
13249 },
13250 );
13251
13252 let buffer = project
13253 .update(cx, |project, cx| {
13254 project.open_local_buffer(path!("/file.ts"), cx)
13255 })
13256 .await
13257 .unwrap();
13258
13259 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13260 let (editor, cx) = cx.add_window_view(|window, cx| {
13261 build_editor_with_project(project.clone(), buffer, window, cx)
13262 });
13263 editor.update_in(cx, |editor, window, cx| {
13264 editor.set_text(
13265 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13266 window,
13267 cx,
13268 )
13269 });
13270
13271 let fake_server = fake_servers.next().await.unwrap();
13272
13273 let format = editor
13274 .update_in(cx, |editor, window, cx| {
13275 editor.perform_code_action_kind(
13276 project.clone(),
13277 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13278 window,
13279 cx,
13280 )
13281 })
13282 .unwrap();
13283 fake_server
13284 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
13285 assert_eq!(
13286 params.text_document.uri,
13287 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13288 );
13289 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
13290 lsp::CodeAction {
13291 title: "Organize Imports".to_string(),
13292 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
13293 edit: Some(lsp::WorkspaceEdit {
13294 changes: Some(
13295 [(
13296 params.text_document.uri.clone(),
13297 vec![lsp::TextEdit::new(
13298 lsp::Range::new(
13299 lsp::Position::new(1, 0),
13300 lsp::Position::new(2, 0),
13301 ),
13302 "".to_string(),
13303 )],
13304 )]
13305 .into_iter()
13306 .collect(),
13307 ),
13308 ..Default::default()
13309 }),
13310 ..Default::default()
13311 },
13312 )]))
13313 })
13314 .next()
13315 .await;
13316 format.await;
13317 assert_eq!(
13318 editor.update(cx, |editor, cx| editor.text(cx)),
13319 "import { a } from 'module';\n\nconst x = a;\n"
13320 );
13321
13322 editor.update_in(cx, |editor, window, cx| {
13323 editor.set_text(
13324 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13325 window,
13326 cx,
13327 )
13328 });
13329 // Ensure we don't lock if code action hangs.
13330 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
13331 move |params, _| async move {
13332 assert_eq!(
13333 params.text_document.uri,
13334 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13335 );
13336 futures::future::pending::<()>().await;
13337 unreachable!()
13338 },
13339 );
13340 let format = editor
13341 .update_in(cx, |editor, window, cx| {
13342 editor.perform_code_action_kind(
13343 project,
13344 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13345 window,
13346 cx,
13347 )
13348 })
13349 .unwrap();
13350 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
13351 format.await;
13352 assert_eq!(
13353 editor.update(cx, |editor, cx| editor.text(cx)),
13354 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
13355 );
13356}
13357
13358#[gpui::test]
13359async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
13360 init_test(cx, |_| {});
13361
13362 let mut cx = EditorLspTestContext::new_rust(
13363 lsp::ServerCapabilities {
13364 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13365 ..Default::default()
13366 },
13367 cx,
13368 )
13369 .await;
13370
13371 cx.set_state(indoc! {"
13372 one.twoˇ
13373 "});
13374
13375 // The format request takes a long time. When it completes, it inserts
13376 // a newline and an indent before the `.`
13377 cx.lsp
13378 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
13379 let executor = cx.background_executor().clone();
13380 async move {
13381 executor.timer(Duration::from_millis(100)).await;
13382 Ok(Some(vec![lsp::TextEdit {
13383 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
13384 new_text: "\n ".into(),
13385 }]))
13386 }
13387 });
13388
13389 // Submit a format request.
13390 let format_1 = cx
13391 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13392 .unwrap();
13393 cx.executor().run_until_parked();
13394
13395 // Submit a second format request.
13396 let format_2 = cx
13397 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13398 .unwrap();
13399 cx.executor().run_until_parked();
13400
13401 // Wait for both format requests to complete
13402 cx.executor().advance_clock(Duration::from_millis(200));
13403 format_1.await.unwrap();
13404 format_2.await.unwrap();
13405
13406 // The formatting edits only happens once.
13407 cx.assert_editor_state(indoc! {"
13408 one
13409 .twoˇ
13410 "});
13411}
13412
13413#[gpui::test]
13414async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
13415 init_test(cx, |settings| {
13416 settings.defaults.formatter = Some(FormatterList::default())
13417 });
13418
13419 let mut cx = EditorLspTestContext::new_rust(
13420 lsp::ServerCapabilities {
13421 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13422 ..Default::default()
13423 },
13424 cx,
13425 )
13426 .await;
13427
13428 // Record which buffer changes have been sent to the language server
13429 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
13430 cx.lsp
13431 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
13432 let buffer_changes = buffer_changes.clone();
13433 move |params, _| {
13434 buffer_changes.lock().extend(
13435 params
13436 .content_changes
13437 .into_iter()
13438 .map(|e| (e.range.unwrap(), e.text)),
13439 );
13440 }
13441 });
13442 // Handle formatting requests to the language server.
13443 cx.lsp
13444 .set_request_handler::<lsp::request::Formatting, _, _>({
13445 move |_, _| {
13446 // Insert blank lines between each line of the buffer.
13447 async move {
13448 // TODO: this assertion is not reliably true. Currently nothing guarantees that we deliver
13449 // DidChangedTextDocument to the LSP before sending the formatting request.
13450 // assert_eq!(
13451 // &buffer_changes.lock()[1..],
13452 // &[
13453 // (
13454 // lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13455 // "".into()
13456 // ),
13457 // (
13458 // lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13459 // "".into()
13460 // ),
13461 // (
13462 // lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13463 // "\n".into()
13464 // ),
13465 // ]
13466 // );
13467
13468 Ok(Some(vec![
13469 lsp::TextEdit {
13470 range: lsp::Range::new(
13471 lsp::Position::new(1, 0),
13472 lsp::Position::new(1, 0),
13473 ),
13474 new_text: "\n".into(),
13475 },
13476 lsp::TextEdit {
13477 range: lsp::Range::new(
13478 lsp::Position::new(2, 0),
13479 lsp::Position::new(2, 0),
13480 ),
13481 new_text: "\n".into(),
13482 },
13483 ]))
13484 }
13485 }
13486 });
13487
13488 // Set up a buffer white some trailing whitespace and no trailing newline.
13489 cx.set_state(
13490 &[
13491 "one ", //
13492 "twoˇ", //
13493 "three ", //
13494 "four", //
13495 ]
13496 .join("\n"),
13497 );
13498
13499 // Submit a format request.
13500 let format = cx
13501 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13502 .unwrap();
13503
13504 cx.run_until_parked();
13505 // After formatting the buffer, the trailing whitespace is stripped,
13506 // a newline is appended, and the edits provided by the language server
13507 // have been applied.
13508 format.await.unwrap();
13509
13510 cx.assert_editor_state(
13511 &[
13512 "one", //
13513 "", //
13514 "twoˇ", //
13515 "", //
13516 "three", //
13517 "four", //
13518 "", //
13519 ]
13520 .join("\n"),
13521 );
13522
13523 // Undoing the formatting undoes the trailing whitespace removal, the
13524 // trailing newline, and the LSP edits.
13525 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13526 cx.assert_editor_state(
13527 &[
13528 "one ", //
13529 "twoˇ", //
13530 "three ", //
13531 "four", //
13532 ]
13533 .join("\n"),
13534 );
13535}
13536
13537#[gpui::test]
13538async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13539 cx: &mut TestAppContext,
13540) {
13541 init_test(cx, |_| {});
13542
13543 cx.update(|cx| {
13544 cx.update_global::<SettingsStore, _>(|settings, cx| {
13545 settings.update_user_settings(cx, |settings| {
13546 settings.editor.auto_signature_help = Some(true);
13547 settings.editor.hover_popover_delay = Some(DelayMs(300));
13548 });
13549 });
13550 });
13551
13552 let mut cx = EditorLspTestContext::new_rust(
13553 lsp::ServerCapabilities {
13554 signature_help_provider: Some(lsp::SignatureHelpOptions {
13555 ..Default::default()
13556 }),
13557 ..Default::default()
13558 },
13559 cx,
13560 )
13561 .await;
13562
13563 let language = Language::new(
13564 LanguageConfig {
13565 name: "Rust".into(),
13566 brackets: BracketPairConfig {
13567 pairs: vec![
13568 BracketPair {
13569 start: "{".to_string(),
13570 end: "}".to_string(),
13571 close: true,
13572 surround: true,
13573 newline: true,
13574 },
13575 BracketPair {
13576 start: "(".to_string(),
13577 end: ")".to_string(),
13578 close: true,
13579 surround: true,
13580 newline: true,
13581 },
13582 BracketPair {
13583 start: "/*".to_string(),
13584 end: " */".to_string(),
13585 close: true,
13586 surround: true,
13587 newline: true,
13588 },
13589 BracketPair {
13590 start: "[".to_string(),
13591 end: "]".to_string(),
13592 close: false,
13593 surround: false,
13594 newline: true,
13595 },
13596 BracketPair {
13597 start: "\"".to_string(),
13598 end: "\"".to_string(),
13599 close: true,
13600 surround: true,
13601 newline: false,
13602 },
13603 BracketPair {
13604 start: "<".to_string(),
13605 end: ">".to_string(),
13606 close: false,
13607 surround: true,
13608 newline: true,
13609 },
13610 ],
13611 ..Default::default()
13612 },
13613 autoclose_before: "})]".to_string(),
13614 ..Default::default()
13615 },
13616 Some(tree_sitter_rust::LANGUAGE.into()),
13617 );
13618 let language = Arc::new(language);
13619
13620 cx.language_registry().add(language.clone());
13621 cx.update_buffer(|buffer, cx| {
13622 buffer.set_language(Some(language), cx);
13623 });
13624
13625 cx.set_state(
13626 &r#"
13627 fn main() {
13628 sampleˇ
13629 }
13630 "#
13631 .unindent(),
13632 );
13633
13634 cx.update_editor(|editor, window, cx| {
13635 editor.handle_input("(", window, cx);
13636 });
13637 cx.assert_editor_state(
13638 &"
13639 fn main() {
13640 sample(ˇ)
13641 }
13642 "
13643 .unindent(),
13644 );
13645
13646 let mocked_response = lsp::SignatureHelp {
13647 signatures: vec![lsp::SignatureInformation {
13648 label: "fn sample(param1: u8, param2: u8)".to_string(),
13649 documentation: None,
13650 parameters: Some(vec![
13651 lsp::ParameterInformation {
13652 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13653 documentation: None,
13654 },
13655 lsp::ParameterInformation {
13656 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13657 documentation: None,
13658 },
13659 ]),
13660 active_parameter: None,
13661 }],
13662 active_signature: Some(0),
13663 active_parameter: Some(0),
13664 };
13665 handle_signature_help_request(&mut cx, mocked_response).await;
13666
13667 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13668 .await;
13669
13670 cx.editor(|editor, _, _| {
13671 let signature_help_state = editor.signature_help_state.popover().cloned();
13672 let signature = signature_help_state.unwrap();
13673 assert_eq!(
13674 signature.signatures[signature.current_signature].label,
13675 "fn sample(param1: u8, param2: u8)"
13676 );
13677 });
13678}
13679
13680#[gpui::test]
13681async fn test_signature_help_delay_only_for_auto(cx: &mut TestAppContext) {
13682 init_test(cx, |_| {});
13683
13684 let delay_ms = 500;
13685 cx.update(|cx| {
13686 cx.update_global::<SettingsStore, _>(|settings, cx| {
13687 settings.update_user_settings(cx, |settings| {
13688 settings.editor.auto_signature_help = Some(true);
13689 settings.editor.show_signature_help_after_edits = Some(false);
13690 settings.editor.hover_popover_delay = Some(DelayMs(delay_ms));
13691 });
13692 });
13693 });
13694
13695 let mut cx = EditorLspTestContext::new_rust(
13696 lsp::ServerCapabilities {
13697 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13698 ..lsp::ServerCapabilities::default()
13699 },
13700 cx,
13701 )
13702 .await;
13703
13704 let mocked_response = lsp::SignatureHelp {
13705 signatures: vec![lsp::SignatureInformation {
13706 label: "fn sample(param1: u8)".to_string(),
13707 documentation: None,
13708 parameters: Some(vec![lsp::ParameterInformation {
13709 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13710 documentation: None,
13711 }]),
13712 active_parameter: None,
13713 }],
13714 active_signature: Some(0),
13715 active_parameter: Some(0),
13716 };
13717
13718 cx.set_state(indoc! {"
13719 fn main() {
13720 sample(ˇ);
13721 }
13722
13723 fn sample(param1: u8) {}
13724 "});
13725
13726 // Manual trigger should show immediately without delay
13727 cx.update_editor(|editor, window, cx| {
13728 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13729 });
13730 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13731 cx.run_until_parked();
13732 cx.editor(|editor, _, _| {
13733 assert!(
13734 editor.signature_help_state.is_shown(),
13735 "Manual trigger should show signature help without delay"
13736 );
13737 });
13738
13739 cx.update_editor(|editor, _, cx| {
13740 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13741 });
13742 cx.run_until_parked();
13743 cx.editor(|editor, _, _| {
13744 assert!(!editor.signature_help_state.is_shown());
13745 });
13746
13747 // Auto trigger (cursor movement into brackets) should respect delay
13748 cx.set_state(indoc! {"
13749 fn main() {
13750 sampleˇ();
13751 }
13752
13753 fn sample(param1: u8) {}
13754 "});
13755 cx.update_editor(|editor, window, cx| {
13756 editor.move_right(&MoveRight, window, cx);
13757 });
13758 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13759 cx.run_until_parked();
13760 cx.editor(|editor, _, _| {
13761 assert!(
13762 !editor.signature_help_state.is_shown(),
13763 "Auto trigger should wait for delay before showing signature help"
13764 );
13765 });
13766
13767 cx.executor()
13768 .advance_clock(Duration::from_millis(delay_ms + 50));
13769 cx.run_until_parked();
13770 cx.editor(|editor, _, _| {
13771 assert!(
13772 editor.signature_help_state.is_shown(),
13773 "Auto trigger should show signature help after delay elapsed"
13774 );
13775 });
13776}
13777
13778#[gpui::test]
13779async fn test_signature_help_after_edits_no_delay(cx: &mut TestAppContext) {
13780 init_test(cx, |_| {});
13781
13782 let delay_ms = 500;
13783 cx.update(|cx| {
13784 cx.update_global::<SettingsStore, _>(|settings, cx| {
13785 settings.update_user_settings(cx, |settings| {
13786 settings.editor.auto_signature_help = Some(false);
13787 settings.editor.show_signature_help_after_edits = Some(true);
13788 settings.editor.hover_popover_delay = Some(DelayMs(delay_ms));
13789 });
13790 });
13791 });
13792
13793 let mut cx = EditorLspTestContext::new_rust(
13794 lsp::ServerCapabilities {
13795 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13796 ..lsp::ServerCapabilities::default()
13797 },
13798 cx,
13799 )
13800 .await;
13801
13802 let language = Arc::new(Language::new(
13803 LanguageConfig {
13804 name: "Rust".into(),
13805 brackets: BracketPairConfig {
13806 pairs: vec![BracketPair {
13807 start: "(".to_string(),
13808 end: ")".to_string(),
13809 close: true,
13810 surround: true,
13811 newline: true,
13812 }],
13813 ..BracketPairConfig::default()
13814 },
13815 autoclose_before: "})".to_string(),
13816 ..LanguageConfig::default()
13817 },
13818 Some(tree_sitter_rust::LANGUAGE.into()),
13819 ));
13820 cx.language_registry().add(language.clone());
13821 cx.update_buffer(|buffer, cx| {
13822 buffer.set_language(Some(language), cx);
13823 });
13824
13825 let mocked_response = lsp::SignatureHelp {
13826 signatures: vec![lsp::SignatureInformation {
13827 label: "fn sample(param1: u8)".to_string(),
13828 documentation: None,
13829 parameters: Some(vec![lsp::ParameterInformation {
13830 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13831 documentation: None,
13832 }]),
13833 active_parameter: None,
13834 }],
13835 active_signature: Some(0),
13836 active_parameter: Some(0),
13837 };
13838
13839 cx.set_state(indoc! {"
13840 fn main() {
13841 sampleˇ
13842 }
13843 "});
13844
13845 // Typing bracket should show signature help immediately without delay
13846 cx.update_editor(|editor, window, cx| {
13847 editor.handle_input("(", window, cx);
13848 });
13849 handle_signature_help_request(&mut cx, mocked_response).await;
13850 cx.run_until_parked();
13851 cx.editor(|editor, _, _| {
13852 assert!(
13853 editor.signature_help_state.is_shown(),
13854 "show_signature_help_after_edits should show signature help without delay"
13855 );
13856 });
13857}
13858
13859#[gpui::test]
13860async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13861 init_test(cx, |_| {});
13862
13863 cx.update(|cx| {
13864 cx.update_global::<SettingsStore, _>(|settings, cx| {
13865 settings.update_user_settings(cx, |settings| {
13866 settings.editor.auto_signature_help = Some(false);
13867 settings.editor.show_signature_help_after_edits = Some(false);
13868 });
13869 });
13870 });
13871
13872 let mut cx = EditorLspTestContext::new_rust(
13873 lsp::ServerCapabilities {
13874 signature_help_provider: Some(lsp::SignatureHelpOptions {
13875 ..Default::default()
13876 }),
13877 ..Default::default()
13878 },
13879 cx,
13880 )
13881 .await;
13882
13883 let language = Language::new(
13884 LanguageConfig {
13885 name: "Rust".into(),
13886 brackets: BracketPairConfig {
13887 pairs: vec![
13888 BracketPair {
13889 start: "{".to_string(),
13890 end: "}".to_string(),
13891 close: true,
13892 surround: true,
13893 newline: true,
13894 },
13895 BracketPair {
13896 start: "(".to_string(),
13897 end: ")".to_string(),
13898 close: true,
13899 surround: true,
13900 newline: true,
13901 },
13902 BracketPair {
13903 start: "/*".to_string(),
13904 end: " */".to_string(),
13905 close: true,
13906 surround: true,
13907 newline: true,
13908 },
13909 BracketPair {
13910 start: "[".to_string(),
13911 end: "]".to_string(),
13912 close: false,
13913 surround: false,
13914 newline: true,
13915 },
13916 BracketPair {
13917 start: "\"".to_string(),
13918 end: "\"".to_string(),
13919 close: true,
13920 surround: true,
13921 newline: false,
13922 },
13923 BracketPair {
13924 start: "<".to_string(),
13925 end: ">".to_string(),
13926 close: false,
13927 surround: true,
13928 newline: true,
13929 },
13930 ],
13931 ..Default::default()
13932 },
13933 autoclose_before: "})]".to_string(),
13934 ..Default::default()
13935 },
13936 Some(tree_sitter_rust::LANGUAGE.into()),
13937 );
13938 let language = Arc::new(language);
13939
13940 cx.language_registry().add(language.clone());
13941 cx.update_buffer(|buffer, cx| {
13942 buffer.set_language(Some(language), cx);
13943 });
13944
13945 // Ensure that signature_help is not called when no signature help is enabled.
13946 cx.set_state(
13947 &r#"
13948 fn main() {
13949 sampleˇ
13950 }
13951 "#
13952 .unindent(),
13953 );
13954 cx.update_editor(|editor, window, cx| {
13955 editor.handle_input("(", window, cx);
13956 });
13957 cx.assert_editor_state(
13958 &"
13959 fn main() {
13960 sample(ˇ)
13961 }
13962 "
13963 .unindent(),
13964 );
13965 cx.editor(|editor, _, _| {
13966 assert!(editor.signature_help_state.task().is_none());
13967 });
13968
13969 let mocked_response = lsp::SignatureHelp {
13970 signatures: vec![lsp::SignatureInformation {
13971 label: "fn sample(param1: u8, param2: u8)".to_string(),
13972 documentation: None,
13973 parameters: Some(vec![
13974 lsp::ParameterInformation {
13975 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13976 documentation: None,
13977 },
13978 lsp::ParameterInformation {
13979 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13980 documentation: None,
13981 },
13982 ]),
13983 active_parameter: None,
13984 }],
13985 active_signature: Some(0),
13986 active_parameter: Some(0),
13987 };
13988
13989 // Ensure that signature_help is called when enabled afte edits
13990 cx.update(|_, cx| {
13991 cx.update_global::<SettingsStore, _>(|settings, cx| {
13992 settings.update_user_settings(cx, |settings| {
13993 settings.editor.auto_signature_help = Some(false);
13994 settings.editor.show_signature_help_after_edits = Some(true);
13995 });
13996 });
13997 });
13998 cx.set_state(
13999 &r#"
14000 fn main() {
14001 sampleˇ
14002 }
14003 "#
14004 .unindent(),
14005 );
14006 cx.update_editor(|editor, window, cx| {
14007 editor.handle_input("(", window, cx);
14008 });
14009 cx.assert_editor_state(
14010 &"
14011 fn main() {
14012 sample(ˇ)
14013 }
14014 "
14015 .unindent(),
14016 );
14017 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14018 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14019 .await;
14020 cx.update_editor(|editor, _, _| {
14021 let signature_help_state = editor.signature_help_state.popover().cloned();
14022 assert!(signature_help_state.is_some());
14023 let signature = signature_help_state.unwrap();
14024 assert_eq!(
14025 signature.signatures[signature.current_signature].label,
14026 "fn sample(param1: u8, param2: u8)"
14027 );
14028 editor.signature_help_state = SignatureHelpState::default();
14029 });
14030
14031 // Ensure that signature_help is called when auto signature help override is enabled
14032 cx.update(|_, cx| {
14033 cx.update_global::<SettingsStore, _>(|settings, cx| {
14034 settings.update_user_settings(cx, |settings| {
14035 settings.editor.auto_signature_help = Some(true);
14036 settings.editor.show_signature_help_after_edits = Some(false);
14037 });
14038 });
14039 });
14040 cx.set_state(
14041 &r#"
14042 fn main() {
14043 sampleˇ
14044 }
14045 "#
14046 .unindent(),
14047 );
14048 cx.update_editor(|editor, window, cx| {
14049 editor.handle_input("(", window, cx);
14050 });
14051 cx.assert_editor_state(
14052 &"
14053 fn main() {
14054 sample(ˇ)
14055 }
14056 "
14057 .unindent(),
14058 );
14059 handle_signature_help_request(&mut cx, mocked_response).await;
14060 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14061 .await;
14062 cx.editor(|editor, _, _| {
14063 let signature_help_state = editor.signature_help_state.popover().cloned();
14064 assert!(signature_help_state.is_some());
14065 let signature = signature_help_state.unwrap();
14066 assert_eq!(
14067 signature.signatures[signature.current_signature].label,
14068 "fn sample(param1: u8, param2: u8)"
14069 );
14070 });
14071}
14072
14073#[gpui::test]
14074async fn test_signature_help(cx: &mut TestAppContext) {
14075 init_test(cx, |_| {});
14076 cx.update(|cx| {
14077 cx.update_global::<SettingsStore, _>(|settings, cx| {
14078 settings.update_user_settings(cx, |settings| {
14079 settings.editor.auto_signature_help = Some(true);
14080 });
14081 });
14082 });
14083
14084 let mut cx = EditorLspTestContext::new_rust(
14085 lsp::ServerCapabilities {
14086 signature_help_provider: Some(lsp::SignatureHelpOptions {
14087 ..Default::default()
14088 }),
14089 ..Default::default()
14090 },
14091 cx,
14092 )
14093 .await;
14094
14095 // A test that directly calls `show_signature_help`
14096 cx.update_editor(|editor, window, cx| {
14097 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14098 });
14099
14100 let mocked_response = lsp::SignatureHelp {
14101 signatures: vec![lsp::SignatureInformation {
14102 label: "fn sample(param1: u8, param2: u8)".to_string(),
14103 documentation: None,
14104 parameters: Some(vec![
14105 lsp::ParameterInformation {
14106 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14107 documentation: None,
14108 },
14109 lsp::ParameterInformation {
14110 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14111 documentation: None,
14112 },
14113 ]),
14114 active_parameter: None,
14115 }],
14116 active_signature: Some(0),
14117 active_parameter: Some(0),
14118 };
14119 handle_signature_help_request(&mut cx, mocked_response).await;
14120
14121 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14122 .await;
14123
14124 cx.editor(|editor, _, _| {
14125 let signature_help_state = editor.signature_help_state.popover().cloned();
14126 assert!(signature_help_state.is_some());
14127 let signature = signature_help_state.unwrap();
14128 assert_eq!(
14129 signature.signatures[signature.current_signature].label,
14130 "fn sample(param1: u8, param2: u8)"
14131 );
14132 });
14133
14134 // When exiting outside from inside the brackets, `signature_help` is closed.
14135 cx.set_state(indoc! {"
14136 fn main() {
14137 sample(ˇ);
14138 }
14139
14140 fn sample(param1: u8, param2: u8) {}
14141 "});
14142
14143 cx.update_editor(|editor, window, cx| {
14144 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14145 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
14146 });
14147 });
14148
14149 let mocked_response = lsp::SignatureHelp {
14150 signatures: Vec::new(),
14151 active_signature: None,
14152 active_parameter: None,
14153 };
14154 handle_signature_help_request(&mut cx, mocked_response).await;
14155
14156 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
14157 .await;
14158
14159 cx.editor(|editor, _, _| {
14160 assert!(!editor.signature_help_state.is_shown());
14161 });
14162
14163 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
14164 cx.set_state(indoc! {"
14165 fn main() {
14166 sample(ˇ);
14167 }
14168
14169 fn sample(param1: u8, param2: u8) {}
14170 "});
14171
14172 let mocked_response = lsp::SignatureHelp {
14173 signatures: vec![lsp::SignatureInformation {
14174 label: "fn sample(param1: u8, param2: u8)".to_string(),
14175 documentation: None,
14176 parameters: Some(vec![
14177 lsp::ParameterInformation {
14178 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14179 documentation: None,
14180 },
14181 lsp::ParameterInformation {
14182 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14183 documentation: None,
14184 },
14185 ]),
14186 active_parameter: None,
14187 }],
14188 active_signature: Some(0),
14189 active_parameter: Some(0),
14190 };
14191 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14192 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14193 .await;
14194 cx.editor(|editor, _, _| {
14195 assert!(editor.signature_help_state.is_shown());
14196 });
14197
14198 // Restore the popover with more parameter input
14199 cx.set_state(indoc! {"
14200 fn main() {
14201 sample(param1, param2ˇ);
14202 }
14203
14204 fn sample(param1: u8, param2: u8) {}
14205 "});
14206
14207 let mocked_response = lsp::SignatureHelp {
14208 signatures: vec![lsp::SignatureInformation {
14209 label: "fn sample(param1: u8, param2: u8)".to_string(),
14210 documentation: None,
14211 parameters: Some(vec![
14212 lsp::ParameterInformation {
14213 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14214 documentation: None,
14215 },
14216 lsp::ParameterInformation {
14217 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14218 documentation: None,
14219 },
14220 ]),
14221 active_parameter: None,
14222 }],
14223 active_signature: Some(0),
14224 active_parameter: Some(1),
14225 };
14226 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14227 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14228 .await;
14229
14230 // When selecting a range, the popover is gone.
14231 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
14232 cx.update_editor(|editor, window, cx| {
14233 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14234 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
14235 })
14236 });
14237 cx.assert_editor_state(indoc! {"
14238 fn main() {
14239 sample(param1, «ˇparam2»);
14240 }
14241
14242 fn sample(param1: u8, param2: u8) {}
14243 "});
14244 cx.editor(|editor, _, _| {
14245 assert!(!editor.signature_help_state.is_shown());
14246 });
14247
14248 // When unselecting again, the popover is back if within the brackets.
14249 cx.update_editor(|editor, window, cx| {
14250 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14251 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14252 })
14253 });
14254 cx.assert_editor_state(indoc! {"
14255 fn main() {
14256 sample(param1, ˇparam2);
14257 }
14258
14259 fn sample(param1: u8, param2: u8) {}
14260 "});
14261 handle_signature_help_request(&mut cx, mocked_response).await;
14262 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14263 .await;
14264 cx.editor(|editor, _, _| {
14265 assert!(editor.signature_help_state.is_shown());
14266 });
14267
14268 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
14269 cx.update_editor(|editor, window, cx| {
14270 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14271 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
14272 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14273 })
14274 });
14275 cx.assert_editor_state(indoc! {"
14276 fn main() {
14277 sample(param1, ˇparam2);
14278 }
14279
14280 fn sample(param1: u8, param2: u8) {}
14281 "});
14282
14283 let mocked_response = lsp::SignatureHelp {
14284 signatures: vec![lsp::SignatureInformation {
14285 label: "fn sample(param1: u8, param2: u8)".to_string(),
14286 documentation: None,
14287 parameters: Some(vec![
14288 lsp::ParameterInformation {
14289 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14290 documentation: None,
14291 },
14292 lsp::ParameterInformation {
14293 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14294 documentation: None,
14295 },
14296 ]),
14297 active_parameter: None,
14298 }],
14299 active_signature: Some(0),
14300 active_parameter: Some(1),
14301 };
14302 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14303 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14304 .await;
14305 cx.update_editor(|editor, _, cx| {
14306 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
14307 });
14308 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
14309 .await;
14310 cx.update_editor(|editor, window, cx| {
14311 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14312 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
14313 })
14314 });
14315 cx.assert_editor_state(indoc! {"
14316 fn main() {
14317 sample(param1, «ˇparam2»);
14318 }
14319
14320 fn sample(param1: u8, param2: u8) {}
14321 "});
14322 cx.update_editor(|editor, window, cx| {
14323 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14324 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14325 })
14326 });
14327 cx.assert_editor_state(indoc! {"
14328 fn main() {
14329 sample(param1, ˇparam2);
14330 }
14331
14332 fn sample(param1: u8, param2: u8) {}
14333 "});
14334 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
14335 .await;
14336}
14337
14338#[gpui::test]
14339async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
14340 init_test(cx, |_| {});
14341
14342 let mut cx = EditorLspTestContext::new_rust(
14343 lsp::ServerCapabilities {
14344 signature_help_provider: Some(lsp::SignatureHelpOptions {
14345 ..Default::default()
14346 }),
14347 ..Default::default()
14348 },
14349 cx,
14350 )
14351 .await;
14352
14353 cx.set_state(indoc! {"
14354 fn main() {
14355 overloadedˇ
14356 }
14357 "});
14358
14359 cx.update_editor(|editor, window, cx| {
14360 editor.handle_input("(", window, cx);
14361 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14362 });
14363
14364 // Mock response with 3 signatures
14365 let mocked_response = lsp::SignatureHelp {
14366 signatures: vec![
14367 lsp::SignatureInformation {
14368 label: "fn overloaded(x: i32)".to_string(),
14369 documentation: None,
14370 parameters: Some(vec![lsp::ParameterInformation {
14371 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14372 documentation: None,
14373 }]),
14374 active_parameter: None,
14375 },
14376 lsp::SignatureInformation {
14377 label: "fn overloaded(x: i32, y: i32)".to_string(),
14378 documentation: None,
14379 parameters: Some(vec![
14380 lsp::ParameterInformation {
14381 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14382 documentation: None,
14383 },
14384 lsp::ParameterInformation {
14385 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14386 documentation: None,
14387 },
14388 ]),
14389 active_parameter: None,
14390 },
14391 lsp::SignatureInformation {
14392 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
14393 documentation: None,
14394 parameters: Some(vec![
14395 lsp::ParameterInformation {
14396 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14397 documentation: None,
14398 },
14399 lsp::ParameterInformation {
14400 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14401 documentation: None,
14402 },
14403 lsp::ParameterInformation {
14404 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
14405 documentation: None,
14406 },
14407 ]),
14408 active_parameter: None,
14409 },
14410 ],
14411 active_signature: Some(1),
14412 active_parameter: Some(0),
14413 };
14414 handle_signature_help_request(&mut cx, mocked_response).await;
14415
14416 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14417 .await;
14418
14419 // Verify we have multiple signatures and the right one is selected
14420 cx.editor(|editor, _, _| {
14421 let popover = editor.signature_help_state.popover().cloned().unwrap();
14422 assert_eq!(popover.signatures.len(), 3);
14423 // active_signature was 1, so that should be the current
14424 assert_eq!(popover.current_signature, 1);
14425 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
14426 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
14427 assert_eq!(
14428 popover.signatures[2].label,
14429 "fn overloaded(x: i32, y: i32, z: i32)"
14430 );
14431 });
14432
14433 // Test navigation functionality
14434 cx.update_editor(|editor, window, cx| {
14435 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14436 });
14437
14438 cx.editor(|editor, _, _| {
14439 let popover = editor.signature_help_state.popover().cloned().unwrap();
14440 assert_eq!(popover.current_signature, 2);
14441 });
14442
14443 // Test wrap around
14444 cx.update_editor(|editor, window, cx| {
14445 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14446 });
14447
14448 cx.editor(|editor, _, _| {
14449 let popover = editor.signature_help_state.popover().cloned().unwrap();
14450 assert_eq!(popover.current_signature, 0);
14451 });
14452
14453 // Test previous navigation
14454 cx.update_editor(|editor, window, cx| {
14455 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
14456 });
14457
14458 cx.editor(|editor, _, _| {
14459 let popover = editor.signature_help_state.popover().cloned().unwrap();
14460 assert_eq!(popover.current_signature, 2);
14461 });
14462}
14463
14464#[gpui::test]
14465async fn test_completion_mode(cx: &mut TestAppContext) {
14466 init_test(cx, |_| {});
14467 let mut cx = EditorLspTestContext::new_rust(
14468 lsp::ServerCapabilities {
14469 completion_provider: Some(lsp::CompletionOptions {
14470 resolve_provider: Some(true),
14471 ..Default::default()
14472 }),
14473 ..Default::default()
14474 },
14475 cx,
14476 )
14477 .await;
14478
14479 struct Run {
14480 run_description: &'static str,
14481 initial_state: String,
14482 buffer_marked_text: String,
14483 completion_label: &'static str,
14484 completion_text: &'static str,
14485 expected_with_insert_mode: String,
14486 expected_with_replace_mode: String,
14487 expected_with_replace_subsequence_mode: String,
14488 expected_with_replace_suffix_mode: String,
14489 }
14490
14491 let runs = [
14492 Run {
14493 run_description: "Start of word matches completion text",
14494 initial_state: "before ediˇ after".into(),
14495 buffer_marked_text: "before <edi|> after".into(),
14496 completion_label: "editor",
14497 completion_text: "editor",
14498 expected_with_insert_mode: "before editorˇ after".into(),
14499 expected_with_replace_mode: "before editorˇ after".into(),
14500 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14501 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14502 },
14503 Run {
14504 run_description: "Accept same text at the middle of the word",
14505 initial_state: "before ediˇtor after".into(),
14506 buffer_marked_text: "before <edi|tor> after".into(),
14507 completion_label: "editor",
14508 completion_text: "editor",
14509 expected_with_insert_mode: "before editorˇtor after".into(),
14510 expected_with_replace_mode: "before editorˇ after".into(),
14511 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14512 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14513 },
14514 Run {
14515 run_description: "End of word matches completion text -- cursor at end",
14516 initial_state: "before torˇ after".into(),
14517 buffer_marked_text: "before <tor|> after".into(),
14518 completion_label: "editor",
14519 completion_text: "editor",
14520 expected_with_insert_mode: "before editorˇ after".into(),
14521 expected_with_replace_mode: "before editorˇ after".into(),
14522 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14523 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14524 },
14525 Run {
14526 run_description: "End of word matches completion text -- cursor at start",
14527 initial_state: "before ˇtor after".into(),
14528 buffer_marked_text: "before <|tor> after".into(),
14529 completion_label: "editor",
14530 completion_text: "editor",
14531 expected_with_insert_mode: "before editorˇtor after".into(),
14532 expected_with_replace_mode: "before editorˇ after".into(),
14533 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14534 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14535 },
14536 Run {
14537 run_description: "Prepend text containing whitespace",
14538 initial_state: "pˇfield: bool".into(),
14539 buffer_marked_text: "<p|field>: bool".into(),
14540 completion_label: "pub ",
14541 completion_text: "pub ",
14542 expected_with_insert_mode: "pub ˇfield: bool".into(),
14543 expected_with_replace_mode: "pub ˇ: bool".into(),
14544 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
14545 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
14546 },
14547 Run {
14548 run_description: "Add element to start of list",
14549 initial_state: "[element_ˇelement_2]".into(),
14550 buffer_marked_text: "[<element_|element_2>]".into(),
14551 completion_label: "element_1",
14552 completion_text: "element_1",
14553 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
14554 expected_with_replace_mode: "[element_1ˇ]".into(),
14555 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
14556 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
14557 },
14558 Run {
14559 run_description: "Add element to start of list -- first and second elements are equal",
14560 initial_state: "[elˇelement]".into(),
14561 buffer_marked_text: "[<el|element>]".into(),
14562 completion_label: "element",
14563 completion_text: "element",
14564 expected_with_insert_mode: "[elementˇelement]".into(),
14565 expected_with_replace_mode: "[elementˇ]".into(),
14566 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
14567 expected_with_replace_suffix_mode: "[elementˇ]".into(),
14568 },
14569 Run {
14570 run_description: "Ends with matching suffix",
14571 initial_state: "SubˇError".into(),
14572 buffer_marked_text: "<Sub|Error>".into(),
14573 completion_label: "SubscriptionError",
14574 completion_text: "SubscriptionError",
14575 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
14576 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14577 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14578 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
14579 },
14580 Run {
14581 run_description: "Suffix is a subsequence -- contiguous",
14582 initial_state: "SubˇErr".into(),
14583 buffer_marked_text: "<Sub|Err>".into(),
14584 completion_label: "SubscriptionError",
14585 completion_text: "SubscriptionError",
14586 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
14587 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14588 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14589 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
14590 },
14591 Run {
14592 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
14593 initial_state: "Suˇscrirr".into(),
14594 buffer_marked_text: "<Su|scrirr>".into(),
14595 completion_label: "SubscriptionError",
14596 completion_text: "SubscriptionError",
14597 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
14598 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14599 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14600 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
14601 },
14602 Run {
14603 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
14604 initial_state: "foo(indˇix)".into(),
14605 buffer_marked_text: "foo(<ind|ix>)".into(),
14606 completion_label: "node_index",
14607 completion_text: "node_index",
14608 expected_with_insert_mode: "foo(node_indexˇix)".into(),
14609 expected_with_replace_mode: "foo(node_indexˇ)".into(),
14610 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
14611 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
14612 },
14613 Run {
14614 run_description: "Replace range ends before cursor - should extend to cursor",
14615 initial_state: "before editˇo after".into(),
14616 buffer_marked_text: "before <{ed}>it|o after".into(),
14617 completion_label: "editor",
14618 completion_text: "editor",
14619 expected_with_insert_mode: "before editorˇo after".into(),
14620 expected_with_replace_mode: "before editorˇo after".into(),
14621 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
14622 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14623 },
14624 Run {
14625 run_description: "Uses label for suffix matching",
14626 initial_state: "before ediˇtor after".into(),
14627 buffer_marked_text: "before <edi|tor> after".into(),
14628 completion_label: "editor",
14629 completion_text: "editor()",
14630 expected_with_insert_mode: "before editor()ˇtor after".into(),
14631 expected_with_replace_mode: "before editor()ˇ after".into(),
14632 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14633 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14634 },
14635 Run {
14636 run_description: "Case insensitive subsequence and suffix matching",
14637 initial_state: "before EDiˇtoR after".into(),
14638 buffer_marked_text: "before <EDi|toR> after".into(),
14639 completion_label: "editor",
14640 completion_text: "editor",
14641 expected_with_insert_mode: "before editorˇtoR after".into(),
14642 expected_with_replace_mode: "before editorˇ after".into(),
14643 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14644 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14645 },
14646 ];
14647
14648 for run in runs {
14649 let run_variations = [
14650 (LspInsertMode::Insert, run.expected_with_insert_mode),
14651 (LspInsertMode::Replace, run.expected_with_replace_mode),
14652 (
14653 LspInsertMode::ReplaceSubsequence,
14654 run.expected_with_replace_subsequence_mode,
14655 ),
14656 (
14657 LspInsertMode::ReplaceSuffix,
14658 run.expected_with_replace_suffix_mode,
14659 ),
14660 ];
14661
14662 for (lsp_insert_mode, expected_text) in run_variations {
14663 eprintln!(
14664 "run = {:?}, mode = {lsp_insert_mode:.?}",
14665 run.run_description,
14666 );
14667
14668 update_test_language_settings(&mut cx, |settings| {
14669 settings.defaults.completions = Some(CompletionSettingsContent {
14670 lsp_insert_mode: Some(lsp_insert_mode),
14671 words: Some(WordsCompletionMode::Disabled),
14672 words_min_length: Some(0),
14673 ..Default::default()
14674 });
14675 });
14676
14677 cx.set_state(&run.initial_state);
14678
14679 // Set up resolve handler before showing completions, since resolve may be
14680 // triggered when menu becomes visible (for documentation), not just on confirm.
14681 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(
14682 move |_, _, _| async move {
14683 Ok(lsp::CompletionItem {
14684 additional_text_edits: None,
14685 ..Default::default()
14686 })
14687 },
14688 );
14689
14690 cx.update_editor(|editor, window, cx| {
14691 editor.show_completions(&ShowCompletions, window, cx);
14692 });
14693
14694 let counter = Arc::new(AtomicUsize::new(0));
14695 handle_completion_request_with_insert_and_replace(
14696 &mut cx,
14697 &run.buffer_marked_text,
14698 vec![(run.completion_label, run.completion_text)],
14699 counter.clone(),
14700 )
14701 .await;
14702 cx.condition(|editor, _| editor.context_menu_visible())
14703 .await;
14704 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14705
14706 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14707 editor
14708 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14709 .unwrap()
14710 });
14711 cx.assert_editor_state(&expected_text);
14712 apply_additional_edits.await.unwrap();
14713 }
14714 }
14715}
14716
14717#[gpui::test]
14718async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14719 init_test(cx, |_| {});
14720 let mut cx = EditorLspTestContext::new_rust(
14721 lsp::ServerCapabilities {
14722 completion_provider: Some(lsp::CompletionOptions {
14723 resolve_provider: Some(true),
14724 ..Default::default()
14725 }),
14726 ..Default::default()
14727 },
14728 cx,
14729 )
14730 .await;
14731
14732 let initial_state = "SubˇError";
14733 let buffer_marked_text = "<Sub|Error>";
14734 let completion_text = "SubscriptionError";
14735 let expected_with_insert_mode = "SubscriptionErrorˇError";
14736 let expected_with_replace_mode = "SubscriptionErrorˇ";
14737
14738 update_test_language_settings(&mut cx, |settings| {
14739 settings.defaults.completions = Some(CompletionSettingsContent {
14740 words: Some(WordsCompletionMode::Disabled),
14741 words_min_length: Some(0),
14742 // set the opposite here to ensure that the action is overriding the default behavior
14743 lsp_insert_mode: Some(LspInsertMode::Insert),
14744 ..Default::default()
14745 });
14746 });
14747
14748 cx.set_state(initial_state);
14749 cx.update_editor(|editor, window, cx| {
14750 editor.show_completions(&ShowCompletions, window, cx);
14751 });
14752
14753 let counter = Arc::new(AtomicUsize::new(0));
14754 handle_completion_request_with_insert_and_replace(
14755 &mut cx,
14756 buffer_marked_text,
14757 vec![(completion_text, completion_text)],
14758 counter.clone(),
14759 )
14760 .await;
14761 cx.condition(|editor, _| editor.context_menu_visible())
14762 .await;
14763 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14764
14765 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14766 editor
14767 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14768 .unwrap()
14769 });
14770 cx.assert_editor_state(expected_with_replace_mode);
14771 handle_resolve_completion_request(&mut cx, None).await;
14772 apply_additional_edits.await.unwrap();
14773
14774 update_test_language_settings(&mut cx, |settings| {
14775 settings.defaults.completions = Some(CompletionSettingsContent {
14776 words: Some(WordsCompletionMode::Disabled),
14777 words_min_length: Some(0),
14778 // set the opposite here to ensure that the action is overriding the default behavior
14779 lsp_insert_mode: Some(LspInsertMode::Replace),
14780 ..Default::default()
14781 });
14782 });
14783
14784 cx.set_state(initial_state);
14785 cx.update_editor(|editor, window, cx| {
14786 editor.show_completions(&ShowCompletions, window, cx);
14787 });
14788 handle_completion_request_with_insert_and_replace(
14789 &mut cx,
14790 buffer_marked_text,
14791 vec![(completion_text, completion_text)],
14792 counter.clone(),
14793 )
14794 .await;
14795 cx.condition(|editor, _| editor.context_menu_visible())
14796 .await;
14797 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14798
14799 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14800 editor
14801 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14802 .unwrap()
14803 });
14804 cx.assert_editor_state(expected_with_insert_mode);
14805 handle_resolve_completion_request(&mut cx, None).await;
14806 apply_additional_edits.await.unwrap();
14807}
14808
14809#[gpui::test]
14810async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14811 init_test(cx, |_| {});
14812 let mut cx = EditorLspTestContext::new_rust(
14813 lsp::ServerCapabilities {
14814 completion_provider: Some(lsp::CompletionOptions {
14815 resolve_provider: Some(true),
14816 ..Default::default()
14817 }),
14818 ..Default::default()
14819 },
14820 cx,
14821 )
14822 .await;
14823
14824 // scenario: surrounding text matches completion text
14825 let completion_text = "to_offset";
14826 let initial_state = indoc! {"
14827 1. buf.to_offˇsuffix
14828 2. buf.to_offˇsuf
14829 3. buf.to_offˇfix
14830 4. buf.to_offˇ
14831 5. into_offˇensive
14832 6. ˇsuffix
14833 7. let ˇ //
14834 8. aaˇzz
14835 9. buf.to_off«zzzzzˇ»suffix
14836 10. buf.«ˇzzzzz»suffix
14837 11. to_off«ˇzzzzz»
14838
14839 buf.to_offˇsuffix // newest cursor
14840 "};
14841 let completion_marked_buffer = indoc! {"
14842 1. buf.to_offsuffix
14843 2. buf.to_offsuf
14844 3. buf.to_offfix
14845 4. buf.to_off
14846 5. into_offensive
14847 6. suffix
14848 7. let //
14849 8. aazz
14850 9. buf.to_offzzzzzsuffix
14851 10. buf.zzzzzsuffix
14852 11. to_offzzzzz
14853
14854 buf.<to_off|suffix> // newest cursor
14855 "};
14856 let expected = indoc! {"
14857 1. buf.to_offsetˇ
14858 2. buf.to_offsetˇsuf
14859 3. buf.to_offsetˇfix
14860 4. buf.to_offsetˇ
14861 5. into_offsetˇensive
14862 6. to_offsetˇsuffix
14863 7. let to_offsetˇ //
14864 8. aato_offsetˇzz
14865 9. buf.to_offsetˇ
14866 10. buf.to_offsetˇsuffix
14867 11. to_offsetˇ
14868
14869 buf.to_offsetˇ // newest cursor
14870 "};
14871 cx.set_state(initial_state);
14872 cx.update_editor(|editor, window, cx| {
14873 editor.show_completions(&ShowCompletions, window, cx);
14874 });
14875 handle_completion_request_with_insert_and_replace(
14876 &mut cx,
14877 completion_marked_buffer,
14878 vec![(completion_text, completion_text)],
14879 Arc::new(AtomicUsize::new(0)),
14880 )
14881 .await;
14882 cx.condition(|editor, _| editor.context_menu_visible())
14883 .await;
14884 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14885 editor
14886 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14887 .unwrap()
14888 });
14889 cx.assert_editor_state(expected);
14890 handle_resolve_completion_request(&mut cx, None).await;
14891 apply_additional_edits.await.unwrap();
14892
14893 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14894 let completion_text = "foo_and_bar";
14895 let initial_state = indoc! {"
14896 1. ooanbˇ
14897 2. zooanbˇ
14898 3. ooanbˇz
14899 4. zooanbˇz
14900 5. ooanˇ
14901 6. oanbˇ
14902
14903 ooanbˇ
14904 "};
14905 let completion_marked_buffer = indoc! {"
14906 1. ooanb
14907 2. zooanb
14908 3. ooanbz
14909 4. zooanbz
14910 5. ooan
14911 6. oanb
14912
14913 <ooanb|>
14914 "};
14915 let expected = indoc! {"
14916 1. foo_and_barˇ
14917 2. zfoo_and_barˇ
14918 3. foo_and_barˇz
14919 4. zfoo_and_barˇz
14920 5. ooanfoo_and_barˇ
14921 6. oanbfoo_and_barˇ
14922
14923 foo_and_barˇ
14924 "};
14925 cx.set_state(initial_state);
14926 cx.update_editor(|editor, window, cx| {
14927 editor.show_completions(&ShowCompletions, window, cx);
14928 });
14929 handle_completion_request_with_insert_and_replace(
14930 &mut cx,
14931 completion_marked_buffer,
14932 vec![(completion_text, completion_text)],
14933 Arc::new(AtomicUsize::new(0)),
14934 )
14935 .await;
14936 cx.condition(|editor, _| editor.context_menu_visible())
14937 .await;
14938 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14939 editor
14940 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14941 .unwrap()
14942 });
14943 cx.assert_editor_state(expected);
14944 handle_resolve_completion_request(&mut cx, None).await;
14945 apply_additional_edits.await.unwrap();
14946
14947 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14948 // (expects the same as if it was inserted at the end)
14949 let completion_text = "foo_and_bar";
14950 let initial_state = indoc! {"
14951 1. ooˇanb
14952 2. zooˇanb
14953 3. ooˇanbz
14954 4. zooˇanbz
14955
14956 ooˇanb
14957 "};
14958 let completion_marked_buffer = indoc! {"
14959 1. ooanb
14960 2. zooanb
14961 3. ooanbz
14962 4. zooanbz
14963
14964 <oo|anb>
14965 "};
14966 let expected = indoc! {"
14967 1. foo_and_barˇ
14968 2. zfoo_and_barˇ
14969 3. foo_and_barˇz
14970 4. zfoo_and_barˇz
14971
14972 foo_and_barˇ
14973 "};
14974 cx.set_state(initial_state);
14975 cx.update_editor(|editor, window, cx| {
14976 editor.show_completions(&ShowCompletions, window, cx);
14977 });
14978 handle_completion_request_with_insert_and_replace(
14979 &mut cx,
14980 completion_marked_buffer,
14981 vec![(completion_text, completion_text)],
14982 Arc::new(AtomicUsize::new(0)),
14983 )
14984 .await;
14985 cx.condition(|editor, _| editor.context_menu_visible())
14986 .await;
14987 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14988 editor
14989 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14990 .unwrap()
14991 });
14992 cx.assert_editor_state(expected);
14993 handle_resolve_completion_request(&mut cx, None).await;
14994 apply_additional_edits.await.unwrap();
14995}
14996
14997// This used to crash
14998#[gpui::test]
14999async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
15000 init_test(cx, |_| {});
15001
15002 let buffer_text = indoc! {"
15003 fn main() {
15004 10.satu;
15005
15006 //
15007 // separate cursors so they open in different excerpts (manually reproducible)
15008 //
15009
15010 10.satu20;
15011 }
15012 "};
15013 let multibuffer_text_with_selections = indoc! {"
15014 fn main() {
15015 10.satuˇ;
15016
15017 //
15018
15019 //
15020
15021 10.satuˇ20;
15022 }
15023 "};
15024 let expected_multibuffer = indoc! {"
15025 fn main() {
15026 10.saturating_sub()ˇ;
15027
15028 //
15029
15030 //
15031
15032 10.saturating_sub()ˇ;
15033 }
15034 "};
15035
15036 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
15037 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
15038
15039 let fs = FakeFs::new(cx.executor());
15040 fs.insert_tree(
15041 path!("/a"),
15042 json!({
15043 "main.rs": buffer_text,
15044 }),
15045 )
15046 .await;
15047
15048 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15049 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15050 language_registry.add(rust_lang());
15051 let mut fake_servers = language_registry.register_fake_lsp(
15052 "Rust",
15053 FakeLspAdapter {
15054 capabilities: lsp::ServerCapabilities {
15055 completion_provider: Some(lsp::CompletionOptions {
15056 resolve_provider: None,
15057 ..lsp::CompletionOptions::default()
15058 }),
15059 ..lsp::ServerCapabilities::default()
15060 },
15061 ..FakeLspAdapter::default()
15062 },
15063 );
15064 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15065 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15066 let buffer = project
15067 .update(cx, |project, cx| {
15068 project.open_local_buffer(path!("/a/main.rs"), cx)
15069 })
15070 .await
15071 .unwrap();
15072
15073 let multi_buffer = cx.new(|cx| {
15074 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
15075 multi_buffer.push_excerpts(
15076 buffer.clone(),
15077 [ExcerptRange::new(0..first_excerpt_end)],
15078 cx,
15079 );
15080 multi_buffer.push_excerpts(
15081 buffer.clone(),
15082 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
15083 cx,
15084 );
15085 multi_buffer
15086 });
15087
15088 let editor = workspace
15089 .update(cx, |_, window, cx| {
15090 cx.new(|cx| {
15091 Editor::new(
15092 EditorMode::Full {
15093 scale_ui_elements_with_buffer_font_size: false,
15094 show_active_line_background: false,
15095 sizing_behavior: SizingBehavior::Default,
15096 },
15097 multi_buffer.clone(),
15098 Some(project.clone()),
15099 window,
15100 cx,
15101 )
15102 })
15103 })
15104 .unwrap();
15105
15106 let pane = workspace
15107 .update(cx, |workspace, _, _| workspace.active_pane().clone())
15108 .unwrap();
15109 pane.update_in(cx, |pane, window, cx| {
15110 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
15111 });
15112
15113 let fake_server = fake_servers.next().await.unwrap();
15114 cx.run_until_parked();
15115
15116 editor.update_in(cx, |editor, window, cx| {
15117 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15118 s.select_ranges([
15119 Point::new(1, 11)..Point::new(1, 11),
15120 Point::new(7, 11)..Point::new(7, 11),
15121 ])
15122 });
15123
15124 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
15125 });
15126
15127 editor.update_in(cx, |editor, window, cx| {
15128 editor.show_completions(&ShowCompletions, window, cx);
15129 });
15130
15131 fake_server
15132 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15133 let completion_item = lsp::CompletionItem {
15134 label: "saturating_sub()".into(),
15135 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15136 lsp::InsertReplaceEdit {
15137 new_text: "saturating_sub()".to_owned(),
15138 insert: lsp::Range::new(
15139 lsp::Position::new(7, 7),
15140 lsp::Position::new(7, 11),
15141 ),
15142 replace: lsp::Range::new(
15143 lsp::Position::new(7, 7),
15144 lsp::Position::new(7, 13),
15145 ),
15146 },
15147 )),
15148 ..lsp::CompletionItem::default()
15149 };
15150
15151 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
15152 })
15153 .next()
15154 .await
15155 .unwrap();
15156
15157 cx.condition(&editor, |editor, _| editor.context_menu_visible())
15158 .await;
15159
15160 editor
15161 .update_in(cx, |editor, window, cx| {
15162 editor
15163 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
15164 .unwrap()
15165 })
15166 .await
15167 .unwrap();
15168
15169 editor.update(cx, |editor, cx| {
15170 assert_text_with_selections(editor, expected_multibuffer, cx);
15171 })
15172}
15173
15174#[gpui::test]
15175async fn test_completion(cx: &mut TestAppContext) {
15176 init_test(cx, |_| {});
15177
15178 let mut cx = EditorLspTestContext::new_rust(
15179 lsp::ServerCapabilities {
15180 completion_provider: Some(lsp::CompletionOptions {
15181 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15182 resolve_provider: Some(true),
15183 ..Default::default()
15184 }),
15185 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15186 ..Default::default()
15187 },
15188 cx,
15189 )
15190 .await;
15191 let counter = Arc::new(AtomicUsize::new(0));
15192
15193 cx.set_state(indoc! {"
15194 oneˇ
15195 two
15196 three
15197 "});
15198 cx.simulate_keystroke(".");
15199 handle_completion_request(
15200 indoc! {"
15201 one.|<>
15202 two
15203 three
15204 "},
15205 vec!["first_completion", "second_completion"],
15206 true,
15207 counter.clone(),
15208 &mut cx,
15209 )
15210 .await;
15211 cx.condition(|editor, _| editor.context_menu_visible())
15212 .await;
15213 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15214
15215 let _handler = handle_signature_help_request(
15216 &mut cx,
15217 lsp::SignatureHelp {
15218 signatures: vec![lsp::SignatureInformation {
15219 label: "test signature".to_string(),
15220 documentation: None,
15221 parameters: Some(vec![lsp::ParameterInformation {
15222 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
15223 documentation: None,
15224 }]),
15225 active_parameter: None,
15226 }],
15227 active_signature: None,
15228 active_parameter: None,
15229 },
15230 );
15231 cx.update_editor(|editor, window, cx| {
15232 assert!(
15233 !editor.signature_help_state.is_shown(),
15234 "No signature help was called for"
15235 );
15236 editor.show_signature_help(&ShowSignatureHelp, window, cx);
15237 });
15238 cx.run_until_parked();
15239 cx.update_editor(|editor, _, _| {
15240 assert!(
15241 !editor.signature_help_state.is_shown(),
15242 "No signature help should be shown when completions menu is open"
15243 );
15244 });
15245
15246 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15247 editor.context_menu_next(&Default::default(), window, cx);
15248 editor
15249 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15250 .unwrap()
15251 });
15252 cx.assert_editor_state(indoc! {"
15253 one.second_completionˇ
15254 two
15255 three
15256 "});
15257
15258 handle_resolve_completion_request(
15259 &mut cx,
15260 Some(vec![
15261 (
15262 //This overlaps with the primary completion edit which is
15263 //misbehavior from the LSP spec, test that we filter it out
15264 indoc! {"
15265 one.second_ˇcompletion
15266 two
15267 threeˇ
15268 "},
15269 "overlapping additional edit",
15270 ),
15271 (
15272 indoc! {"
15273 one.second_completion
15274 two
15275 threeˇ
15276 "},
15277 "\nadditional edit",
15278 ),
15279 ]),
15280 )
15281 .await;
15282 apply_additional_edits.await.unwrap();
15283 cx.assert_editor_state(indoc! {"
15284 one.second_completionˇ
15285 two
15286 three
15287 additional edit
15288 "});
15289
15290 cx.set_state(indoc! {"
15291 one.second_completion
15292 twoˇ
15293 threeˇ
15294 additional edit
15295 "});
15296 cx.simulate_keystroke(" ");
15297 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15298 cx.simulate_keystroke("s");
15299 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15300
15301 cx.assert_editor_state(indoc! {"
15302 one.second_completion
15303 two sˇ
15304 three sˇ
15305 additional edit
15306 "});
15307 handle_completion_request(
15308 indoc! {"
15309 one.second_completion
15310 two s
15311 three <s|>
15312 additional edit
15313 "},
15314 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
15315 true,
15316 counter.clone(),
15317 &mut cx,
15318 )
15319 .await;
15320 cx.condition(|editor, _| editor.context_menu_visible())
15321 .await;
15322 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15323
15324 cx.simulate_keystroke("i");
15325
15326 handle_completion_request(
15327 indoc! {"
15328 one.second_completion
15329 two si
15330 three <si|>
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), 3);
15342
15343 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15344 editor
15345 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15346 .unwrap()
15347 });
15348 cx.assert_editor_state(indoc! {"
15349 one.second_completion
15350 two sixth_completionˇ
15351 three sixth_completionˇ
15352 additional edit
15353 "});
15354
15355 apply_additional_edits.await.unwrap();
15356
15357 update_test_language_settings(&mut cx, |settings| {
15358 settings.defaults.show_completions_on_input = Some(false);
15359 });
15360 cx.set_state("editorˇ");
15361 cx.simulate_keystroke(".");
15362 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15363 cx.simulate_keystrokes("c l o");
15364 cx.assert_editor_state("editor.cloˇ");
15365 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15366 cx.update_editor(|editor, window, cx| {
15367 editor.show_completions(&ShowCompletions, window, cx);
15368 });
15369 handle_completion_request(
15370 "editor.<clo|>",
15371 vec!["close", "clobber"],
15372 true,
15373 counter.clone(),
15374 &mut cx,
15375 )
15376 .await;
15377 cx.condition(|editor, _| editor.context_menu_visible())
15378 .await;
15379 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
15380
15381 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15382 editor
15383 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15384 .unwrap()
15385 });
15386 cx.assert_editor_state("editor.clobberˇ");
15387 handle_resolve_completion_request(&mut cx, None).await;
15388 apply_additional_edits.await.unwrap();
15389}
15390
15391#[gpui::test]
15392async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
15393 init_test(cx, |_| {});
15394
15395 let fs = FakeFs::new(cx.executor());
15396 fs.insert_tree(
15397 path!("/a"),
15398 json!({
15399 "main.rs": "",
15400 }),
15401 )
15402 .await;
15403
15404 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15405 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15406 language_registry.add(rust_lang());
15407 let command_calls = Arc::new(AtomicUsize::new(0));
15408 let registered_command = "_the/command";
15409
15410 let closure_command_calls = command_calls.clone();
15411 let mut fake_servers = language_registry.register_fake_lsp(
15412 "Rust",
15413 FakeLspAdapter {
15414 capabilities: lsp::ServerCapabilities {
15415 completion_provider: Some(lsp::CompletionOptions {
15416 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15417 ..lsp::CompletionOptions::default()
15418 }),
15419 execute_command_provider: Some(lsp::ExecuteCommandOptions {
15420 commands: vec![registered_command.to_owned()],
15421 ..lsp::ExecuteCommandOptions::default()
15422 }),
15423 ..lsp::ServerCapabilities::default()
15424 },
15425 initializer: Some(Box::new(move |fake_server| {
15426 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15427 move |params, _| async move {
15428 Ok(Some(lsp::CompletionResponse::Array(vec![
15429 lsp::CompletionItem {
15430 label: "registered_command".to_owned(),
15431 text_edit: gen_text_edit(¶ms, ""),
15432 command: Some(lsp::Command {
15433 title: registered_command.to_owned(),
15434 command: "_the/command".to_owned(),
15435 arguments: Some(vec![serde_json::Value::Bool(true)]),
15436 }),
15437 ..lsp::CompletionItem::default()
15438 },
15439 lsp::CompletionItem {
15440 label: "unregistered_command".to_owned(),
15441 text_edit: gen_text_edit(¶ms, ""),
15442 command: Some(lsp::Command {
15443 title: "????????????".to_owned(),
15444 command: "????????????".to_owned(),
15445 arguments: Some(vec![serde_json::Value::Null]),
15446 }),
15447 ..lsp::CompletionItem::default()
15448 },
15449 ])))
15450 },
15451 );
15452 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
15453 let command_calls = closure_command_calls.clone();
15454 move |params, _| {
15455 assert_eq!(params.command, registered_command);
15456 let command_calls = command_calls.clone();
15457 async move {
15458 command_calls.fetch_add(1, atomic::Ordering::Release);
15459 Ok(Some(json!(null)))
15460 }
15461 }
15462 });
15463 })),
15464 ..FakeLspAdapter::default()
15465 },
15466 );
15467 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15468 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15469 let editor = workspace
15470 .update(cx, |workspace, window, cx| {
15471 workspace.open_abs_path(
15472 PathBuf::from(path!("/a/main.rs")),
15473 OpenOptions::default(),
15474 window,
15475 cx,
15476 )
15477 })
15478 .unwrap()
15479 .await
15480 .unwrap()
15481 .downcast::<Editor>()
15482 .unwrap();
15483 let _fake_server = fake_servers.next().await.unwrap();
15484 cx.run_until_parked();
15485
15486 editor.update_in(cx, |editor, window, cx| {
15487 cx.focus_self(window);
15488 editor.move_to_end(&MoveToEnd, window, cx);
15489 editor.handle_input(".", window, cx);
15490 });
15491 cx.run_until_parked();
15492 editor.update(cx, |editor, _| {
15493 assert!(editor.context_menu_visible());
15494 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15495 {
15496 let completion_labels = menu
15497 .completions
15498 .borrow()
15499 .iter()
15500 .map(|c| c.label.text.clone())
15501 .collect::<Vec<_>>();
15502 assert_eq!(
15503 completion_labels,
15504 &["registered_command", "unregistered_command",],
15505 );
15506 } else {
15507 panic!("expected completion menu to be open");
15508 }
15509 });
15510
15511 editor
15512 .update_in(cx, |editor, window, cx| {
15513 editor
15514 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15515 .unwrap()
15516 })
15517 .await
15518 .unwrap();
15519 cx.run_until_parked();
15520 assert_eq!(
15521 command_calls.load(atomic::Ordering::Acquire),
15522 1,
15523 "For completion with a registered command, Zed should send a command execution request",
15524 );
15525
15526 editor.update_in(cx, |editor, window, cx| {
15527 cx.focus_self(window);
15528 editor.handle_input(".", window, cx);
15529 });
15530 cx.run_until_parked();
15531 editor.update(cx, |editor, _| {
15532 assert!(editor.context_menu_visible());
15533 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15534 {
15535 let completion_labels = menu
15536 .completions
15537 .borrow()
15538 .iter()
15539 .map(|c| c.label.text.clone())
15540 .collect::<Vec<_>>();
15541 assert_eq!(
15542 completion_labels,
15543 &["registered_command", "unregistered_command",],
15544 );
15545 } else {
15546 panic!("expected completion menu to be open");
15547 }
15548 });
15549 editor
15550 .update_in(cx, |editor, window, cx| {
15551 editor.context_menu_next(&Default::default(), window, cx);
15552 editor
15553 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15554 .unwrap()
15555 })
15556 .await
15557 .unwrap();
15558 cx.run_until_parked();
15559 assert_eq!(
15560 command_calls.load(atomic::Ordering::Acquire),
15561 1,
15562 "For completion with an unregistered command, Zed should not send a command execution request",
15563 );
15564}
15565
15566#[gpui::test]
15567async fn test_completion_reuse(cx: &mut TestAppContext) {
15568 init_test(cx, |_| {});
15569
15570 let mut cx = EditorLspTestContext::new_rust(
15571 lsp::ServerCapabilities {
15572 completion_provider: Some(lsp::CompletionOptions {
15573 trigger_characters: Some(vec![".".to_string()]),
15574 ..Default::default()
15575 }),
15576 ..Default::default()
15577 },
15578 cx,
15579 )
15580 .await;
15581
15582 let counter = Arc::new(AtomicUsize::new(0));
15583 cx.set_state("objˇ");
15584 cx.simulate_keystroke(".");
15585
15586 // Initial completion request returns complete results
15587 let is_incomplete = false;
15588 handle_completion_request(
15589 "obj.|<>",
15590 vec!["a", "ab", "abc"],
15591 is_incomplete,
15592 counter.clone(),
15593 &mut cx,
15594 )
15595 .await;
15596 cx.run_until_parked();
15597 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15598 cx.assert_editor_state("obj.ˇ");
15599 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15600
15601 // Type "a" - filters existing completions
15602 cx.simulate_keystroke("a");
15603 cx.run_until_parked();
15604 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15605 cx.assert_editor_state("obj.aˇ");
15606 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15607
15608 // Type "b" - filters existing completions
15609 cx.simulate_keystroke("b");
15610 cx.run_until_parked();
15611 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15612 cx.assert_editor_state("obj.abˇ");
15613 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15614
15615 // Type "c" - filters existing completions
15616 cx.simulate_keystroke("c");
15617 cx.run_until_parked();
15618 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15619 cx.assert_editor_state("obj.abcˇ");
15620 check_displayed_completions(vec!["abc"], &mut cx);
15621
15622 // Backspace to delete "c" - filters existing completions
15623 cx.update_editor(|editor, window, cx| {
15624 editor.backspace(&Backspace, window, cx);
15625 });
15626 cx.run_until_parked();
15627 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15628 cx.assert_editor_state("obj.abˇ");
15629 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15630
15631 // Moving cursor to the left dismisses menu.
15632 cx.update_editor(|editor, window, cx| {
15633 editor.move_left(&MoveLeft, window, cx);
15634 });
15635 cx.run_until_parked();
15636 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15637 cx.assert_editor_state("obj.aˇb");
15638 cx.update_editor(|editor, _, _| {
15639 assert_eq!(editor.context_menu_visible(), false);
15640 });
15641
15642 // Type "b" - new request
15643 cx.simulate_keystroke("b");
15644 let is_incomplete = false;
15645 handle_completion_request(
15646 "obj.<ab|>a",
15647 vec!["ab", "abc"],
15648 is_incomplete,
15649 counter.clone(),
15650 &mut cx,
15651 )
15652 .await;
15653 cx.run_until_parked();
15654 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15655 cx.assert_editor_state("obj.abˇb");
15656 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15657
15658 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
15659 cx.update_editor(|editor, window, cx| {
15660 editor.backspace(&Backspace, window, cx);
15661 });
15662 let is_incomplete = false;
15663 handle_completion_request(
15664 "obj.<a|>b",
15665 vec!["a", "ab", "abc"],
15666 is_incomplete,
15667 counter.clone(),
15668 &mut cx,
15669 )
15670 .await;
15671 cx.run_until_parked();
15672 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15673 cx.assert_editor_state("obj.aˇb");
15674 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15675
15676 // Backspace to delete "a" - dismisses menu.
15677 cx.update_editor(|editor, window, cx| {
15678 editor.backspace(&Backspace, window, cx);
15679 });
15680 cx.run_until_parked();
15681 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15682 cx.assert_editor_state("obj.ˇb");
15683 cx.update_editor(|editor, _, _| {
15684 assert_eq!(editor.context_menu_visible(), false);
15685 });
15686}
15687
15688#[gpui::test]
15689async fn test_word_completion(cx: &mut TestAppContext) {
15690 let lsp_fetch_timeout_ms = 10;
15691 init_test(cx, |language_settings| {
15692 language_settings.defaults.completions = Some(CompletionSettingsContent {
15693 words_min_length: Some(0),
15694 lsp_fetch_timeout_ms: Some(10),
15695 lsp_insert_mode: Some(LspInsertMode::Insert),
15696 ..Default::default()
15697 });
15698 });
15699
15700 let mut cx = EditorLspTestContext::new_rust(
15701 lsp::ServerCapabilities {
15702 completion_provider: Some(lsp::CompletionOptions {
15703 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15704 ..lsp::CompletionOptions::default()
15705 }),
15706 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15707 ..lsp::ServerCapabilities::default()
15708 },
15709 cx,
15710 )
15711 .await;
15712
15713 let throttle_completions = Arc::new(AtomicBool::new(false));
15714
15715 let lsp_throttle_completions = throttle_completions.clone();
15716 let _completion_requests_handler =
15717 cx.lsp
15718 .server
15719 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
15720 let lsp_throttle_completions = lsp_throttle_completions.clone();
15721 let cx = cx.clone();
15722 async move {
15723 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
15724 cx.background_executor()
15725 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
15726 .await;
15727 }
15728 Ok(Some(lsp::CompletionResponse::Array(vec![
15729 lsp::CompletionItem {
15730 label: "first".into(),
15731 ..lsp::CompletionItem::default()
15732 },
15733 lsp::CompletionItem {
15734 label: "last".into(),
15735 ..lsp::CompletionItem::default()
15736 },
15737 ])))
15738 }
15739 });
15740
15741 cx.set_state(indoc! {"
15742 oneˇ
15743 two
15744 three
15745 "});
15746 cx.simulate_keystroke(".");
15747 cx.executor().run_until_parked();
15748 cx.condition(|editor, _| editor.context_menu_visible())
15749 .await;
15750 cx.update_editor(|editor, window, cx| {
15751 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15752 {
15753 assert_eq!(
15754 completion_menu_entries(menu),
15755 &["first", "last"],
15756 "When LSP server is fast to reply, no fallback word completions are used"
15757 );
15758 } else {
15759 panic!("expected completion menu to be open");
15760 }
15761 editor.cancel(&Cancel, window, cx);
15762 });
15763 cx.executor().run_until_parked();
15764 cx.condition(|editor, _| !editor.context_menu_visible())
15765 .await;
15766
15767 throttle_completions.store(true, atomic::Ordering::Release);
15768 cx.simulate_keystroke(".");
15769 cx.executor()
15770 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
15771 cx.executor().run_until_parked();
15772 cx.condition(|editor, _| editor.context_menu_visible())
15773 .await;
15774 cx.update_editor(|editor, _, _| {
15775 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15776 {
15777 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
15778 "When LSP server is slow, document words can be shown instead, if configured accordingly");
15779 } else {
15780 panic!("expected completion menu to be open");
15781 }
15782 });
15783}
15784
15785#[gpui::test]
15786async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
15787 init_test(cx, |language_settings| {
15788 language_settings.defaults.completions = Some(CompletionSettingsContent {
15789 words: Some(WordsCompletionMode::Enabled),
15790 words_min_length: Some(0),
15791 lsp_insert_mode: Some(LspInsertMode::Insert),
15792 ..Default::default()
15793 });
15794 });
15795
15796 let mut cx = EditorLspTestContext::new_rust(
15797 lsp::ServerCapabilities {
15798 completion_provider: Some(lsp::CompletionOptions {
15799 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15800 ..lsp::CompletionOptions::default()
15801 }),
15802 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15803 ..lsp::ServerCapabilities::default()
15804 },
15805 cx,
15806 )
15807 .await;
15808
15809 let _completion_requests_handler =
15810 cx.lsp
15811 .server
15812 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15813 Ok(Some(lsp::CompletionResponse::Array(vec![
15814 lsp::CompletionItem {
15815 label: "first".into(),
15816 ..lsp::CompletionItem::default()
15817 },
15818 lsp::CompletionItem {
15819 label: "last".into(),
15820 ..lsp::CompletionItem::default()
15821 },
15822 ])))
15823 });
15824
15825 cx.set_state(indoc! {"ˇ
15826 first
15827 last
15828 second
15829 "});
15830 cx.simulate_keystroke(".");
15831 cx.executor().run_until_parked();
15832 cx.condition(|editor, _| editor.context_menu_visible())
15833 .await;
15834 cx.update_editor(|editor, _, _| {
15835 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15836 {
15837 assert_eq!(
15838 completion_menu_entries(menu),
15839 &["first", "last", "second"],
15840 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15841 );
15842 } else {
15843 panic!("expected completion menu to be open");
15844 }
15845 });
15846}
15847
15848#[gpui::test]
15849async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15850 init_test(cx, |language_settings| {
15851 language_settings.defaults.completions = Some(CompletionSettingsContent {
15852 words: Some(WordsCompletionMode::Disabled),
15853 words_min_length: Some(0),
15854 lsp_insert_mode: Some(LspInsertMode::Insert),
15855 ..Default::default()
15856 });
15857 });
15858
15859 let mut cx = EditorLspTestContext::new_rust(
15860 lsp::ServerCapabilities {
15861 completion_provider: Some(lsp::CompletionOptions {
15862 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15863 ..lsp::CompletionOptions::default()
15864 }),
15865 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15866 ..lsp::ServerCapabilities::default()
15867 },
15868 cx,
15869 )
15870 .await;
15871
15872 let _completion_requests_handler =
15873 cx.lsp
15874 .server
15875 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15876 panic!("LSP completions should not be queried when dealing with word completions")
15877 });
15878
15879 cx.set_state(indoc! {"ˇ
15880 first
15881 last
15882 second
15883 "});
15884 cx.update_editor(|editor, window, cx| {
15885 editor.show_word_completions(&ShowWordCompletions, window, cx);
15886 });
15887 cx.executor().run_until_parked();
15888 cx.condition(|editor, _| editor.context_menu_visible())
15889 .await;
15890 cx.update_editor(|editor, _, _| {
15891 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15892 {
15893 assert_eq!(
15894 completion_menu_entries(menu),
15895 &["first", "last", "second"],
15896 "`ShowWordCompletions` action should show word completions"
15897 );
15898 } else {
15899 panic!("expected completion menu to be open");
15900 }
15901 });
15902
15903 cx.simulate_keystroke("l");
15904 cx.executor().run_until_parked();
15905 cx.condition(|editor, _| editor.context_menu_visible())
15906 .await;
15907 cx.update_editor(|editor, _, _| {
15908 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15909 {
15910 assert_eq!(
15911 completion_menu_entries(menu),
15912 &["last"],
15913 "After showing word completions, further editing should filter them and not query the LSP"
15914 );
15915 } else {
15916 panic!("expected completion menu to be open");
15917 }
15918 });
15919}
15920
15921#[gpui::test]
15922async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15923 init_test(cx, |language_settings| {
15924 language_settings.defaults.completions = Some(CompletionSettingsContent {
15925 words_min_length: Some(0),
15926 lsp: Some(false),
15927 lsp_insert_mode: Some(LspInsertMode::Insert),
15928 ..Default::default()
15929 });
15930 });
15931
15932 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15933
15934 cx.set_state(indoc! {"ˇ
15935 0_usize
15936 let
15937 33
15938 4.5f32
15939 "});
15940 cx.update_editor(|editor, window, cx| {
15941 editor.show_completions(&ShowCompletions, window, cx);
15942 });
15943 cx.executor().run_until_parked();
15944 cx.condition(|editor, _| editor.context_menu_visible())
15945 .await;
15946 cx.update_editor(|editor, window, cx| {
15947 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15948 {
15949 assert_eq!(
15950 completion_menu_entries(menu),
15951 &["let"],
15952 "With no digits in the completion query, no digits should be in the word completions"
15953 );
15954 } else {
15955 panic!("expected completion menu to be open");
15956 }
15957 editor.cancel(&Cancel, window, cx);
15958 });
15959
15960 cx.set_state(indoc! {"3ˇ
15961 0_usize
15962 let
15963 3
15964 33.35f32
15965 "});
15966 cx.update_editor(|editor, window, cx| {
15967 editor.show_completions(&ShowCompletions, window, cx);
15968 });
15969 cx.executor().run_until_parked();
15970 cx.condition(|editor, _| editor.context_menu_visible())
15971 .await;
15972 cx.update_editor(|editor, _, _| {
15973 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15974 {
15975 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15976 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15977 } else {
15978 panic!("expected completion menu to be open");
15979 }
15980 });
15981}
15982
15983#[gpui::test]
15984async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15985 init_test(cx, |language_settings| {
15986 language_settings.defaults.completions = Some(CompletionSettingsContent {
15987 words: Some(WordsCompletionMode::Enabled),
15988 words_min_length: Some(3),
15989 lsp_insert_mode: Some(LspInsertMode::Insert),
15990 ..Default::default()
15991 });
15992 });
15993
15994 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15995 cx.set_state(indoc! {"ˇ
15996 wow
15997 wowen
15998 wowser
15999 "});
16000 cx.simulate_keystroke("w");
16001 cx.executor().run_until_parked();
16002 cx.update_editor(|editor, _, _| {
16003 if editor.context_menu.borrow_mut().is_some() {
16004 panic!(
16005 "expected completion menu to be hidden, as words completion threshold is not met"
16006 );
16007 }
16008 });
16009
16010 cx.update_editor(|editor, window, cx| {
16011 editor.show_word_completions(&ShowWordCompletions, window, cx);
16012 });
16013 cx.executor().run_until_parked();
16014 cx.update_editor(|editor, window, cx| {
16015 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16016 {
16017 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");
16018 } else {
16019 panic!("expected completion menu to be open after the word completions are called with an action");
16020 }
16021
16022 editor.cancel(&Cancel, window, cx);
16023 });
16024 cx.update_editor(|editor, _, _| {
16025 if editor.context_menu.borrow_mut().is_some() {
16026 panic!("expected completion menu to be hidden after canceling");
16027 }
16028 });
16029
16030 cx.simulate_keystroke("o");
16031 cx.executor().run_until_parked();
16032 cx.update_editor(|editor, _, _| {
16033 if editor.context_menu.borrow_mut().is_some() {
16034 panic!(
16035 "expected completion menu to be hidden, as words completion threshold is not met still"
16036 );
16037 }
16038 });
16039
16040 cx.simulate_keystroke("w");
16041 cx.executor().run_until_parked();
16042 cx.update_editor(|editor, _, _| {
16043 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16044 {
16045 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
16046 } else {
16047 panic!("expected completion menu to be open after the word completions threshold is met");
16048 }
16049 });
16050}
16051
16052#[gpui::test]
16053async fn test_word_completions_disabled(cx: &mut TestAppContext) {
16054 init_test(cx, |language_settings| {
16055 language_settings.defaults.completions = Some(CompletionSettingsContent {
16056 words: Some(WordsCompletionMode::Enabled),
16057 words_min_length: Some(0),
16058 lsp_insert_mode: Some(LspInsertMode::Insert),
16059 ..Default::default()
16060 });
16061 });
16062
16063 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16064 cx.update_editor(|editor, _, _| {
16065 editor.disable_word_completions();
16066 });
16067 cx.set_state(indoc! {"ˇ
16068 wow
16069 wowen
16070 wowser
16071 "});
16072 cx.simulate_keystroke("w");
16073 cx.executor().run_until_parked();
16074 cx.update_editor(|editor, _, _| {
16075 if editor.context_menu.borrow_mut().is_some() {
16076 panic!(
16077 "expected completion menu to be hidden, as words completion are disabled for this editor"
16078 );
16079 }
16080 });
16081
16082 cx.update_editor(|editor, window, cx| {
16083 editor.show_word_completions(&ShowWordCompletions, window, cx);
16084 });
16085 cx.executor().run_until_parked();
16086 cx.update_editor(|editor, _, _| {
16087 if editor.context_menu.borrow_mut().is_some() {
16088 panic!(
16089 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
16090 );
16091 }
16092 });
16093}
16094
16095#[gpui::test]
16096async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
16097 init_test(cx, |language_settings| {
16098 language_settings.defaults.completions = Some(CompletionSettingsContent {
16099 words: Some(WordsCompletionMode::Disabled),
16100 words_min_length: Some(0),
16101 lsp_insert_mode: Some(LspInsertMode::Insert),
16102 ..Default::default()
16103 });
16104 });
16105
16106 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16107 cx.update_editor(|editor, _, _| {
16108 editor.set_completion_provider(None);
16109 });
16110 cx.set_state(indoc! {"ˇ
16111 wow
16112 wowen
16113 wowser
16114 "});
16115 cx.simulate_keystroke("w");
16116 cx.executor().run_until_parked();
16117 cx.update_editor(|editor, _, _| {
16118 if editor.context_menu.borrow_mut().is_some() {
16119 panic!("expected completion menu to be hidden, as disabled in settings");
16120 }
16121 });
16122}
16123
16124fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
16125 let position = || lsp::Position {
16126 line: params.text_document_position.position.line,
16127 character: params.text_document_position.position.character,
16128 };
16129 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16130 range: lsp::Range {
16131 start: position(),
16132 end: position(),
16133 },
16134 new_text: text.to_string(),
16135 }))
16136}
16137
16138#[gpui::test]
16139async fn test_multiline_completion(cx: &mut TestAppContext) {
16140 init_test(cx, |_| {});
16141
16142 let fs = FakeFs::new(cx.executor());
16143 fs.insert_tree(
16144 path!("/a"),
16145 json!({
16146 "main.ts": "a",
16147 }),
16148 )
16149 .await;
16150
16151 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16152 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16153 let typescript_language = Arc::new(Language::new(
16154 LanguageConfig {
16155 name: "TypeScript".into(),
16156 matcher: LanguageMatcher {
16157 path_suffixes: vec!["ts".to_string()],
16158 ..LanguageMatcher::default()
16159 },
16160 line_comments: vec!["// ".into()],
16161 ..LanguageConfig::default()
16162 },
16163 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16164 ));
16165 language_registry.add(typescript_language.clone());
16166 let mut fake_servers = language_registry.register_fake_lsp(
16167 "TypeScript",
16168 FakeLspAdapter {
16169 capabilities: lsp::ServerCapabilities {
16170 completion_provider: Some(lsp::CompletionOptions {
16171 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16172 ..lsp::CompletionOptions::default()
16173 }),
16174 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
16175 ..lsp::ServerCapabilities::default()
16176 },
16177 // Emulate vtsls label generation
16178 label_for_completion: Some(Box::new(|item, _| {
16179 let text = if let Some(description) = item
16180 .label_details
16181 .as_ref()
16182 .and_then(|label_details| label_details.description.as_ref())
16183 {
16184 format!("{} {}", item.label, description)
16185 } else if let Some(detail) = &item.detail {
16186 format!("{} {}", item.label, detail)
16187 } else {
16188 item.label.clone()
16189 };
16190 Some(language::CodeLabel::plain(text, None))
16191 })),
16192 ..FakeLspAdapter::default()
16193 },
16194 );
16195 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16196 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16197 let worktree_id = workspace
16198 .update(cx, |workspace, _window, cx| {
16199 workspace.project().update(cx, |project, cx| {
16200 project.worktrees(cx).next().unwrap().read(cx).id()
16201 })
16202 })
16203 .unwrap();
16204 let _buffer = project
16205 .update(cx, |project, cx| {
16206 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
16207 })
16208 .await
16209 .unwrap();
16210 let editor = workspace
16211 .update(cx, |workspace, window, cx| {
16212 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
16213 })
16214 .unwrap()
16215 .await
16216 .unwrap()
16217 .downcast::<Editor>()
16218 .unwrap();
16219 let fake_server = fake_servers.next().await.unwrap();
16220 cx.run_until_parked();
16221
16222 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
16223 let multiline_label_2 = "a\nb\nc\n";
16224 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
16225 let multiline_description = "d\ne\nf\n";
16226 let multiline_detail_2 = "g\nh\ni\n";
16227
16228 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
16229 move |params, _| async move {
16230 Ok(Some(lsp::CompletionResponse::Array(vec![
16231 lsp::CompletionItem {
16232 label: multiline_label.to_string(),
16233 text_edit: gen_text_edit(¶ms, "new_text_1"),
16234 ..lsp::CompletionItem::default()
16235 },
16236 lsp::CompletionItem {
16237 label: "single line label 1".to_string(),
16238 detail: Some(multiline_detail.to_string()),
16239 text_edit: gen_text_edit(¶ms, "new_text_2"),
16240 ..lsp::CompletionItem::default()
16241 },
16242 lsp::CompletionItem {
16243 label: "single line label 2".to_string(),
16244 label_details: Some(lsp::CompletionItemLabelDetails {
16245 description: Some(multiline_description.to_string()),
16246 detail: None,
16247 }),
16248 text_edit: gen_text_edit(¶ms, "new_text_2"),
16249 ..lsp::CompletionItem::default()
16250 },
16251 lsp::CompletionItem {
16252 label: multiline_label_2.to_string(),
16253 detail: Some(multiline_detail_2.to_string()),
16254 text_edit: gen_text_edit(¶ms, "new_text_3"),
16255 ..lsp::CompletionItem::default()
16256 },
16257 lsp::CompletionItem {
16258 label: "Label with many spaces and \t but without newlines".to_string(),
16259 detail: Some(
16260 "Details with many spaces and \t but without newlines".to_string(),
16261 ),
16262 text_edit: gen_text_edit(¶ms, "new_text_4"),
16263 ..lsp::CompletionItem::default()
16264 },
16265 ])))
16266 },
16267 );
16268
16269 editor.update_in(cx, |editor, window, cx| {
16270 cx.focus_self(window);
16271 editor.move_to_end(&MoveToEnd, window, cx);
16272 editor.handle_input(".", window, cx);
16273 });
16274 cx.run_until_parked();
16275 completion_handle.next().await.unwrap();
16276
16277 editor.update(cx, |editor, _| {
16278 assert!(editor.context_menu_visible());
16279 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16280 {
16281 let completion_labels = menu
16282 .completions
16283 .borrow()
16284 .iter()
16285 .map(|c| c.label.text.clone())
16286 .collect::<Vec<_>>();
16287 assert_eq!(
16288 completion_labels,
16289 &[
16290 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
16291 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
16292 "single line label 2 d e f ",
16293 "a b c g h i ",
16294 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
16295 ],
16296 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
16297 );
16298
16299 for completion in menu
16300 .completions
16301 .borrow()
16302 .iter() {
16303 assert_eq!(
16304 completion.label.filter_range,
16305 0..completion.label.text.len(),
16306 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
16307 );
16308 }
16309 } else {
16310 panic!("expected completion menu to be open");
16311 }
16312 });
16313}
16314
16315#[gpui::test]
16316async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
16317 init_test(cx, |_| {});
16318 let mut cx = EditorLspTestContext::new_rust(
16319 lsp::ServerCapabilities {
16320 completion_provider: Some(lsp::CompletionOptions {
16321 trigger_characters: Some(vec![".".to_string()]),
16322 ..Default::default()
16323 }),
16324 ..Default::default()
16325 },
16326 cx,
16327 )
16328 .await;
16329 cx.lsp
16330 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16331 Ok(Some(lsp::CompletionResponse::Array(vec![
16332 lsp::CompletionItem {
16333 label: "first".into(),
16334 ..Default::default()
16335 },
16336 lsp::CompletionItem {
16337 label: "last".into(),
16338 ..Default::default()
16339 },
16340 ])))
16341 });
16342 cx.set_state("variableˇ");
16343 cx.simulate_keystroke(".");
16344 cx.executor().run_until_parked();
16345
16346 cx.update_editor(|editor, _, _| {
16347 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16348 {
16349 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
16350 } else {
16351 panic!("expected completion menu to be open");
16352 }
16353 });
16354
16355 cx.update_editor(|editor, window, cx| {
16356 editor.move_page_down(&MovePageDown::default(), window, cx);
16357 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16358 {
16359 assert!(
16360 menu.selected_item == 1,
16361 "expected PageDown to select the last item from the context menu"
16362 );
16363 } else {
16364 panic!("expected completion menu to stay open after PageDown");
16365 }
16366 });
16367
16368 cx.update_editor(|editor, window, cx| {
16369 editor.move_page_up(&MovePageUp::default(), window, cx);
16370 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16371 {
16372 assert!(
16373 menu.selected_item == 0,
16374 "expected PageUp to select the first item from the context menu"
16375 );
16376 } else {
16377 panic!("expected completion menu to stay open after PageUp");
16378 }
16379 });
16380}
16381
16382#[gpui::test]
16383async fn test_as_is_completions(cx: &mut TestAppContext) {
16384 init_test(cx, |_| {});
16385 let mut cx = EditorLspTestContext::new_rust(
16386 lsp::ServerCapabilities {
16387 completion_provider: Some(lsp::CompletionOptions {
16388 ..Default::default()
16389 }),
16390 ..Default::default()
16391 },
16392 cx,
16393 )
16394 .await;
16395 cx.lsp
16396 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16397 Ok(Some(lsp::CompletionResponse::Array(vec![
16398 lsp::CompletionItem {
16399 label: "unsafe".into(),
16400 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16401 range: lsp::Range {
16402 start: lsp::Position {
16403 line: 1,
16404 character: 2,
16405 },
16406 end: lsp::Position {
16407 line: 1,
16408 character: 3,
16409 },
16410 },
16411 new_text: "unsafe".to_string(),
16412 })),
16413 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
16414 ..Default::default()
16415 },
16416 ])))
16417 });
16418 cx.set_state("fn a() {}\n nˇ");
16419 cx.executor().run_until_parked();
16420 cx.update_editor(|editor, window, cx| {
16421 editor.trigger_completion_on_input("n", true, window, cx)
16422 });
16423 cx.executor().run_until_parked();
16424
16425 cx.update_editor(|editor, window, cx| {
16426 editor.confirm_completion(&Default::default(), window, cx)
16427 });
16428 cx.executor().run_until_parked();
16429 cx.assert_editor_state("fn a() {}\n unsafeˇ");
16430}
16431
16432#[gpui::test]
16433async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
16434 init_test(cx, |_| {});
16435 let language =
16436 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
16437 let mut cx = EditorLspTestContext::new(
16438 language,
16439 lsp::ServerCapabilities {
16440 completion_provider: Some(lsp::CompletionOptions {
16441 ..lsp::CompletionOptions::default()
16442 }),
16443 ..lsp::ServerCapabilities::default()
16444 },
16445 cx,
16446 )
16447 .await;
16448
16449 cx.set_state(
16450 "#ifndef BAR_H
16451#define BAR_H
16452
16453#include <stdbool.h>
16454
16455int fn_branch(bool do_branch1, bool do_branch2);
16456
16457#endif // BAR_H
16458ˇ",
16459 );
16460 cx.executor().run_until_parked();
16461 cx.update_editor(|editor, window, cx| {
16462 editor.handle_input("#", window, cx);
16463 });
16464 cx.executor().run_until_parked();
16465 cx.update_editor(|editor, window, cx| {
16466 editor.handle_input("i", window, cx);
16467 });
16468 cx.executor().run_until_parked();
16469 cx.update_editor(|editor, window, cx| {
16470 editor.handle_input("n", window, cx);
16471 });
16472 cx.executor().run_until_parked();
16473 cx.assert_editor_state(
16474 "#ifndef BAR_H
16475#define BAR_H
16476
16477#include <stdbool.h>
16478
16479int fn_branch(bool do_branch1, bool do_branch2);
16480
16481#endif // BAR_H
16482#inˇ",
16483 );
16484
16485 cx.lsp
16486 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16487 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16488 is_incomplete: false,
16489 item_defaults: None,
16490 items: vec![lsp::CompletionItem {
16491 kind: Some(lsp::CompletionItemKind::SNIPPET),
16492 label_details: Some(lsp::CompletionItemLabelDetails {
16493 detail: Some("header".to_string()),
16494 description: None,
16495 }),
16496 label: " include".to_string(),
16497 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16498 range: lsp::Range {
16499 start: lsp::Position {
16500 line: 8,
16501 character: 1,
16502 },
16503 end: lsp::Position {
16504 line: 8,
16505 character: 1,
16506 },
16507 },
16508 new_text: "include \"$0\"".to_string(),
16509 })),
16510 sort_text: Some("40b67681include".to_string()),
16511 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16512 filter_text: Some("include".to_string()),
16513 insert_text: Some("include \"$0\"".to_string()),
16514 ..lsp::CompletionItem::default()
16515 }],
16516 })))
16517 });
16518 cx.update_editor(|editor, window, cx| {
16519 editor.show_completions(&ShowCompletions, window, cx);
16520 });
16521 cx.executor().run_until_parked();
16522 cx.update_editor(|editor, window, cx| {
16523 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16524 });
16525 cx.executor().run_until_parked();
16526 cx.assert_editor_state(
16527 "#ifndef BAR_H
16528#define BAR_H
16529
16530#include <stdbool.h>
16531
16532int fn_branch(bool do_branch1, bool do_branch2);
16533
16534#endif // BAR_H
16535#include \"ˇ\"",
16536 );
16537
16538 cx.lsp
16539 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16540 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16541 is_incomplete: true,
16542 item_defaults: None,
16543 items: vec![lsp::CompletionItem {
16544 kind: Some(lsp::CompletionItemKind::FILE),
16545 label: "AGL/".to_string(),
16546 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16547 range: lsp::Range {
16548 start: lsp::Position {
16549 line: 8,
16550 character: 10,
16551 },
16552 end: lsp::Position {
16553 line: 8,
16554 character: 11,
16555 },
16556 },
16557 new_text: "AGL/".to_string(),
16558 })),
16559 sort_text: Some("40b67681AGL/".to_string()),
16560 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16561 filter_text: Some("AGL/".to_string()),
16562 insert_text: Some("AGL/".to_string()),
16563 ..lsp::CompletionItem::default()
16564 }],
16565 })))
16566 });
16567 cx.update_editor(|editor, window, cx| {
16568 editor.show_completions(&ShowCompletions, window, cx);
16569 });
16570 cx.executor().run_until_parked();
16571 cx.update_editor(|editor, window, cx| {
16572 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16573 });
16574 cx.executor().run_until_parked();
16575 cx.assert_editor_state(
16576 r##"#ifndef BAR_H
16577#define BAR_H
16578
16579#include <stdbool.h>
16580
16581int fn_branch(bool do_branch1, bool do_branch2);
16582
16583#endif // BAR_H
16584#include "AGL/ˇ"##,
16585 );
16586
16587 cx.update_editor(|editor, window, cx| {
16588 editor.handle_input("\"", window, cx);
16589 });
16590 cx.executor().run_until_parked();
16591 cx.assert_editor_state(
16592 r##"#ifndef BAR_H
16593#define BAR_H
16594
16595#include <stdbool.h>
16596
16597int fn_branch(bool do_branch1, bool do_branch2);
16598
16599#endif // BAR_H
16600#include "AGL/"ˇ"##,
16601 );
16602}
16603
16604#[gpui::test]
16605async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
16606 init_test(cx, |_| {});
16607
16608 let mut cx = EditorLspTestContext::new_rust(
16609 lsp::ServerCapabilities {
16610 completion_provider: Some(lsp::CompletionOptions {
16611 trigger_characters: Some(vec![".".to_string()]),
16612 resolve_provider: Some(true),
16613 ..Default::default()
16614 }),
16615 ..Default::default()
16616 },
16617 cx,
16618 )
16619 .await;
16620
16621 cx.set_state("fn main() { let a = 2ˇ; }");
16622 cx.simulate_keystroke(".");
16623 let completion_item = lsp::CompletionItem {
16624 label: "Some".into(),
16625 kind: Some(lsp::CompletionItemKind::SNIPPET),
16626 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16627 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16628 kind: lsp::MarkupKind::Markdown,
16629 value: "```rust\nSome(2)\n```".to_string(),
16630 })),
16631 deprecated: Some(false),
16632 sort_text: Some("Some".to_string()),
16633 filter_text: Some("Some".to_string()),
16634 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16635 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16636 range: lsp::Range {
16637 start: lsp::Position {
16638 line: 0,
16639 character: 22,
16640 },
16641 end: lsp::Position {
16642 line: 0,
16643 character: 22,
16644 },
16645 },
16646 new_text: "Some(2)".to_string(),
16647 })),
16648 additional_text_edits: Some(vec![lsp::TextEdit {
16649 range: lsp::Range {
16650 start: lsp::Position {
16651 line: 0,
16652 character: 20,
16653 },
16654 end: lsp::Position {
16655 line: 0,
16656 character: 22,
16657 },
16658 },
16659 new_text: "".to_string(),
16660 }]),
16661 ..Default::default()
16662 };
16663
16664 let closure_completion_item = completion_item.clone();
16665 let counter = Arc::new(AtomicUsize::new(0));
16666 let counter_clone = counter.clone();
16667 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16668 let task_completion_item = closure_completion_item.clone();
16669 counter_clone.fetch_add(1, atomic::Ordering::Release);
16670 async move {
16671 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16672 is_incomplete: true,
16673 item_defaults: None,
16674 items: vec![task_completion_item],
16675 })))
16676 }
16677 });
16678
16679 cx.condition(|editor, _| editor.context_menu_visible())
16680 .await;
16681 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
16682 assert!(request.next().await.is_some());
16683 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16684
16685 cx.simulate_keystrokes("S o m");
16686 cx.condition(|editor, _| editor.context_menu_visible())
16687 .await;
16688 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
16689 assert!(request.next().await.is_some());
16690 assert!(request.next().await.is_some());
16691 assert!(request.next().await.is_some());
16692 request.close();
16693 assert!(request.next().await.is_none());
16694 assert_eq!(
16695 counter.load(atomic::Ordering::Acquire),
16696 4,
16697 "With the completions menu open, only one LSP request should happen per input"
16698 );
16699}
16700
16701#[gpui::test]
16702async fn test_toggle_comment(cx: &mut TestAppContext) {
16703 init_test(cx, |_| {});
16704 let mut cx = EditorTestContext::new(cx).await;
16705 let language = Arc::new(Language::new(
16706 LanguageConfig {
16707 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16708 ..Default::default()
16709 },
16710 Some(tree_sitter_rust::LANGUAGE.into()),
16711 ));
16712 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16713
16714 // If multiple selections intersect a line, the line is only toggled once.
16715 cx.set_state(indoc! {"
16716 fn a() {
16717 «//b();
16718 ˇ»// «c();
16719 //ˇ» d();
16720 }
16721 "});
16722
16723 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16724
16725 cx.assert_editor_state(indoc! {"
16726 fn a() {
16727 «b();
16728 ˇ»«c();
16729 ˇ» d();
16730 }
16731 "});
16732
16733 // The comment prefix is inserted at the same column for every line in a
16734 // selection.
16735 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16736
16737 cx.assert_editor_state(indoc! {"
16738 fn a() {
16739 // «b();
16740 ˇ»// «c();
16741 ˇ» // d();
16742 }
16743 "});
16744
16745 // If a selection ends at the beginning of a line, that line is not toggled.
16746 cx.set_selections_state(indoc! {"
16747 fn a() {
16748 // b();
16749 «// c();
16750 ˇ» // d();
16751 }
16752 "});
16753
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 span a single line and is empty, the line is toggled.
16765 cx.set_state(indoc! {"
16766 fn a() {
16767 a();
16768 b();
16769 ˇ
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 a();
16778 b();
16779 //•ˇ
16780 }
16781 "});
16782
16783 // If a selection span multiple lines, empty lines are not toggled.
16784 cx.set_state(indoc! {"
16785 fn a() {
16786 «a();
16787
16788 c();ˇ»
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
16798 // c();ˇ»
16799 }
16800 "});
16801
16802 // If a selection includes multiple comment prefixes, all lines are uncommented.
16803 cx.set_state(indoc! {"
16804 fn a() {
16805 «// a();
16806 /// b();
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 b();
16817 c();ˇ»
16818 }
16819 "});
16820}
16821
16822#[gpui::test]
16823async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16824 init_test(cx, |_| {});
16825 let mut cx = EditorTestContext::new(cx).await;
16826 let language = Arc::new(Language::new(
16827 LanguageConfig {
16828 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16829 ..Default::default()
16830 },
16831 Some(tree_sitter_rust::LANGUAGE.into()),
16832 ));
16833 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16834
16835 let toggle_comments = &ToggleComments {
16836 advance_downwards: false,
16837 ignore_indent: true,
16838 };
16839
16840 // If multiple selections intersect a line, the line is only toggled once.
16841 cx.set_state(indoc! {"
16842 fn a() {
16843 // «b();
16844 // c();
16845 // ˇ» d();
16846 }
16847 "});
16848
16849 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16850
16851 cx.assert_editor_state(indoc! {"
16852 fn a() {
16853 «b();
16854 c();
16855 ˇ» d();
16856 }
16857 "});
16858
16859 // The comment prefix is inserted at the beginning of each line
16860 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16861
16862 cx.assert_editor_state(indoc! {"
16863 fn a() {
16864 // «b();
16865 // c();
16866 // ˇ» d();
16867 }
16868 "});
16869
16870 // If a selection ends at the beginning of a line, that line is not toggled.
16871 cx.set_selections_state(indoc! {"
16872 fn a() {
16873 // b();
16874 // «c();
16875 ˇ»// d();
16876 }
16877 "});
16878
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 span a single line and is empty, the line is toggled.
16890 cx.set_state(indoc! {"
16891 fn a() {
16892 a();
16893 b();
16894 ˇ
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 a();
16903 b();
16904 //ˇ
16905 }
16906 "});
16907
16908 // If a selection span multiple lines, empty lines are not toggled.
16909 cx.set_state(indoc! {"
16910 fn a() {
16911 «a();
16912
16913 c();ˇ»
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
16923 // c();ˇ»
16924 }
16925 "});
16926
16927 // If a selection includes multiple comment prefixes, all lines are uncommented.
16928 cx.set_state(indoc! {"
16929 fn a() {
16930 // «a();
16931 /// b();
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 b();
16942 c();ˇ»
16943 }
16944 "});
16945}
16946
16947#[gpui::test]
16948async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16949 init_test(cx, |_| {});
16950
16951 let language = Arc::new(Language::new(
16952 LanguageConfig {
16953 line_comments: vec!["// ".into()],
16954 ..Default::default()
16955 },
16956 Some(tree_sitter_rust::LANGUAGE.into()),
16957 ));
16958
16959 let mut cx = EditorTestContext::new(cx).await;
16960
16961 cx.language_registry().add(language.clone());
16962 cx.update_buffer(|buffer, cx| {
16963 buffer.set_language(Some(language), cx);
16964 });
16965
16966 let toggle_comments = &ToggleComments {
16967 advance_downwards: true,
16968 ignore_indent: false,
16969 };
16970
16971 // Single cursor on one line -> advance
16972 // Cursor moves horizontally 3 characters as well on non-blank line
16973 cx.set_state(indoc!(
16974 "fn a() {
16975 ˇdog();
16976 cat();
16977 }"
16978 ));
16979 cx.update_editor(|editor, window, cx| {
16980 editor.toggle_comments(toggle_comments, window, cx);
16981 });
16982 cx.assert_editor_state(indoc!(
16983 "fn a() {
16984 // dog();
16985 catˇ();
16986 }"
16987 ));
16988
16989 // Single selection on one line -> don't advance
16990 cx.set_state(indoc!(
16991 "fn a() {
16992 «dog()ˇ»;
16993 cat();
16994 }"
16995 ));
16996 cx.update_editor(|editor, window, cx| {
16997 editor.toggle_comments(toggle_comments, window, cx);
16998 });
16999 cx.assert_editor_state(indoc!(
17000 "fn a() {
17001 // «dog()ˇ»;
17002 cat();
17003 }"
17004 ));
17005
17006 // Multiple cursors on one line -> advance
17007 cx.set_state(indoc!(
17008 "fn a() {
17009 ˇdˇog();
17010 cat();
17011 }"
17012 ));
17013 cx.update_editor(|editor, window, cx| {
17014 editor.toggle_comments(toggle_comments, window, cx);
17015 });
17016 cx.assert_editor_state(indoc!(
17017 "fn a() {
17018 // dog();
17019 catˇ(ˇ);
17020 }"
17021 ));
17022
17023 // Multiple cursors on one line, with selection -> don't advance
17024 cx.set_state(indoc!(
17025 "fn a() {
17026 ˇdˇog«()ˇ»;
17027 cat();
17028 }"
17029 ));
17030 cx.update_editor(|editor, window, cx| {
17031 editor.toggle_comments(toggle_comments, window, cx);
17032 });
17033 cx.assert_editor_state(indoc!(
17034 "fn a() {
17035 // ˇdˇog«()ˇ»;
17036 cat();
17037 }"
17038 ));
17039
17040 // Single cursor on one line -> advance
17041 // Cursor moves to column 0 on blank line
17042 cx.set_state(indoc!(
17043 "fn a() {
17044 ˇdog();
17045
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 // dog();
17055 ˇ
17056 cat();
17057 }"
17058 ));
17059
17060 // Single cursor on one line -> advance
17061 // Cursor starts and ends at column 0
17062 cx.set_state(indoc!(
17063 "fn a() {
17064 ˇ dog();
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 ˇ cat();
17075 }"
17076 ));
17077}
17078
17079#[gpui::test]
17080async fn test_toggle_block_comment(cx: &mut TestAppContext) {
17081 init_test(cx, |_| {});
17082
17083 let mut cx = EditorTestContext::new(cx).await;
17084
17085 let html_language = Arc::new(
17086 Language::new(
17087 LanguageConfig {
17088 name: "HTML".into(),
17089 block_comment: Some(BlockCommentConfig {
17090 start: "<!-- ".into(),
17091 prefix: "".into(),
17092 end: " -->".into(),
17093 tab_size: 0,
17094 }),
17095 ..Default::default()
17096 },
17097 Some(tree_sitter_html::LANGUAGE.into()),
17098 )
17099 .with_injection_query(
17100 r#"
17101 (script_element
17102 (raw_text) @injection.content
17103 (#set! injection.language "javascript"))
17104 "#,
17105 )
17106 .unwrap(),
17107 );
17108
17109 let javascript_language = Arc::new(Language::new(
17110 LanguageConfig {
17111 name: "JavaScript".into(),
17112 line_comments: vec!["// ".into()],
17113 ..Default::default()
17114 },
17115 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17116 ));
17117
17118 cx.language_registry().add(html_language.clone());
17119 cx.language_registry().add(javascript_language);
17120 cx.update_buffer(|buffer, cx| {
17121 buffer.set_language(Some(html_language), cx);
17122 });
17123
17124 // Toggle comments for empty selections
17125 cx.set_state(
17126 &r#"
17127 <p>A</p>ˇ
17128 <p>B</p>ˇ
17129 <p>C</p>ˇ
17130 "#
17131 .unindent(),
17132 );
17133 cx.update_editor(|editor, window, cx| {
17134 editor.toggle_comments(&ToggleComments::default(), window, cx)
17135 });
17136 cx.assert_editor_state(
17137 &r#"
17138 <!-- <p>A</p>ˇ -->
17139 <!-- <p>B</p>ˇ -->
17140 <!-- <p>C</p>ˇ -->
17141 "#
17142 .unindent(),
17143 );
17144 cx.update_editor(|editor, window, cx| {
17145 editor.toggle_comments(&ToggleComments::default(), window, cx)
17146 });
17147 cx.assert_editor_state(
17148 &r#"
17149 <p>A</p>ˇ
17150 <p>B</p>ˇ
17151 <p>C</p>ˇ
17152 "#
17153 .unindent(),
17154 );
17155
17156 // Toggle comments for mixture of empty and non-empty selections, where
17157 // multiple selections occupy a given line.
17158 cx.set_state(
17159 &r#"
17160 <p>A«</p>
17161 <p>ˇ»B</p>ˇ
17162 <p>C«</p>
17163 <p>ˇ»D</p>ˇ
17164 "#
17165 .unindent(),
17166 );
17167
17168 cx.update_editor(|editor, window, cx| {
17169 editor.toggle_comments(&ToggleComments::default(), window, cx)
17170 });
17171 cx.assert_editor_state(
17172 &r#"
17173 <!-- <p>A«</p>
17174 <p>ˇ»B</p>ˇ -->
17175 <!-- <p>C«</p>
17176 <p>ˇ»D</p>ˇ -->
17177 "#
17178 .unindent(),
17179 );
17180 cx.update_editor(|editor, window, cx| {
17181 editor.toggle_comments(&ToggleComments::default(), window, cx)
17182 });
17183 cx.assert_editor_state(
17184 &r#"
17185 <p>A«</p>
17186 <p>ˇ»B</p>ˇ
17187 <p>C«</p>
17188 <p>ˇ»D</p>ˇ
17189 "#
17190 .unindent(),
17191 );
17192
17193 // Toggle comments when different languages are active for different
17194 // selections.
17195 cx.set_state(
17196 &r#"
17197 ˇ<script>
17198 ˇvar x = new Y();
17199 ˇ</script>
17200 "#
17201 .unindent(),
17202 );
17203 cx.executor().run_until_parked();
17204 cx.update_editor(|editor, window, cx| {
17205 editor.toggle_comments(&ToggleComments::default(), window, cx)
17206 });
17207 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
17208 // Uncommenting and commenting from this position brings in even more wrong artifacts.
17209 cx.assert_editor_state(
17210 &r#"
17211 <!-- ˇ<script> -->
17212 // ˇvar x = new Y();
17213 <!-- ˇ</script> -->
17214 "#
17215 .unindent(),
17216 );
17217}
17218
17219#[gpui::test]
17220fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
17221 init_test(cx, |_| {});
17222
17223 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17224 let multibuffer = cx.new(|cx| {
17225 let mut multibuffer = MultiBuffer::new(ReadWrite);
17226 multibuffer.push_excerpts(
17227 buffer.clone(),
17228 [
17229 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
17230 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
17231 ],
17232 cx,
17233 );
17234 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
17235 multibuffer
17236 });
17237
17238 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
17239 editor.update_in(cx, |editor, window, cx| {
17240 assert_eq!(editor.text(cx), "aaaa\nbbbb");
17241 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17242 s.select_ranges([
17243 Point::new(0, 0)..Point::new(0, 0),
17244 Point::new(1, 0)..Point::new(1, 0),
17245 ])
17246 });
17247
17248 editor.handle_input("X", window, cx);
17249 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
17250 assert_eq!(
17251 editor.selections.ranges(&editor.display_snapshot(cx)),
17252 [
17253 Point::new(0, 1)..Point::new(0, 1),
17254 Point::new(1, 1)..Point::new(1, 1),
17255 ]
17256 );
17257
17258 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
17259 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17260 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
17261 });
17262 editor.backspace(&Default::default(), window, cx);
17263 assert_eq!(editor.text(cx), "Xa\nbbb");
17264 assert_eq!(
17265 editor.selections.ranges(&editor.display_snapshot(cx)),
17266 [Point::new(1, 0)..Point::new(1, 0)]
17267 );
17268
17269 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17270 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
17271 });
17272 editor.backspace(&Default::default(), window, cx);
17273 assert_eq!(editor.text(cx), "X\nbb");
17274 assert_eq!(
17275 editor.selections.ranges(&editor.display_snapshot(cx)),
17276 [Point::new(0, 1)..Point::new(0, 1)]
17277 );
17278 });
17279}
17280
17281#[gpui::test]
17282fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
17283 init_test(cx, |_| {});
17284
17285 let markers = vec![('[', ']').into(), ('(', ')').into()];
17286 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
17287 indoc! {"
17288 [aaaa
17289 (bbbb]
17290 cccc)",
17291 },
17292 markers.clone(),
17293 );
17294 let excerpt_ranges = markers.into_iter().map(|marker| {
17295 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
17296 ExcerptRange::new(context)
17297 });
17298 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
17299 let multibuffer = cx.new(|cx| {
17300 let mut multibuffer = MultiBuffer::new(ReadWrite);
17301 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
17302 multibuffer
17303 });
17304
17305 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
17306 editor.update_in(cx, |editor, window, cx| {
17307 let (expected_text, selection_ranges) = marked_text_ranges(
17308 indoc! {"
17309 aaaa
17310 bˇbbb
17311 bˇbbˇb
17312 cccc"
17313 },
17314 true,
17315 );
17316 assert_eq!(editor.text(cx), expected_text);
17317 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17318 s.select_ranges(
17319 selection_ranges
17320 .iter()
17321 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
17322 )
17323 });
17324
17325 editor.handle_input("X", window, cx);
17326
17327 let (expected_text, expected_selections) = marked_text_ranges(
17328 indoc! {"
17329 aaaa
17330 bXˇbbXb
17331 bXˇbbXˇb
17332 cccc"
17333 },
17334 false,
17335 );
17336 assert_eq!(editor.text(cx), expected_text);
17337 assert_eq!(
17338 editor.selections.ranges(&editor.display_snapshot(cx)),
17339 expected_selections
17340 .iter()
17341 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
17342 .collect::<Vec<_>>()
17343 );
17344
17345 editor.newline(&Newline, window, cx);
17346 let (expected_text, expected_selections) = marked_text_ranges(
17347 indoc! {"
17348 aaaa
17349 bX
17350 ˇbbX
17351 b
17352 bX
17353 ˇbbX
17354 ˇb
17355 cccc"
17356 },
17357 false,
17358 );
17359 assert_eq!(editor.text(cx), expected_text);
17360 assert_eq!(
17361 editor.selections.ranges(&editor.display_snapshot(cx)),
17362 expected_selections
17363 .iter()
17364 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
17365 .collect::<Vec<_>>()
17366 );
17367 });
17368}
17369
17370#[gpui::test]
17371fn test_refresh_selections(cx: &mut TestAppContext) {
17372 init_test(cx, |_| {});
17373
17374 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17375 let mut excerpt1_id = None;
17376 let multibuffer = cx.new(|cx| {
17377 let mut multibuffer = MultiBuffer::new(ReadWrite);
17378 excerpt1_id = multibuffer
17379 .push_excerpts(
17380 buffer.clone(),
17381 [
17382 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17383 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17384 ],
17385 cx,
17386 )
17387 .into_iter()
17388 .next();
17389 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17390 multibuffer
17391 });
17392
17393 let editor = cx.add_window(|window, cx| {
17394 let mut editor = build_editor(multibuffer.clone(), window, cx);
17395 let snapshot = editor.snapshot(window, cx);
17396 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17397 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
17398 });
17399 editor.begin_selection(
17400 Point::new(2, 1).to_display_point(&snapshot),
17401 true,
17402 1,
17403 window,
17404 cx,
17405 );
17406 assert_eq!(
17407 editor.selections.ranges(&editor.display_snapshot(cx)),
17408 [
17409 Point::new(1, 3)..Point::new(1, 3),
17410 Point::new(2, 1)..Point::new(2, 1),
17411 ]
17412 );
17413 editor
17414 });
17415
17416 // Refreshing selections is a no-op when excerpts haven't changed.
17417 _ = editor.update(cx, |editor, window, cx| {
17418 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17419 assert_eq!(
17420 editor.selections.ranges(&editor.display_snapshot(cx)),
17421 [
17422 Point::new(1, 3)..Point::new(1, 3),
17423 Point::new(2, 1)..Point::new(2, 1),
17424 ]
17425 );
17426 });
17427
17428 multibuffer.update(cx, |multibuffer, cx| {
17429 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17430 });
17431 _ = editor.update(cx, |editor, window, cx| {
17432 // Removing an excerpt causes the first selection to become degenerate.
17433 assert_eq!(
17434 editor.selections.ranges(&editor.display_snapshot(cx)),
17435 [
17436 Point::new(0, 0)..Point::new(0, 0),
17437 Point::new(0, 1)..Point::new(0, 1)
17438 ]
17439 );
17440
17441 // Refreshing selections will relocate the first selection to the original buffer
17442 // location.
17443 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17444 assert_eq!(
17445 editor.selections.ranges(&editor.display_snapshot(cx)),
17446 [
17447 Point::new(0, 1)..Point::new(0, 1),
17448 Point::new(0, 3)..Point::new(0, 3)
17449 ]
17450 );
17451 assert!(editor.selections.pending_anchor().is_some());
17452 });
17453}
17454
17455#[gpui::test]
17456fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
17457 init_test(cx, |_| {});
17458
17459 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17460 let mut excerpt1_id = None;
17461 let multibuffer = cx.new(|cx| {
17462 let mut multibuffer = MultiBuffer::new(ReadWrite);
17463 excerpt1_id = multibuffer
17464 .push_excerpts(
17465 buffer.clone(),
17466 [
17467 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17468 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17469 ],
17470 cx,
17471 )
17472 .into_iter()
17473 .next();
17474 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17475 multibuffer
17476 });
17477
17478 let editor = cx.add_window(|window, cx| {
17479 let mut editor = build_editor(multibuffer.clone(), window, cx);
17480 let snapshot = editor.snapshot(window, cx);
17481 editor.begin_selection(
17482 Point::new(1, 3).to_display_point(&snapshot),
17483 false,
17484 1,
17485 window,
17486 cx,
17487 );
17488 assert_eq!(
17489 editor.selections.ranges(&editor.display_snapshot(cx)),
17490 [Point::new(1, 3)..Point::new(1, 3)]
17491 );
17492 editor
17493 });
17494
17495 multibuffer.update(cx, |multibuffer, cx| {
17496 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17497 });
17498 _ = editor.update(cx, |editor, window, cx| {
17499 assert_eq!(
17500 editor.selections.ranges(&editor.display_snapshot(cx)),
17501 [Point::new(0, 0)..Point::new(0, 0)]
17502 );
17503
17504 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
17505 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17506 assert_eq!(
17507 editor.selections.ranges(&editor.display_snapshot(cx)),
17508 [Point::new(0, 3)..Point::new(0, 3)]
17509 );
17510 assert!(editor.selections.pending_anchor().is_some());
17511 });
17512}
17513
17514#[gpui::test]
17515async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
17516 init_test(cx, |_| {});
17517
17518 let language = Arc::new(
17519 Language::new(
17520 LanguageConfig {
17521 brackets: BracketPairConfig {
17522 pairs: vec![
17523 BracketPair {
17524 start: "{".to_string(),
17525 end: "}".to_string(),
17526 close: true,
17527 surround: true,
17528 newline: true,
17529 },
17530 BracketPair {
17531 start: "/* ".to_string(),
17532 end: " */".to_string(),
17533 close: true,
17534 surround: true,
17535 newline: true,
17536 },
17537 ],
17538 ..Default::default()
17539 },
17540 ..Default::default()
17541 },
17542 Some(tree_sitter_rust::LANGUAGE.into()),
17543 )
17544 .with_indents_query("")
17545 .unwrap(),
17546 );
17547
17548 let text = concat!(
17549 "{ }\n", //
17550 " x\n", //
17551 " /* */\n", //
17552 "x\n", //
17553 "{{} }\n", //
17554 );
17555
17556 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17557 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17558 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17559 editor
17560 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
17561 .await;
17562
17563 editor.update_in(cx, |editor, window, cx| {
17564 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17565 s.select_display_ranges([
17566 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
17567 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
17568 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
17569 ])
17570 });
17571 editor.newline(&Newline, window, cx);
17572
17573 assert_eq!(
17574 editor.buffer().read(cx).read(cx).text(),
17575 concat!(
17576 "{ \n", // Suppress rustfmt
17577 "\n", //
17578 "}\n", //
17579 " x\n", //
17580 " /* \n", //
17581 " \n", //
17582 " */\n", //
17583 "x\n", //
17584 "{{} \n", //
17585 "}\n", //
17586 )
17587 );
17588 });
17589}
17590
17591#[gpui::test]
17592fn test_highlighted_ranges(cx: &mut TestAppContext) {
17593 init_test(cx, |_| {});
17594
17595 let editor = cx.add_window(|window, cx| {
17596 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
17597 build_editor(buffer, window, cx)
17598 });
17599
17600 _ = editor.update(cx, |editor, window, cx| {
17601 struct Type1;
17602 struct Type2;
17603
17604 let buffer = editor.buffer.read(cx).snapshot(cx);
17605
17606 let anchor_range =
17607 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
17608
17609 editor.highlight_background::<Type1>(
17610 &[
17611 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
17612 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
17613 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
17614 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
17615 ],
17616 |_, _| Hsla::red(),
17617 cx,
17618 );
17619 editor.highlight_background::<Type2>(
17620 &[
17621 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
17622 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
17623 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
17624 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
17625 ],
17626 |_, _| Hsla::green(),
17627 cx,
17628 );
17629
17630 let snapshot = editor.snapshot(window, cx);
17631 let highlighted_ranges = editor.sorted_background_highlights_in_range(
17632 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
17633 &snapshot,
17634 cx.theme(),
17635 );
17636 assert_eq!(
17637 highlighted_ranges,
17638 &[
17639 (
17640 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
17641 Hsla::green(),
17642 ),
17643 (
17644 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
17645 Hsla::red(),
17646 ),
17647 (
17648 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
17649 Hsla::green(),
17650 ),
17651 (
17652 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17653 Hsla::red(),
17654 ),
17655 ]
17656 );
17657 assert_eq!(
17658 editor.sorted_background_highlights_in_range(
17659 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
17660 &snapshot,
17661 cx.theme(),
17662 ),
17663 &[(
17664 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17665 Hsla::red(),
17666 )]
17667 );
17668 });
17669}
17670
17671#[gpui::test]
17672async fn test_following(cx: &mut TestAppContext) {
17673 init_test(cx, |_| {});
17674
17675 let fs = FakeFs::new(cx.executor());
17676 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17677
17678 let buffer = project.update(cx, |project, cx| {
17679 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
17680 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
17681 });
17682 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17683 let follower = cx.update(|cx| {
17684 cx.open_window(
17685 WindowOptions {
17686 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
17687 gpui::Point::new(px(0.), px(0.)),
17688 gpui::Point::new(px(10.), px(80.)),
17689 ))),
17690 ..Default::default()
17691 },
17692 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
17693 )
17694 .unwrap()
17695 });
17696
17697 let is_still_following = Rc::new(RefCell::new(true));
17698 let follower_edit_event_count = Rc::new(RefCell::new(0));
17699 let pending_update = Rc::new(RefCell::new(None));
17700 let leader_entity = leader.root(cx).unwrap();
17701 let follower_entity = follower.root(cx).unwrap();
17702 _ = follower.update(cx, {
17703 let update = pending_update.clone();
17704 let is_still_following = is_still_following.clone();
17705 let follower_edit_event_count = follower_edit_event_count.clone();
17706 |_, window, cx| {
17707 cx.subscribe_in(
17708 &leader_entity,
17709 window,
17710 move |_, leader, event, window, cx| {
17711 leader.read(cx).add_event_to_update_proto(
17712 event,
17713 &mut update.borrow_mut(),
17714 window,
17715 cx,
17716 );
17717 },
17718 )
17719 .detach();
17720
17721 cx.subscribe_in(
17722 &follower_entity,
17723 window,
17724 move |_, _, event: &EditorEvent, _window, _cx| {
17725 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
17726 *is_still_following.borrow_mut() = false;
17727 }
17728
17729 if let EditorEvent::BufferEdited = event {
17730 *follower_edit_event_count.borrow_mut() += 1;
17731 }
17732 },
17733 )
17734 .detach();
17735 }
17736 });
17737
17738 // Update the selections only
17739 _ = leader.update(cx, |leader, window, cx| {
17740 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17741 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17742 });
17743 });
17744 follower
17745 .update(cx, |follower, window, cx| {
17746 follower.apply_update_proto(
17747 &project,
17748 pending_update.borrow_mut().take().unwrap(),
17749 window,
17750 cx,
17751 )
17752 })
17753 .unwrap()
17754 .await
17755 .unwrap();
17756 _ = follower.update(cx, |follower, _, cx| {
17757 assert_eq!(
17758 follower.selections.ranges(&follower.display_snapshot(cx)),
17759 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
17760 );
17761 });
17762 assert!(*is_still_following.borrow());
17763 assert_eq!(*follower_edit_event_count.borrow(), 0);
17764
17765 // Update the scroll position only
17766 _ = leader.update(cx, |leader, window, cx| {
17767 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17768 });
17769 follower
17770 .update(cx, |follower, window, cx| {
17771 follower.apply_update_proto(
17772 &project,
17773 pending_update.borrow_mut().take().unwrap(),
17774 window,
17775 cx,
17776 )
17777 })
17778 .unwrap()
17779 .await
17780 .unwrap();
17781 assert_eq!(
17782 follower
17783 .update(cx, |follower, _, cx| follower.scroll_position(cx))
17784 .unwrap(),
17785 gpui::Point::new(1.5, 3.5)
17786 );
17787 assert!(*is_still_following.borrow());
17788 assert_eq!(*follower_edit_event_count.borrow(), 0);
17789
17790 // Update the selections and scroll position. The follower's scroll position is updated
17791 // via autoscroll, not via the leader's exact scroll position.
17792 _ = leader.update(cx, |leader, window, cx| {
17793 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17794 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
17795 });
17796 leader.request_autoscroll(Autoscroll::newest(), cx);
17797 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17798 });
17799 follower
17800 .update(cx, |follower, window, cx| {
17801 follower.apply_update_proto(
17802 &project,
17803 pending_update.borrow_mut().take().unwrap(),
17804 window,
17805 cx,
17806 )
17807 })
17808 .unwrap()
17809 .await
17810 .unwrap();
17811 _ = follower.update(cx, |follower, _, cx| {
17812 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17813 assert_eq!(
17814 follower.selections.ranges(&follower.display_snapshot(cx)),
17815 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17816 );
17817 });
17818 assert!(*is_still_following.borrow());
17819
17820 // Creating a pending selection that precedes another selection
17821 _ = leader.update(cx, |leader, window, cx| {
17822 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17823 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17824 });
17825 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17826 });
17827 follower
17828 .update(cx, |follower, window, cx| {
17829 follower.apply_update_proto(
17830 &project,
17831 pending_update.borrow_mut().take().unwrap(),
17832 window,
17833 cx,
17834 )
17835 })
17836 .unwrap()
17837 .await
17838 .unwrap();
17839 _ = follower.update(cx, |follower, _, cx| {
17840 assert_eq!(
17841 follower.selections.ranges(&follower.display_snapshot(cx)),
17842 vec![
17843 MultiBufferOffset(0)..MultiBufferOffset(0),
17844 MultiBufferOffset(1)..MultiBufferOffset(1)
17845 ]
17846 );
17847 });
17848 assert!(*is_still_following.borrow());
17849
17850 // Extend the pending selection so that it surrounds another selection
17851 _ = leader.update(cx, |leader, window, cx| {
17852 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17853 });
17854 follower
17855 .update(cx, |follower, window, cx| {
17856 follower.apply_update_proto(
17857 &project,
17858 pending_update.borrow_mut().take().unwrap(),
17859 window,
17860 cx,
17861 )
17862 })
17863 .unwrap()
17864 .await
17865 .unwrap();
17866 _ = follower.update(cx, |follower, _, cx| {
17867 assert_eq!(
17868 follower.selections.ranges(&follower.display_snapshot(cx)),
17869 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17870 );
17871 });
17872
17873 // Scrolling locally breaks the follow
17874 _ = follower.update(cx, |follower, window, cx| {
17875 let top_anchor = follower
17876 .buffer()
17877 .read(cx)
17878 .read(cx)
17879 .anchor_after(MultiBufferOffset(0));
17880 follower.set_scroll_anchor(
17881 ScrollAnchor {
17882 anchor: top_anchor,
17883 offset: gpui::Point::new(0.0, 0.5),
17884 },
17885 window,
17886 cx,
17887 );
17888 });
17889 assert!(!(*is_still_following.borrow()));
17890}
17891
17892#[gpui::test]
17893async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17894 init_test(cx, |_| {});
17895
17896 let fs = FakeFs::new(cx.executor());
17897 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17898 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17899 let pane = workspace
17900 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17901 .unwrap();
17902
17903 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17904
17905 let leader = pane.update_in(cx, |_, window, cx| {
17906 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17907 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17908 });
17909
17910 // Start following the editor when it has no excerpts.
17911 let mut state_message =
17912 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17913 let workspace_entity = workspace.root(cx).unwrap();
17914 let follower_1 = cx
17915 .update_window(*workspace.deref(), |_, window, cx| {
17916 Editor::from_state_proto(
17917 workspace_entity,
17918 ViewId {
17919 creator: CollaboratorId::PeerId(PeerId::default()),
17920 id: 0,
17921 },
17922 &mut state_message,
17923 window,
17924 cx,
17925 )
17926 })
17927 .unwrap()
17928 .unwrap()
17929 .await
17930 .unwrap();
17931
17932 let update_message = Rc::new(RefCell::new(None));
17933 follower_1.update_in(cx, {
17934 let update = update_message.clone();
17935 |_, window, cx| {
17936 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17937 leader.read(cx).add_event_to_update_proto(
17938 event,
17939 &mut update.borrow_mut(),
17940 window,
17941 cx,
17942 );
17943 })
17944 .detach();
17945 }
17946 });
17947
17948 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17949 (
17950 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17951 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17952 )
17953 });
17954
17955 // Insert some excerpts.
17956 leader.update(cx, |leader, cx| {
17957 leader.buffer.update(cx, |multibuffer, cx| {
17958 multibuffer.set_excerpts_for_path(
17959 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17960 buffer_1.clone(),
17961 vec![
17962 Point::row_range(0..3),
17963 Point::row_range(1..6),
17964 Point::row_range(12..15),
17965 ],
17966 0,
17967 cx,
17968 );
17969 multibuffer.set_excerpts_for_path(
17970 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17971 buffer_2.clone(),
17972 vec![Point::row_range(0..6), Point::row_range(8..12)],
17973 0,
17974 cx,
17975 );
17976 });
17977 });
17978
17979 // Apply the update of adding the excerpts.
17980 follower_1
17981 .update_in(cx, |follower, window, cx| {
17982 follower.apply_update_proto(
17983 &project,
17984 update_message.borrow().clone().unwrap(),
17985 window,
17986 cx,
17987 )
17988 })
17989 .await
17990 .unwrap();
17991 assert_eq!(
17992 follower_1.update(cx, |editor, cx| editor.text(cx)),
17993 leader.update(cx, |editor, cx| editor.text(cx))
17994 );
17995 update_message.borrow_mut().take();
17996
17997 // Start following separately after it already has excerpts.
17998 let mut state_message =
17999 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
18000 let workspace_entity = workspace.root(cx).unwrap();
18001 let follower_2 = cx
18002 .update_window(*workspace.deref(), |_, window, cx| {
18003 Editor::from_state_proto(
18004 workspace_entity,
18005 ViewId {
18006 creator: CollaboratorId::PeerId(PeerId::default()),
18007 id: 0,
18008 },
18009 &mut state_message,
18010 window,
18011 cx,
18012 )
18013 })
18014 .unwrap()
18015 .unwrap()
18016 .await
18017 .unwrap();
18018 assert_eq!(
18019 follower_2.update(cx, |editor, cx| editor.text(cx)),
18020 leader.update(cx, |editor, cx| editor.text(cx))
18021 );
18022
18023 // Remove some excerpts.
18024 leader.update(cx, |leader, cx| {
18025 leader.buffer.update(cx, |multibuffer, cx| {
18026 let excerpt_ids = multibuffer.excerpt_ids();
18027 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
18028 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
18029 });
18030 });
18031
18032 // Apply the update of removing the excerpts.
18033 follower_1
18034 .update_in(cx, |follower, window, cx| {
18035 follower.apply_update_proto(
18036 &project,
18037 update_message.borrow().clone().unwrap(),
18038 window,
18039 cx,
18040 )
18041 })
18042 .await
18043 .unwrap();
18044 follower_2
18045 .update_in(cx, |follower, window, cx| {
18046 follower.apply_update_proto(
18047 &project,
18048 update_message.borrow().clone().unwrap(),
18049 window,
18050 cx,
18051 )
18052 })
18053 .await
18054 .unwrap();
18055 update_message.borrow_mut().take();
18056 assert_eq!(
18057 follower_1.update(cx, |editor, cx| editor.text(cx)),
18058 leader.update(cx, |editor, cx| editor.text(cx))
18059 );
18060}
18061
18062#[gpui::test]
18063async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18064 init_test(cx, |_| {});
18065
18066 let mut cx = EditorTestContext::new(cx).await;
18067 let lsp_store =
18068 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
18069
18070 cx.set_state(indoc! {"
18071 ˇfn func(abc def: i32) -> u32 {
18072 }
18073 "});
18074
18075 cx.update(|_, cx| {
18076 lsp_store.update(cx, |lsp_store, cx| {
18077 lsp_store
18078 .update_diagnostics(
18079 LanguageServerId(0),
18080 lsp::PublishDiagnosticsParams {
18081 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
18082 version: None,
18083 diagnostics: vec![
18084 lsp::Diagnostic {
18085 range: lsp::Range::new(
18086 lsp::Position::new(0, 11),
18087 lsp::Position::new(0, 12),
18088 ),
18089 severity: Some(lsp::DiagnosticSeverity::ERROR),
18090 ..Default::default()
18091 },
18092 lsp::Diagnostic {
18093 range: lsp::Range::new(
18094 lsp::Position::new(0, 12),
18095 lsp::Position::new(0, 15),
18096 ),
18097 severity: Some(lsp::DiagnosticSeverity::ERROR),
18098 ..Default::default()
18099 },
18100 lsp::Diagnostic {
18101 range: lsp::Range::new(
18102 lsp::Position::new(0, 25),
18103 lsp::Position::new(0, 28),
18104 ),
18105 severity: Some(lsp::DiagnosticSeverity::ERROR),
18106 ..Default::default()
18107 },
18108 ],
18109 },
18110 None,
18111 DiagnosticSourceKind::Pushed,
18112 &[],
18113 cx,
18114 )
18115 .unwrap()
18116 });
18117 });
18118
18119 executor.run_until_parked();
18120
18121 cx.update_editor(|editor, window, cx| {
18122 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
18123 });
18124
18125 cx.assert_editor_state(indoc! {"
18126 fn func(abc def: i32) -> ˇu32 {
18127 }
18128 "});
18129
18130 cx.update_editor(|editor, window, cx| {
18131 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
18132 });
18133
18134 cx.assert_editor_state(indoc! {"
18135 fn func(abc ˇdef: i32) -> u32 {
18136 }
18137 "});
18138
18139 cx.update_editor(|editor, window, cx| {
18140 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
18141 });
18142
18143 cx.assert_editor_state(indoc! {"
18144 fn func(abcˇ def: i32) -> u32 {
18145 }
18146 "});
18147
18148 cx.update_editor(|editor, window, cx| {
18149 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
18150 });
18151
18152 cx.assert_editor_state(indoc! {"
18153 fn func(abc def: i32) -> ˇu32 {
18154 }
18155 "});
18156}
18157
18158#[gpui::test]
18159async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18160 init_test(cx, |_| {});
18161
18162 let mut cx = EditorTestContext::new(cx).await;
18163
18164 let diff_base = r#"
18165 use some::mod;
18166
18167 const A: u32 = 42;
18168
18169 fn main() {
18170 println!("hello");
18171
18172 println!("world");
18173 }
18174 "#
18175 .unindent();
18176
18177 // Edits are modified, removed, modified, added
18178 cx.set_state(
18179 &r#"
18180 use some::modified;
18181
18182 ˇ
18183 fn main() {
18184 println!("hello there");
18185
18186 println!("around the");
18187 println!("world");
18188 }
18189 "#
18190 .unindent(),
18191 );
18192
18193 cx.set_head_text(&diff_base);
18194 executor.run_until_parked();
18195
18196 cx.update_editor(|editor, window, cx| {
18197 //Wrap around the bottom of the buffer
18198 for _ in 0..3 {
18199 editor.go_to_next_hunk(&GoToHunk, window, cx);
18200 }
18201 });
18202
18203 cx.assert_editor_state(
18204 &r#"
18205 ˇuse some::modified;
18206
18207
18208 fn main() {
18209 println!("hello there");
18210
18211 println!("around the");
18212 println!("world");
18213 }
18214 "#
18215 .unindent(),
18216 );
18217
18218 cx.update_editor(|editor, window, cx| {
18219 //Wrap around the top of the buffer
18220 for _ in 0..2 {
18221 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18222 }
18223 });
18224
18225 cx.assert_editor_state(
18226 &r#"
18227 use some::modified;
18228
18229
18230 fn main() {
18231 ˇ println!("hello there");
18232
18233 println!("around the");
18234 println!("world");
18235 }
18236 "#
18237 .unindent(),
18238 );
18239
18240 cx.update_editor(|editor, window, cx| {
18241 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18242 });
18243
18244 cx.assert_editor_state(
18245 &r#"
18246 use some::modified;
18247
18248 ˇ
18249 fn main() {
18250 println!("hello there");
18251
18252 println!("around the");
18253 println!("world");
18254 }
18255 "#
18256 .unindent(),
18257 );
18258
18259 cx.update_editor(|editor, window, cx| {
18260 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18261 });
18262
18263 cx.assert_editor_state(
18264 &r#"
18265 ˇuse some::modified;
18266
18267
18268 fn main() {
18269 println!("hello there");
18270
18271 println!("around the");
18272 println!("world");
18273 }
18274 "#
18275 .unindent(),
18276 );
18277
18278 cx.update_editor(|editor, window, cx| {
18279 for _ in 0..2 {
18280 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18281 }
18282 });
18283
18284 cx.assert_editor_state(
18285 &r#"
18286 use some::modified;
18287
18288
18289 fn main() {
18290 ˇ println!("hello there");
18291
18292 println!("around the");
18293 println!("world");
18294 }
18295 "#
18296 .unindent(),
18297 );
18298
18299 cx.update_editor(|editor, window, cx| {
18300 editor.fold(&Fold, window, cx);
18301 });
18302
18303 cx.update_editor(|editor, window, cx| {
18304 editor.go_to_next_hunk(&GoToHunk, window, cx);
18305 });
18306
18307 cx.assert_editor_state(
18308 &r#"
18309 ˇuse some::modified;
18310
18311
18312 fn main() {
18313 println!("hello there");
18314
18315 println!("around the");
18316 println!("world");
18317 }
18318 "#
18319 .unindent(),
18320 );
18321}
18322
18323#[test]
18324fn test_split_words() {
18325 fn split(text: &str) -> Vec<&str> {
18326 split_words(text).collect()
18327 }
18328
18329 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
18330 assert_eq!(split("hello_world"), &["hello_", "world"]);
18331 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
18332 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
18333 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
18334 assert_eq!(split("helloworld"), &["helloworld"]);
18335
18336 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
18337}
18338
18339#[test]
18340fn test_split_words_for_snippet_prefix() {
18341 fn split(text: &str) -> Vec<&str> {
18342 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
18343 }
18344
18345 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
18346 assert_eq!(split("hello_world"), &["hello_world"]);
18347 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
18348 assert_eq!(split("Hello_World"), &["Hello_World"]);
18349 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
18350 assert_eq!(split("helloworld"), &["helloworld"]);
18351 assert_eq!(
18352 split("this@is!@#$^many . symbols"),
18353 &[
18354 "symbols",
18355 " symbols",
18356 ". symbols",
18357 " . symbols",
18358 " . symbols",
18359 " . symbols",
18360 "many . symbols",
18361 "^many . symbols",
18362 "$^many . symbols",
18363 "#$^many . symbols",
18364 "@#$^many . symbols",
18365 "!@#$^many . symbols",
18366 "is!@#$^many . symbols",
18367 "@is!@#$^many . symbols",
18368 "this@is!@#$^many . symbols",
18369 ],
18370 );
18371 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
18372}
18373
18374#[gpui::test]
18375async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
18376 init_test(cx, |_| {});
18377
18378 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
18379
18380 #[track_caller]
18381 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
18382 let _state_context = cx.set_state(before);
18383 cx.run_until_parked();
18384 cx.update_editor(|editor, window, cx| {
18385 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
18386 });
18387 cx.run_until_parked();
18388 cx.assert_editor_state(after);
18389 }
18390
18391 // Outside bracket jumps to outside of matching bracket
18392 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
18393 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
18394
18395 // Inside bracket jumps to inside of matching bracket
18396 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
18397 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
18398
18399 // When outside a bracket and inside, favor jumping to the inside bracket
18400 assert(
18401 "console.log('foo', [1, 2, 3]ˇ);",
18402 "console.log('foo', ˇ[1, 2, 3]);",
18403 &mut cx,
18404 );
18405 assert(
18406 "console.log(ˇ'foo', [1, 2, 3]);",
18407 "console.log('foo'ˇ, [1, 2, 3]);",
18408 &mut cx,
18409 );
18410
18411 // Bias forward if two options are equally likely
18412 assert(
18413 "let result = curried_fun()ˇ();",
18414 "let result = curried_fun()()ˇ;",
18415 &mut cx,
18416 );
18417
18418 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
18419 assert(
18420 indoc! {"
18421 function test() {
18422 console.log('test')ˇ
18423 }"},
18424 indoc! {"
18425 function test() {
18426 console.logˇ('test')
18427 }"},
18428 &mut cx,
18429 );
18430}
18431
18432#[gpui::test]
18433async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
18434 init_test(cx, |_| {});
18435 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
18436 language_registry.add(markdown_lang());
18437 language_registry.add(rust_lang());
18438 let buffer = cx.new(|cx| {
18439 let mut buffer = language::Buffer::local(
18440 indoc! {"
18441 ```rs
18442 impl Worktree {
18443 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18444 }
18445 }
18446 ```
18447 "},
18448 cx,
18449 );
18450 buffer.set_language_registry(language_registry.clone());
18451 buffer.set_language(Some(markdown_lang()), cx);
18452 buffer
18453 });
18454 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18455 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
18456 cx.executor().run_until_parked();
18457 _ = editor.update(cx, |editor, window, cx| {
18458 // Case 1: Test outer enclosing brackets
18459 select_ranges(
18460 editor,
18461 &indoc! {"
18462 ```rs
18463 impl Worktree {
18464 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18465 }
18466 }ˇ
18467 ```
18468 "},
18469 window,
18470 cx,
18471 );
18472 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18473 assert_text_with_selections(
18474 editor,
18475 &indoc! {"
18476 ```rs
18477 impl Worktree ˇ{
18478 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18479 }
18480 }
18481 ```
18482 "},
18483 cx,
18484 );
18485 // Case 2: Test inner enclosing brackets
18486 select_ranges(
18487 editor,
18488 &indoc! {"
18489 ```rs
18490 impl Worktree {
18491 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18492 }ˇ
18493 }
18494 ```
18495 "},
18496 window,
18497 cx,
18498 );
18499 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18500 assert_text_with_selections(
18501 editor,
18502 &indoc! {"
18503 ```rs
18504 impl Worktree {
18505 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
18506 }
18507 }
18508 ```
18509 "},
18510 cx,
18511 );
18512 });
18513}
18514
18515#[gpui::test]
18516async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
18517 init_test(cx, |_| {});
18518
18519 let fs = FakeFs::new(cx.executor());
18520 fs.insert_tree(
18521 path!("/a"),
18522 json!({
18523 "main.rs": "fn main() { let a = 5; }",
18524 "other.rs": "// Test file",
18525 }),
18526 )
18527 .await;
18528 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18529
18530 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18531 language_registry.add(Arc::new(Language::new(
18532 LanguageConfig {
18533 name: "Rust".into(),
18534 matcher: LanguageMatcher {
18535 path_suffixes: vec!["rs".to_string()],
18536 ..Default::default()
18537 },
18538 brackets: BracketPairConfig {
18539 pairs: vec![BracketPair {
18540 start: "{".to_string(),
18541 end: "}".to_string(),
18542 close: true,
18543 surround: true,
18544 newline: true,
18545 }],
18546 disabled_scopes_by_bracket_ix: Vec::new(),
18547 },
18548 ..Default::default()
18549 },
18550 Some(tree_sitter_rust::LANGUAGE.into()),
18551 )));
18552 let mut fake_servers = language_registry.register_fake_lsp(
18553 "Rust",
18554 FakeLspAdapter {
18555 capabilities: lsp::ServerCapabilities {
18556 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18557 first_trigger_character: "{".to_string(),
18558 more_trigger_character: None,
18559 }),
18560 ..Default::default()
18561 },
18562 ..Default::default()
18563 },
18564 );
18565
18566 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18567
18568 let cx = &mut VisualTestContext::from_window(*workspace, cx);
18569
18570 let worktree_id = workspace
18571 .update(cx, |workspace, _, cx| {
18572 workspace.project().update(cx, |project, cx| {
18573 project.worktrees(cx).next().unwrap().read(cx).id()
18574 })
18575 })
18576 .unwrap();
18577
18578 let buffer = project
18579 .update(cx, |project, cx| {
18580 project.open_local_buffer(path!("/a/main.rs"), cx)
18581 })
18582 .await
18583 .unwrap();
18584 let editor_handle = workspace
18585 .update(cx, |workspace, window, cx| {
18586 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
18587 })
18588 .unwrap()
18589 .await
18590 .unwrap()
18591 .downcast::<Editor>()
18592 .unwrap();
18593
18594 let fake_server = fake_servers.next().await.unwrap();
18595
18596 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
18597 |params, _| async move {
18598 assert_eq!(
18599 params.text_document_position.text_document.uri,
18600 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
18601 );
18602 assert_eq!(
18603 params.text_document_position.position,
18604 lsp::Position::new(0, 21),
18605 );
18606
18607 Ok(Some(vec![lsp::TextEdit {
18608 new_text: "]".to_string(),
18609 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18610 }]))
18611 },
18612 );
18613
18614 editor_handle.update_in(cx, |editor, window, cx| {
18615 window.focus(&editor.focus_handle(cx), cx);
18616 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18617 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
18618 });
18619 editor.handle_input("{", window, cx);
18620 });
18621
18622 cx.executor().run_until_parked();
18623
18624 buffer.update(cx, |buffer, _| {
18625 assert_eq!(
18626 buffer.text(),
18627 "fn main() { let a = {5}; }",
18628 "No extra braces from on type formatting should appear in the buffer"
18629 )
18630 });
18631}
18632
18633#[gpui::test(iterations = 20, seeds(31))]
18634async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
18635 init_test(cx, |_| {});
18636
18637 let mut cx = EditorLspTestContext::new_rust(
18638 lsp::ServerCapabilities {
18639 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18640 first_trigger_character: ".".to_string(),
18641 more_trigger_character: None,
18642 }),
18643 ..Default::default()
18644 },
18645 cx,
18646 )
18647 .await;
18648
18649 cx.update_buffer(|buffer, _| {
18650 // This causes autoindent to be async.
18651 buffer.set_sync_parse_timeout(None)
18652 });
18653
18654 cx.set_state("fn c() {\n d()ˇ\n}\n");
18655 cx.simulate_keystroke("\n");
18656 cx.run_until_parked();
18657
18658 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
18659 let mut request =
18660 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
18661 let buffer_cloned = buffer_cloned.clone();
18662 async move {
18663 buffer_cloned.update(&mut cx, |buffer, _| {
18664 assert_eq!(
18665 buffer.text(),
18666 "fn c() {\n d()\n .\n}\n",
18667 "OnTypeFormatting should triggered after autoindent applied"
18668 )
18669 });
18670
18671 Ok(Some(vec![]))
18672 }
18673 });
18674
18675 cx.simulate_keystroke(".");
18676 cx.run_until_parked();
18677
18678 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
18679 assert!(request.next().await.is_some());
18680 request.close();
18681 assert!(request.next().await.is_none());
18682}
18683
18684#[gpui::test]
18685async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
18686 init_test(cx, |_| {});
18687
18688 let fs = FakeFs::new(cx.executor());
18689 fs.insert_tree(
18690 path!("/a"),
18691 json!({
18692 "main.rs": "fn main() { let a = 5; }",
18693 "other.rs": "// Test file",
18694 }),
18695 )
18696 .await;
18697
18698 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18699
18700 let server_restarts = Arc::new(AtomicUsize::new(0));
18701 let closure_restarts = Arc::clone(&server_restarts);
18702 let language_server_name = "test language server";
18703 let language_name: LanguageName = "Rust".into();
18704
18705 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18706 language_registry.add(Arc::new(Language::new(
18707 LanguageConfig {
18708 name: language_name.clone(),
18709 matcher: LanguageMatcher {
18710 path_suffixes: vec!["rs".to_string()],
18711 ..Default::default()
18712 },
18713 ..Default::default()
18714 },
18715 Some(tree_sitter_rust::LANGUAGE.into()),
18716 )));
18717 let mut fake_servers = language_registry.register_fake_lsp(
18718 "Rust",
18719 FakeLspAdapter {
18720 name: language_server_name,
18721 initialization_options: Some(json!({
18722 "testOptionValue": true
18723 })),
18724 initializer: Some(Box::new(move |fake_server| {
18725 let task_restarts = Arc::clone(&closure_restarts);
18726 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
18727 task_restarts.fetch_add(1, atomic::Ordering::Release);
18728 futures::future::ready(Ok(()))
18729 });
18730 })),
18731 ..Default::default()
18732 },
18733 );
18734
18735 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18736 let _buffer = project
18737 .update(cx, |project, cx| {
18738 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
18739 })
18740 .await
18741 .unwrap();
18742 let _fake_server = fake_servers.next().await.unwrap();
18743 update_test_language_settings(cx, |language_settings| {
18744 language_settings.languages.0.insert(
18745 language_name.clone().0.to_string(),
18746 LanguageSettingsContent {
18747 tab_size: NonZeroU32::new(8),
18748 ..Default::default()
18749 },
18750 );
18751 });
18752 cx.executor().run_until_parked();
18753 assert_eq!(
18754 server_restarts.load(atomic::Ordering::Acquire),
18755 0,
18756 "Should not restart LSP server on an unrelated change"
18757 );
18758
18759 update_test_project_settings(cx, |project_settings| {
18760 project_settings.lsp.0.insert(
18761 "Some other server name".into(),
18762 LspSettings {
18763 binary: None,
18764 settings: None,
18765 initialization_options: Some(json!({
18766 "some other init value": false
18767 })),
18768 enable_lsp_tasks: false,
18769 fetch: None,
18770 },
18771 );
18772 });
18773 cx.executor().run_until_parked();
18774 assert_eq!(
18775 server_restarts.load(atomic::Ordering::Acquire),
18776 0,
18777 "Should not restart LSP server on an unrelated LSP settings change"
18778 );
18779
18780 update_test_project_settings(cx, |project_settings| {
18781 project_settings.lsp.0.insert(
18782 language_server_name.into(),
18783 LspSettings {
18784 binary: None,
18785 settings: None,
18786 initialization_options: Some(json!({
18787 "anotherInitValue": false
18788 })),
18789 enable_lsp_tasks: false,
18790 fetch: None,
18791 },
18792 );
18793 });
18794 cx.executor().run_until_parked();
18795 assert_eq!(
18796 server_restarts.load(atomic::Ordering::Acquire),
18797 1,
18798 "Should restart LSP server on a related LSP settings change"
18799 );
18800
18801 update_test_project_settings(cx, |project_settings| {
18802 project_settings.lsp.0.insert(
18803 language_server_name.into(),
18804 LspSettings {
18805 binary: None,
18806 settings: None,
18807 initialization_options: Some(json!({
18808 "anotherInitValue": false
18809 })),
18810 enable_lsp_tasks: false,
18811 fetch: None,
18812 },
18813 );
18814 });
18815 cx.executor().run_until_parked();
18816 assert_eq!(
18817 server_restarts.load(atomic::Ordering::Acquire),
18818 1,
18819 "Should not restart LSP server on a related LSP settings change that is the same"
18820 );
18821
18822 update_test_project_settings(cx, |project_settings| {
18823 project_settings.lsp.0.insert(
18824 language_server_name.into(),
18825 LspSettings {
18826 binary: None,
18827 settings: None,
18828 initialization_options: None,
18829 enable_lsp_tasks: false,
18830 fetch: None,
18831 },
18832 );
18833 });
18834 cx.executor().run_until_parked();
18835 assert_eq!(
18836 server_restarts.load(atomic::Ordering::Acquire),
18837 2,
18838 "Should restart LSP server on another related LSP settings change"
18839 );
18840}
18841
18842#[gpui::test]
18843async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18844 init_test(cx, |_| {});
18845
18846 let mut cx = EditorLspTestContext::new_rust(
18847 lsp::ServerCapabilities {
18848 completion_provider: Some(lsp::CompletionOptions {
18849 trigger_characters: Some(vec![".".to_string()]),
18850 resolve_provider: Some(true),
18851 ..Default::default()
18852 }),
18853 ..Default::default()
18854 },
18855 cx,
18856 )
18857 .await;
18858
18859 cx.set_state("fn main() { let a = 2ˇ; }");
18860 cx.simulate_keystroke(".");
18861 let completion_item = lsp::CompletionItem {
18862 label: "some".into(),
18863 kind: Some(lsp::CompletionItemKind::SNIPPET),
18864 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18865 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18866 kind: lsp::MarkupKind::Markdown,
18867 value: "```rust\nSome(2)\n```".to_string(),
18868 })),
18869 deprecated: Some(false),
18870 sort_text: Some("fffffff2".to_string()),
18871 filter_text: Some("some".to_string()),
18872 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18873 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18874 range: lsp::Range {
18875 start: lsp::Position {
18876 line: 0,
18877 character: 22,
18878 },
18879 end: lsp::Position {
18880 line: 0,
18881 character: 22,
18882 },
18883 },
18884 new_text: "Some(2)".to_string(),
18885 })),
18886 additional_text_edits: Some(vec![lsp::TextEdit {
18887 range: lsp::Range {
18888 start: lsp::Position {
18889 line: 0,
18890 character: 20,
18891 },
18892 end: lsp::Position {
18893 line: 0,
18894 character: 22,
18895 },
18896 },
18897 new_text: "".to_string(),
18898 }]),
18899 ..Default::default()
18900 };
18901
18902 let closure_completion_item = completion_item.clone();
18903 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18904 let task_completion_item = closure_completion_item.clone();
18905 async move {
18906 Ok(Some(lsp::CompletionResponse::Array(vec![
18907 task_completion_item,
18908 ])))
18909 }
18910 });
18911
18912 request.next().await;
18913
18914 cx.condition(|editor, _| editor.context_menu_visible())
18915 .await;
18916 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18917 editor
18918 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18919 .unwrap()
18920 });
18921 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18922
18923 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18924 let task_completion_item = completion_item.clone();
18925 async move { Ok(task_completion_item) }
18926 })
18927 .next()
18928 .await
18929 .unwrap();
18930 apply_additional_edits.await.unwrap();
18931 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18932}
18933
18934#[gpui::test]
18935async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18936 init_test(cx, |_| {});
18937
18938 let mut cx = EditorLspTestContext::new_rust(
18939 lsp::ServerCapabilities {
18940 completion_provider: Some(lsp::CompletionOptions {
18941 trigger_characters: Some(vec![".".to_string()]),
18942 resolve_provider: Some(true),
18943 ..Default::default()
18944 }),
18945 ..Default::default()
18946 },
18947 cx,
18948 )
18949 .await;
18950
18951 cx.set_state("fn main() { let a = 2ˇ; }");
18952 cx.simulate_keystroke(".");
18953
18954 let item1 = lsp::CompletionItem {
18955 label: "method id()".to_string(),
18956 filter_text: Some("id".to_string()),
18957 detail: None,
18958 documentation: None,
18959 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18960 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18961 new_text: ".id".to_string(),
18962 })),
18963 ..lsp::CompletionItem::default()
18964 };
18965
18966 let item2 = lsp::CompletionItem {
18967 label: "other".to_string(),
18968 filter_text: Some("other".to_string()),
18969 detail: None,
18970 documentation: None,
18971 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18972 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18973 new_text: ".other".to_string(),
18974 })),
18975 ..lsp::CompletionItem::default()
18976 };
18977
18978 let item1 = item1.clone();
18979 cx.set_request_handler::<lsp::request::Completion, _, _>({
18980 let item1 = item1.clone();
18981 move |_, _, _| {
18982 let item1 = item1.clone();
18983 let item2 = item2.clone();
18984 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
18985 }
18986 })
18987 .next()
18988 .await;
18989
18990 cx.condition(|editor, _| editor.context_menu_visible())
18991 .await;
18992 cx.update_editor(|editor, _, _| {
18993 let context_menu = editor.context_menu.borrow_mut();
18994 let context_menu = context_menu
18995 .as_ref()
18996 .expect("Should have the context menu deployed");
18997 match context_menu {
18998 CodeContextMenu::Completions(completions_menu) => {
18999 let completions = completions_menu.completions.borrow_mut();
19000 assert_eq!(
19001 completions
19002 .iter()
19003 .map(|completion| &completion.label.text)
19004 .collect::<Vec<_>>(),
19005 vec!["method id()", "other"]
19006 )
19007 }
19008 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
19009 }
19010 });
19011
19012 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
19013 let item1 = item1.clone();
19014 move |_, item_to_resolve, _| {
19015 let item1 = item1.clone();
19016 async move {
19017 if item1 == item_to_resolve {
19018 Ok(lsp::CompletionItem {
19019 label: "method id()".to_string(),
19020 filter_text: Some("id".to_string()),
19021 detail: Some("Now resolved!".to_string()),
19022 documentation: Some(lsp::Documentation::String("Docs".to_string())),
19023 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19024 range: lsp::Range::new(
19025 lsp::Position::new(0, 22),
19026 lsp::Position::new(0, 22),
19027 ),
19028 new_text: ".id".to_string(),
19029 })),
19030 ..lsp::CompletionItem::default()
19031 })
19032 } else {
19033 Ok(item_to_resolve)
19034 }
19035 }
19036 }
19037 })
19038 .next()
19039 .await
19040 .unwrap();
19041 cx.run_until_parked();
19042
19043 cx.update_editor(|editor, window, cx| {
19044 editor.context_menu_next(&Default::default(), window, cx);
19045 });
19046 cx.run_until_parked();
19047
19048 cx.update_editor(|editor, _, _| {
19049 let context_menu = editor.context_menu.borrow_mut();
19050 let context_menu = context_menu
19051 .as_ref()
19052 .expect("Should have the context menu deployed");
19053 match context_menu {
19054 CodeContextMenu::Completions(completions_menu) => {
19055 let completions = completions_menu.completions.borrow_mut();
19056 assert_eq!(
19057 completions
19058 .iter()
19059 .map(|completion| &completion.label.text)
19060 .collect::<Vec<_>>(),
19061 vec!["method id() Now resolved!", "other"],
19062 "Should update first completion label, but not second as the filter text did not match."
19063 );
19064 }
19065 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
19066 }
19067 });
19068}
19069
19070#[gpui::test]
19071async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
19072 init_test(cx, |_| {});
19073 let mut cx = EditorLspTestContext::new_rust(
19074 lsp::ServerCapabilities {
19075 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
19076 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
19077 completion_provider: Some(lsp::CompletionOptions {
19078 resolve_provider: Some(true),
19079 ..Default::default()
19080 }),
19081 ..Default::default()
19082 },
19083 cx,
19084 )
19085 .await;
19086 cx.set_state(indoc! {"
19087 struct TestStruct {
19088 field: i32
19089 }
19090
19091 fn mainˇ() {
19092 let unused_var = 42;
19093 let test_struct = TestStruct { field: 42 };
19094 }
19095 "});
19096 let symbol_range = cx.lsp_range(indoc! {"
19097 struct TestStruct {
19098 field: i32
19099 }
19100
19101 «fn main»() {
19102 let unused_var = 42;
19103 let test_struct = TestStruct { field: 42 };
19104 }
19105 "});
19106 let mut hover_requests =
19107 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
19108 Ok(Some(lsp::Hover {
19109 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
19110 kind: lsp::MarkupKind::Markdown,
19111 value: "Function documentation".to_string(),
19112 }),
19113 range: Some(symbol_range),
19114 }))
19115 });
19116
19117 // Case 1: Test that code action menu hide hover popover
19118 cx.dispatch_action(Hover);
19119 hover_requests.next().await;
19120 cx.condition(|editor, _| editor.hover_state.visible()).await;
19121 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
19122 move |_, _, _| async move {
19123 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
19124 lsp::CodeAction {
19125 title: "Remove unused variable".to_string(),
19126 kind: Some(CodeActionKind::QUICKFIX),
19127 edit: Some(lsp::WorkspaceEdit {
19128 changes: Some(
19129 [(
19130 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
19131 vec![lsp::TextEdit {
19132 range: lsp::Range::new(
19133 lsp::Position::new(5, 4),
19134 lsp::Position::new(5, 27),
19135 ),
19136 new_text: "".to_string(),
19137 }],
19138 )]
19139 .into_iter()
19140 .collect(),
19141 ),
19142 ..Default::default()
19143 }),
19144 ..Default::default()
19145 },
19146 )]))
19147 },
19148 );
19149 cx.update_editor(|editor, window, cx| {
19150 editor.toggle_code_actions(
19151 &ToggleCodeActions {
19152 deployed_from: None,
19153 quick_launch: false,
19154 },
19155 window,
19156 cx,
19157 );
19158 });
19159 code_action_requests.next().await;
19160 cx.run_until_parked();
19161 cx.condition(|editor, _| editor.context_menu_visible())
19162 .await;
19163 cx.update_editor(|editor, _, _| {
19164 assert!(
19165 !editor.hover_state.visible(),
19166 "Hover popover should be hidden when code action menu is shown"
19167 );
19168 // Hide code actions
19169 editor.context_menu.take();
19170 });
19171
19172 // Case 2: Test that code completions hide hover popover
19173 cx.dispatch_action(Hover);
19174 hover_requests.next().await;
19175 cx.condition(|editor, _| editor.hover_state.visible()).await;
19176 let counter = Arc::new(AtomicUsize::new(0));
19177 let mut completion_requests =
19178 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19179 let counter = counter.clone();
19180 async move {
19181 counter.fetch_add(1, atomic::Ordering::Release);
19182 Ok(Some(lsp::CompletionResponse::Array(vec![
19183 lsp::CompletionItem {
19184 label: "main".into(),
19185 kind: Some(lsp::CompletionItemKind::FUNCTION),
19186 detail: Some("() -> ()".to_string()),
19187 ..Default::default()
19188 },
19189 lsp::CompletionItem {
19190 label: "TestStruct".into(),
19191 kind: Some(lsp::CompletionItemKind::STRUCT),
19192 detail: Some("struct TestStruct".to_string()),
19193 ..Default::default()
19194 },
19195 ])))
19196 }
19197 });
19198 cx.update_editor(|editor, window, cx| {
19199 editor.show_completions(&ShowCompletions, window, cx);
19200 });
19201 completion_requests.next().await;
19202 cx.condition(|editor, _| editor.context_menu_visible())
19203 .await;
19204 cx.update_editor(|editor, _, _| {
19205 assert!(
19206 !editor.hover_state.visible(),
19207 "Hover popover should be hidden when completion menu is shown"
19208 );
19209 });
19210}
19211
19212#[gpui::test]
19213async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
19214 init_test(cx, |_| {});
19215
19216 let mut cx = EditorLspTestContext::new_rust(
19217 lsp::ServerCapabilities {
19218 completion_provider: Some(lsp::CompletionOptions {
19219 trigger_characters: Some(vec![".".to_string()]),
19220 resolve_provider: Some(true),
19221 ..Default::default()
19222 }),
19223 ..Default::default()
19224 },
19225 cx,
19226 )
19227 .await;
19228
19229 cx.set_state("fn main() { let a = 2ˇ; }");
19230 cx.simulate_keystroke(".");
19231
19232 let unresolved_item_1 = lsp::CompletionItem {
19233 label: "id".to_string(),
19234 filter_text: Some("id".to_string()),
19235 detail: None,
19236 documentation: None,
19237 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19238 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19239 new_text: ".id".to_string(),
19240 })),
19241 ..lsp::CompletionItem::default()
19242 };
19243 let resolved_item_1 = lsp::CompletionItem {
19244 additional_text_edits: Some(vec![lsp::TextEdit {
19245 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
19246 new_text: "!!".to_string(),
19247 }]),
19248 ..unresolved_item_1.clone()
19249 };
19250 let unresolved_item_2 = lsp::CompletionItem {
19251 label: "other".to_string(),
19252 filter_text: Some("other".to_string()),
19253 detail: None,
19254 documentation: None,
19255 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19256 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19257 new_text: ".other".to_string(),
19258 })),
19259 ..lsp::CompletionItem::default()
19260 };
19261 let resolved_item_2 = lsp::CompletionItem {
19262 additional_text_edits: Some(vec![lsp::TextEdit {
19263 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
19264 new_text: "??".to_string(),
19265 }]),
19266 ..unresolved_item_2.clone()
19267 };
19268
19269 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
19270 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
19271 cx.lsp
19272 .server
19273 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19274 let unresolved_item_1 = unresolved_item_1.clone();
19275 let resolved_item_1 = resolved_item_1.clone();
19276 let unresolved_item_2 = unresolved_item_2.clone();
19277 let resolved_item_2 = resolved_item_2.clone();
19278 let resolve_requests_1 = resolve_requests_1.clone();
19279 let resolve_requests_2 = resolve_requests_2.clone();
19280 move |unresolved_request, _| {
19281 let unresolved_item_1 = unresolved_item_1.clone();
19282 let resolved_item_1 = resolved_item_1.clone();
19283 let unresolved_item_2 = unresolved_item_2.clone();
19284 let resolved_item_2 = resolved_item_2.clone();
19285 let resolve_requests_1 = resolve_requests_1.clone();
19286 let resolve_requests_2 = resolve_requests_2.clone();
19287 async move {
19288 if unresolved_request == unresolved_item_1 {
19289 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
19290 Ok(resolved_item_1.clone())
19291 } else if unresolved_request == unresolved_item_2 {
19292 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
19293 Ok(resolved_item_2.clone())
19294 } else {
19295 panic!("Unexpected completion item {unresolved_request:?}")
19296 }
19297 }
19298 }
19299 })
19300 .detach();
19301
19302 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19303 let unresolved_item_1 = unresolved_item_1.clone();
19304 let unresolved_item_2 = unresolved_item_2.clone();
19305 async move {
19306 Ok(Some(lsp::CompletionResponse::Array(vec![
19307 unresolved_item_1,
19308 unresolved_item_2,
19309 ])))
19310 }
19311 })
19312 .next()
19313 .await;
19314
19315 cx.condition(|editor, _| editor.context_menu_visible())
19316 .await;
19317 cx.update_editor(|editor, _, _| {
19318 let context_menu = editor.context_menu.borrow_mut();
19319 let context_menu = context_menu
19320 .as_ref()
19321 .expect("Should have the context menu deployed");
19322 match context_menu {
19323 CodeContextMenu::Completions(completions_menu) => {
19324 let completions = completions_menu.completions.borrow_mut();
19325 assert_eq!(
19326 completions
19327 .iter()
19328 .map(|completion| &completion.label.text)
19329 .collect::<Vec<_>>(),
19330 vec!["id", "other"]
19331 )
19332 }
19333 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
19334 }
19335 });
19336 cx.run_until_parked();
19337
19338 cx.update_editor(|editor, window, cx| {
19339 editor.context_menu_next(&ContextMenuNext, window, cx);
19340 });
19341 cx.run_until_parked();
19342 cx.update_editor(|editor, window, cx| {
19343 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19344 });
19345 cx.run_until_parked();
19346 cx.update_editor(|editor, window, cx| {
19347 editor.context_menu_next(&ContextMenuNext, window, cx);
19348 });
19349 cx.run_until_parked();
19350 cx.update_editor(|editor, window, cx| {
19351 editor
19352 .compose_completion(&ComposeCompletion::default(), window, cx)
19353 .expect("No task returned")
19354 })
19355 .await
19356 .expect("Completion failed");
19357 cx.run_until_parked();
19358
19359 cx.update_editor(|editor, _, cx| {
19360 assert_eq!(
19361 resolve_requests_1.load(atomic::Ordering::Acquire),
19362 1,
19363 "Should always resolve once despite multiple selections"
19364 );
19365 assert_eq!(
19366 resolve_requests_2.load(atomic::Ordering::Acquire),
19367 1,
19368 "Should always resolve once after multiple selections and applying the completion"
19369 );
19370 assert_eq!(
19371 editor.text(cx),
19372 "fn main() { let a = ??.other; }",
19373 "Should use resolved data when applying the completion"
19374 );
19375 });
19376}
19377
19378#[gpui::test]
19379async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
19380 init_test(cx, |_| {});
19381
19382 let item_0 = lsp::CompletionItem {
19383 label: "abs".into(),
19384 insert_text: Some("abs".into()),
19385 data: Some(json!({ "very": "special"})),
19386 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
19387 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19388 lsp::InsertReplaceEdit {
19389 new_text: "abs".to_string(),
19390 insert: lsp::Range::default(),
19391 replace: lsp::Range::default(),
19392 },
19393 )),
19394 ..lsp::CompletionItem::default()
19395 };
19396 let items = iter::once(item_0.clone())
19397 .chain((11..51).map(|i| lsp::CompletionItem {
19398 label: format!("item_{}", i),
19399 insert_text: Some(format!("item_{}", i)),
19400 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
19401 ..lsp::CompletionItem::default()
19402 }))
19403 .collect::<Vec<_>>();
19404
19405 let default_commit_characters = vec!["?".to_string()];
19406 let default_data = json!({ "default": "data"});
19407 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
19408 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
19409 let default_edit_range = lsp::Range {
19410 start: lsp::Position {
19411 line: 0,
19412 character: 5,
19413 },
19414 end: lsp::Position {
19415 line: 0,
19416 character: 5,
19417 },
19418 };
19419
19420 let mut cx = EditorLspTestContext::new_rust(
19421 lsp::ServerCapabilities {
19422 completion_provider: Some(lsp::CompletionOptions {
19423 trigger_characters: Some(vec![".".to_string()]),
19424 resolve_provider: Some(true),
19425 ..Default::default()
19426 }),
19427 ..Default::default()
19428 },
19429 cx,
19430 )
19431 .await;
19432
19433 cx.set_state("fn main() { let a = 2ˇ; }");
19434 cx.simulate_keystroke(".");
19435
19436 let completion_data = default_data.clone();
19437 let completion_characters = default_commit_characters.clone();
19438 let completion_items = items.clone();
19439 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19440 let default_data = completion_data.clone();
19441 let default_commit_characters = completion_characters.clone();
19442 let items = completion_items.clone();
19443 async move {
19444 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
19445 items,
19446 item_defaults: Some(lsp::CompletionListItemDefaults {
19447 data: Some(default_data.clone()),
19448 commit_characters: Some(default_commit_characters.clone()),
19449 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
19450 default_edit_range,
19451 )),
19452 insert_text_format: Some(default_insert_text_format),
19453 insert_text_mode: Some(default_insert_text_mode),
19454 }),
19455 ..lsp::CompletionList::default()
19456 })))
19457 }
19458 })
19459 .next()
19460 .await;
19461
19462 let resolved_items = Arc::new(Mutex::new(Vec::new()));
19463 cx.lsp
19464 .server
19465 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19466 let closure_resolved_items = resolved_items.clone();
19467 move |item_to_resolve, _| {
19468 let closure_resolved_items = closure_resolved_items.clone();
19469 async move {
19470 closure_resolved_items.lock().push(item_to_resolve.clone());
19471 Ok(item_to_resolve)
19472 }
19473 }
19474 })
19475 .detach();
19476
19477 cx.condition(|editor, _| editor.context_menu_visible())
19478 .await;
19479 cx.run_until_parked();
19480 cx.update_editor(|editor, _, _| {
19481 let menu = editor.context_menu.borrow_mut();
19482 match menu.as_ref().expect("should have the completions menu") {
19483 CodeContextMenu::Completions(completions_menu) => {
19484 assert_eq!(
19485 completions_menu
19486 .entries
19487 .borrow()
19488 .iter()
19489 .map(|mat| mat.string.clone())
19490 .collect::<Vec<String>>(),
19491 items
19492 .iter()
19493 .map(|completion| completion.label.clone())
19494 .collect::<Vec<String>>()
19495 );
19496 }
19497 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
19498 }
19499 });
19500 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
19501 // with 4 from the end.
19502 assert_eq!(
19503 *resolved_items.lock(),
19504 [&items[0..16], &items[items.len() - 4..items.len()]]
19505 .concat()
19506 .iter()
19507 .cloned()
19508 .map(|mut item| {
19509 if item.data.is_none() {
19510 item.data = Some(default_data.clone());
19511 }
19512 item
19513 })
19514 .collect::<Vec<lsp::CompletionItem>>(),
19515 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
19516 );
19517 resolved_items.lock().clear();
19518
19519 cx.update_editor(|editor, window, cx| {
19520 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19521 });
19522 cx.run_until_parked();
19523 // Completions that have already been resolved are skipped.
19524 assert_eq!(
19525 *resolved_items.lock(),
19526 items[items.len() - 17..items.len() - 4]
19527 .iter()
19528 .cloned()
19529 .map(|mut item| {
19530 if item.data.is_none() {
19531 item.data = Some(default_data.clone());
19532 }
19533 item
19534 })
19535 .collect::<Vec<lsp::CompletionItem>>()
19536 );
19537 resolved_items.lock().clear();
19538}
19539
19540#[gpui::test]
19541async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
19542 init_test(cx, |_| {});
19543
19544 let mut cx = EditorLspTestContext::new(
19545 Language::new(
19546 LanguageConfig {
19547 matcher: LanguageMatcher {
19548 path_suffixes: vec!["jsx".into()],
19549 ..Default::default()
19550 },
19551 overrides: [(
19552 "element".into(),
19553 LanguageConfigOverride {
19554 completion_query_characters: Override::Set(['-'].into_iter().collect()),
19555 ..Default::default()
19556 },
19557 )]
19558 .into_iter()
19559 .collect(),
19560 ..Default::default()
19561 },
19562 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
19563 )
19564 .with_override_query("(jsx_self_closing_element) @element")
19565 .unwrap(),
19566 lsp::ServerCapabilities {
19567 completion_provider: Some(lsp::CompletionOptions {
19568 trigger_characters: Some(vec![":".to_string()]),
19569 ..Default::default()
19570 }),
19571 ..Default::default()
19572 },
19573 cx,
19574 )
19575 .await;
19576
19577 cx.lsp
19578 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19579 Ok(Some(lsp::CompletionResponse::Array(vec![
19580 lsp::CompletionItem {
19581 label: "bg-blue".into(),
19582 ..Default::default()
19583 },
19584 lsp::CompletionItem {
19585 label: "bg-red".into(),
19586 ..Default::default()
19587 },
19588 lsp::CompletionItem {
19589 label: "bg-yellow".into(),
19590 ..Default::default()
19591 },
19592 ])))
19593 });
19594
19595 cx.set_state(r#"<p class="bgˇ" />"#);
19596
19597 // Trigger completion when typing a dash, because the dash is an extra
19598 // word character in the 'element' scope, which contains the cursor.
19599 cx.simulate_keystroke("-");
19600 cx.executor().run_until_parked();
19601 cx.update_editor(|editor, _, _| {
19602 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19603 {
19604 assert_eq!(
19605 completion_menu_entries(menu),
19606 &["bg-blue", "bg-red", "bg-yellow"]
19607 );
19608 } else {
19609 panic!("expected completion menu to be open");
19610 }
19611 });
19612
19613 cx.simulate_keystroke("l");
19614 cx.executor().run_until_parked();
19615 cx.update_editor(|editor, _, _| {
19616 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19617 {
19618 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
19619 } else {
19620 panic!("expected completion menu to be open");
19621 }
19622 });
19623
19624 // When filtering completions, consider the character after the '-' to
19625 // be the start of a subword.
19626 cx.set_state(r#"<p class="yelˇ" />"#);
19627 cx.simulate_keystroke("l");
19628 cx.executor().run_until_parked();
19629 cx.update_editor(|editor, _, _| {
19630 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19631 {
19632 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
19633 } else {
19634 panic!("expected completion menu to be open");
19635 }
19636 });
19637}
19638
19639fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
19640 let entries = menu.entries.borrow();
19641 entries.iter().map(|mat| mat.string.clone()).collect()
19642}
19643
19644#[gpui::test]
19645async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
19646 init_test(cx, |settings| {
19647 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19648 });
19649
19650 let fs = FakeFs::new(cx.executor());
19651 fs.insert_file(path!("/file.ts"), Default::default()).await;
19652
19653 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
19654 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19655
19656 language_registry.add(Arc::new(Language::new(
19657 LanguageConfig {
19658 name: "TypeScript".into(),
19659 matcher: LanguageMatcher {
19660 path_suffixes: vec!["ts".to_string()],
19661 ..Default::default()
19662 },
19663 ..Default::default()
19664 },
19665 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19666 )));
19667 update_test_language_settings(cx, |settings| {
19668 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19669 });
19670
19671 let test_plugin = "test_plugin";
19672 let _ = language_registry.register_fake_lsp(
19673 "TypeScript",
19674 FakeLspAdapter {
19675 prettier_plugins: vec![test_plugin],
19676 ..Default::default()
19677 },
19678 );
19679
19680 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19681 let buffer = project
19682 .update(cx, |project, cx| {
19683 project.open_local_buffer(path!("/file.ts"), cx)
19684 })
19685 .await
19686 .unwrap();
19687
19688 let buffer_text = "one\ntwo\nthree\n";
19689 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19690 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19691 editor.update_in(cx, |editor, window, cx| {
19692 editor.set_text(buffer_text, window, cx)
19693 });
19694
19695 editor
19696 .update_in(cx, |editor, window, cx| {
19697 editor.perform_format(
19698 project.clone(),
19699 FormatTrigger::Manual,
19700 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19701 window,
19702 cx,
19703 )
19704 })
19705 .unwrap()
19706 .await;
19707 assert_eq!(
19708 editor.update(cx, |editor, cx| editor.text(cx)),
19709 buffer_text.to_string() + prettier_format_suffix,
19710 "Test prettier formatting was not applied to the original buffer text",
19711 );
19712
19713 update_test_language_settings(cx, |settings| {
19714 settings.defaults.formatter = Some(FormatterList::default())
19715 });
19716 let format = editor.update_in(cx, |editor, window, cx| {
19717 editor.perform_format(
19718 project.clone(),
19719 FormatTrigger::Manual,
19720 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19721 window,
19722 cx,
19723 )
19724 });
19725 format.await.unwrap();
19726 assert_eq!(
19727 editor.update(cx, |editor, cx| editor.text(cx)),
19728 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
19729 "Autoformatting (via test prettier) was not applied to the original buffer text",
19730 );
19731}
19732
19733#[gpui::test]
19734async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
19735 init_test(cx, |settings| {
19736 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19737 });
19738
19739 let fs = FakeFs::new(cx.executor());
19740 fs.insert_file(path!("/file.settings"), Default::default())
19741 .await;
19742
19743 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
19744 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19745
19746 let ts_lang = Arc::new(Language::new(
19747 LanguageConfig {
19748 name: "TypeScript".into(),
19749 matcher: LanguageMatcher {
19750 path_suffixes: vec!["ts".to_string()],
19751 ..LanguageMatcher::default()
19752 },
19753 prettier_parser_name: Some("typescript".to_string()),
19754 ..LanguageConfig::default()
19755 },
19756 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19757 ));
19758
19759 language_registry.add(ts_lang.clone());
19760
19761 update_test_language_settings(cx, |settings| {
19762 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19763 });
19764
19765 let test_plugin = "test_plugin";
19766 let _ = language_registry.register_fake_lsp(
19767 "TypeScript",
19768 FakeLspAdapter {
19769 prettier_plugins: vec![test_plugin],
19770 ..Default::default()
19771 },
19772 );
19773
19774 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19775 let buffer = project
19776 .update(cx, |project, cx| {
19777 project.open_local_buffer(path!("/file.settings"), cx)
19778 })
19779 .await
19780 .unwrap();
19781
19782 project.update(cx, |project, cx| {
19783 project.set_language_for_buffer(&buffer, ts_lang, cx)
19784 });
19785
19786 let buffer_text = "one\ntwo\nthree\n";
19787 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19788 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19789 editor.update_in(cx, |editor, window, cx| {
19790 editor.set_text(buffer_text, window, cx)
19791 });
19792
19793 editor
19794 .update_in(cx, |editor, window, cx| {
19795 editor.perform_format(
19796 project.clone(),
19797 FormatTrigger::Manual,
19798 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19799 window,
19800 cx,
19801 )
19802 })
19803 .unwrap()
19804 .await;
19805 assert_eq!(
19806 editor.update(cx, |editor, cx| editor.text(cx)),
19807 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
19808 "Test prettier formatting was not applied to the original buffer text",
19809 );
19810
19811 update_test_language_settings(cx, |settings| {
19812 settings.defaults.formatter = Some(FormatterList::default())
19813 });
19814 let format = editor.update_in(cx, |editor, window, cx| {
19815 editor.perform_format(
19816 project.clone(),
19817 FormatTrigger::Manual,
19818 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19819 window,
19820 cx,
19821 )
19822 });
19823 format.await.unwrap();
19824
19825 assert_eq!(
19826 editor.update(cx, |editor, cx| editor.text(cx)),
19827 buffer_text.to_string()
19828 + prettier_format_suffix
19829 + "\ntypescript\n"
19830 + prettier_format_suffix
19831 + "\ntypescript",
19832 "Autoformatting (via test prettier) was not applied to the original buffer text",
19833 );
19834}
19835
19836#[gpui::test]
19837async fn test_addition_reverts(cx: &mut TestAppContext) {
19838 init_test(cx, |_| {});
19839 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19840 let base_text = indoc! {r#"
19841 struct Row;
19842 struct Row1;
19843 struct Row2;
19844
19845 struct Row4;
19846 struct Row5;
19847 struct Row6;
19848
19849 struct Row8;
19850 struct Row9;
19851 struct Row10;"#};
19852
19853 // When addition hunks are not adjacent to carets, no hunk revert is performed
19854 assert_hunk_revert(
19855 indoc! {r#"struct Row;
19856 struct Row1;
19857 struct Row1.1;
19858 struct Row1.2;
19859 struct Row2;ˇ
19860
19861 struct Row4;
19862 struct Row5;
19863 struct Row6;
19864
19865 struct Row8;
19866 ˇstruct Row9;
19867 struct Row9.1;
19868 struct Row9.2;
19869 struct Row9.3;
19870 struct Row10;"#},
19871 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
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 base_text,
19889 &mut cx,
19890 );
19891 // Same for selections
19892 assert_hunk_revert(
19893 indoc! {r#"struct Row;
19894 struct Row1;
19895 struct Row2;
19896 struct Row2.1;
19897 struct Row2.2;
19898 «ˇ
19899 struct Row4;
19900 struct» Row5;
19901 «struct Row6;
19902 ˇ»
19903 struct Row9.1;
19904 struct Row9.2;
19905 struct Row9.3;
19906 struct Row8;
19907 struct Row9;
19908 struct Row10;"#},
19909 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
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 base_text,
19927 &mut cx,
19928 );
19929
19930 // When carets and selections intersect the addition hunks, those are reverted.
19931 // Adjacent carets got merged.
19932 assert_hunk_revert(
19933 indoc! {r#"struct Row;
19934 ˇ// something on the top
19935 struct Row1;
19936 struct Row2;
19937 struct Roˇw3.1;
19938 struct Row2.2;
19939 struct Row2.3;ˇ
19940
19941 struct Row4;
19942 struct ˇRow5.1;
19943 struct Row5.2;
19944 struct «Rowˇ»5.3;
19945 struct Row5;
19946 struct Row6;
19947 ˇ
19948 struct Row9.1;
19949 struct «Rowˇ»9.2;
19950 struct «ˇRow»9.3;
19951 struct Row8;
19952 struct Row9;
19953 «ˇ// something on bottom»
19954 struct Row10;"#},
19955 vec![
19956 DiffHunkStatusKind::Added,
19957 DiffHunkStatusKind::Added,
19958 DiffHunkStatusKind::Added,
19959 DiffHunkStatusKind::Added,
19960 DiffHunkStatusKind::Added,
19961 ],
19962 indoc! {r#"struct Row;
19963 ˇstruct Row1;
19964 struct Row2;
19965 ˇ
19966 struct Row4;
19967 ˇstruct Row5;
19968 struct Row6;
19969 ˇ
19970 ˇstruct Row8;
19971 struct Row9;
19972 ˇstruct Row10;"#},
19973 base_text,
19974 &mut cx,
19975 );
19976}
19977
19978#[gpui::test]
19979async fn test_modification_reverts(cx: &mut TestAppContext) {
19980 init_test(cx, |_| {});
19981 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19982 let base_text = indoc! {r#"
19983 struct Row;
19984 struct Row1;
19985 struct Row2;
19986
19987 struct Row4;
19988 struct Row5;
19989 struct Row6;
19990
19991 struct Row8;
19992 struct Row9;
19993 struct Row10;"#};
19994
19995 // Modification hunks behave the same as the addition ones.
19996 assert_hunk_revert(
19997 indoc! {r#"struct Row;
19998 struct Row1;
19999 struct Row33;
20000 ˇ
20001 struct Row4;
20002 struct Row5;
20003 struct Row6;
20004 ˇ
20005 struct Row99;
20006 struct Row9;
20007 struct Row10;"#},
20008 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
20009 indoc! {r#"struct Row;
20010 struct Row1;
20011 struct Row33;
20012 ˇ
20013 struct Row4;
20014 struct Row5;
20015 struct Row6;
20016 ˇ
20017 struct Row99;
20018 struct Row9;
20019 struct Row10;"#},
20020 base_text,
20021 &mut cx,
20022 );
20023 assert_hunk_revert(
20024 indoc! {r#"struct Row;
20025 struct Row1;
20026 struct Row33;
20027 «ˇ
20028 struct Row4;
20029 struct» Row5;
20030 «struct Row6;
20031 ˇ»
20032 struct Row99;
20033 struct Row9;
20034 struct Row10;"#},
20035 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
20036 indoc! {r#"struct Row;
20037 struct Row1;
20038 struct Row33;
20039 «ˇ
20040 struct Row4;
20041 struct» Row5;
20042 «struct Row6;
20043 ˇ»
20044 struct Row99;
20045 struct Row9;
20046 struct Row10;"#},
20047 base_text,
20048 &mut cx,
20049 );
20050
20051 assert_hunk_revert(
20052 indoc! {r#"ˇstruct Row1.1;
20053 struct Row1;
20054 «ˇstr»uct Row22;
20055
20056 struct ˇRow44;
20057 struct Row5;
20058 struct «Rˇ»ow66;ˇ
20059
20060 «struˇ»ct Row88;
20061 struct Row9;
20062 struct Row1011;ˇ"#},
20063 vec![
20064 DiffHunkStatusKind::Modified,
20065 DiffHunkStatusKind::Modified,
20066 DiffHunkStatusKind::Modified,
20067 DiffHunkStatusKind::Modified,
20068 DiffHunkStatusKind::Modified,
20069 DiffHunkStatusKind::Modified,
20070 ],
20071 indoc! {r#"struct Row;
20072 ˇstruct Row1;
20073 struct Row2;
20074 ˇ
20075 struct Row4;
20076 ˇstruct Row5;
20077 struct Row6;
20078 ˇ
20079 struct Row8;
20080 ˇstruct Row9;
20081 struct Row10;ˇ"#},
20082 base_text,
20083 &mut cx,
20084 );
20085}
20086
20087#[gpui::test]
20088async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
20089 init_test(cx, |_| {});
20090 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
20091 let base_text = indoc! {r#"
20092 one
20093
20094 two
20095 three
20096 "#};
20097
20098 cx.set_head_text(base_text);
20099 cx.set_state("\nˇ\n");
20100 cx.executor().run_until_parked();
20101 cx.update_editor(|editor, _window, cx| {
20102 editor.expand_selected_diff_hunks(cx);
20103 });
20104 cx.executor().run_until_parked();
20105 cx.update_editor(|editor, window, cx| {
20106 editor.backspace(&Default::default(), window, cx);
20107 });
20108 cx.run_until_parked();
20109 cx.assert_state_with_diff(
20110 indoc! {r#"
20111
20112 - two
20113 - threeˇ
20114 +
20115 "#}
20116 .to_string(),
20117 );
20118}
20119
20120#[gpui::test]
20121async fn test_deletion_reverts(cx: &mut TestAppContext) {
20122 init_test(cx, |_| {});
20123 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
20124 let base_text = indoc! {r#"struct Row;
20125struct Row1;
20126struct Row2;
20127
20128struct Row4;
20129struct Row5;
20130struct Row6;
20131
20132struct Row8;
20133struct Row9;
20134struct Row10;"#};
20135
20136 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
20137 assert_hunk_revert(
20138 indoc! {r#"struct Row;
20139 struct Row2;
20140
20141 ˇstruct Row4;
20142 struct Row5;
20143 struct Row6;
20144 ˇ
20145 struct Row8;
20146 struct Row10;"#},
20147 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
20148 indoc! {r#"struct Row;
20149 struct Row2;
20150
20151 ˇstruct Row4;
20152 struct Row5;
20153 struct Row6;
20154 ˇ
20155 struct Row8;
20156 struct Row10;"#},
20157 base_text,
20158 &mut cx,
20159 );
20160 assert_hunk_revert(
20161 indoc! {r#"struct Row;
20162 struct Row2;
20163
20164 «ˇstruct Row4;
20165 struct» Row5;
20166 «struct Row6;
20167 ˇ»
20168 struct Row8;
20169 struct Row10;"#},
20170 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
20171 indoc! {r#"struct Row;
20172 struct Row2;
20173
20174 «ˇstruct Row4;
20175 struct» Row5;
20176 «struct Row6;
20177 ˇ»
20178 struct Row8;
20179 struct Row10;"#},
20180 base_text,
20181 &mut cx,
20182 );
20183
20184 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
20185 assert_hunk_revert(
20186 indoc! {r#"struct Row;
20187 ˇstruct Row2;
20188
20189 struct Row4;
20190 struct Row5;
20191 struct Row6;
20192
20193 struct Row8;ˇ
20194 struct Row10;"#},
20195 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
20196 indoc! {r#"struct Row;
20197 struct Row1;
20198 ˇstruct Row2;
20199
20200 struct Row4;
20201 struct Row5;
20202 struct Row6;
20203
20204 struct Row8;ˇ
20205 struct Row9;
20206 struct Row10;"#},
20207 base_text,
20208 &mut cx,
20209 );
20210 assert_hunk_revert(
20211 indoc! {r#"struct Row;
20212 struct Row2«ˇ;
20213 struct Row4;
20214 struct» Row5;
20215 «struct Row6;
20216
20217 struct Row8;ˇ»
20218 struct Row10;"#},
20219 vec![
20220 DiffHunkStatusKind::Deleted,
20221 DiffHunkStatusKind::Deleted,
20222 DiffHunkStatusKind::Deleted,
20223 ],
20224 indoc! {r#"struct Row;
20225 struct Row1;
20226 struct Row2«ˇ;
20227
20228 struct Row4;
20229 struct» Row5;
20230 «struct Row6;
20231
20232 struct Row8;ˇ»
20233 struct Row9;
20234 struct Row10;"#},
20235 base_text,
20236 &mut cx,
20237 );
20238}
20239
20240#[gpui::test]
20241async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
20242 init_test(cx, |_| {});
20243
20244 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
20245 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
20246 let base_text_3 =
20247 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
20248
20249 let text_1 = edit_first_char_of_every_line(base_text_1);
20250 let text_2 = edit_first_char_of_every_line(base_text_2);
20251 let text_3 = edit_first_char_of_every_line(base_text_3);
20252
20253 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
20254 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
20255 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
20256
20257 let multibuffer = cx.new(|cx| {
20258 let mut multibuffer = MultiBuffer::new(ReadWrite);
20259 multibuffer.push_excerpts(
20260 buffer_1.clone(),
20261 [
20262 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20263 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20264 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20265 ],
20266 cx,
20267 );
20268 multibuffer.push_excerpts(
20269 buffer_2.clone(),
20270 [
20271 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20272 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20273 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20274 ],
20275 cx,
20276 );
20277 multibuffer.push_excerpts(
20278 buffer_3.clone(),
20279 [
20280 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20281 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20282 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20283 ],
20284 cx,
20285 );
20286 multibuffer
20287 });
20288
20289 let fs = FakeFs::new(cx.executor());
20290 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20291 let (editor, cx) = cx
20292 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
20293 editor.update_in(cx, |editor, _window, cx| {
20294 for (buffer, diff_base) in [
20295 (buffer_1.clone(), base_text_1),
20296 (buffer_2.clone(), base_text_2),
20297 (buffer_3.clone(), base_text_3),
20298 ] {
20299 let diff = cx.new(|cx| {
20300 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
20301 });
20302 editor
20303 .buffer
20304 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20305 }
20306 });
20307 cx.executor().run_until_parked();
20308
20309 editor.update_in(cx, |editor, window, cx| {
20310 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}");
20311 editor.select_all(&SelectAll, window, cx);
20312 editor.git_restore(&Default::default(), window, cx);
20313 });
20314 cx.executor().run_until_parked();
20315
20316 // When all ranges are selected, all buffer hunks are reverted.
20317 editor.update(cx, |editor, cx| {
20318 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");
20319 });
20320 buffer_1.update(cx, |buffer, _| {
20321 assert_eq!(buffer.text(), base_text_1);
20322 });
20323 buffer_2.update(cx, |buffer, _| {
20324 assert_eq!(buffer.text(), base_text_2);
20325 });
20326 buffer_3.update(cx, |buffer, _| {
20327 assert_eq!(buffer.text(), base_text_3);
20328 });
20329
20330 editor.update_in(cx, |editor, window, cx| {
20331 editor.undo(&Default::default(), window, cx);
20332 });
20333
20334 editor.update_in(cx, |editor, window, cx| {
20335 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20336 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
20337 });
20338 editor.git_restore(&Default::default(), window, cx);
20339 });
20340
20341 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
20342 // but not affect buffer_2 and its related excerpts.
20343 editor.update(cx, |editor, cx| {
20344 assert_eq!(
20345 editor.text(cx),
20346 "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}"
20347 );
20348 });
20349 buffer_1.update(cx, |buffer, _| {
20350 assert_eq!(buffer.text(), base_text_1);
20351 });
20352 buffer_2.update(cx, |buffer, _| {
20353 assert_eq!(
20354 buffer.text(),
20355 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
20356 );
20357 });
20358 buffer_3.update(cx, |buffer, _| {
20359 assert_eq!(
20360 buffer.text(),
20361 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
20362 );
20363 });
20364
20365 fn edit_first_char_of_every_line(text: &str) -> String {
20366 text.split('\n')
20367 .map(|line| format!("X{}", &line[1..]))
20368 .collect::<Vec<_>>()
20369 .join("\n")
20370 }
20371}
20372
20373#[gpui::test]
20374async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
20375 init_test(cx, |_| {});
20376
20377 let cols = 4;
20378 let rows = 10;
20379 let sample_text_1 = sample_text(rows, cols, 'a');
20380 assert_eq!(
20381 sample_text_1,
20382 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
20383 );
20384 let sample_text_2 = sample_text(rows, cols, 'l');
20385 assert_eq!(
20386 sample_text_2,
20387 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
20388 );
20389 let sample_text_3 = sample_text(rows, cols, 'v');
20390 assert_eq!(
20391 sample_text_3,
20392 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
20393 );
20394
20395 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
20396 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
20397 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
20398
20399 let multi_buffer = cx.new(|cx| {
20400 let mut multibuffer = MultiBuffer::new(ReadWrite);
20401 multibuffer.push_excerpts(
20402 buffer_1.clone(),
20403 [
20404 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20405 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20406 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20407 ],
20408 cx,
20409 );
20410 multibuffer.push_excerpts(
20411 buffer_2.clone(),
20412 [
20413 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20414 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20415 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20416 ],
20417 cx,
20418 );
20419 multibuffer.push_excerpts(
20420 buffer_3.clone(),
20421 [
20422 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20423 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20424 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20425 ],
20426 cx,
20427 );
20428 multibuffer
20429 });
20430
20431 let fs = FakeFs::new(cx.executor());
20432 fs.insert_tree(
20433 "/a",
20434 json!({
20435 "main.rs": sample_text_1,
20436 "other.rs": sample_text_2,
20437 "lib.rs": sample_text_3,
20438 }),
20439 )
20440 .await;
20441 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20442 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20443 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20444 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20445 Editor::new(
20446 EditorMode::full(),
20447 multi_buffer,
20448 Some(project.clone()),
20449 window,
20450 cx,
20451 )
20452 });
20453 let multibuffer_item_id = workspace
20454 .update(cx, |workspace, window, cx| {
20455 assert!(
20456 workspace.active_item(cx).is_none(),
20457 "active item should be None before the first item is added"
20458 );
20459 workspace.add_item_to_active_pane(
20460 Box::new(multi_buffer_editor.clone()),
20461 None,
20462 true,
20463 window,
20464 cx,
20465 );
20466 let active_item = workspace
20467 .active_item(cx)
20468 .expect("should have an active item after adding the multi buffer");
20469 assert_eq!(
20470 active_item.buffer_kind(cx),
20471 ItemBufferKind::Multibuffer,
20472 "A multi buffer was expected to active after adding"
20473 );
20474 active_item.item_id()
20475 })
20476 .unwrap();
20477 cx.executor().run_until_parked();
20478
20479 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20480 editor.change_selections(
20481 SelectionEffects::scroll(Autoscroll::Next),
20482 window,
20483 cx,
20484 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
20485 );
20486 editor.open_excerpts(&OpenExcerpts, window, cx);
20487 });
20488 cx.executor().run_until_parked();
20489 let first_item_id = workspace
20490 .update(cx, |workspace, window, cx| {
20491 let active_item = workspace
20492 .active_item(cx)
20493 .expect("should have an active item after navigating into the 1st buffer");
20494 let first_item_id = active_item.item_id();
20495 assert_ne!(
20496 first_item_id, multibuffer_item_id,
20497 "Should navigate into the 1st buffer and activate it"
20498 );
20499 assert_eq!(
20500 active_item.buffer_kind(cx),
20501 ItemBufferKind::Singleton,
20502 "New active item should be a singleton buffer"
20503 );
20504 assert_eq!(
20505 active_item
20506 .act_as::<Editor>(cx)
20507 .expect("should have navigated into an editor for the 1st buffer")
20508 .read(cx)
20509 .text(cx),
20510 sample_text_1
20511 );
20512
20513 workspace
20514 .go_back(workspace.active_pane().downgrade(), window, cx)
20515 .detach_and_log_err(cx);
20516
20517 first_item_id
20518 })
20519 .unwrap();
20520 cx.executor().run_until_parked();
20521 workspace
20522 .update(cx, |workspace, _, cx| {
20523 let active_item = workspace
20524 .active_item(cx)
20525 .expect("should have an active item after navigating back");
20526 assert_eq!(
20527 active_item.item_id(),
20528 multibuffer_item_id,
20529 "Should navigate back to the multi buffer"
20530 );
20531 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20532 })
20533 .unwrap();
20534
20535 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20536 editor.change_selections(
20537 SelectionEffects::scroll(Autoscroll::Next),
20538 window,
20539 cx,
20540 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
20541 );
20542 editor.open_excerpts(&OpenExcerpts, window, cx);
20543 });
20544 cx.executor().run_until_parked();
20545 let second_item_id = workspace
20546 .update(cx, |workspace, window, cx| {
20547 let active_item = workspace
20548 .active_item(cx)
20549 .expect("should have an active item after navigating into the 2nd buffer");
20550 let second_item_id = active_item.item_id();
20551 assert_ne!(
20552 second_item_id, multibuffer_item_id,
20553 "Should navigate away from the multibuffer"
20554 );
20555 assert_ne!(
20556 second_item_id, first_item_id,
20557 "Should navigate into the 2nd buffer and activate it"
20558 );
20559 assert_eq!(
20560 active_item.buffer_kind(cx),
20561 ItemBufferKind::Singleton,
20562 "New active item should be a singleton buffer"
20563 );
20564 assert_eq!(
20565 active_item
20566 .act_as::<Editor>(cx)
20567 .expect("should have navigated into an editor")
20568 .read(cx)
20569 .text(cx),
20570 sample_text_2
20571 );
20572
20573 workspace
20574 .go_back(workspace.active_pane().downgrade(), window, cx)
20575 .detach_and_log_err(cx);
20576
20577 second_item_id
20578 })
20579 .unwrap();
20580 cx.executor().run_until_parked();
20581 workspace
20582 .update(cx, |workspace, _, cx| {
20583 let active_item = workspace
20584 .active_item(cx)
20585 .expect("should have an active item after navigating back from the 2nd buffer");
20586 assert_eq!(
20587 active_item.item_id(),
20588 multibuffer_item_id,
20589 "Should navigate back from the 2nd buffer to the multi buffer"
20590 );
20591 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20592 })
20593 .unwrap();
20594
20595 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20596 editor.change_selections(
20597 SelectionEffects::scroll(Autoscroll::Next),
20598 window,
20599 cx,
20600 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
20601 );
20602 editor.open_excerpts(&OpenExcerpts, window, cx);
20603 });
20604 cx.executor().run_until_parked();
20605 workspace
20606 .update(cx, |workspace, window, cx| {
20607 let active_item = workspace
20608 .active_item(cx)
20609 .expect("should have an active item after navigating into the 3rd buffer");
20610 let third_item_id = active_item.item_id();
20611 assert_ne!(
20612 third_item_id, multibuffer_item_id,
20613 "Should navigate into the 3rd buffer and activate it"
20614 );
20615 assert_ne!(third_item_id, first_item_id);
20616 assert_ne!(third_item_id, second_item_id);
20617 assert_eq!(
20618 active_item.buffer_kind(cx),
20619 ItemBufferKind::Singleton,
20620 "New active item should be a singleton buffer"
20621 );
20622 assert_eq!(
20623 active_item
20624 .act_as::<Editor>(cx)
20625 .expect("should have navigated into an editor")
20626 .read(cx)
20627 .text(cx),
20628 sample_text_3
20629 );
20630
20631 workspace
20632 .go_back(workspace.active_pane().downgrade(), window, cx)
20633 .detach_and_log_err(cx);
20634 })
20635 .unwrap();
20636 cx.executor().run_until_parked();
20637 workspace
20638 .update(cx, |workspace, _, cx| {
20639 let active_item = workspace
20640 .active_item(cx)
20641 .expect("should have an active item after navigating back from the 3rd buffer");
20642 assert_eq!(
20643 active_item.item_id(),
20644 multibuffer_item_id,
20645 "Should navigate back from the 3rd buffer to the multi buffer"
20646 );
20647 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20648 })
20649 .unwrap();
20650}
20651
20652#[gpui::test]
20653async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20654 init_test(cx, |_| {});
20655
20656 let mut cx = EditorTestContext::new(cx).await;
20657
20658 let diff_base = r#"
20659 use some::mod;
20660
20661 const A: u32 = 42;
20662
20663 fn main() {
20664 println!("hello");
20665
20666 println!("world");
20667 }
20668 "#
20669 .unindent();
20670
20671 cx.set_state(
20672 &r#"
20673 use some::modified;
20674
20675 ˇ
20676 fn main() {
20677 println!("hello there");
20678
20679 println!("around the");
20680 println!("world");
20681 }
20682 "#
20683 .unindent(),
20684 );
20685
20686 cx.set_head_text(&diff_base);
20687 executor.run_until_parked();
20688
20689 cx.update_editor(|editor, window, cx| {
20690 editor.go_to_next_hunk(&GoToHunk, window, cx);
20691 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20692 });
20693 executor.run_until_parked();
20694 cx.assert_state_with_diff(
20695 r#"
20696 use some::modified;
20697
20698
20699 fn main() {
20700 - println!("hello");
20701 + ˇ println!("hello there");
20702
20703 println!("around the");
20704 println!("world");
20705 }
20706 "#
20707 .unindent(),
20708 );
20709
20710 cx.update_editor(|editor, window, cx| {
20711 for _ in 0..2 {
20712 editor.go_to_next_hunk(&GoToHunk, window, cx);
20713 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20714 }
20715 });
20716 executor.run_until_parked();
20717 cx.assert_state_with_diff(
20718 r#"
20719 - use some::mod;
20720 + ˇuse some::modified;
20721
20722
20723 fn main() {
20724 - println!("hello");
20725 + println!("hello there");
20726
20727 + println!("around the");
20728 println!("world");
20729 }
20730 "#
20731 .unindent(),
20732 );
20733
20734 cx.update_editor(|editor, window, cx| {
20735 editor.go_to_next_hunk(&GoToHunk, window, cx);
20736 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20737 });
20738 executor.run_until_parked();
20739 cx.assert_state_with_diff(
20740 r#"
20741 - use some::mod;
20742 + use some::modified;
20743
20744 - const A: u32 = 42;
20745 ˇ
20746 fn main() {
20747 - println!("hello");
20748 + println!("hello there");
20749
20750 + println!("around the");
20751 println!("world");
20752 }
20753 "#
20754 .unindent(),
20755 );
20756
20757 cx.update_editor(|editor, window, cx| {
20758 editor.cancel(&Cancel, window, cx);
20759 });
20760
20761 cx.assert_state_with_diff(
20762 r#"
20763 use some::modified;
20764
20765 ˇ
20766 fn main() {
20767 println!("hello there");
20768
20769 println!("around the");
20770 println!("world");
20771 }
20772 "#
20773 .unindent(),
20774 );
20775}
20776
20777#[gpui::test]
20778async fn test_diff_base_change_with_expanded_diff_hunks(
20779 executor: BackgroundExecutor,
20780 cx: &mut TestAppContext,
20781) {
20782 init_test(cx, |_| {});
20783
20784 let mut cx = EditorTestContext::new(cx).await;
20785
20786 let diff_base = r#"
20787 use some::mod1;
20788 use some::mod2;
20789
20790 const A: u32 = 42;
20791 const B: u32 = 42;
20792 const C: u32 = 42;
20793
20794 fn main() {
20795 println!("hello");
20796
20797 println!("world");
20798 }
20799 "#
20800 .unindent();
20801
20802 cx.set_state(
20803 &r#"
20804 use some::mod2;
20805
20806 const A: u32 = 42;
20807 const C: u32 = 42;
20808
20809 fn main(ˇ) {
20810 //println!("hello");
20811
20812 println!("world");
20813 //
20814 //
20815 }
20816 "#
20817 .unindent(),
20818 );
20819
20820 cx.set_head_text(&diff_base);
20821 executor.run_until_parked();
20822
20823 cx.update_editor(|editor, window, cx| {
20824 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20825 });
20826 executor.run_until_parked();
20827 cx.assert_state_with_diff(
20828 r#"
20829 - use some::mod1;
20830 use some::mod2;
20831
20832 const A: u32 = 42;
20833 - const B: u32 = 42;
20834 const C: u32 = 42;
20835
20836 fn main(ˇ) {
20837 - println!("hello");
20838 + //println!("hello");
20839
20840 println!("world");
20841 + //
20842 + //
20843 }
20844 "#
20845 .unindent(),
20846 );
20847
20848 cx.set_head_text("new diff base!");
20849 executor.run_until_parked();
20850 cx.assert_state_with_diff(
20851 r#"
20852 - new diff base!
20853 + use some::mod2;
20854 +
20855 + const A: u32 = 42;
20856 + const C: u32 = 42;
20857 +
20858 + fn main(ˇ) {
20859 + //println!("hello");
20860 +
20861 + println!("world");
20862 + //
20863 + //
20864 + }
20865 "#
20866 .unindent(),
20867 );
20868}
20869
20870#[gpui::test]
20871async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
20872 init_test(cx, |_| {});
20873
20874 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20875 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20876 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20877 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20878 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
20879 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
20880
20881 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
20882 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
20883 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
20884
20885 let multi_buffer = cx.new(|cx| {
20886 let mut multibuffer = MultiBuffer::new(ReadWrite);
20887 multibuffer.push_excerpts(
20888 buffer_1.clone(),
20889 [
20890 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20891 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20892 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20893 ],
20894 cx,
20895 );
20896 multibuffer.push_excerpts(
20897 buffer_2.clone(),
20898 [
20899 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20900 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20901 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20902 ],
20903 cx,
20904 );
20905 multibuffer.push_excerpts(
20906 buffer_3.clone(),
20907 [
20908 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20909 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20910 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20911 ],
20912 cx,
20913 );
20914 multibuffer
20915 });
20916
20917 let editor =
20918 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20919 editor
20920 .update(cx, |editor, _window, cx| {
20921 for (buffer, diff_base) in [
20922 (buffer_1.clone(), file_1_old),
20923 (buffer_2.clone(), file_2_old),
20924 (buffer_3.clone(), file_3_old),
20925 ] {
20926 let diff = cx.new(|cx| {
20927 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
20928 });
20929 editor
20930 .buffer
20931 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20932 }
20933 })
20934 .unwrap();
20935
20936 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20937 cx.run_until_parked();
20938
20939 cx.assert_editor_state(
20940 &"
20941 ˇaaa
20942 ccc
20943 ddd
20944
20945 ggg
20946 hhh
20947
20948
20949 lll
20950 mmm
20951 NNN
20952
20953 qqq
20954 rrr
20955
20956 uuu
20957 111
20958 222
20959 333
20960
20961 666
20962 777
20963
20964 000
20965 !!!"
20966 .unindent(),
20967 );
20968
20969 cx.update_editor(|editor, window, cx| {
20970 editor.select_all(&SelectAll, window, cx);
20971 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20972 });
20973 cx.executor().run_until_parked();
20974
20975 cx.assert_state_with_diff(
20976 "
20977 «aaa
20978 - bbb
20979 ccc
20980 ddd
20981
20982 ggg
20983 hhh
20984
20985
20986 lll
20987 mmm
20988 - nnn
20989 + NNN
20990
20991 qqq
20992 rrr
20993
20994 uuu
20995 111
20996 222
20997 333
20998
20999 + 666
21000 777
21001
21002 000
21003 !!!ˇ»"
21004 .unindent(),
21005 );
21006}
21007
21008#[gpui::test]
21009async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
21010 init_test(cx, |_| {});
21011
21012 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
21013 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
21014
21015 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
21016 let multi_buffer = cx.new(|cx| {
21017 let mut multibuffer = MultiBuffer::new(ReadWrite);
21018 multibuffer.push_excerpts(
21019 buffer.clone(),
21020 [
21021 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
21022 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
21023 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
21024 ],
21025 cx,
21026 );
21027 multibuffer
21028 });
21029
21030 let editor =
21031 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
21032 editor
21033 .update(cx, |editor, _window, cx| {
21034 let diff = cx.new(|cx| {
21035 BufferDiff::new_with_base_text(base, &buffer.read(cx).text_snapshot(), cx)
21036 });
21037 editor
21038 .buffer
21039 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
21040 })
21041 .unwrap();
21042
21043 let mut cx = EditorTestContext::for_editor(editor, cx).await;
21044 cx.run_until_parked();
21045
21046 cx.update_editor(|editor, window, cx| {
21047 editor.expand_all_diff_hunks(&Default::default(), window, cx)
21048 });
21049 cx.executor().run_until_parked();
21050
21051 // When the start of a hunk coincides with the start of its excerpt,
21052 // the hunk is expanded. When the start of a hunk is earlier than
21053 // the start of its excerpt, the hunk is not expanded.
21054 cx.assert_state_with_diff(
21055 "
21056 ˇaaa
21057 - bbb
21058 + BBB
21059
21060 - ddd
21061 - eee
21062 + DDD
21063 + EEE
21064 fff
21065
21066 iii
21067 "
21068 .unindent(),
21069 );
21070}
21071
21072#[gpui::test]
21073async fn test_edits_around_expanded_insertion_hunks(
21074 executor: BackgroundExecutor,
21075 cx: &mut TestAppContext,
21076) {
21077 init_test(cx, |_| {});
21078
21079 let mut cx = EditorTestContext::new(cx).await;
21080
21081 let diff_base = r#"
21082 use some::mod1;
21083 use some::mod2;
21084
21085 const A: u32 = 42;
21086
21087 fn main() {
21088 println!("hello");
21089
21090 println!("world");
21091 }
21092 "#
21093 .unindent();
21094 executor.run_until_parked();
21095 cx.set_state(
21096 &r#"
21097 use some::mod1;
21098 use some::mod2;
21099
21100 const A: u32 = 42;
21101 const B: u32 = 42;
21102 const C: u32 = 42;
21103 ˇ
21104
21105 fn main() {
21106 println!("hello");
21107
21108 println!("world");
21109 }
21110 "#
21111 .unindent(),
21112 );
21113
21114 cx.set_head_text(&diff_base);
21115 executor.run_until_parked();
21116
21117 cx.update_editor(|editor, window, cx| {
21118 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21119 });
21120 executor.run_until_parked();
21121
21122 cx.assert_state_with_diff(
21123 r#"
21124 use some::mod1;
21125 use some::mod2;
21126
21127 const A: u32 = 42;
21128 + const B: u32 = 42;
21129 + const C: u32 = 42;
21130 + ˇ
21131
21132 fn main() {
21133 println!("hello");
21134
21135 println!("world");
21136 }
21137 "#
21138 .unindent(),
21139 );
21140
21141 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
21142 executor.run_until_parked();
21143
21144 cx.assert_state_with_diff(
21145 r#"
21146 use some::mod1;
21147 use some::mod2;
21148
21149 const A: u32 = 42;
21150 + const B: u32 = 42;
21151 + const C: u32 = 42;
21152 + const D: u32 = 42;
21153 + ˇ
21154
21155 fn main() {
21156 println!("hello");
21157
21158 println!("world");
21159 }
21160 "#
21161 .unindent(),
21162 );
21163
21164 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
21165 executor.run_until_parked();
21166
21167 cx.assert_state_with_diff(
21168 r#"
21169 use some::mod1;
21170 use some::mod2;
21171
21172 const A: u32 = 42;
21173 + const B: u32 = 42;
21174 + const C: u32 = 42;
21175 + const D: u32 = 42;
21176 + const E: u32 = 42;
21177 + ˇ
21178
21179 fn main() {
21180 println!("hello");
21181
21182 println!("world");
21183 }
21184 "#
21185 .unindent(),
21186 );
21187
21188 cx.update_editor(|editor, window, cx| {
21189 editor.delete_line(&DeleteLine, window, cx);
21190 });
21191 executor.run_until_parked();
21192
21193 cx.assert_state_with_diff(
21194 r#"
21195 use some::mod1;
21196 use some::mod2;
21197
21198 const A: u32 = 42;
21199 + const B: u32 = 42;
21200 + const C: u32 = 42;
21201 + const D: u32 = 42;
21202 + const E: u32 = 42;
21203 ˇ
21204 fn main() {
21205 println!("hello");
21206
21207 println!("world");
21208 }
21209 "#
21210 .unindent(),
21211 );
21212
21213 cx.update_editor(|editor, window, cx| {
21214 editor.move_up(&MoveUp, window, cx);
21215 editor.delete_line(&DeleteLine, window, cx);
21216 editor.move_up(&MoveUp, window, cx);
21217 editor.delete_line(&DeleteLine, window, cx);
21218 editor.move_up(&MoveUp, window, cx);
21219 editor.delete_line(&DeleteLine, window, cx);
21220 });
21221 executor.run_until_parked();
21222 cx.assert_state_with_diff(
21223 r#"
21224 use some::mod1;
21225 use some::mod2;
21226
21227 const A: u32 = 42;
21228 + const B: u32 = 42;
21229 ˇ
21230 fn main() {
21231 println!("hello");
21232
21233 println!("world");
21234 }
21235 "#
21236 .unindent(),
21237 );
21238
21239 cx.update_editor(|editor, window, cx| {
21240 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
21241 editor.delete_line(&DeleteLine, window, cx);
21242 });
21243 executor.run_until_parked();
21244 cx.assert_state_with_diff(
21245 r#"
21246 ˇ
21247 fn main() {
21248 println!("hello");
21249
21250 println!("world");
21251 }
21252 "#
21253 .unindent(),
21254 );
21255}
21256
21257#[gpui::test]
21258async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
21259 init_test(cx, |_| {});
21260
21261 let mut cx = EditorTestContext::new(cx).await;
21262 cx.set_head_text(indoc! { "
21263 one
21264 two
21265 three
21266 four
21267 five
21268 "
21269 });
21270 cx.set_state(indoc! { "
21271 one
21272 ˇthree
21273 five
21274 "});
21275 cx.run_until_parked();
21276 cx.update_editor(|editor, window, cx| {
21277 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21278 });
21279 cx.assert_state_with_diff(
21280 indoc! { "
21281 one
21282 - two
21283 ˇthree
21284 - four
21285 five
21286 "}
21287 .to_string(),
21288 );
21289 cx.update_editor(|editor, window, cx| {
21290 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21291 });
21292
21293 cx.assert_state_with_diff(
21294 indoc! { "
21295 one
21296 ˇthree
21297 five
21298 "}
21299 .to_string(),
21300 );
21301
21302 cx.update_editor(|editor, window, cx| {
21303 editor.move_up(&MoveUp, window, cx);
21304 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21305 });
21306 cx.assert_state_with_diff(
21307 indoc! { "
21308 ˇone
21309 - two
21310 three
21311 five
21312 "}
21313 .to_string(),
21314 );
21315
21316 cx.update_editor(|editor, window, cx| {
21317 editor.move_down(&MoveDown, window, cx);
21318 editor.move_down(&MoveDown, window, cx);
21319 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21320 });
21321 cx.assert_state_with_diff(
21322 indoc! { "
21323 one
21324 - two
21325 ˇthree
21326 - four
21327 five
21328 "}
21329 .to_string(),
21330 );
21331
21332 cx.set_state(indoc! { "
21333 one
21334 ˇTWO
21335 three
21336 four
21337 five
21338 "});
21339 cx.run_until_parked();
21340 cx.update_editor(|editor, window, cx| {
21341 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21342 });
21343
21344 cx.assert_state_with_diff(
21345 indoc! { "
21346 one
21347 - two
21348 + ˇTWO
21349 three
21350 four
21351 five
21352 "}
21353 .to_string(),
21354 );
21355 cx.update_editor(|editor, window, cx| {
21356 editor.move_up(&Default::default(), window, cx);
21357 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21358 });
21359 cx.assert_state_with_diff(
21360 indoc! { "
21361 one
21362 ˇTWO
21363 three
21364 four
21365 five
21366 "}
21367 .to_string(),
21368 );
21369}
21370
21371#[gpui::test]
21372async fn test_toggling_adjacent_diff_hunks_2(
21373 executor: BackgroundExecutor,
21374 cx: &mut TestAppContext,
21375) {
21376 init_test(cx, |_| {});
21377
21378 let mut cx = EditorTestContext::new(cx).await;
21379
21380 let diff_base = r#"
21381 lineA
21382 lineB
21383 lineC
21384 lineD
21385 "#
21386 .unindent();
21387
21388 cx.set_state(
21389 &r#"
21390 ˇlineA1
21391 lineB
21392 lineD
21393 "#
21394 .unindent(),
21395 );
21396 cx.set_head_text(&diff_base);
21397 executor.run_until_parked();
21398
21399 cx.update_editor(|editor, window, cx| {
21400 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21401 });
21402 executor.run_until_parked();
21403 cx.assert_state_with_diff(
21404 r#"
21405 - lineA
21406 + ˇlineA1
21407 lineB
21408 lineD
21409 "#
21410 .unindent(),
21411 );
21412
21413 cx.update_editor(|editor, window, cx| {
21414 editor.move_down(&MoveDown, window, cx);
21415 editor.move_right(&MoveRight, window, cx);
21416 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21417 });
21418 executor.run_until_parked();
21419 cx.assert_state_with_diff(
21420 r#"
21421 - lineA
21422 + lineA1
21423 lˇineB
21424 - lineC
21425 lineD
21426 "#
21427 .unindent(),
21428 );
21429}
21430
21431#[gpui::test]
21432async fn test_edits_around_expanded_deletion_hunks(
21433 executor: BackgroundExecutor,
21434 cx: &mut TestAppContext,
21435) {
21436 init_test(cx, |_| {});
21437
21438 let mut cx = EditorTestContext::new(cx).await;
21439
21440 let diff_base = r#"
21441 use some::mod1;
21442 use some::mod2;
21443
21444 const A: u32 = 42;
21445 const B: u32 = 42;
21446 const C: u32 = 42;
21447
21448
21449 fn main() {
21450 println!("hello");
21451
21452 println!("world");
21453 }
21454 "#
21455 .unindent();
21456 executor.run_until_parked();
21457 cx.set_state(
21458 &r#"
21459 use some::mod1;
21460 use some::mod2;
21461
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 );
21474
21475 cx.set_head_text(&diff_base);
21476 executor.run_until_parked();
21477
21478 cx.update_editor(|editor, window, cx| {
21479 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21480 });
21481 executor.run_until_parked();
21482
21483 cx.assert_state_with_diff(
21484 r#"
21485 use some::mod1;
21486 use some::mod2;
21487
21488 - const A: u32 = 42;
21489 ˇconst B: u32 = 42;
21490 const C: u32 = 42;
21491
21492
21493 fn main() {
21494 println!("hello");
21495
21496 println!("world");
21497 }
21498 "#
21499 .unindent(),
21500 );
21501
21502 cx.update_editor(|editor, window, cx| {
21503 editor.delete_line(&DeleteLine, window, cx);
21504 });
21505 executor.run_until_parked();
21506 cx.assert_state_with_diff(
21507 r#"
21508 use some::mod1;
21509 use some::mod2;
21510
21511 - const A: u32 = 42;
21512 - const B: u32 = 42;
21513 ˇconst C: u32 = 42;
21514
21515
21516 fn main() {
21517 println!("hello");
21518
21519 println!("world");
21520 }
21521 "#
21522 .unindent(),
21523 );
21524
21525 cx.update_editor(|editor, window, cx| {
21526 editor.delete_line(&DeleteLine, window, cx);
21527 });
21528 executor.run_until_parked();
21529 cx.assert_state_with_diff(
21530 r#"
21531 use some::mod1;
21532 use some::mod2;
21533
21534 - const A: u32 = 42;
21535 - const B: u32 = 42;
21536 - const C: u32 = 42;
21537 ˇ
21538
21539 fn main() {
21540 println!("hello");
21541
21542 println!("world");
21543 }
21544 "#
21545 .unindent(),
21546 );
21547
21548 cx.update_editor(|editor, window, cx| {
21549 editor.handle_input("replacement", window, cx);
21550 });
21551 executor.run_until_parked();
21552 cx.assert_state_with_diff(
21553 r#"
21554 use some::mod1;
21555 use some::mod2;
21556
21557 - const A: u32 = 42;
21558 - const B: u32 = 42;
21559 - const C: u32 = 42;
21560 -
21561 + replacementˇ
21562
21563 fn main() {
21564 println!("hello");
21565
21566 println!("world");
21567 }
21568 "#
21569 .unindent(),
21570 );
21571}
21572
21573#[gpui::test]
21574async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21575 init_test(cx, |_| {});
21576
21577 let mut cx = EditorTestContext::new(cx).await;
21578
21579 let base_text = r#"
21580 one
21581 two
21582 three
21583 four
21584 five
21585 "#
21586 .unindent();
21587 executor.run_until_parked();
21588 cx.set_state(
21589 &r#"
21590 one
21591 two
21592 fˇour
21593 five
21594 "#
21595 .unindent(),
21596 );
21597
21598 cx.set_head_text(&base_text);
21599 executor.run_until_parked();
21600
21601 cx.update_editor(|editor, window, cx| {
21602 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21603 });
21604 executor.run_until_parked();
21605
21606 cx.assert_state_with_diff(
21607 r#"
21608 one
21609 two
21610 - three
21611 fˇour
21612 five
21613 "#
21614 .unindent(),
21615 );
21616
21617 cx.update_editor(|editor, window, cx| {
21618 editor.backspace(&Backspace, window, cx);
21619 editor.backspace(&Backspace, window, cx);
21620 });
21621 executor.run_until_parked();
21622 cx.assert_state_with_diff(
21623 r#"
21624 one
21625 two
21626 - threeˇ
21627 - four
21628 + our
21629 five
21630 "#
21631 .unindent(),
21632 );
21633}
21634
21635#[gpui::test]
21636async fn test_edit_after_expanded_modification_hunk(
21637 executor: BackgroundExecutor,
21638 cx: &mut TestAppContext,
21639) {
21640 init_test(cx, |_| {});
21641
21642 let mut cx = EditorTestContext::new(cx).await;
21643
21644 let diff_base = r#"
21645 use some::mod1;
21646 use some::mod2;
21647
21648 const A: u32 = 42;
21649 const B: u32 = 42;
21650 const C: u32 = 42;
21651 const D: u32 = 42;
21652
21653
21654 fn main() {
21655 println!("hello");
21656
21657 println!("world");
21658 }"#
21659 .unindent();
21660
21661 cx.set_state(
21662 &r#"
21663 use some::mod1;
21664 use some::mod2;
21665
21666 const A: u32 = 42;
21667 const B: u32 = 42;
21668 const C: u32 = 43ˇ
21669 const D: u32 = 42;
21670
21671
21672 fn main() {
21673 println!("hello");
21674
21675 println!("world");
21676 }"#
21677 .unindent(),
21678 );
21679
21680 cx.set_head_text(&diff_base);
21681 executor.run_until_parked();
21682 cx.update_editor(|editor, window, cx| {
21683 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21684 });
21685 executor.run_until_parked();
21686
21687 cx.assert_state_with_diff(
21688 r#"
21689 use some::mod1;
21690 use some::mod2;
21691
21692 const A: u32 = 42;
21693 const B: u32 = 42;
21694 - const C: u32 = 42;
21695 + const C: u32 = 43ˇ
21696 const D: u32 = 42;
21697
21698
21699 fn main() {
21700 println!("hello");
21701
21702 println!("world");
21703 }"#
21704 .unindent(),
21705 );
21706
21707 cx.update_editor(|editor, window, cx| {
21708 editor.handle_input("\nnew_line\n", window, cx);
21709 });
21710 executor.run_until_parked();
21711
21712 cx.assert_state_with_diff(
21713 r#"
21714 use some::mod1;
21715 use some::mod2;
21716
21717 const A: u32 = 42;
21718 const B: u32 = 42;
21719 - const C: u32 = 42;
21720 + const C: u32 = 43
21721 + new_line
21722 + ˇ
21723 const D: u32 = 42;
21724
21725
21726 fn main() {
21727 println!("hello");
21728
21729 println!("world");
21730 }"#
21731 .unindent(),
21732 );
21733}
21734
21735#[gpui::test]
21736async fn test_stage_and_unstage_added_file_hunk(
21737 executor: BackgroundExecutor,
21738 cx: &mut TestAppContext,
21739) {
21740 init_test(cx, |_| {});
21741
21742 let mut cx = EditorTestContext::new(cx).await;
21743 cx.update_editor(|editor, _, cx| {
21744 editor.set_expand_all_diff_hunks(cx);
21745 });
21746
21747 let working_copy = r#"
21748 ˇfn main() {
21749 println!("hello, world!");
21750 }
21751 "#
21752 .unindent();
21753
21754 cx.set_state(&working_copy);
21755 executor.run_until_parked();
21756
21757 cx.assert_state_with_diff(
21758 r#"
21759 + ˇfn main() {
21760 + println!("hello, world!");
21761 + }
21762 "#
21763 .unindent(),
21764 );
21765 cx.assert_index_text(None);
21766
21767 cx.update_editor(|editor, window, cx| {
21768 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21769 });
21770 executor.run_until_parked();
21771 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
21772 cx.assert_state_with_diff(
21773 r#"
21774 + ˇfn main() {
21775 + println!("hello, world!");
21776 + }
21777 "#
21778 .unindent(),
21779 );
21780
21781 cx.update_editor(|editor, window, cx| {
21782 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21783 });
21784 executor.run_until_parked();
21785 cx.assert_index_text(None);
21786}
21787
21788async fn setup_indent_guides_editor(
21789 text: &str,
21790 cx: &mut TestAppContext,
21791) -> (BufferId, EditorTestContext) {
21792 init_test(cx, |_| {});
21793
21794 let mut cx = EditorTestContext::new(cx).await;
21795
21796 let buffer_id = cx.update_editor(|editor, window, cx| {
21797 editor.set_text(text, window, cx);
21798 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
21799
21800 buffer_ids[0]
21801 });
21802
21803 (buffer_id, cx)
21804}
21805
21806fn assert_indent_guides(
21807 range: Range<u32>,
21808 expected: Vec<IndentGuide>,
21809 active_indices: Option<Vec<usize>>,
21810 cx: &mut EditorTestContext,
21811) {
21812 let indent_guides = cx.update_editor(|editor, window, cx| {
21813 let snapshot = editor.snapshot(window, cx).display_snapshot;
21814 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
21815 editor,
21816 MultiBufferRow(range.start)..MultiBufferRow(range.end),
21817 true,
21818 &snapshot,
21819 cx,
21820 );
21821
21822 indent_guides.sort_by(|a, b| {
21823 a.depth.cmp(&b.depth).then(
21824 a.start_row
21825 .cmp(&b.start_row)
21826 .then(a.end_row.cmp(&b.end_row)),
21827 )
21828 });
21829 indent_guides
21830 });
21831
21832 if let Some(expected) = active_indices {
21833 let active_indices = cx.update_editor(|editor, window, cx| {
21834 let snapshot = editor.snapshot(window, cx).display_snapshot;
21835 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
21836 });
21837
21838 assert_eq!(
21839 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
21840 expected,
21841 "Active indent guide indices do not match"
21842 );
21843 }
21844
21845 assert_eq!(indent_guides, expected, "Indent guides do not match");
21846}
21847
21848fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
21849 IndentGuide {
21850 buffer_id,
21851 start_row: MultiBufferRow(start_row),
21852 end_row: MultiBufferRow(end_row),
21853 depth,
21854 tab_size: 4,
21855 settings: IndentGuideSettings {
21856 enabled: true,
21857 line_width: 1,
21858 active_line_width: 1,
21859 coloring: IndentGuideColoring::default(),
21860 background_coloring: IndentGuideBackgroundColoring::default(),
21861 },
21862 }
21863}
21864
21865#[gpui::test]
21866async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
21867 let (buffer_id, mut cx) = setup_indent_guides_editor(
21868 &"
21869 fn main() {
21870 let a = 1;
21871 }"
21872 .unindent(),
21873 cx,
21874 )
21875 .await;
21876
21877 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21878}
21879
21880#[gpui::test]
21881async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
21882 let (buffer_id, mut cx) = setup_indent_guides_editor(
21883 &"
21884 fn main() {
21885 let a = 1;
21886 let b = 2;
21887 }"
21888 .unindent(),
21889 cx,
21890 )
21891 .await;
21892
21893 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
21894}
21895
21896#[gpui::test]
21897async fn test_indent_guide_nested(cx: &mut TestAppContext) {
21898 let (buffer_id, mut cx) = setup_indent_guides_editor(
21899 &"
21900 fn main() {
21901 let a = 1;
21902 if a == 3 {
21903 let b = 2;
21904 } else {
21905 let c = 3;
21906 }
21907 }"
21908 .unindent(),
21909 cx,
21910 )
21911 .await;
21912
21913 assert_indent_guides(
21914 0..8,
21915 vec![
21916 indent_guide(buffer_id, 1, 6, 0),
21917 indent_guide(buffer_id, 3, 3, 1),
21918 indent_guide(buffer_id, 5, 5, 1),
21919 ],
21920 None,
21921 &mut cx,
21922 );
21923}
21924
21925#[gpui::test]
21926async fn test_indent_guide_tab(cx: &mut TestAppContext) {
21927 let (buffer_id, mut cx) = setup_indent_guides_editor(
21928 &"
21929 fn main() {
21930 let a = 1;
21931 let b = 2;
21932 let c = 3;
21933 }"
21934 .unindent(),
21935 cx,
21936 )
21937 .await;
21938
21939 assert_indent_guides(
21940 0..5,
21941 vec![
21942 indent_guide(buffer_id, 1, 3, 0),
21943 indent_guide(buffer_id, 2, 2, 1),
21944 ],
21945 None,
21946 &mut cx,
21947 );
21948}
21949
21950#[gpui::test]
21951async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
21952 let (buffer_id, mut cx) = setup_indent_guides_editor(
21953 &"
21954 fn main() {
21955 let a = 1;
21956
21957 let c = 3;
21958 }"
21959 .unindent(),
21960 cx,
21961 )
21962 .await;
21963
21964 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
21965}
21966
21967#[gpui::test]
21968async fn test_indent_guide_complex(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 if a == 3 {
21977 let b = 2;
21978 } else {
21979 let c = 3;
21980 }
21981 }"
21982 .unindent(),
21983 cx,
21984 )
21985 .await;
21986
21987 assert_indent_guides(
21988 0..11,
21989 vec![
21990 indent_guide(buffer_id, 1, 9, 0),
21991 indent_guide(buffer_id, 6, 6, 1),
21992 indent_guide(buffer_id, 8, 8, 1),
21993 ],
21994 None,
21995 &mut cx,
21996 );
21997}
21998
21999#[gpui::test]
22000async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
22001 let (buffer_id, mut cx) = setup_indent_guides_editor(
22002 &"
22003 fn main() {
22004 let a = 1;
22005
22006 let c = 3;
22007
22008 if a == 3 {
22009 let b = 2;
22010 } else {
22011 let c = 3;
22012 }
22013 }"
22014 .unindent(),
22015 cx,
22016 )
22017 .await;
22018
22019 assert_indent_guides(
22020 1..11,
22021 vec![
22022 indent_guide(buffer_id, 1, 9, 0),
22023 indent_guide(buffer_id, 6, 6, 1),
22024 indent_guide(buffer_id, 8, 8, 1),
22025 ],
22026 None,
22027 &mut cx,
22028 );
22029}
22030
22031#[gpui::test]
22032async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
22033 let (buffer_id, mut cx) = setup_indent_guides_editor(
22034 &"
22035 fn main() {
22036 let a = 1;
22037
22038 let c = 3;
22039
22040 if a == 3 {
22041 let b = 2;
22042 } else {
22043 let c = 3;
22044 }
22045 }"
22046 .unindent(),
22047 cx,
22048 )
22049 .await;
22050
22051 assert_indent_guides(
22052 1..10,
22053 vec![
22054 indent_guide(buffer_id, 1, 9, 0),
22055 indent_guide(buffer_id, 6, 6, 1),
22056 indent_guide(buffer_id, 8, 8, 1),
22057 ],
22058 None,
22059 &mut cx,
22060 );
22061}
22062
22063#[gpui::test]
22064async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
22065 let (buffer_id, mut cx) = setup_indent_guides_editor(
22066 &"
22067 fn main() {
22068 if a {
22069 b(
22070 c,
22071 d,
22072 )
22073 } else {
22074 e(
22075 f
22076 )
22077 }
22078 }"
22079 .unindent(),
22080 cx,
22081 )
22082 .await;
22083
22084 assert_indent_guides(
22085 0..11,
22086 vec![
22087 indent_guide(buffer_id, 1, 10, 0),
22088 indent_guide(buffer_id, 2, 5, 1),
22089 indent_guide(buffer_id, 7, 9, 1),
22090 indent_guide(buffer_id, 3, 4, 2),
22091 indent_guide(buffer_id, 8, 8, 2),
22092 ],
22093 None,
22094 &mut cx,
22095 );
22096
22097 cx.update_editor(|editor, window, cx| {
22098 editor.fold_at(MultiBufferRow(2), window, cx);
22099 assert_eq!(
22100 editor.display_text(cx),
22101 "
22102 fn main() {
22103 if a {
22104 b(⋯
22105 )
22106 } else {
22107 e(
22108 f
22109 )
22110 }
22111 }"
22112 .unindent()
22113 );
22114 });
22115
22116 assert_indent_guides(
22117 0..11,
22118 vec![
22119 indent_guide(buffer_id, 1, 10, 0),
22120 indent_guide(buffer_id, 2, 5, 1),
22121 indent_guide(buffer_id, 7, 9, 1),
22122 indent_guide(buffer_id, 8, 8, 2),
22123 ],
22124 None,
22125 &mut cx,
22126 );
22127}
22128
22129#[gpui::test]
22130async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
22131 let (buffer_id, mut cx) = setup_indent_guides_editor(
22132 &"
22133 block1
22134 block2
22135 block3
22136 block4
22137 block2
22138 block1
22139 block1"
22140 .unindent(),
22141 cx,
22142 )
22143 .await;
22144
22145 assert_indent_guides(
22146 1..10,
22147 vec![
22148 indent_guide(buffer_id, 1, 4, 0),
22149 indent_guide(buffer_id, 2, 3, 1),
22150 indent_guide(buffer_id, 3, 3, 2),
22151 ],
22152 None,
22153 &mut cx,
22154 );
22155}
22156
22157#[gpui::test]
22158async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
22159 let (buffer_id, mut cx) = setup_indent_guides_editor(
22160 &"
22161 block1
22162 block2
22163 block3
22164
22165 block1
22166 block1"
22167 .unindent(),
22168 cx,
22169 )
22170 .await;
22171
22172 assert_indent_guides(
22173 0..6,
22174 vec![
22175 indent_guide(buffer_id, 1, 2, 0),
22176 indent_guide(buffer_id, 2, 2, 1),
22177 ],
22178 None,
22179 &mut cx,
22180 );
22181}
22182
22183#[gpui::test]
22184async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
22185 let (buffer_id, mut cx) = setup_indent_guides_editor(
22186 &"
22187 function component() {
22188 \treturn (
22189 \t\t\t
22190 \t\t<div>
22191 \t\t\t<abc></abc>
22192 \t\t</div>
22193 \t)
22194 }"
22195 .unindent(),
22196 cx,
22197 )
22198 .await;
22199
22200 assert_indent_guides(
22201 0..8,
22202 vec![
22203 indent_guide(buffer_id, 1, 6, 0),
22204 indent_guide(buffer_id, 2, 5, 1),
22205 indent_guide(buffer_id, 4, 4, 2),
22206 ],
22207 None,
22208 &mut cx,
22209 );
22210}
22211
22212#[gpui::test]
22213async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
22214 let (buffer_id, mut cx) = setup_indent_guides_editor(
22215 &"
22216 function component() {
22217 \treturn (
22218 \t
22219 \t\t<div>
22220 \t\t\t<abc></abc>
22221 \t\t</div>
22222 \t)
22223 }"
22224 .unindent(),
22225 cx,
22226 )
22227 .await;
22228
22229 assert_indent_guides(
22230 0..8,
22231 vec![
22232 indent_guide(buffer_id, 1, 6, 0),
22233 indent_guide(buffer_id, 2, 5, 1),
22234 indent_guide(buffer_id, 4, 4, 2),
22235 ],
22236 None,
22237 &mut cx,
22238 );
22239}
22240
22241#[gpui::test]
22242async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
22243 let (buffer_id, mut cx) = setup_indent_guides_editor(
22244 &"
22245 block1
22246
22247
22248
22249 block2
22250 "
22251 .unindent(),
22252 cx,
22253 )
22254 .await;
22255
22256 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
22257}
22258
22259#[gpui::test]
22260async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
22261 let (buffer_id, mut cx) = setup_indent_guides_editor(
22262 &"
22263 def a:
22264 \tb = 3
22265 \tif True:
22266 \t\tc = 4
22267 \t\td = 5
22268 \tprint(b)
22269 "
22270 .unindent(),
22271 cx,
22272 )
22273 .await;
22274
22275 assert_indent_guides(
22276 0..6,
22277 vec![
22278 indent_guide(buffer_id, 1, 5, 0),
22279 indent_guide(buffer_id, 3, 4, 1),
22280 ],
22281 None,
22282 &mut cx,
22283 );
22284}
22285
22286#[gpui::test]
22287async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
22288 let (buffer_id, mut cx) = setup_indent_guides_editor(
22289 &"
22290 fn main() {
22291 let a = 1;
22292 }"
22293 .unindent(),
22294 cx,
22295 )
22296 .await;
22297
22298 cx.update_editor(|editor, window, cx| {
22299 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22300 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22301 });
22302 });
22303
22304 assert_indent_guides(
22305 0..3,
22306 vec![indent_guide(buffer_id, 1, 1, 0)],
22307 Some(vec![0]),
22308 &mut cx,
22309 );
22310}
22311
22312#[gpui::test]
22313async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
22314 let (buffer_id, mut cx) = setup_indent_guides_editor(
22315 &"
22316 fn main() {
22317 if 1 == 2 {
22318 let a = 1;
22319 }
22320 }"
22321 .unindent(),
22322 cx,
22323 )
22324 .await;
22325
22326 cx.update_editor(|editor, window, cx| {
22327 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22328 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22329 });
22330 });
22331
22332 assert_indent_guides(
22333 0..4,
22334 vec![
22335 indent_guide(buffer_id, 1, 3, 0),
22336 indent_guide(buffer_id, 2, 2, 1),
22337 ],
22338 Some(vec![1]),
22339 &mut cx,
22340 );
22341
22342 cx.update_editor(|editor, window, cx| {
22343 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22344 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
22345 });
22346 });
22347
22348 assert_indent_guides(
22349 0..4,
22350 vec![
22351 indent_guide(buffer_id, 1, 3, 0),
22352 indent_guide(buffer_id, 2, 2, 1),
22353 ],
22354 Some(vec![1]),
22355 &mut cx,
22356 );
22357
22358 cx.update_editor(|editor, window, cx| {
22359 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22360 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
22361 });
22362 });
22363
22364 assert_indent_guides(
22365 0..4,
22366 vec![
22367 indent_guide(buffer_id, 1, 3, 0),
22368 indent_guide(buffer_id, 2, 2, 1),
22369 ],
22370 Some(vec![0]),
22371 &mut cx,
22372 );
22373}
22374
22375#[gpui::test]
22376async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
22377 let (buffer_id, mut cx) = setup_indent_guides_editor(
22378 &"
22379 fn main() {
22380 let a = 1;
22381
22382 let b = 2;
22383 }"
22384 .unindent(),
22385 cx,
22386 )
22387 .await;
22388
22389 cx.update_editor(|editor, window, cx| {
22390 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22391 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
22392 });
22393 });
22394
22395 assert_indent_guides(
22396 0..5,
22397 vec![indent_guide(buffer_id, 1, 3, 0)],
22398 Some(vec![0]),
22399 &mut cx,
22400 );
22401}
22402
22403#[gpui::test]
22404async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
22405 let (buffer_id, mut cx) = setup_indent_guides_editor(
22406 &"
22407 def m:
22408 a = 1
22409 pass"
22410 .unindent(),
22411 cx,
22412 )
22413 .await;
22414
22415 cx.update_editor(|editor, window, cx| {
22416 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22417 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22418 });
22419 });
22420
22421 assert_indent_guides(
22422 0..3,
22423 vec![indent_guide(buffer_id, 1, 2, 0)],
22424 Some(vec![0]),
22425 &mut cx,
22426 );
22427}
22428
22429#[gpui::test]
22430async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
22431 init_test(cx, |_| {});
22432 let mut cx = EditorTestContext::new(cx).await;
22433 let text = indoc! {
22434 "
22435 impl A {
22436 fn b() {
22437 0;
22438 3;
22439 5;
22440 6;
22441 7;
22442 }
22443 }
22444 "
22445 };
22446 let base_text = indoc! {
22447 "
22448 impl A {
22449 fn b() {
22450 0;
22451 1;
22452 2;
22453 3;
22454 4;
22455 }
22456 fn c() {
22457 5;
22458 6;
22459 7;
22460 }
22461 }
22462 "
22463 };
22464
22465 cx.update_editor(|editor, window, cx| {
22466 editor.set_text(text, window, cx);
22467
22468 editor.buffer().update(cx, |multibuffer, cx| {
22469 let buffer = multibuffer.as_singleton().unwrap();
22470 let diff = cx.new(|cx| {
22471 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
22472 });
22473
22474 multibuffer.set_all_diff_hunks_expanded(cx);
22475 multibuffer.add_diff(diff, cx);
22476
22477 buffer.read(cx).remote_id()
22478 })
22479 });
22480 cx.run_until_parked();
22481
22482 cx.assert_state_with_diff(
22483 indoc! { "
22484 impl A {
22485 fn b() {
22486 0;
22487 - 1;
22488 - 2;
22489 3;
22490 - 4;
22491 - }
22492 - fn c() {
22493 5;
22494 6;
22495 7;
22496 }
22497 }
22498 ˇ"
22499 }
22500 .to_string(),
22501 );
22502
22503 let mut actual_guides = cx.update_editor(|editor, window, cx| {
22504 editor
22505 .snapshot(window, cx)
22506 .buffer_snapshot()
22507 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
22508 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
22509 .collect::<Vec<_>>()
22510 });
22511 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
22512 assert_eq!(
22513 actual_guides,
22514 vec![
22515 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
22516 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
22517 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
22518 ]
22519 );
22520}
22521
22522#[gpui::test]
22523async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22524 init_test(cx, |_| {});
22525 let mut cx = EditorTestContext::new(cx).await;
22526
22527 let diff_base = r#"
22528 a
22529 b
22530 c
22531 "#
22532 .unindent();
22533
22534 cx.set_state(
22535 &r#"
22536 ˇA
22537 b
22538 C
22539 "#
22540 .unindent(),
22541 );
22542 cx.set_head_text(&diff_base);
22543 cx.update_editor(|editor, window, cx| {
22544 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22545 });
22546 executor.run_until_parked();
22547
22548 let both_hunks_expanded = r#"
22549 - a
22550 + ˇA
22551 b
22552 - c
22553 + C
22554 "#
22555 .unindent();
22556
22557 cx.assert_state_with_diff(both_hunks_expanded.clone());
22558
22559 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22560 let snapshot = editor.snapshot(window, cx);
22561 let hunks = editor
22562 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22563 .collect::<Vec<_>>();
22564 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22565 hunks
22566 .into_iter()
22567 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22568 .collect::<Vec<_>>()
22569 });
22570 assert_eq!(hunk_ranges.len(), 2);
22571
22572 cx.update_editor(|editor, _, cx| {
22573 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22574 });
22575 executor.run_until_parked();
22576
22577 let second_hunk_expanded = r#"
22578 ˇA
22579 b
22580 - c
22581 + C
22582 "#
22583 .unindent();
22584
22585 cx.assert_state_with_diff(second_hunk_expanded);
22586
22587 cx.update_editor(|editor, _, cx| {
22588 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22589 });
22590 executor.run_until_parked();
22591
22592 cx.assert_state_with_diff(both_hunks_expanded.clone());
22593
22594 cx.update_editor(|editor, _, cx| {
22595 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22596 });
22597 executor.run_until_parked();
22598
22599 let first_hunk_expanded = r#"
22600 - a
22601 + ˇA
22602 b
22603 C
22604 "#
22605 .unindent();
22606
22607 cx.assert_state_with_diff(first_hunk_expanded);
22608
22609 cx.update_editor(|editor, _, cx| {
22610 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22611 });
22612 executor.run_until_parked();
22613
22614 cx.assert_state_with_diff(both_hunks_expanded);
22615
22616 cx.set_state(
22617 &r#"
22618 ˇA
22619 b
22620 "#
22621 .unindent(),
22622 );
22623 cx.run_until_parked();
22624
22625 // TODO this cursor position seems bad
22626 cx.assert_state_with_diff(
22627 r#"
22628 - ˇa
22629 + A
22630 b
22631 "#
22632 .unindent(),
22633 );
22634
22635 cx.update_editor(|editor, window, cx| {
22636 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22637 });
22638
22639 cx.assert_state_with_diff(
22640 r#"
22641 - ˇa
22642 + A
22643 b
22644 - c
22645 "#
22646 .unindent(),
22647 );
22648
22649 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22650 let snapshot = editor.snapshot(window, cx);
22651 let hunks = editor
22652 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22653 .collect::<Vec<_>>();
22654 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22655 hunks
22656 .into_iter()
22657 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22658 .collect::<Vec<_>>()
22659 });
22660 assert_eq!(hunk_ranges.len(), 2);
22661
22662 cx.update_editor(|editor, _, cx| {
22663 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22664 });
22665 executor.run_until_parked();
22666
22667 cx.assert_state_with_diff(
22668 r#"
22669 - ˇa
22670 + A
22671 b
22672 "#
22673 .unindent(),
22674 );
22675}
22676
22677#[gpui::test]
22678async fn test_toggle_deletion_hunk_at_start_of_file(
22679 executor: BackgroundExecutor,
22680 cx: &mut TestAppContext,
22681) {
22682 init_test(cx, |_| {});
22683 let mut cx = EditorTestContext::new(cx).await;
22684
22685 let diff_base = r#"
22686 a
22687 b
22688 c
22689 "#
22690 .unindent();
22691
22692 cx.set_state(
22693 &r#"
22694 ˇb
22695 c
22696 "#
22697 .unindent(),
22698 );
22699 cx.set_head_text(&diff_base);
22700 cx.update_editor(|editor, window, cx| {
22701 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22702 });
22703 executor.run_until_parked();
22704
22705 let hunk_expanded = r#"
22706 - a
22707 ˇb
22708 c
22709 "#
22710 .unindent();
22711
22712 cx.assert_state_with_diff(hunk_expanded.clone());
22713
22714 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22715 let snapshot = editor.snapshot(window, cx);
22716 let hunks = editor
22717 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22718 .collect::<Vec<_>>();
22719 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22720 hunks
22721 .into_iter()
22722 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22723 .collect::<Vec<_>>()
22724 });
22725 assert_eq!(hunk_ranges.len(), 1);
22726
22727 cx.update_editor(|editor, _, cx| {
22728 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22729 });
22730 executor.run_until_parked();
22731
22732 let hunk_collapsed = r#"
22733 ˇb
22734 c
22735 "#
22736 .unindent();
22737
22738 cx.assert_state_with_diff(hunk_collapsed);
22739
22740 cx.update_editor(|editor, _, cx| {
22741 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22742 });
22743 executor.run_until_parked();
22744
22745 cx.assert_state_with_diff(hunk_expanded);
22746}
22747
22748#[gpui::test]
22749async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
22750 executor: BackgroundExecutor,
22751 cx: &mut TestAppContext,
22752) {
22753 init_test(cx, |_| {});
22754 let mut cx = EditorTestContext::new(cx).await;
22755
22756 cx.set_state("ˇnew\nsecond\nthird\n");
22757 cx.set_head_text("old\nsecond\nthird\n");
22758 cx.update_editor(|editor, window, cx| {
22759 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
22760 });
22761 executor.run_until_parked();
22762 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22763
22764 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
22765 cx.update_editor(|editor, window, cx| {
22766 let snapshot = editor.snapshot(window, cx);
22767 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22768 let hunks = editor
22769 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22770 .collect::<Vec<_>>();
22771 assert_eq!(hunks.len(), 1);
22772 let hunk_range = Anchor::range_in_buffer(excerpt_id, hunks[0].buffer_range.clone());
22773 editor.toggle_single_diff_hunk(hunk_range, cx)
22774 });
22775 executor.run_until_parked();
22776 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
22777
22778 // Keep the editor scrolled to the top so the full hunk remains visible.
22779 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22780}
22781
22782#[gpui::test]
22783async fn test_display_diff_hunks(cx: &mut TestAppContext) {
22784 init_test(cx, |_| {});
22785
22786 let fs = FakeFs::new(cx.executor());
22787 fs.insert_tree(
22788 path!("/test"),
22789 json!({
22790 ".git": {},
22791 "file-1": "ONE\n",
22792 "file-2": "TWO\n",
22793 "file-3": "THREE\n",
22794 }),
22795 )
22796 .await;
22797
22798 fs.set_head_for_repo(
22799 path!("/test/.git").as_ref(),
22800 &[
22801 ("file-1", "one\n".into()),
22802 ("file-2", "two\n".into()),
22803 ("file-3", "three\n".into()),
22804 ],
22805 "deadbeef",
22806 );
22807
22808 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
22809 let mut buffers = vec![];
22810 for i in 1..=3 {
22811 let buffer = project
22812 .update(cx, |project, cx| {
22813 let path = format!(path!("/test/file-{}"), i);
22814 project.open_local_buffer(path, cx)
22815 })
22816 .await
22817 .unwrap();
22818 buffers.push(buffer);
22819 }
22820
22821 let multibuffer = cx.new(|cx| {
22822 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
22823 multibuffer.set_all_diff_hunks_expanded(cx);
22824 for buffer in &buffers {
22825 let snapshot = buffer.read(cx).snapshot();
22826 multibuffer.set_excerpts_for_path(
22827 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
22828 buffer.clone(),
22829 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
22830 2,
22831 cx,
22832 );
22833 }
22834 multibuffer
22835 });
22836
22837 let editor = cx.add_window(|window, cx| {
22838 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
22839 });
22840 cx.run_until_parked();
22841
22842 let snapshot = editor
22843 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22844 .unwrap();
22845 let hunks = snapshot
22846 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
22847 .map(|hunk| match hunk {
22848 DisplayDiffHunk::Unfolded {
22849 display_row_range, ..
22850 } => display_row_range,
22851 DisplayDiffHunk::Folded { .. } => unreachable!(),
22852 })
22853 .collect::<Vec<_>>();
22854 assert_eq!(
22855 hunks,
22856 [
22857 DisplayRow(2)..DisplayRow(4),
22858 DisplayRow(7)..DisplayRow(9),
22859 DisplayRow(12)..DisplayRow(14),
22860 ]
22861 );
22862}
22863
22864#[gpui::test]
22865async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
22866 init_test(cx, |_| {});
22867
22868 let mut cx = EditorTestContext::new(cx).await;
22869 cx.set_head_text(indoc! { "
22870 one
22871 two
22872 three
22873 four
22874 five
22875 "
22876 });
22877 cx.set_index_text(indoc! { "
22878 one
22879 two
22880 three
22881 four
22882 five
22883 "
22884 });
22885 cx.set_state(indoc! {"
22886 one
22887 TWO
22888 ˇTHREE
22889 FOUR
22890 five
22891 "});
22892 cx.run_until_parked();
22893 cx.update_editor(|editor, window, cx| {
22894 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22895 });
22896 cx.run_until_parked();
22897 cx.assert_index_text(Some(indoc! {"
22898 one
22899 TWO
22900 THREE
22901 FOUR
22902 five
22903 "}));
22904 cx.set_state(indoc! { "
22905 one
22906 TWO
22907 ˇTHREE-HUNDRED
22908 FOUR
22909 five
22910 "});
22911 cx.run_until_parked();
22912 cx.update_editor(|editor, window, cx| {
22913 let snapshot = editor.snapshot(window, cx);
22914 let hunks = editor
22915 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22916 .collect::<Vec<_>>();
22917 assert_eq!(hunks.len(), 1);
22918 assert_eq!(
22919 hunks[0].status(),
22920 DiffHunkStatus {
22921 kind: DiffHunkStatusKind::Modified,
22922 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
22923 }
22924 );
22925
22926 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22927 });
22928 cx.run_until_parked();
22929 cx.assert_index_text(Some(indoc! {"
22930 one
22931 TWO
22932 THREE-HUNDRED
22933 FOUR
22934 five
22935 "}));
22936}
22937
22938#[gpui::test]
22939fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
22940 init_test(cx, |_| {});
22941
22942 let editor = cx.add_window(|window, cx| {
22943 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
22944 build_editor(buffer, window, cx)
22945 });
22946
22947 let render_args = Arc::new(Mutex::new(None));
22948 let snapshot = editor
22949 .update(cx, |editor, window, cx| {
22950 let snapshot = editor.buffer().read(cx).snapshot(cx);
22951 let range =
22952 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
22953
22954 struct RenderArgs {
22955 row: MultiBufferRow,
22956 folded: bool,
22957 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
22958 }
22959
22960 let crease = Crease::inline(
22961 range,
22962 FoldPlaceholder::test(),
22963 {
22964 let toggle_callback = render_args.clone();
22965 move |row, folded, callback, _window, _cx| {
22966 *toggle_callback.lock() = Some(RenderArgs {
22967 row,
22968 folded,
22969 callback,
22970 });
22971 div()
22972 }
22973 },
22974 |_row, _folded, _window, _cx| div(),
22975 );
22976
22977 editor.insert_creases(Some(crease), cx);
22978 let snapshot = editor.snapshot(window, cx);
22979 let _div =
22980 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
22981 snapshot
22982 })
22983 .unwrap();
22984
22985 let render_args = render_args.lock().take().unwrap();
22986 assert_eq!(render_args.row, MultiBufferRow(1));
22987 assert!(!render_args.folded);
22988 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22989
22990 cx.update_window(*editor, |_, window, cx| {
22991 (render_args.callback)(true, window, cx)
22992 })
22993 .unwrap();
22994 let snapshot = editor
22995 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22996 .unwrap();
22997 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
22998
22999 cx.update_window(*editor, |_, window, cx| {
23000 (render_args.callback)(false, window, cx)
23001 })
23002 .unwrap();
23003 let snapshot = editor
23004 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
23005 .unwrap();
23006 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
23007}
23008
23009#[gpui::test]
23010async fn test_input_text(cx: &mut TestAppContext) {
23011 init_test(cx, |_| {});
23012 let mut cx = EditorTestContext::new(cx).await;
23013
23014 cx.set_state(
23015 &r#"ˇone
23016 two
23017
23018 three
23019 fourˇ
23020 five
23021
23022 siˇx"#
23023 .unindent(),
23024 );
23025
23026 cx.dispatch_action(HandleInput(String::new()));
23027 cx.assert_editor_state(
23028 &r#"ˇone
23029 two
23030
23031 three
23032 fourˇ
23033 five
23034
23035 siˇx"#
23036 .unindent(),
23037 );
23038
23039 cx.dispatch_action(HandleInput("AAAA".to_string()));
23040 cx.assert_editor_state(
23041 &r#"AAAAˇone
23042 two
23043
23044 three
23045 fourAAAAˇ
23046 five
23047
23048 siAAAAˇx"#
23049 .unindent(),
23050 );
23051}
23052
23053#[gpui::test]
23054async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
23055 init_test(cx, |_| {});
23056
23057 let mut cx = EditorTestContext::new(cx).await;
23058 cx.set_state(
23059 r#"let foo = 1;
23060let foo = 2;
23061let foo = 3;
23062let fooˇ = 4;
23063let foo = 5;
23064let foo = 6;
23065let foo = 7;
23066let foo = 8;
23067let foo = 9;
23068let foo = 10;
23069let foo = 11;
23070let foo = 12;
23071let foo = 13;
23072let foo = 14;
23073let foo = 15;"#,
23074 );
23075
23076 cx.update_editor(|e, window, cx| {
23077 assert_eq!(
23078 e.next_scroll_position,
23079 NextScrollCursorCenterTopBottom::Center,
23080 "Default next scroll direction is center",
23081 );
23082
23083 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
23084 assert_eq!(
23085 e.next_scroll_position,
23086 NextScrollCursorCenterTopBottom::Top,
23087 "After center, next scroll direction should be top",
23088 );
23089
23090 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
23091 assert_eq!(
23092 e.next_scroll_position,
23093 NextScrollCursorCenterTopBottom::Bottom,
23094 "After top, next scroll direction should be bottom",
23095 );
23096
23097 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
23098 assert_eq!(
23099 e.next_scroll_position,
23100 NextScrollCursorCenterTopBottom::Center,
23101 "After bottom, scrolling should start over",
23102 );
23103
23104 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
23105 assert_eq!(
23106 e.next_scroll_position,
23107 NextScrollCursorCenterTopBottom::Top,
23108 "Scrolling continues if retriggered fast enough"
23109 );
23110 });
23111
23112 cx.executor()
23113 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
23114 cx.executor().run_until_parked();
23115 cx.update_editor(|e, _, _| {
23116 assert_eq!(
23117 e.next_scroll_position,
23118 NextScrollCursorCenterTopBottom::Center,
23119 "If scrolling is not triggered fast enough, it should reset"
23120 );
23121 });
23122}
23123
23124#[gpui::test]
23125async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
23126 init_test(cx, |_| {});
23127 let mut cx = EditorLspTestContext::new_rust(
23128 lsp::ServerCapabilities {
23129 definition_provider: Some(lsp::OneOf::Left(true)),
23130 references_provider: Some(lsp::OneOf::Left(true)),
23131 ..lsp::ServerCapabilities::default()
23132 },
23133 cx,
23134 )
23135 .await;
23136
23137 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
23138 let go_to_definition = cx
23139 .lsp
23140 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
23141 move |params, _| async move {
23142 if empty_go_to_definition {
23143 Ok(None)
23144 } else {
23145 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
23146 uri: params.text_document_position_params.text_document.uri,
23147 range: lsp::Range::new(
23148 lsp::Position::new(4, 3),
23149 lsp::Position::new(4, 6),
23150 ),
23151 })))
23152 }
23153 },
23154 );
23155 let references = cx
23156 .lsp
23157 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23158 Ok(Some(vec![lsp::Location {
23159 uri: params.text_document_position.text_document.uri,
23160 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
23161 }]))
23162 });
23163 (go_to_definition, references)
23164 };
23165
23166 cx.set_state(
23167 &r#"fn one() {
23168 let mut a = ˇtwo();
23169 }
23170
23171 fn two() {}"#
23172 .unindent(),
23173 );
23174 set_up_lsp_handlers(false, &mut cx);
23175 let navigated = cx
23176 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
23177 .await
23178 .expect("Failed to navigate to definition");
23179 assert_eq!(
23180 navigated,
23181 Navigated::Yes,
23182 "Should have navigated to definition from the GetDefinition response"
23183 );
23184 cx.assert_editor_state(
23185 &r#"fn one() {
23186 let mut a = two();
23187 }
23188
23189 fn «twoˇ»() {}"#
23190 .unindent(),
23191 );
23192
23193 let editors = cx.update_workspace(|workspace, _, cx| {
23194 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23195 });
23196 cx.update_editor(|_, _, test_editor_cx| {
23197 assert_eq!(
23198 editors.len(),
23199 1,
23200 "Initially, only one, test, editor should be open in the workspace"
23201 );
23202 assert_eq!(
23203 test_editor_cx.entity(),
23204 editors.last().expect("Asserted len is 1").clone()
23205 );
23206 });
23207
23208 set_up_lsp_handlers(true, &mut cx);
23209 let navigated = cx
23210 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
23211 .await
23212 .expect("Failed to navigate to lookup references");
23213 assert_eq!(
23214 navigated,
23215 Navigated::Yes,
23216 "Should have navigated to references as a fallback after empty GoToDefinition response"
23217 );
23218 // We should not change the selections in the existing file,
23219 // if opening another milti buffer with the references
23220 cx.assert_editor_state(
23221 &r#"fn one() {
23222 let mut a = two();
23223 }
23224
23225 fn «twoˇ»() {}"#
23226 .unindent(),
23227 );
23228 let editors = cx.update_workspace(|workspace, _, cx| {
23229 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23230 });
23231 cx.update_editor(|_, _, test_editor_cx| {
23232 assert_eq!(
23233 editors.len(),
23234 2,
23235 "After falling back to references search, we open a new editor with the results"
23236 );
23237 let references_fallback_text = editors
23238 .into_iter()
23239 .find(|new_editor| *new_editor != test_editor_cx.entity())
23240 .expect("Should have one non-test editor now")
23241 .read(test_editor_cx)
23242 .text(test_editor_cx);
23243 assert_eq!(
23244 references_fallback_text, "fn one() {\n let mut a = two();\n}",
23245 "Should use the range from the references response and not the GoToDefinition one"
23246 );
23247 });
23248}
23249
23250#[gpui::test]
23251async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
23252 init_test(cx, |_| {});
23253 cx.update(|cx| {
23254 let mut editor_settings = EditorSettings::get_global(cx).clone();
23255 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
23256 EditorSettings::override_global(editor_settings, cx);
23257 });
23258 let mut cx = EditorLspTestContext::new_rust(
23259 lsp::ServerCapabilities {
23260 definition_provider: Some(lsp::OneOf::Left(true)),
23261 references_provider: Some(lsp::OneOf::Left(true)),
23262 ..lsp::ServerCapabilities::default()
23263 },
23264 cx,
23265 )
23266 .await;
23267 let original_state = r#"fn one() {
23268 let mut a = ˇtwo();
23269 }
23270
23271 fn two() {}"#
23272 .unindent();
23273 cx.set_state(&original_state);
23274
23275 let mut go_to_definition = cx
23276 .lsp
23277 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
23278 move |_, _| async move { Ok(None) },
23279 );
23280 let _references = cx
23281 .lsp
23282 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
23283 panic!("Should not call for references with no go to definition fallback")
23284 });
23285
23286 let navigated = cx
23287 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
23288 .await
23289 .expect("Failed to navigate to lookup references");
23290 go_to_definition
23291 .next()
23292 .await
23293 .expect("Should have called the go_to_definition handler");
23294
23295 assert_eq!(
23296 navigated,
23297 Navigated::No,
23298 "Should have navigated to references as a fallback after empty GoToDefinition response"
23299 );
23300 cx.assert_editor_state(&original_state);
23301 let editors = cx.update_workspace(|workspace, _, cx| {
23302 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23303 });
23304 cx.update_editor(|_, _, _| {
23305 assert_eq!(
23306 editors.len(),
23307 1,
23308 "After unsuccessful fallback, no other editor should have been opened"
23309 );
23310 });
23311}
23312
23313#[gpui::test]
23314async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
23315 init_test(cx, |_| {});
23316 let mut cx = EditorLspTestContext::new_rust(
23317 lsp::ServerCapabilities {
23318 references_provider: Some(lsp::OneOf::Left(true)),
23319 ..lsp::ServerCapabilities::default()
23320 },
23321 cx,
23322 )
23323 .await;
23324
23325 cx.set_state(
23326 &r#"
23327 fn one() {
23328 let mut a = two();
23329 }
23330
23331 fn ˇtwo() {}"#
23332 .unindent(),
23333 );
23334 cx.lsp
23335 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23336 Ok(Some(vec![
23337 lsp::Location {
23338 uri: params.text_document_position.text_document.uri.clone(),
23339 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
23340 },
23341 lsp::Location {
23342 uri: params.text_document_position.text_document.uri,
23343 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
23344 },
23345 ]))
23346 });
23347 let navigated = cx
23348 .update_editor(|editor, window, cx| {
23349 editor.find_all_references(&FindAllReferences::default(), window, cx)
23350 })
23351 .unwrap()
23352 .await
23353 .expect("Failed to navigate to references");
23354 assert_eq!(
23355 navigated,
23356 Navigated::Yes,
23357 "Should have navigated to references from the FindAllReferences response"
23358 );
23359 cx.assert_editor_state(
23360 &r#"fn one() {
23361 let mut a = two();
23362 }
23363
23364 fn ˇtwo() {}"#
23365 .unindent(),
23366 );
23367
23368 let editors = cx.update_workspace(|workspace, _, cx| {
23369 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23370 });
23371 cx.update_editor(|_, _, _| {
23372 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
23373 });
23374
23375 cx.set_state(
23376 &r#"fn one() {
23377 let mut a = ˇtwo();
23378 }
23379
23380 fn two() {}"#
23381 .unindent(),
23382 );
23383 let navigated = cx
23384 .update_editor(|editor, window, cx| {
23385 editor.find_all_references(&FindAllReferences::default(), window, cx)
23386 })
23387 .unwrap()
23388 .await
23389 .expect("Failed to navigate to references");
23390 assert_eq!(
23391 navigated,
23392 Navigated::Yes,
23393 "Should have navigated to references from the FindAllReferences response"
23394 );
23395 cx.assert_editor_state(
23396 &r#"fn one() {
23397 let mut a = ˇtwo();
23398 }
23399
23400 fn two() {}"#
23401 .unindent(),
23402 );
23403 let editors = cx.update_workspace(|workspace, _, cx| {
23404 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23405 });
23406 cx.update_editor(|_, _, _| {
23407 assert_eq!(
23408 editors.len(),
23409 2,
23410 "should have re-used the previous multibuffer"
23411 );
23412 });
23413
23414 cx.set_state(
23415 &r#"fn one() {
23416 let mut a = ˇtwo();
23417 }
23418 fn three() {}
23419 fn two() {}"#
23420 .unindent(),
23421 );
23422 cx.lsp
23423 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23424 Ok(Some(vec![
23425 lsp::Location {
23426 uri: params.text_document_position.text_document.uri.clone(),
23427 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
23428 },
23429 lsp::Location {
23430 uri: params.text_document_position.text_document.uri,
23431 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
23432 },
23433 ]))
23434 });
23435 let navigated = cx
23436 .update_editor(|editor, window, cx| {
23437 editor.find_all_references(&FindAllReferences::default(), window, cx)
23438 })
23439 .unwrap()
23440 .await
23441 .expect("Failed to navigate to references");
23442 assert_eq!(
23443 navigated,
23444 Navigated::Yes,
23445 "Should have navigated to references from the FindAllReferences response"
23446 );
23447 cx.assert_editor_state(
23448 &r#"fn one() {
23449 let mut a = ˇtwo();
23450 }
23451 fn three() {}
23452 fn two() {}"#
23453 .unindent(),
23454 );
23455 let editors = cx.update_workspace(|workspace, _, cx| {
23456 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23457 });
23458 cx.update_editor(|_, _, _| {
23459 assert_eq!(
23460 editors.len(),
23461 3,
23462 "should have used a new multibuffer as offsets changed"
23463 );
23464 });
23465}
23466#[gpui::test]
23467async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
23468 init_test(cx, |_| {});
23469
23470 let language = Arc::new(Language::new(
23471 LanguageConfig::default(),
23472 Some(tree_sitter_rust::LANGUAGE.into()),
23473 ));
23474
23475 let text = r#"
23476 #[cfg(test)]
23477 mod tests() {
23478 #[test]
23479 fn runnable_1() {
23480 let a = 1;
23481 }
23482
23483 #[test]
23484 fn runnable_2() {
23485 let a = 1;
23486 let b = 2;
23487 }
23488 }
23489 "#
23490 .unindent();
23491
23492 let fs = FakeFs::new(cx.executor());
23493 fs.insert_file("/file.rs", Default::default()).await;
23494
23495 let project = Project::test(fs, ["/a".as_ref()], cx).await;
23496 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23497 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23498 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
23499 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
23500
23501 let editor = cx.new_window_entity(|window, cx| {
23502 Editor::new(
23503 EditorMode::full(),
23504 multi_buffer,
23505 Some(project.clone()),
23506 window,
23507 cx,
23508 )
23509 });
23510
23511 editor.update_in(cx, |editor, window, cx| {
23512 let snapshot = editor.buffer().read(cx).snapshot(cx);
23513 editor.tasks.insert(
23514 (buffer.read(cx).remote_id(), 3),
23515 RunnableTasks {
23516 templates: vec![],
23517 offset: snapshot.anchor_before(MultiBufferOffset(43)),
23518 column: 0,
23519 extra_variables: HashMap::default(),
23520 context_range: BufferOffset(43)..BufferOffset(85),
23521 },
23522 );
23523 editor.tasks.insert(
23524 (buffer.read(cx).remote_id(), 8),
23525 RunnableTasks {
23526 templates: vec![],
23527 offset: snapshot.anchor_before(MultiBufferOffset(86)),
23528 column: 0,
23529 extra_variables: HashMap::default(),
23530 context_range: BufferOffset(86)..BufferOffset(191),
23531 },
23532 );
23533
23534 // Test finding task when cursor is inside function body
23535 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23536 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
23537 });
23538 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23539 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
23540
23541 // Test finding task when cursor is on function name
23542 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23543 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
23544 });
23545 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23546 assert_eq!(row, 8, "Should find task when cursor is on function name");
23547 });
23548}
23549
23550#[gpui::test]
23551async fn test_folding_buffers(cx: &mut TestAppContext) {
23552 init_test(cx, |_| {});
23553
23554 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23555 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
23556 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
23557
23558 let fs = FakeFs::new(cx.executor());
23559 fs.insert_tree(
23560 path!("/a"),
23561 json!({
23562 "first.rs": sample_text_1,
23563 "second.rs": sample_text_2,
23564 "third.rs": sample_text_3,
23565 }),
23566 )
23567 .await;
23568 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23569 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23570 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23571 let worktree = project.update(cx, |project, cx| {
23572 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23573 assert_eq!(worktrees.len(), 1);
23574 worktrees.pop().unwrap()
23575 });
23576 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23577
23578 let buffer_1 = project
23579 .update(cx, |project, cx| {
23580 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23581 })
23582 .await
23583 .unwrap();
23584 let buffer_2 = project
23585 .update(cx, |project, cx| {
23586 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23587 })
23588 .await
23589 .unwrap();
23590 let buffer_3 = project
23591 .update(cx, |project, cx| {
23592 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23593 })
23594 .await
23595 .unwrap();
23596
23597 let multi_buffer = cx.new(|cx| {
23598 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23599 multi_buffer.push_excerpts(
23600 buffer_1.clone(),
23601 [
23602 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23603 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23604 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23605 ],
23606 cx,
23607 );
23608 multi_buffer.push_excerpts(
23609 buffer_2.clone(),
23610 [
23611 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23612 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23613 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23614 ],
23615 cx,
23616 );
23617 multi_buffer.push_excerpts(
23618 buffer_3.clone(),
23619 [
23620 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23621 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23622 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23623 ],
23624 cx,
23625 );
23626 multi_buffer
23627 });
23628 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23629 Editor::new(
23630 EditorMode::full(),
23631 multi_buffer.clone(),
23632 Some(project.clone()),
23633 window,
23634 cx,
23635 )
23636 });
23637
23638 assert_eq!(
23639 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23640 "\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",
23641 );
23642
23643 multi_buffer_editor.update(cx, |editor, cx| {
23644 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23645 });
23646 assert_eq!(
23647 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23648 "\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",
23649 "After folding the first buffer, its text should not be displayed"
23650 );
23651
23652 multi_buffer_editor.update(cx, |editor, cx| {
23653 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23654 });
23655 assert_eq!(
23656 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23657 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
23658 "After folding the second buffer, its text should not be displayed"
23659 );
23660
23661 multi_buffer_editor.update(cx, |editor, cx| {
23662 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23663 });
23664 assert_eq!(
23665 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23666 "\n\n\n\n\n",
23667 "After folding the third buffer, its text should not be displayed"
23668 );
23669
23670 // Emulate selection inside the fold logic, that should work
23671 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23672 editor
23673 .snapshot(window, cx)
23674 .next_line_boundary(Point::new(0, 4));
23675 });
23676
23677 multi_buffer_editor.update(cx, |editor, cx| {
23678 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23679 });
23680 assert_eq!(
23681 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23682 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
23683 "After unfolding the second buffer, its text should be displayed"
23684 );
23685
23686 // Typing inside of buffer 1 causes that buffer to be unfolded.
23687 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23688 assert_eq!(
23689 multi_buffer
23690 .read(cx)
23691 .snapshot(cx)
23692 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
23693 .collect::<String>(),
23694 "bbbb"
23695 );
23696 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23697 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
23698 });
23699 editor.handle_input("B", window, cx);
23700 });
23701
23702 assert_eq!(
23703 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23704 "\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",
23705 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
23706 );
23707
23708 multi_buffer_editor.update(cx, |editor, cx| {
23709 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23710 });
23711 assert_eq!(
23712 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23713 "\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",
23714 "After unfolding the all buffers, all original text should be displayed"
23715 );
23716}
23717
23718#[gpui::test]
23719async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
23720 init_test(cx, |_| {});
23721
23722 let sample_text_1 = "1111\n2222\n3333".to_string();
23723 let sample_text_2 = "4444\n5555\n6666".to_string();
23724 let sample_text_3 = "7777\n8888\n9999".to_string();
23725
23726 let fs = FakeFs::new(cx.executor());
23727 fs.insert_tree(
23728 path!("/a"),
23729 json!({
23730 "first.rs": sample_text_1,
23731 "second.rs": sample_text_2,
23732 "third.rs": sample_text_3,
23733 }),
23734 )
23735 .await;
23736 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23737 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23738 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23739 let worktree = project.update(cx, |project, cx| {
23740 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23741 assert_eq!(worktrees.len(), 1);
23742 worktrees.pop().unwrap()
23743 });
23744 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23745
23746 let buffer_1 = project
23747 .update(cx, |project, cx| {
23748 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23749 })
23750 .await
23751 .unwrap();
23752 let buffer_2 = project
23753 .update(cx, |project, cx| {
23754 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23755 })
23756 .await
23757 .unwrap();
23758 let buffer_3 = project
23759 .update(cx, |project, cx| {
23760 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23761 })
23762 .await
23763 .unwrap();
23764
23765 let multi_buffer = cx.new(|cx| {
23766 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23767 multi_buffer.push_excerpts(
23768 buffer_1.clone(),
23769 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23770 cx,
23771 );
23772 multi_buffer.push_excerpts(
23773 buffer_2.clone(),
23774 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23775 cx,
23776 );
23777 multi_buffer.push_excerpts(
23778 buffer_3.clone(),
23779 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23780 cx,
23781 );
23782 multi_buffer
23783 });
23784
23785 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23786 Editor::new(
23787 EditorMode::full(),
23788 multi_buffer,
23789 Some(project.clone()),
23790 window,
23791 cx,
23792 )
23793 });
23794
23795 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
23796 assert_eq!(
23797 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23798 full_text,
23799 );
23800
23801 multi_buffer_editor.update(cx, |editor, cx| {
23802 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23803 });
23804 assert_eq!(
23805 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23806 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
23807 "After folding the first buffer, its text should not be displayed"
23808 );
23809
23810 multi_buffer_editor.update(cx, |editor, cx| {
23811 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23812 });
23813
23814 assert_eq!(
23815 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23816 "\n\n\n\n\n\n7777\n8888\n9999",
23817 "After folding the second buffer, its text should not be displayed"
23818 );
23819
23820 multi_buffer_editor.update(cx, |editor, cx| {
23821 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23822 });
23823 assert_eq!(
23824 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23825 "\n\n\n\n\n",
23826 "After folding the third buffer, its text should not be displayed"
23827 );
23828
23829 multi_buffer_editor.update(cx, |editor, cx| {
23830 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23831 });
23832 assert_eq!(
23833 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23834 "\n\n\n\n4444\n5555\n6666\n\n",
23835 "After unfolding the second buffer, its text should be displayed"
23836 );
23837
23838 multi_buffer_editor.update(cx, |editor, cx| {
23839 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
23840 });
23841 assert_eq!(
23842 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23843 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
23844 "After unfolding the first buffer, its text should be displayed"
23845 );
23846
23847 multi_buffer_editor.update(cx, |editor, cx| {
23848 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23849 });
23850 assert_eq!(
23851 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23852 full_text,
23853 "After unfolding all buffers, all original text should be displayed"
23854 );
23855}
23856
23857#[gpui::test]
23858async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
23859 init_test(cx, |_| {});
23860
23861 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23862
23863 let fs = FakeFs::new(cx.executor());
23864 fs.insert_tree(
23865 path!("/a"),
23866 json!({
23867 "main.rs": sample_text,
23868 }),
23869 )
23870 .await;
23871 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23872 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23873 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23874 let worktree = project.update(cx, |project, cx| {
23875 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23876 assert_eq!(worktrees.len(), 1);
23877 worktrees.pop().unwrap()
23878 });
23879 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23880
23881 let buffer_1 = project
23882 .update(cx, |project, cx| {
23883 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23884 })
23885 .await
23886 .unwrap();
23887
23888 let multi_buffer = cx.new(|cx| {
23889 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23890 multi_buffer.push_excerpts(
23891 buffer_1.clone(),
23892 [ExcerptRange::new(
23893 Point::new(0, 0)
23894 ..Point::new(
23895 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
23896 0,
23897 ),
23898 )],
23899 cx,
23900 );
23901 multi_buffer
23902 });
23903 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23904 Editor::new(
23905 EditorMode::full(),
23906 multi_buffer,
23907 Some(project.clone()),
23908 window,
23909 cx,
23910 )
23911 });
23912
23913 let selection_range = Point::new(1, 0)..Point::new(2, 0);
23914 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23915 enum TestHighlight {}
23916 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
23917 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
23918 editor.highlight_text::<TestHighlight>(
23919 vec![highlight_range.clone()],
23920 HighlightStyle::color(Hsla::green()),
23921 cx,
23922 );
23923 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23924 s.select_ranges(Some(highlight_range))
23925 });
23926 });
23927
23928 let full_text = format!("\n\n{sample_text}");
23929 assert_eq!(
23930 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23931 full_text,
23932 );
23933}
23934
23935#[gpui::test]
23936async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
23937 init_test(cx, |_| {});
23938 cx.update(|cx| {
23939 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
23940 "keymaps/default-linux.json",
23941 cx,
23942 )
23943 .unwrap();
23944 cx.bind_keys(default_key_bindings);
23945 });
23946
23947 let (editor, cx) = cx.add_window_view(|window, cx| {
23948 let multi_buffer = MultiBuffer::build_multi(
23949 [
23950 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
23951 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
23952 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
23953 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
23954 ],
23955 cx,
23956 );
23957 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
23958
23959 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
23960 // fold all but the second buffer, so that we test navigating between two
23961 // adjacent folded buffers, as well as folded buffers at the start and
23962 // end the multibuffer
23963 editor.fold_buffer(buffer_ids[0], cx);
23964 editor.fold_buffer(buffer_ids[2], cx);
23965 editor.fold_buffer(buffer_ids[3], cx);
23966
23967 editor
23968 });
23969 cx.simulate_resize(size(px(1000.), px(1000.)));
23970
23971 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
23972 cx.assert_excerpts_with_selections(indoc! {"
23973 [EXCERPT]
23974 ˇ[FOLDED]
23975 [EXCERPT]
23976 a1
23977 b1
23978 [EXCERPT]
23979 [FOLDED]
23980 [EXCERPT]
23981 [FOLDED]
23982 "
23983 });
23984 cx.simulate_keystroke("down");
23985 cx.assert_excerpts_with_selections(indoc! {"
23986 [EXCERPT]
23987 [FOLDED]
23988 [EXCERPT]
23989 ˇa1
23990 b1
23991 [EXCERPT]
23992 [FOLDED]
23993 [EXCERPT]
23994 [FOLDED]
23995 "
23996 });
23997 cx.simulate_keystroke("down");
23998 cx.assert_excerpts_with_selections(indoc! {"
23999 [EXCERPT]
24000 [FOLDED]
24001 [EXCERPT]
24002 a1
24003 ˇb1
24004 [EXCERPT]
24005 [FOLDED]
24006 [EXCERPT]
24007 [FOLDED]
24008 "
24009 });
24010 cx.simulate_keystroke("down");
24011 cx.assert_excerpts_with_selections(indoc! {"
24012 [EXCERPT]
24013 [FOLDED]
24014 [EXCERPT]
24015 a1
24016 b1
24017 ˇ[EXCERPT]
24018 [FOLDED]
24019 [EXCERPT]
24020 [FOLDED]
24021 "
24022 });
24023 cx.simulate_keystroke("down");
24024 cx.assert_excerpts_with_selections(indoc! {"
24025 [EXCERPT]
24026 [FOLDED]
24027 [EXCERPT]
24028 a1
24029 b1
24030 [EXCERPT]
24031 ˇ[FOLDED]
24032 [EXCERPT]
24033 [FOLDED]
24034 "
24035 });
24036 for _ in 0..5 {
24037 cx.simulate_keystroke("down");
24038 cx.assert_excerpts_with_selections(indoc! {"
24039 [EXCERPT]
24040 [FOLDED]
24041 [EXCERPT]
24042 a1
24043 b1
24044 [EXCERPT]
24045 [FOLDED]
24046 [EXCERPT]
24047 ˇ[FOLDED]
24048 "
24049 });
24050 }
24051
24052 cx.simulate_keystroke("up");
24053 cx.assert_excerpts_with_selections(indoc! {"
24054 [EXCERPT]
24055 [FOLDED]
24056 [EXCERPT]
24057 a1
24058 b1
24059 [EXCERPT]
24060 ˇ[FOLDED]
24061 [EXCERPT]
24062 [FOLDED]
24063 "
24064 });
24065 cx.simulate_keystroke("up");
24066 cx.assert_excerpts_with_selections(indoc! {"
24067 [EXCERPT]
24068 [FOLDED]
24069 [EXCERPT]
24070 a1
24071 b1
24072 ˇ[EXCERPT]
24073 [FOLDED]
24074 [EXCERPT]
24075 [FOLDED]
24076 "
24077 });
24078 cx.simulate_keystroke("up");
24079 cx.assert_excerpts_with_selections(indoc! {"
24080 [EXCERPT]
24081 [FOLDED]
24082 [EXCERPT]
24083 a1
24084 ˇb1
24085 [EXCERPT]
24086 [FOLDED]
24087 [EXCERPT]
24088 [FOLDED]
24089 "
24090 });
24091 cx.simulate_keystroke("up");
24092 cx.assert_excerpts_with_selections(indoc! {"
24093 [EXCERPT]
24094 [FOLDED]
24095 [EXCERPT]
24096 ˇa1
24097 b1
24098 [EXCERPT]
24099 [FOLDED]
24100 [EXCERPT]
24101 [FOLDED]
24102 "
24103 });
24104 for _ in 0..5 {
24105 cx.simulate_keystroke("up");
24106 cx.assert_excerpts_with_selections(indoc! {"
24107 [EXCERPT]
24108 ˇ[FOLDED]
24109 [EXCERPT]
24110 a1
24111 b1
24112 [EXCERPT]
24113 [FOLDED]
24114 [EXCERPT]
24115 [FOLDED]
24116 "
24117 });
24118 }
24119}
24120
24121#[gpui::test]
24122async fn test_edit_prediction_text(cx: &mut TestAppContext) {
24123 init_test(cx, |_| {});
24124
24125 // Simple insertion
24126 assert_highlighted_edits(
24127 "Hello, world!",
24128 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
24129 true,
24130 cx,
24131 |highlighted_edits, cx| {
24132 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
24133 assert_eq!(highlighted_edits.highlights.len(), 1);
24134 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
24135 assert_eq!(
24136 highlighted_edits.highlights[0].1.background_color,
24137 Some(cx.theme().status().created_background)
24138 );
24139 },
24140 )
24141 .await;
24142
24143 // Replacement
24144 assert_highlighted_edits(
24145 "This is a test.",
24146 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
24147 false,
24148 cx,
24149 |highlighted_edits, cx| {
24150 assert_eq!(highlighted_edits.text, "That is a test.");
24151 assert_eq!(highlighted_edits.highlights.len(), 1);
24152 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
24153 assert_eq!(
24154 highlighted_edits.highlights[0].1.background_color,
24155 Some(cx.theme().status().created_background)
24156 );
24157 },
24158 )
24159 .await;
24160
24161 // Multiple edits
24162 assert_highlighted_edits(
24163 "Hello, world!",
24164 vec![
24165 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
24166 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
24167 ],
24168 false,
24169 cx,
24170 |highlighted_edits, cx| {
24171 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
24172 assert_eq!(highlighted_edits.highlights.len(), 2);
24173 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
24174 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
24175 assert_eq!(
24176 highlighted_edits.highlights[0].1.background_color,
24177 Some(cx.theme().status().created_background)
24178 );
24179 assert_eq!(
24180 highlighted_edits.highlights[1].1.background_color,
24181 Some(cx.theme().status().created_background)
24182 );
24183 },
24184 )
24185 .await;
24186
24187 // Multiple lines with edits
24188 assert_highlighted_edits(
24189 "First line\nSecond line\nThird line\nFourth line",
24190 vec![
24191 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
24192 (
24193 Point::new(2, 0)..Point::new(2, 10),
24194 "New third line".to_string(),
24195 ),
24196 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
24197 ],
24198 false,
24199 cx,
24200 |highlighted_edits, cx| {
24201 assert_eq!(
24202 highlighted_edits.text,
24203 "Second modified\nNew third line\nFourth updated line"
24204 );
24205 assert_eq!(highlighted_edits.highlights.len(), 3);
24206 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
24207 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
24208 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
24209 for highlight in &highlighted_edits.highlights {
24210 assert_eq!(
24211 highlight.1.background_color,
24212 Some(cx.theme().status().created_background)
24213 );
24214 }
24215 },
24216 )
24217 .await;
24218}
24219
24220#[gpui::test]
24221async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
24222 init_test(cx, |_| {});
24223
24224 // Deletion
24225 assert_highlighted_edits(
24226 "Hello, world!",
24227 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
24228 true,
24229 cx,
24230 |highlighted_edits, cx| {
24231 assert_eq!(highlighted_edits.text, "Hello, world!");
24232 assert_eq!(highlighted_edits.highlights.len(), 1);
24233 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
24234 assert_eq!(
24235 highlighted_edits.highlights[0].1.background_color,
24236 Some(cx.theme().status().deleted_background)
24237 );
24238 },
24239 )
24240 .await;
24241
24242 // Insertion
24243 assert_highlighted_edits(
24244 "Hello, world!",
24245 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
24246 true,
24247 cx,
24248 |highlighted_edits, cx| {
24249 assert_eq!(highlighted_edits.highlights.len(), 1);
24250 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
24251 assert_eq!(
24252 highlighted_edits.highlights[0].1.background_color,
24253 Some(cx.theme().status().created_background)
24254 );
24255 },
24256 )
24257 .await;
24258}
24259
24260async fn assert_highlighted_edits(
24261 text: &str,
24262 edits: Vec<(Range<Point>, String)>,
24263 include_deletions: bool,
24264 cx: &mut TestAppContext,
24265 assertion_fn: impl Fn(HighlightedText, &App),
24266) {
24267 let window = cx.add_window(|window, cx| {
24268 let buffer = MultiBuffer::build_simple(text, cx);
24269 Editor::new(EditorMode::full(), buffer, None, window, cx)
24270 });
24271 let cx = &mut VisualTestContext::from_window(*window, cx);
24272
24273 let (buffer, snapshot) = window
24274 .update(cx, |editor, _window, cx| {
24275 (
24276 editor.buffer().clone(),
24277 editor.buffer().read(cx).snapshot(cx),
24278 )
24279 })
24280 .unwrap();
24281
24282 let edits = edits
24283 .into_iter()
24284 .map(|(range, edit)| {
24285 (
24286 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
24287 edit,
24288 )
24289 })
24290 .collect::<Vec<_>>();
24291
24292 let text_anchor_edits = edits
24293 .clone()
24294 .into_iter()
24295 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
24296 .collect::<Vec<_>>();
24297
24298 let edit_preview = window
24299 .update(cx, |_, _window, cx| {
24300 buffer
24301 .read(cx)
24302 .as_singleton()
24303 .unwrap()
24304 .read(cx)
24305 .preview_edits(text_anchor_edits.into(), cx)
24306 })
24307 .unwrap()
24308 .await;
24309
24310 cx.update(|_window, cx| {
24311 let highlighted_edits = edit_prediction_edit_text(
24312 snapshot.as_singleton().unwrap().2,
24313 &edits,
24314 &edit_preview,
24315 include_deletions,
24316 cx,
24317 );
24318 assertion_fn(highlighted_edits, cx)
24319 });
24320}
24321
24322#[track_caller]
24323fn assert_breakpoint(
24324 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
24325 path: &Arc<Path>,
24326 expected: Vec<(u32, Breakpoint)>,
24327) {
24328 if expected.is_empty() {
24329 assert!(!breakpoints.contains_key(path), "{}", path.display());
24330 } else {
24331 let mut breakpoint = breakpoints
24332 .get(path)
24333 .unwrap()
24334 .iter()
24335 .map(|breakpoint| {
24336 (
24337 breakpoint.row,
24338 Breakpoint {
24339 message: breakpoint.message.clone(),
24340 state: breakpoint.state,
24341 condition: breakpoint.condition.clone(),
24342 hit_condition: breakpoint.hit_condition.clone(),
24343 },
24344 )
24345 })
24346 .collect::<Vec<_>>();
24347
24348 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
24349
24350 assert_eq!(expected, breakpoint);
24351 }
24352}
24353
24354fn add_log_breakpoint_at_cursor(
24355 editor: &mut Editor,
24356 log_message: &str,
24357 window: &mut Window,
24358 cx: &mut Context<Editor>,
24359) {
24360 let (anchor, bp) = editor
24361 .breakpoints_at_cursors(window, cx)
24362 .first()
24363 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
24364 .unwrap_or_else(|| {
24365 let snapshot = editor.snapshot(window, cx);
24366 let cursor_position: Point =
24367 editor.selections.newest(&snapshot.display_snapshot).head();
24368
24369 let breakpoint_position = snapshot
24370 .buffer_snapshot()
24371 .anchor_before(Point::new(cursor_position.row, 0));
24372
24373 (breakpoint_position, Breakpoint::new_log(log_message))
24374 });
24375
24376 editor.edit_breakpoint_at_anchor(
24377 anchor,
24378 bp,
24379 BreakpointEditAction::EditLogMessage(log_message.into()),
24380 cx,
24381 );
24382}
24383
24384#[gpui::test]
24385async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
24386 init_test(cx, |_| {});
24387
24388 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24389 let fs = FakeFs::new(cx.executor());
24390 fs.insert_tree(
24391 path!("/a"),
24392 json!({
24393 "main.rs": sample_text,
24394 }),
24395 )
24396 .await;
24397 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24398 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24399 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24400
24401 let fs = FakeFs::new(cx.executor());
24402 fs.insert_tree(
24403 path!("/a"),
24404 json!({
24405 "main.rs": sample_text,
24406 }),
24407 )
24408 .await;
24409 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24410 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24411 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24412 let worktree_id = workspace
24413 .update(cx, |workspace, _window, cx| {
24414 workspace.project().update(cx, |project, cx| {
24415 project.worktrees(cx).next().unwrap().read(cx).id()
24416 })
24417 })
24418 .unwrap();
24419
24420 let buffer = project
24421 .update(cx, |project, cx| {
24422 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24423 })
24424 .await
24425 .unwrap();
24426
24427 let (editor, cx) = cx.add_window_view(|window, cx| {
24428 Editor::new(
24429 EditorMode::full(),
24430 MultiBuffer::build_from_buffer(buffer, cx),
24431 Some(project.clone()),
24432 window,
24433 cx,
24434 )
24435 });
24436
24437 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24438 let abs_path = project.read_with(cx, |project, cx| {
24439 project
24440 .absolute_path(&project_path, cx)
24441 .map(Arc::from)
24442 .unwrap()
24443 });
24444
24445 // assert we can add breakpoint on the first line
24446 editor.update_in(cx, |editor, window, cx| {
24447 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24448 editor.move_to_end(&MoveToEnd, window, cx);
24449 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24450 });
24451
24452 let breakpoints = editor.update(cx, |editor, cx| {
24453 editor
24454 .breakpoint_store()
24455 .as_ref()
24456 .unwrap()
24457 .read(cx)
24458 .all_source_breakpoints(cx)
24459 });
24460
24461 assert_eq!(1, breakpoints.len());
24462 assert_breakpoint(
24463 &breakpoints,
24464 &abs_path,
24465 vec![
24466 (0, Breakpoint::new_standard()),
24467 (3, Breakpoint::new_standard()),
24468 ],
24469 );
24470
24471 editor.update_in(cx, |editor, window, cx| {
24472 editor.move_to_beginning(&MoveToBeginning, window, cx);
24473 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24474 });
24475
24476 let breakpoints = editor.update(cx, |editor, cx| {
24477 editor
24478 .breakpoint_store()
24479 .as_ref()
24480 .unwrap()
24481 .read(cx)
24482 .all_source_breakpoints(cx)
24483 });
24484
24485 assert_eq!(1, breakpoints.len());
24486 assert_breakpoint(
24487 &breakpoints,
24488 &abs_path,
24489 vec![(3, Breakpoint::new_standard())],
24490 );
24491
24492 editor.update_in(cx, |editor, window, cx| {
24493 editor.move_to_end(&MoveToEnd, window, cx);
24494 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24495 });
24496
24497 let breakpoints = editor.update(cx, |editor, cx| {
24498 editor
24499 .breakpoint_store()
24500 .as_ref()
24501 .unwrap()
24502 .read(cx)
24503 .all_source_breakpoints(cx)
24504 });
24505
24506 assert_eq!(0, breakpoints.len());
24507 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24508}
24509
24510#[gpui::test]
24511async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
24512 init_test(cx, |_| {});
24513
24514 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24515
24516 let fs = FakeFs::new(cx.executor());
24517 fs.insert_tree(
24518 path!("/a"),
24519 json!({
24520 "main.rs": sample_text,
24521 }),
24522 )
24523 .await;
24524 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24525 let (workspace, cx) =
24526 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24527
24528 let worktree_id = workspace.update(cx, |workspace, cx| {
24529 workspace.project().update(cx, |project, cx| {
24530 project.worktrees(cx).next().unwrap().read(cx).id()
24531 })
24532 });
24533
24534 let buffer = project
24535 .update(cx, |project, cx| {
24536 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24537 })
24538 .await
24539 .unwrap();
24540
24541 let (editor, cx) = cx.add_window_view(|window, cx| {
24542 Editor::new(
24543 EditorMode::full(),
24544 MultiBuffer::build_from_buffer(buffer, cx),
24545 Some(project.clone()),
24546 window,
24547 cx,
24548 )
24549 });
24550
24551 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24552 let abs_path = project.read_with(cx, |project, cx| {
24553 project
24554 .absolute_path(&project_path, cx)
24555 .map(Arc::from)
24556 .unwrap()
24557 });
24558
24559 editor.update_in(cx, |editor, window, cx| {
24560 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24561 });
24562
24563 let breakpoints = editor.update(cx, |editor, cx| {
24564 editor
24565 .breakpoint_store()
24566 .as_ref()
24567 .unwrap()
24568 .read(cx)
24569 .all_source_breakpoints(cx)
24570 });
24571
24572 assert_breakpoint(
24573 &breakpoints,
24574 &abs_path,
24575 vec![(0, Breakpoint::new_log("hello world"))],
24576 );
24577
24578 // Removing a log message from a log breakpoint should remove it
24579 editor.update_in(cx, |editor, window, cx| {
24580 add_log_breakpoint_at_cursor(editor, "", 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(&breakpoints, &abs_path, vec![]);
24593
24594 editor.update_in(cx, |editor, window, cx| {
24595 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24596 editor.move_to_end(&MoveToEnd, window, cx);
24597 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24598 // Not adding a log message to a standard breakpoint shouldn't remove it
24599 add_log_breakpoint_at_cursor(editor, "", window, cx);
24600 });
24601
24602 let breakpoints = editor.update(cx, |editor, cx| {
24603 editor
24604 .breakpoint_store()
24605 .as_ref()
24606 .unwrap()
24607 .read(cx)
24608 .all_source_breakpoints(cx)
24609 });
24610
24611 assert_breakpoint(
24612 &breakpoints,
24613 &abs_path,
24614 vec![
24615 (0, Breakpoint::new_standard()),
24616 (3, Breakpoint::new_standard()),
24617 ],
24618 );
24619
24620 editor.update_in(cx, |editor, window, cx| {
24621 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24622 });
24623
24624 let breakpoints = editor.update(cx, |editor, cx| {
24625 editor
24626 .breakpoint_store()
24627 .as_ref()
24628 .unwrap()
24629 .read(cx)
24630 .all_source_breakpoints(cx)
24631 });
24632
24633 assert_breakpoint(
24634 &breakpoints,
24635 &abs_path,
24636 vec![
24637 (0, Breakpoint::new_standard()),
24638 (3, Breakpoint::new_log("hello world")),
24639 ],
24640 );
24641
24642 editor.update_in(cx, |editor, window, cx| {
24643 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
24644 });
24645
24646 let breakpoints = editor.update(cx, |editor, cx| {
24647 editor
24648 .breakpoint_store()
24649 .as_ref()
24650 .unwrap()
24651 .read(cx)
24652 .all_source_breakpoints(cx)
24653 });
24654
24655 assert_breakpoint(
24656 &breakpoints,
24657 &abs_path,
24658 vec![
24659 (0, Breakpoint::new_standard()),
24660 (3, Breakpoint::new_log("hello Earth!!")),
24661 ],
24662 );
24663}
24664
24665/// This also tests that Editor::breakpoint_at_cursor_head is working properly
24666/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
24667/// or when breakpoints were placed out of order. This tests for a regression too
24668#[gpui::test]
24669async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
24670 init_test(cx, |_| {});
24671
24672 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24673 let fs = FakeFs::new(cx.executor());
24674 fs.insert_tree(
24675 path!("/a"),
24676 json!({
24677 "main.rs": sample_text,
24678 }),
24679 )
24680 .await;
24681 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24682 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24683 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24684
24685 let fs = FakeFs::new(cx.executor());
24686 fs.insert_tree(
24687 path!("/a"),
24688 json!({
24689 "main.rs": sample_text,
24690 }),
24691 )
24692 .await;
24693 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24694 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24695 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24696 let worktree_id = workspace
24697 .update(cx, |workspace, _window, cx| {
24698 workspace.project().update(cx, |project, cx| {
24699 project.worktrees(cx).next().unwrap().read(cx).id()
24700 })
24701 })
24702 .unwrap();
24703
24704 let buffer = project
24705 .update(cx, |project, cx| {
24706 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24707 })
24708 .await
24709 .unwrap();
24710
24711 let (editor, cx) = cx.add_window_view(|window, cx| {
24712 Editor::new(
24713 EditorMode::full(),
24714 MultiBuffer::build_from_buffer(buffer, cx),
24715 Some(project.clone()),
24716 window,
24717 cx,
24718 )
24719 });
24720
24721 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24722 let abs_path = project.read_with(cx, |project, cx| {
24723 project
24724 .absolute_path(&project_path, cx)
24725 .map(Arc::from)
24726 .unwrap()
24727 });
24728
24729 // assert we can add breakpoint on the first line
24730 editor.update_in(cx, |editor, window, cx| {
24731 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24732 editor.move_to_end(&MoveToEnd, window, cx);
24733 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24734 editor.move_up(&MoveUp, window, cx);
24735 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24736 });
24737
24738 let breakpoints = editor.update(cx, |editor, cx| {
24739 editor
24740 .breakpoint_store()
24741 .as_ref()
24742 .unwrap()
24743 .read(cx)
24744 .all_source_breakpoints(cx)
24745 });
24746
24747 assert_eq!(1, breakpoints.len());
24748 assert_breakpoint(
24749 &breakpoints,
24750 &abs_path,
24751 vec![
24752 (0, Breakpoint::new_standard()),
24753 (2, Breakpoint::new_standard()),
24754 (3, Breakpoint::new_standard()),
24755 ],
24756 );
24757
24758 editor.update_in(cx, |editor, window, cx| {
24759 editor.move_to_beginning(&MoveToBeginning, window, cx);
24760 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24761 editor.move_to_end(&MoveToEnd, window, cx);
24762 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24763 // Disabling a breakpoint that doesn't exist should do nothing
24764 editor.move_up(&MoveUp, window, cx);
24765 editor.move_up(&MoveUp, window, cx);
24766 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24767 });
24768
24769 let breakpoints = editor.update(cx, |editor, cx| {
24770 editor
24771 .breakpoint_store()
24772 .as_ref()
24773 .unwrap()
24774 .read(cx)
24775 .all_source_breakpoints(cx)
24776 });
24777
24778 let disable_breakpoint = {
24779 let mut bp = Breakpoint::new_standard();
24780 bp.state = BreakpointState::Disabled;
24781 bp
24782 };
24783
24784 assert_eq!(1, breakpoints.len());
24785 assert_breakpoint(
24786 &breakpoints,
24787 &abs_path,
24788 vec![
24789 (0, disable_breakpoint.clone()),
24790 (2, Breakpoint::new_standard()),
24791 (3, disable_breakpoint.clone()),
24792 ],
24793 );
24794
24795 editor.update_in(cx, |editor, window, cx| {
24796 editor.move_to_beginning(&MoveToBeginning, window, cx);
24797 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24798 editor.move_to_end(&MoveToEnd, window, cx);
24799 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24800 editor.move_up(&MoveUp, window, cx);
24801 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24802 });
24803
24804 let breakpoints = editor.update(cx, |editor, cx| {
24805 editor
24806 .breakpoint_store()
24807 .as_ref()
24808 .unwrap()
24809 .read(cx)
24810 .all_source_breakpoints(cx)
24811 });
24812
24813 assert_eq!(1, breakpoints.len());
24814 assert_breakpoint(
24815 &breakpoints,
24816 &abs_path,
24817 vec![
24818 (0, Breakpoint::new_standard()),
24819 (2, disable_breakpoint),
24820 (3, Breakpoint::new_standard()),
24821 ],
24822 );
24823}
24824
24825#[gpui::test]
24826async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
24827 init_test(cx, |_| {});
24828 let capabilities = lsp::ServerCapabilities {
24829 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
24830 prepare_provider: Some(true),
24831 work_done_progress_options: Default::default(),
24832 })),
24833 ..Default::default()
24834 };
24835 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24836
24837 cx.set_state(indoc! {"
24838 struct Fˇoo {}
24839 "});
24840
24841 cx.update_editor(|editor, _, cx| {
24842 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24843 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24844 editor.highlight_background::<DocumentHighlightRead>(
24845 &[highlight_range],
24846 |_, theme| theme.colors().editor_document_highlight_read_background,
24847 cx,
24848 );
24849 });
24850
24851 let mut prepare_rename_handler = cx
24852 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
24853 move |_, _, _| async move {
24854 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
24855 start: lsp::Position {
24856 line: 0,
24857 character: 7,
24858 },
24859 end: lsp::Position {
24860 line: 0,
24861 character: 10,
24862 },
24863 })))
24864 },
24865 );
24866 let prepare_rename_task = cx
24867 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24868 .expect("Prepare rename was not started");
24869 prepare_rename_handler.next().await.unwrap();
24870 prepare_rename_task.await.expect("Prepare rename failed");
24871
24872 let mut rename_handler =
24873 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24874 let edit = lsp::TextEdit {
24875 range: lsp::Range {
24876 start: lsp::Position {
24877 line: 0,
24878 character: 7,
24879 },
24880 end: lsp::Position {
24881 line: 0,
24882 character: 10,
24883 },
24884 },
24885 new_text: "FooRenamed".to_string(),
24886 };
24887 Ok(Some(lsp::WorkspaceEdit::new(
24888 // Specify the same edit twice
24889 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
24890 )))
24891 });
24892 let rename_task = cx
24893 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24894 .expect("Confirm rename was not started");
24895 rename_handler.next().await.unwrap();
24896 rename_task.await.expect("Confirm rename failed");
24897 cx.run_until_parked();
24898
24899 // Despite two edits, only one is actually applied as those are identical
24900 cx.assert_editor_state(indoc! {"
24901 struct FooRenamedˇ {}
24902 "});
24903}
24904
24905#[gpui::test]
24906async fn test_rename_without_prepare(cx: &mut TestAppContext) {
24907 init_test(cx, |_| {});
24908 // These capabilities indicate that the server does not support prepare rename.
24909 let capabilities = lsp::ServerCapabilities {
24910 rename_provider: Some(lsp::OneOf::Left(true)),
24911 ..Default::default()
24912 };
24913 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24914
24915 cx.set_state(indoc! {"
24916 struct Fˇoo {}
24917 "});
24918
24919 cx.update_editor(|editor, _window, cx| {
24920 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24921 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24922 editor.highlight_background::<DocumentHighlightRead>(
24923 &[highlight_range],
24924 |_, theme| theme.colors().editor_document_highlight_read_background,
24925 cx,
24926 );
24927 });
24928
24929 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24930 .expect("Prepare rename was not started")
24931 .await
24932 .expect("Prepare rename failed");
24933
24934 let mut rename_handler =
24935 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24936 let edit = lsp::TextEdit {
24937 range: lsp::Range {
24938 start: lsp::Position {
24939 line: 0,
24940 character: 7,
24941 },
24942 end: lsp::Position {
24943 line: 0,
24944 character: 10,
24945 },
24946 },
24947 new_text: "FooRenamed".to_string(),
24948 };
24949 Ok(Some(lsp::WorkspaceEdit::new(
24950 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
24951 )))
24952 });
24953 let rename_task = cx
24954 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24955 .expect("Confirm rename was not started");
24956 rename_handler.next().await.unwrap();
24957 rename_task.await.expect("Confirm rename failed");
24958 cx.run_until_parked();
24959
24960 // Correct range is renamed, as `surrounding_word` is used to find it.
24961 cx.assert_editor_state(indoc! {"
24962 struct FooRenamedˇ {}
24963 "});
24964}
24965
24966#[gpui::test]
24967async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
24968 init_test(cx, |_| {});
24969 let mut cx = EditorTestContext::new(cx).await;
24970
24971 let language = Arc::new(
24972 Language::new(
24973 LanguageConfig::default(),
24974 Some(tree_sitter_html::LANGUAGE.into()),
24975 )
24976 .with_brackets_query(
24977 r#"
24978 ("<" @open "/>" @close)
24979 ("</" @open ">" @close)
24980 ("<" @open ">" @close)
24981 ("\"" @open "\"" @close)
24982 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
24983 "#,
24984 )
24985 .unwrap(),
24986 );
24987 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24988
24989 cx.set_state(indoc! {"
24990 <span>ˇ</span>
24991 "});
24992 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24993 cx.assert_editor_state(indoc! {"
24994 <span>
24995 ˇ
24996 </span>
24997 "});
24998
24999 cx.set_state(indoc! {"
25000 <span><span></span>ˇ</span>
25001 "});
25002 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
25003 cx.assert_editor_state(indoc! {"
25004 <span><span></span>
25005 ˇ</span>
25006 "});
25007
25008 cx.set_state(indoc! {"
25009 <span>ˇ
25010 </span>
25011 "});
25012 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
25013 cx.assert_editor_state(indoc! {"
25014 <span>
25015 ˇ
25016 </span>
25017 "});
25018}
25019
25020#[gpui::test(iterations = 10)]
25021async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
25022 init_test(cx, |_| {});
25023
25024 let fs = FakeFs::new(cx.executor());
25025 fs.insert_tree(
25026 path!("/dir"),
25027 json!({
25028 "a.ts": "a",
25029 }),
25030 )
25031 .await;
25032
25033 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
25034 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25035 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
25036
25037 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25038 language_registry.add(Arc::new(Language::new(
25039 LanguageConfig {
25040 name: "TypeScript".into(),
25041 matcher: LanguageMatcher {
25042 path_suffixes: vec!["ts".to_string()],
25043 ..Default::default()
25044 },
25045 ..Default::default()
25046 },
25047 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
25048 )));
25049 let mut fake_language_servers = language_registry.register_fake_lsp(
25050 "TypeScript",
25051 FakeLspAdapter {
25052 capabilities: lsp::ServerCapabilities {
25053 code_lens_provider: Some(lsp::CodeLensOptions {
25054 resolve_provider: Some(true),
25055 }),
25056 execute_command_provider: Some(lsp::ExecuteCommandOptions {
25057 commands: vec!["_the/command".to_string()],
25058 ..lsp::ExecuteCommandOptions::default()
25059 }),
25060 ..lsp::ServerCapabilities::default()
25061 },
25062 ..FakeLspAdapter::default()
25063 },
25064 );
25065
25066 let editor = workspace
25067 .update(cx, |workspace, window, cx| {
25068 workspace.open_abs_path(
25069 PathBuf::from(path!("/dir/a.ts")),
25070 OpenOptions::default(),
25071 window,
25072 cx,
25073 )
25074 })
25075 .unwrap()
25076 .await
25077 .unwrap()
25078 .downcast::<Editor>()
25079 .unwrap();
25080 cx.executor().run_until_parked();
25081
25082 let fake_server = fake_language_servers.next().await.unwrap();
25083
25084 let buffer = editor.update(cx, |editor, cx| {
25085 editor
25086 .buffer()
25087 .read(cx)
25088 .as_singleton()
25089 .expect("have opened a single file by path")
25090 });
25091
25092 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
25093 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
25094 drop(buffer_snapshot);
25095 let actions = cx
25096 .update_window(*workspace, |_, window, cx| {
25097 project.code_actions(&buffer, anchor..anchor, window, cx)
25098 })
25099 .unwrap();
25100
25101 fake_server
25102 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
25103 Ok(Some(vec![
25104 lsp::CodeLens {
25105 range: lsp::Range::default(),
25106 command: Some(lsp::Command {
25107 title: "Code lens command".to_owned(),
25108 command: "_the/command".to_owned(),
25109 arguments: None,
25110 }),
25111 data: None,
25112 },
25113 lsp::CodeLens {
25114 range: lsp::Range::default(),
25115 command: Some(lsp::Command {
25116 title: "Command not in capabilities".to_owned(),
25117 command: "not in capabilities".to_owned(),
25118 arguments: None,
25119 }),
25120 data: None,
25121 },
25122 lsp::CodeLens {
25123 range: lsp::Range {
25124 start: lsp::Position {
25125 line: 1,
25126 character: 1,
25127 },
25128 end: lsp::Position {
25129 line: 1,
25130 character: 1,
25131 },
25132 },
25133 command: Some(lsp::Command {
25134 title: "Command not in range".to_owned(),
25135 command: "_the/command".to_owned(),
25136 arguments: None,
25137 }),
25138 data: None,
25139 },
25140 ]))
25141 })
25142 .next()
25143 .await;
25144
25145 let actions = actions.await.unwrap();
25146 assert_eq!(
25147 actions.len(),
25148 1,
25149 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
25150 );
25151 let action = actions[0].clone();
25152 let apply = project.update(cx, |project, cx| {
25153 project.apply_code_action(buffer.clone(), action, true, cx)
25154 });
25155
25156 // Resolving the code action does not populate its edits. In absence of
25157 // edits, we must execute the given command.
25158 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
25159 |mut lens, _| async move {
25160 let lens_command = lens.command.as_mut().expect("should have a command");
25161 assert_eq!(lens_command.title, "Code lens command");
25162 lens_command.arguments = Some(vec![json!("the-argument")]);
25163 Ok(lens)
25164 },
25165 );
25166
25167 // While executing the command, the language server sends the editor
25168 // a `workspaceEdit` request.
25169 fake_server
25170 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
25171 let fake = fake_server.clone();
25172 move |params, _| {
25173 assert_eq!(params.command, "_the/command");
25174 let fake = fake.clone();
25175 async move {
25176 fake.server
25177 .request::<lsp::request::ApplyWorkspaceEdit>(
25178 lsp::ApplyWorkspaceEditParams {
25179 label: None,
25180 edit: lsp::WorkspaceEdit {
25181 changes: Some(
25182 [(
25183 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
25184 vec![lsp::TextEdit {
25185 range: lsp::Range::new(
25186 lsp::Position::new(0, 0),
25187 lsp::Position::new(0, 0),
25188 ),
25189 new_text: "X".into(),
25190 }],
25191 )]
25192 .into_iter()
25193 .collect(),
25194 ),
25195 ..lsp::WorkspaceEdit::default()
25196 },
25197 },
25198 )
25199 .await
25200 .into_response()
25201 .unwrap();
25202 Ok(Some(json!(null)))
25203 }
25204 }
25205 })
25206 .next()
25207 .await;
25208
25209 // Applying the code lens command returns a project transaction containing the edits
25210 // sent by the language server in its `workspaceEdit` request.
25211 let transaction = apply.await.unwrap();
25212 assert!(transaction.0.contains_key(&buffer));
25213 buffer.update(cx, |buffer, cx| {
25214 assert_eq!(buffer.text(), "Xa");
25215 buffer.undo(cx);
25216 assert_eq!(buffer.text(), "a");
25217 });
25218
25219 let actions_after_edits = cx
25220 .update_window(*workspace, |_, window, cx| {
25221 project.code_actions(&buffer, anchor..anchor, window, cx)
25222 })
25223 .unwrap()
25224 .await
25225 .unwrap();
25226 assert_eq!(
25227 actions, actions_after_edits,
25228 "For the same selection, same code lens actions should be returned"
25229 );
25230
25231 let _responses =
25232 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
25233 panic!("No more code lens requests are expected");
25234 });
25235 editor.update_in(cx, |editor, window, cx| {
25236 editor.select_all(&SelectAll, window, cx);
25237 });
25238 cx.executor().run_until_parked();
25239 let new_actions = cx
25240 .update_window(*workspace, |_, window, cx| {
25241 project.code_actions(&buffer, anchor..anchor, window, cx)
25242 })
25243 .unwrap()
25244 .await
25245 .unwrap();
25246 assert_eq!(
25247 actions, new_actions,
25248 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
25249 );
25250}
25251
25252#[gpui::test]
25253async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
25254 init_test(cx, |_| {});
25255
25256 let fs = FakeFs::new(cx.executor());
25257 let main_text = r#"fn main() {
25258println!("1");
25259println!("2");
25260println!("3");
25261println!("4");
25262println!("5");
25263}"#;
25264 let lib_text = "mod foo {}";
25265 fs.insert_tree(
25266 path!("/a"),
25267 json!({
25268 "lib.rs": lib_text,
25269 "main.rs": main_text,
25270 }),
25271 )
25272 .await;
25273
25274 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25275 let (workspace, cx) =
25276 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25277 let worktree_id = workspace.update(cx, |workspace, cx| {
25278 workspace.project().update(cx, |project, cx| {
25279 project.worktrees(cx).next().unwrap().read(cx).id()
25280 })
25281 });
25282
25283 let expected_ranges = vec![
25284 Point::new(0, 0)..Point::new(0, 0),
25285 Point::new(1, 0)..Point::new(1, 1),
25286 Point::new(2, 0)..Point::new(2, 2),
25287 Point::new(3, 0)..Point::new(3, 3),
25288 ];
25289
25290 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25291 let editor_1 = workspace
25292 .update_in(cx, |workspace, window, cx| {
25293 workspace.open_path(
25294 (worktree_id, rel_path("main.rs")),
25295 Some(pane_1.downgrade()),
25296 true,
25297 window,
25298 cx,
25299 )
25300 })
25301 .unwrap()
25302 .await
25303 .downcast::<Editor>()
25304 .unwrap();
25305 pane_1.update(cx, |pane, cx| {
25306 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25307 open_editor.update(cx, |editor, cx| {
25308 assert_eq!(
25309 editor.display_text(cx),
25310 main_text,
25311 "Original main.rs text on initial open",
25312 );
25313 assert_eq!(
25314 editor
25315 .selections
25316 .all::<Point>(&editor.display_snapshot(cx))
25317 .into_iter()
25318 .map(|s| s.range())
25319 .collect::<Vec<_>>(),
25320 vec![Point::zero()..Point::zero()],
25321 "Default selections on initial open",
25322 );
25323 })
25324 });
25325 editor_1.update_in(cx, |editor, window, cx| {
25326 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25327 s.select_ranges(expected_ranges.clone());
25328 });
25329 });
25330
25331 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
25332 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
25333 });
25334 let editor_2 = workspace
25335 .update_in(cx, |workspace, window, cx| {
25336 workspace.open_path(
25337 (worktree_id, rel_path("main.rs")),
25338 Some(pane_2.downgrade()),
25339 true,
25340 window,
25341 cx,
25342 )
25343 })
25344 .unwrap()
25345 .await
25346 .downcast::<Editor>()
25347 .unwrap();
25348 pane_2.update(cx, |pane, cx| {
25349 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25350 open_editor.update(cx, |editor, cx| {
25351 assert_eq!(
25352 editor.display_text(cx),
25353 main_text,
25354 "Original main.rs text on initial open in another panel",
25355 );
25356 assert_eq!(
25357 editor
25358 .selections
25359 .all::<Point>(&editor.display_snapshot(cx))
25360 .into_iter()
25361 .map(|s| s.range())
25362 .collect::<Vec<_>>(),
25363 vec![Point::zero()..Point::zero()],
25364 "Default selections on initial open in another panel",
25365 );
25366 })
25367 });
25368
25369 editor_2.update_in(cx, |editor, window, cx| {
25370 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
25371 });
25372
25373 let _other_editor_1 = workspace
25374 .update_in(cx, |workspace, window, cx| {
25375 workspace.open_path(
25376 (worktree_id, rel_path("lib.rs")),
25377 Some(pane_1.downgrade()),
25378 true,
25379 window,
25380 cx,
25381 )
25382 })
25383 .unwrap()
25384 .await
25385 .downcast::<Editor>()
25386 .unwrap();
25387 pane_1
25388 .update_in(cx, |pane, window, cx| {
25389 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
25390 })
25391 .await
25392 .unwrap();
25393 drop(editor_1);
25394 pane_1.update(cx, |pane, cx| {
25395 pane.active_item()
25396 .unwrap()
25397 .downcast::<Editor>()
25398 .unwrap()
25399 .update(cx, |editor, cx| {
25400 assert_eq!(
25401 editor.display_text(cx),
25402 lib_text,
25403 "Other file should be open and active",
25404 );
25405 });
25406 assert_eq!(pane.items().count(), 1, "No other editors should be open");
25407 });
25408
25409 let _other_editor_2 = workspace
25410 .update_in(cx, |workspace, window, cx| {
25411 workspace.open_path(
25412 (worktree_id, rel_path("lib.rs")),
25413 Some(pane_2.downgrade()),
25414 true,
25415 window,
25416 cx,
25417 )
25418 })
25419 .unwrap()
25420 .await
25421 .downcast::<Editor>()
25422 .unwrap();
25423 pane_2
25424 .update_in(cx, |pane, window, cx| {
25425 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
25426 })
25427 .await
25428 .unwrap();
25429 drop(editor_2);
25430 pane_2.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 lib_text,
25436 "Other file should be open and active in another panel too",
25437 );
25438 });
25439 assert_eq!(
25440 pane.items().count(),
25441 1,
25442 "No other editors should be open in another pane",
25443 );
25444 });
25445
25446 let _editor_1_reopened = workspace
25447 .update_in(cx, |workspace, window, cx| {
25448 workspace.open_path(
25449 (worktree_id, rel_path("main.rs")),
25450 Some(pane_1.downgrade()),
25451 true,
25452 window,
25453 cx,
25454 )
25455 })
25456 .unwrap()
25457 .await
25458 .downcast::<Editor>()
25459 .unwrap();
25460 let _editor_2_reopened = workspace
25461 .update_in(cx, |workspace, window, cx| {
25462 workspace.open_path(
25463 (worktree_id, rel_path("main.rs")),
25464 Some(pane_2.downgrade()),
25465 true,
25466 window,
25467 cx,
25468 )
25469 })
25470 .unwrap()
25471 .await
25472 .downcast::<Editor>()
25473 .unwrap();
25474 pane_1.update(cx, |pane, cx| {
25475 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25476 open_editor.update(cx, |editor, cx| {
25477 assert_eq!(
25478 editor.display_text(cx),
25479 main_text,
25480 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
25481 );
25482 assert_eq!(
25483 editor
25484 .selections
25485 .all::<Point>(&editor.display_snapshot(cx))
25486 .into_iter()
25487 .map(|s| s.range())
25488 .collect::<Vec<_>>(),
25489 expected_ranges,
25490 "Previous editor in the 1st panel had selections and should get them restored on reopen",
25491 );
25492 })
25493 });
25494 pane_2.update(cx, |pane, cx| {
25495 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25496 open_editor.update(cx, |editor, cx| {
25497 assert_eq!(
25498 editor.display_text(cx),
25499 r#"fn main() {
25500⋯rintln!("1");
25501⋯intln!("2");
25502⋯ntln!("3");
25503println!("4");
25504println!("5");
25505}"#,
25506 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
25507 );
25508 assert_eq!(
25509 editor
25510 .selections
25511 .all::<Point>(&editor.display_snapshot(cx))
25512 .into_iter()
25513 .map(|s| s.range())
25514 .collect::<Vec<_>>(),
25515 vec![Point::zero()..Point::zero()],
25516 "Previous editor in the 2nd pane had no selections changed hence should restore none",
25517 );
25518 })
25519 });
25520}
25521
25522#[gpui::test]
25523async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
25524 init_test(cx, |_| {});
25525
25526 let fs = FakeFs::new(cx.executor());
25527 let main_text = r#"fn main() {
25528println!("1");
25529println!("2");
25530println!("3");
25531println!("4");
25532println!("5");
25533}"#;
25534 let lib_text = "mod foo {}";
25535 fs.insert_tree(
25536 path!("/a"),
25537 json!({
25538 "lib.rs": lib_text,
25539 "main.rs": main_text,
25540 }),
25541 )
25542 .await;
25543
25544 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25545 let (workspace, cx) =
25546 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25547 let worktree_id = workspace.update(cx, |workspace, cx| {
25548 workspace.project().update(cx, |project, cx| {
25549 project.worktrees(cx).next().unwrap().read(cx).id()
25550 })
25551 });
25552
25553 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25554 let editor = workspace
25555 .update_in(cx, |workspace, window, cx| {
25556 workspace.open_path(
25557 (worktree_id, rel_path("main.rs")),
25558 Some(pane.downgrade()),
25559 true,
25560 window,
25561 cx,
25562 )
25563 })
25564 .unwrap()
25565 .await
25566 .downcast::<Editor>()
25567 .unwrap();
25568 pane.update(cx, |pane, cx| {
25569 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25570 open_editor.update(cx, |editor, cx| {
25571 assert_eq!(
25572 editor.display_text(cx),
25573 main_text,
25574 "Original main.rs text on initial open",
25575 );
25576 })
25577 });
25578 editor.update_in(cx, |editor, window, cx| {
25579 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
25580 });
25581
25582 cx.update_global(|store: &mut SettingsStore, cx| {
25583 store.update_user_settings(cx, |s| {
25584 s.workspace.restore_on_file_reopen = Some(false);
25585 });
25586 });
25587 editor.update_in(cx, |editor, window, cx| {
25588 editor.fold_ranges(
25589 vec![
25590 Point::new(1, 0)..Point::new(1, 1),
25591 Point::new(2, 0)..Point::new(2, 2),
25592 Point::new(3, 0)..Point::new(3, 3),
25593 ],
25594 false,
25595 window,
25596 cx,
25597 );
25598 });
25599 pane.update_in(cx, |pane, window, cx| {
25600 pane.close_all_items(&CloseAllItems::default(), window, cx)
25601 })
25602 .await
25603 .unwrap();
25604 pane.update(cx, |pane, _| {
25605 assert!(pane.active_item().is_none());
25606 });
25607 cx.update_global(|store: &mut SettingsStore, cx| {
25608 store.update_user_settings(cx, |s| {
25609 s.workspace.restore_on_file_reopen = Some(true);
25610 });
25611 });
25612
25613 let _editor_reopened = workspace
25614 .update_in(cx, |workspace, window, cx| {
25615 workspace.open_path(
25616 (worktree_id, rel_path("main.rs")),
25617 Some(pane.downgrade()),
25618 true,
25619 window,
25620 cx,
25621 )
25622 })
25623 .unwrap()
25624 .await
25625 .downcast::<Editor>()
25626 .unwrap();
25627 pane.update(cx, |pane, cx| {
25628 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25629 open_editor.update(cx, |editor, cx| {
25630 assert_eq!(
25631 editor.display_text(cx),
25632 main_text,
25633 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
25634 );
25635 })
25636 });
25637}
25638
25639#[gpui::test]
25640async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
25641 struct EmptyModalView {
25642 focus_handle: gpui::FocusHandle,
25643 }
25644 impl EventEmitter<DismissEvent> for EmptyModalView {}
25645 impl Render for EmptyModalView {
25646 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
25647 div()
25648 }
25649 }
25650 impl Focusable for EmptyModalView {
25651 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
25652 self.focus_handle.clone()
25653 }
25654 }
25655 impl workspace::ModalView for EmptyModalView {}
25656 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
25657 EmptyModalView {
25658 focus_handle: cx.focus_handle(),
25659 }
25660 }
25661
25662 init_test(cx, |_| {});
25663
25664 let fs = FakeFs::new(cx.executor());
25665 let project = Project::test(fs, [], cx).await;
25666 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25667 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
25668 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
25669 let editor = cx.new_window_entity(|window, cx| {
25670 Editor::new(
25671 EditorMode::full(),
25672 buffer,
25673 Some(project.clone()),
25674 window,
25675 cx,
25676 )
25677 });
25678 workspace
25679 .update(cx, |workspace, window, cx| {
25680 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
25681 })
25682 .unwrap();
25683 editor.update_in(cx, |editor, window, cx| {
25684 editor.open_context_menu(&OpenContextMenu, window, cx);
25685 assert!(editor.mouse_context_menu.is_some());
25686 });
25687 workspace
25688 .update(cx, |workspace, window, cx| {
25689 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
25690 })
25691 .unwrap();
25692 cx.read(|cx| {
25693 assert!(editor.read(cx).mouse_context_menu.is_none());
25694 });
25695}
25696
25697fn set_linked_edit_ranges(
25698 opening: (Point, Point),
25699 closing: (Point, Point),
25700 editor: &mut Editor,
25701 cx: &mut Context<Editor>,
25702) {
25703 let Some((buffer, _)) = editor
25704 .buffer
25705 .read(cx)
25706 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
25707 else {
25708 panic!("Failed to get buffer for selection position");
25709 };
25710 let buffer = buffer.read(cx);
25711 let buffer_id = buffer.remote_id();
25712 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
25713 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
25714 let mut linked_ranges = HashMap::default();
25715 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
25716 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
25717}
25718
25719#[gpui::test]
25720async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
25721 init_test(cx, |_| {});
25722
25723 let fs = FakeFs::new(cx.executor());
25724 fs.insert_file(path!("/file.html"), Default::default())
25725 .await;
25726
25727 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
25728
25729 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25730 let html_language = Arc::new(Language::new(
25731 LanguageConfig {
25732 name: "HTML".into(),
25733 matcher: LanguageMatcher {
25734 path_suffixes: vec!["html".to_string()],
25735 ..LanguageMatcher::default()
25736 },
25737 brackets: BracketPairConfig {
25738 pairs: vec![BracketPair {
25739 start: "<".into(),
25740 end: ">".into(),
25741 close: true,
25742 ..Default::default()
25743 }],
25744 ..Default::default()
25745 },
25746 ..Default::default()
25747 },
25748 Some(tree_sitter_html::LANGUAGE.into()),
25749 ));
25750 language_registry.add(html_language);
25751 let mut fake_servers = language_registry.register_fake_lsp(
25752 "HTML",
25753 FakeLspAdapter {
25754 capabilities: lsp::ServerCapabilities {
25755 completion_provider: Some(lsp::CompletionOptions {
25756 resolve_provider: Some(true),
25757 ..Default::default()
25758 }),
25759 ..Default::default()
25760 },
25761 ..Default::default()
25762 },
25763 );
25764
25765 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25766 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25767
25768 let worktree_id = workspace
25769 .update(cx, |workspace, _window, cx| {
25770 workspace.project().update(cx, |project, cx| {
25771 project.worktrees(cx).next().unwrap().read(cx).id()
25772 })
25773 })
25774 .unwrap();
25775 project
25776 .update(cx, |project, cx| {
25777 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
25778 })
25779 .await
25780 .unwrap();
25781 let editor = workspace
25782 .update(cx, |workspace, window, cx| {
25783 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
25784 })
25785 .unwrap()
25786 .await
25787 .unwrap()
25788 .downcast::<Editor>()
25789 .unwrap();
25790
25791 let fake_server = fake_servers.next().await.unwrap();
25792 cx.run_until_parked();
25793 editor.update_in(cx, |editor, window, cx| {
25794 editor.set_text("<ad></ad>", window, cx);
25795 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
25796 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
25797 });
25798 set_linked_edit_ranges(
25799 (Point::new(0, 1), Point::new(0, 3)),
25800 (Point::new(0, 6), Point::new(0, 8)),
25801 editor,
25802 cx,
25803 );
25804 });
25805 let mut completion_handle =
25806 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25807 Ok(Some(lsp::CompletionResponse::Array(vec![
25808 lsp::CompletionItem {
25809 label: "head".to_string(),
25810 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25811 lsp::InsertReplaceEdit {
25812 new_text: "head".to_string(),
25813 insert: lsp::Range::new(
25814 lsp::Position::new(0, 1),
25815 lsp::Position::new(0, 3),
25816 ),
25817 replace: lsp::Range::new(
25818 lsp::Position::new(0, 1),
25819 lsp::Position::new(0, 3),
25820 ),
25821 },
25822 )),
25823 ..Default::default()
25824 },
25825 ])))
25826 });
25827 editor.update_in(cx, |editor, window, cx| {
25828 editor.show_completions(&ShowCompletions, window, cx);
25829 });
25830 cx.run_until_parked();
25831 completion_handle.next().await.unwrap();
25832 editor.update(cx, |editor, _| {
25833 assert!(
25834 editor.context_menu_visible(),
25835 "Completion menu should be visible"
25836 );
25837 });
25838 editor.update_in(cx, |editor, window, cx| {
25839 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
25840 });
25841 cx.executor().run_until_parked();
25842 editor.update(cx, |editor, cx| {
25843 assert_eq!(editor.text(cx), "<head></head>");
25844 });
25845}
25846
25847#[gpui::test]
25848async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
25849 init_test(cx, |_| {});
25850
25851 let mut cx = EditorTestContext::new(cx).await;
25852 let language = Arc::new(Language::new(
25853 LanguageConfig {
25854 name: "TSX".into(),
25855 matcher: LanguageMatcher {
25856 path_suffixes: vec!["tsx".to_string()],
25857 ..LanguageMatcher::default()
25858 },
25859 brackets: BracketPairConfig {
25860 pairs: vec![BracketPair {
25861 start: "<".into(),
25862 end: ">".into(),
25863 close: true,
25864 ..Default::default()
25865 }],
25866 ..Default::default()
25867 },
25868 linked_edit_characters: HashSet::from_iter(['.']),
25869 ..Default::default()
25870 },
25871 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
25872 ));
25873 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25874
25875 // Test typing > does not extend linked pair
25876 cx.set_state("<divˇ<div></div>");
25877 cx.update_editor(|editor, _, cx| {
25878 set_linked_edit_ranges(
25879 (Point::new(0, 1), Point::new(0, 4)),
25880 (Point::new(0, 11), Point::new(0, 14)),
25881 editor,
25882 cx,
25883 );
25884 });
25885 cx.update_editor(|editor, window, cx| {
25886 editor.handle_input(">", window, cx);
25887 });
25888 cx.assert_editor_state("<div>ˇ<div></div>");
25889
25890 // Test typing . do extend linked pair
25891 cx.set_state("<Animatedˇ></Animated>");
25892 cx.update_editor(|editor, _, cx| {
25893 set_linked_edit_ranges(
25894 (Point::new(0, 1), Point::new(0, 9)),
25895 (Point::new(0, 12), Point::new(0, 20)),
25896 editor,
25897 cx,
25898 );
25899 });
25900 cx.update_editor(|editor, window, cx| {
25901 editor.handle_input(".", window, cx);
25902 });
25903 cx.assert_editor_state("<Animated.ˇ></Animated.>");
25904 cx.update_editor(|editor, _, cx| {
25905 set_linked_edit_ranges(
25906 (Point::new(0, 1), Point::new(0, 10)),
25907 (Point::new(0, 13), Point::new(0, 21)),
25908 editor,
25909 cx,
25910 );
25911 });
25912 cx.update_editor(|editor, window, cx| {
25913 editor.handle_input("V", window, cx);
25914 });
25915 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
25916}
25917
25918#[gpui::test]
25919async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
25920 init_test(cx, |_| {});
25921
25922 let fs = FakeFs::new(cx.executor());
25923 fs.insert_tree(
25924 path!("/root"),
25925 json!({
25926 "a": {
25927 "main.rs": "fn main() {}",
25928 },
25929 "foo": {
25930 "bar": {
25931 "external_file.rs": "pub mod external {}",
25932 }
25933 }
25934 }),
25935 )
25936 .await;
25937
25938 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
25939 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25940 language_registry.add(rust_lang());
25941 let _fake_servers = language_registry.register_fake_lsp(
25942 "Rust",
25943 FakeLspAdapter {
25944 ..FakeLspAdapter::default()
25945 },
25946 );
25947 let (workspace, cx) =
25948 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25949 let worktree_id = workspace.update(cx, |workspace, cx| {
25950 workspace.project().update(cx, |project, cx| {
25951 project.worktrees(cx).next().unwrap().read(cx).id()
25952 })
25953 });
25954
25955 let assert_language_servers_count =
25956 |expected: usize, context: &str, cx: &mut VisualTestContext| {
25957 project.update(cx, |project, cx| {
25958 let current = project
25959 .lsp_store()
25960 .read(cx)
25961 .as_local()
25962 .unwrap()
25963 .language_servers
25964 .len();
25965 assert_eq!(expected, current, "{context}");
25966 });
25967 };
25968
25969 assert_language_servers_count(
25970 0,
25971 "No servers should be running before any file is open",
25972 cx,
25973 );
25974 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25975 let main_editor = workspace
25976 .update_in(cx, |workspace, window, cx| {
25977 workspace.open_path(
25978 (worktree_id, rel_path("main.rs")),
25979 Some(pane.downgrade()),
25980 true,
25981 window,
25982 cx,
25983 )
25984 })
25985 .unwrap()
25986 .await
25987 .downcast::<Editor>()
25988 .unwrap();
25989 pane.update(cx, |pane, cx| {
25990 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25991 open_editor.update(cx, |editor, cx| {
25992 assert_eq!(
25993 editor.display_text(cx),
25994 "fn main() {}",
25995 "Original main.rs text on initial open",
25996 );
25997 });
25998 assert_eq!(open_editor, main_editor);
25999 });
26000 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
26001
26002 let external_editor = workspace
26003 .update_in(cx, |workspace, window, cx| {
26004 workspace.open_abs_path(
26005 PathBuf::from("/root/foo/bar/external_file.rs"),
26006 OpenOptions::default(),
26007 window,
26008 cx,
26009 )
26010 })
26011 .await
26012 .expect("opening external file")
26013 .downcast::<Editor>()
26014 .expect("downcasted external file's open element to editor");
26015 pane.update(cx, |pane, cx| {
26016 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26017 open_editor.update(cx, |editor, cx| {
26018 assert_eq!(
26019 editor.display_text(cx),
26020 "pub mod external {}",
26021 "External file is open now",
26022 );
26023 });
26024 assert_eq!(open_editor, external_editor);
26025 });
26026 assert_language_servers_count(
26027 1,
26028 "Second, external, *.rs file should join the existing server",
26029 cx,
26030 );
26031
26032 pane.update_in(cx, |pane, window, cx| {
26033 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26034 })
26035 .await
26036 .unwrap();
26037 pane.update_in(cx, |pane, window, cx| {
26038 pane.navigate_backward(&Default::default(), window, cx);
26039 });
26040 cx.run_until_parked();
26041 pane.update(cx, |pane, cx| {
26042 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26043 open_editor.update(cx, |editor, cx| {
26044 assert_eq!(
26045 editor.display_text(cx),
26046 "pub mod external {}",
26047 "External file is open now",
26048 );
26049 });
26050 });
26051 assert_language_servers_count(
26052 1,
26053 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
26054 cx,
26055 );
26056
26057 cx.update(|_, cx| {
26058 workspace::reload(cx);
26059 });
26060 assert_language_servers_count(
26061 1,
26062 "After reloading the worktree with local and external files opened, only one project should be started",
26063 cx,
26064 );
26065}
26066
26067#[gpui::test]
26068async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
26069 init_test(cx, |_| {});
26070
26071 let mut cx = EditorTestContext::new(cx).await;
26072 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26073 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26074
26075 // test cursor move to start of each line on tab
26076 // for `if`, `elif`, `else`, `while`, `with` and `for`
26077 cx.set_state(indoc! {"
26078 def main():
26079 ˇ for item in items:
26080 ˇ while item.active:
26081 ˇ if item.value > 10:
26082 ˇ continue
26083 ˇ elif item.value < 0:
26084 ˇ break
26085 ˇ else:
26086 ˇ with item.context() as ctx:
26087 ˇ yield count
26088 ˇ else:
26089 ˇ log('while else')
26090 ˇ else:
26091 ˇ log('for else')
26092 "});
26093 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26094 cx.wait_for_autoindent_applied().await;
26095 cx.assert_editor_state(indoc! {"
26096 def main():
26097 ˇfor item in items:
26098 ˇwhile item.active:
26099 ˇif item.value > 10:
26100 ˇcontinue
26101 ˇelif item.value < 0:
26102 ˇbreak
26103 ˇelse:
26104 ˇwith item.context() as ctx:
26105 ˇyield count
26106 ˇelse:
26107 ˇlog('while else')
26108 ˇelse:
26109 ˇlog('for else')
26110 "});
26111 // test relative indent is preserved when tab
26112 // for `if`, `elif`, `else`, `while`, `with` and `for`
26113 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26114 cx.wait_for_autoindent_applied().await;
26115 cx.assert_editor_state(indoc! {"
26116 def main():
26117 ˇfor item in items:
26118 ˇwhile item.active:
26119 ˇif item.value > 10:
26120 ˇcontinue
26121 ˇelif item.value < 0:
26122 ˇbreak
26123 ˇelse:
26124 ˇwith item.context() as ctx:
26125 ˇyield count
26126 ˇelse:
26127 ˇlog('while else')
26128 ˇelse:
26129 ˇlog('for else')
26130 "});
26131
26132 // test cursor move to start of each line on tab
26133 // for `try`, `except`, `else`, `finally`, `match` and `def`
26134 cx.set_state(indoc! {"
26135 def main():
26136 ˇ try:
26137 ˇ fetch()
26138 ˇ except ValueError:
26139 ˇ handle_error()
26140 ˇ else:
26141 ˇ match value:
26142 ˇ case _:
26143 ˇ finally:
26144 ˇ def status():
26145 ˇ return 0
26146 "});
26147 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26148 cx.wait_for_autoindent_applied().await;
26149 cx.assert_editor_state(indoc! {"
26150 def main():
26151 ˇtry:
26152 ˇfetch()
26153 ˇexcept ValueError:
26154 ˇhandle_error()
26155 ˇelse:
26156 ˇmatch value:
26157 ˇcase _:
26158 ˇfinally:
26159 ˇdef status():
26160 ˇreturn 0
26161 "});
26162 // test relative indent is preserved when tab
26163 // for `try`, `except`, `else`, `finally`, `match` and `def`
26164 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26165 cx.wait_for_autoindent_applied().await;
26166 cx.assert_editor_state(indoc! {"
26167 def main():
26168 ˇtry:
26169 ˇfetch()
26170 ˇexcept ValueError:
26171 ˇhandle_error()
26172 ˇelse:
26173 ˇmatch value:
26174 ˇcase _:
26175 ˇfinally:
26176 ˇdef status():
26177 ˇreturn 0
26178 "});
26179}
26180
26181#[gpui::test]
26182async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
26183 init_test(cx, |_| {});
26184
26185 let mut cx = EditorTestContext::new(cx).await;
26186 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26187 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26188
26189 // test `else` auto outdents when typed inside `if` block
26190 cx.set_state(indoc! {"
26191 def main():
26192 if i == 2:
26193 return
26194 ˇ
26195 "});
26196 cx.update_editor(|editor, window, cx| {
26197 editor.handle_input("else:", window, cx);
26198 });
26199 cx.wait_for_autoindent_applied().await;
26200 cx.assert_editor_state(indoc! {"
26201 def main():
26202 if i == 2:
26203 return
26204 else:ˇ
26205 "});
26206
26207 // test `except` auto outdents when typed inside `try` block
26208 cx.set_state(indoc! {"
26209 def main():
26210 try:
26211 i = 2
26212 ˇ
26213 "});
26214 cx.update_editor(|editor, window, cx| {
26215 editor.handle_input("except:", window, cx);
26216 });
26217 cx.wait_for_autoindent_applied().await;
26218 cx.assert_editor_state(indoc! {"
26219 def main():
26220 try:
26221 i = 2
26222 except:ˇ
26223 "});
26224
26225 // test `else` auto outdents when typed inside `except` block
26226 cx.set_state(indoc! {"
26227 def main():
26228 try:
26229 i = 2
26230 except:
26231 j = 2
26232 ˇ
26233 "});
26234 cx.update_editor(|editor, window, cx| {
26235 editor.handle_input("else:", window, cx);
26236 });
26237 cx.wait_for_autoindent_applied().await;
26238 cx.assert_editor_state(indoc! {"
26239 def main():
26240 try:
26241 i = 2
26242 except:
26243 j = 2
26244 else:ˇ
26245 "});
26246
26247 // test `finally` auto outdents when typed inside `else` block
26248 cx.set_state(indoc! {"
26249 def main():
26250 try:
26251 i = 2
26252 except:
26253 j = 2
26254 else:
26255 k = 2
26256 ˇ
26257 "});
26258 cx.update_editor(|editor, window, cx| {
26259 editor.handle_input("finally:", window, cx);
26260 });
26261 cx.wait_for_autoindent_applied().await;
26262 cx.assert_editor_state(indoc! {"
26263 def main():
26264 try:
26265 i = 2
26266 except:
26267 j = 2
26268 else:
26269 k = 2
26270 finally:ˇ
26271 "});
26272
26273 // test `else` does not outdents when typed inside `except` block right after for block
26274 cx.set_state(indoc! {"
26275 def main():
26276 try:
26277 i = 2
26278 except:
26279 for i in range(n):
26280 pass
26281 ˇ
26282 "});
26283 cx.update_editor(|editor, window, cx| {
26284 editor.handle_input("else:", window, cx);
26285 });
26286 cx.wait_for_autoindent_applied().await;
26287 cx.assert_editor_state(indoc! {"
26288 def main():
26289 try:
26290 i = 2
26291 except:
26292 for i in range(n):
26293 pass
26294 else:ˇ
26295 "});
26296
26297 // test `finally` auto outdents when typed inside `else` block right after for block
26298 cx.set_state(indoc! {"
26299 def main():
26300 try:
26301 i = 2
26302 except:
26303 j = 2
26304 else:
26305 for i in range(n):
26306 pass
26307 ˇ
26308 "});
26309 cx.update_editor(|editor, window, cx| {
26310 editor.handle_input("finally:", window, cx);
26311 });
26312 cx.wait_for_autoindent_applied().await;
26313 cx.assert_editor_state(indoc! {"
26314 def main():
26315 try:
26316 i = 2
26317 except:
26318 j = 2
26319 else:
26320 for i in range(n):
26321 pass
26322 finally:ˇ
26323 "});
26324
26325 // test `except` outdents to inner "try" block
26326 cx.set_state(indoc! {"
26327 def main():
26328 try:
26329 i = 2
26330 if i == 2:
26331 try:
26332 i = 3
26333 ˇ
26334 "});
26335 cx.update_editor(|editor, window, cx| {
26336 editor.handle_input("except:", window, cx);
26337 });
26338 cx.wait_for_autoindent_applied().await;
26339 cx.assert_editor_state(indoc! {"
26340 def main():
26341 try:
26342 i = 2
26343 if i == 2:
26344 try:
26345 i = 3
26346 except:ˇ
26347 "});
26348
26349 // test `except` outdents to outer "try" block
26350 cx.set_state(indoc! {"
26351 def main():
26352 try:
26353 i = 2
26354 if i == 2:
26355 try:
26356 i = 3
26357 ˇ
26358 "});
26359 cx.update_editor(|editor, window, cx| {
26360 editor.handle_input("except:", 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 if i == 2:
26368 try:
26369 i = 3
26370 except:ˇ
26371 "});
26372
26373 // test `else` stays at correct indent when typed after `for` block
26374 cx.set_state(indoc! {"
26375 def main():
26376 for i in range(10):
26377 if i == 3:
26378 break
26379 ˇ
26380 "});
26381 cx.update_editor(|editor, window, cx| {
26382 editor.handle_input("else:", window, cx);
26383 });
26384 cx.wait_for_autoindent_applied().await;
26385 cx.assert_editor_state(indoc! {"
26386 def main():
26387 for i in range(10):
26388 if i == 3:
26389 break
26390 else:ˇ
26391 "});
26392
26393 // test does not outdent on typing after line with square brackets
26394 cx.set_state(indoc! {"
26395 def f() -> list[str]:
26396 ˇ
26397 "});
26398 cx.update_editor(|editor, window, cx| {
26399 editor.handle_input("a", window, cx);
26400 });
26401 cx.wait_for_autoindent_applied().await;
26402 cx.assert_editor_state(indoc! {"
26403 def f() -> list[str]:
26404 aˇ
26405 "});
26406
26407 // test does not outdent on typing : after case keyword
26408 cx.set_state(indoc! {"
26409 match 1:
26410 caseˇ
26411 "});
26412 cx.update_editor(|editor, window, cx| {
26413 editor.handle_input(":", window, cx);
26414 });
26415 cx.wait_for_autoindent_applied().await;
26416 cx.assert_editor_state(indoc! {"
26417 match 1:
26418 case:ˇ
26419 "});
26420}
26421
26422#[gpui::test]
26423async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
26424 init_test(cx, |_| {});
26425 update_test_language_settings(cx, |settings| {
26426 settings.defaults.extend_comment_on_newline = Some(false);
26427 });
26428 let mut cx = EditorTestContext::new(cx).await;
26429 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26430 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26431
26432 // test correct indent after newline on comment
26433 cx.set_state(indoc! {"
26434 # COMMENT:ˇ
26435 "});
26436 cx.update_editor(|editor, window, cx| {
26437 editor.newline(&Newline, window, cx);
26438 });
26439 cx.wait_for_autoindent_applied().await;
26440 cx.assert_editor_state(indoc! {"
26441 # COMMENT:
26442 ˇ
26443 "});
26444
26445 // test correct indent after newline in brackets
26446 cx.set_state(indoc! {"
26447 {ˇ}
26448 "});
26449 cx.update_editor(|editor, window, cx| {
26450 editor.newline(&Newline, window, cx);
26451 });
26452 cx.wait_for_autoindent_applied().await;
26453 cx.assert_editor_state(indoc! {"
26454 {
26455 ˇ
26456 }
26457 "});
26458
26459 cx.set_state(indoc! {"
26460 (ˇ)
26461 "});
26462 cx.update_editor(|editor, window, cx| {
26463 editor.newline(&Newline, window, cx);
26464 });
26465 cx.run_until_parked();
26466 cx.assert_editor_state(indoc! {"
26467 (
26468 ˇ
26469 )
26470 "});
26471
26472 // do not indent after empty lists or dictionaries
26473 cx.set_state(indoc! {"
26474 a = []ˇ
26475 "});
26476 cx.update_editor(|editor, window, cx| {
26477 editor.newline(&Newline, window, cx);
26478 });
26479 cx.run_until_parked();
26480 cx.assert_editor_state(indoc! {"
26481 a = []
26482 ˇ
26483 "});
26484}
26485
26486#[gpui::test]
26487async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
26488 init_test(cx, |_| {});
26489
26490 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
26491 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
26492 language_registry.add(markdown_lang());
26493 language_registry.add(python_lang);
26494
26495 let mut cx = EditorTestContext::new(cx).await;
26496 cx.update_buffer(|buffer, cx| {
26497 buffer.set_language_registry(language_registry);
26498 buffer.set_language(Some(markdown_lang()), cx);
26499 });
26500
26501 // Test that `else:` correctly outdents to match `if:` inside the Python code block
26502 cx.set_state(indoc! {"
26503 # Heading
26504
26505 ```python
26506 def main():
26507 if condition:
26508 pass
26509 ˇ
26510 ```
26511 "});
26512 cx.update_editor(|editor, window, cx| {
26513 editor.handle_input("else:", window, cx);
26514 });
26515 cx.run_until_parked();
26516 cx.assert_editor_state(indoc! {"
26517 # Heading
26518
26519 ```python
26520 def main():
26521 if condition:
26522 pass
26523 else:ˇ
26524 ```
26525 "});
26526}
26527
26528#[gpui::test]
26529async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
26530 init_test(cx, |_| {});
26531
26532 let mut cx = EditorTestContext::new(cx).await;
26533 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26534 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26535
26536 // test cursor move to start of each line on tab
26537 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
26538 cx.set_state(indoc! {"
26539 function main() {
26540 ˇ for item in $items; do
26541 ˇ while [ -n \"$item\" ]; do
26542 ˇ if [ \"$value\" -gt 10 ]; then
26543 ˇ continue
26544 ˇ elif [ \"$value\" -lt 0 ]; then
26545 ˇ break
26546 ˇ else
26547 ˇ echo \"$item\"
26548 ˇ fi
26549 ˇ done
26550 ˇ done
26551 ˇ}
26552 "});
26553 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26554 cx.wait_for_autoindent_applied().await;
26555 cx.assert_editor_state(indoc! {"
26556 function main() {
26557 ˇfor item in $items; do
26558 ˇwhile [ -n \"$item\" ]; do
26559 ˇif [ \"$value\" -gt 10 ]; then
26560 ˇcontinue
26561 ˇelif [ \"$value\" -lt 0 ]; then
26562 ˇbreak
26563 ˇelse
26564 ˇecho \"$item\"
26565 ˇfi
26566 ˇdone
26567 ˇdone
26568 ˇ}
26569 "});
26570 // test relative indent is preserved when tab
26571 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26572 cx.wait_for_autoindent_applied().await;
26573 cx.assert_editor_state(indoc! {"
26574 function main() {
26575 ˇfor item in $items; do
26576 ˇwhile [ -n \"$item\" ]; do
26577 ˇif [ \"$value\" -gt 10 ]; then
26578 ˇcontinue
26579 ˇelif [ \"$value\" -lt 0 ]; then
26580 ˇbreak
26581 ˇelse
26582 ˇecho \"$item\"
26583 ˇfi
26584 ˇdone
26585 ˇdone
26586 ˇ}
26587 "});
26588
26589 // test cursor move to start of each line on tab
26590 // for `case` statement with patterns
26591 cx.set_state(indoc! {"
26592 function handle() {
26593 ˇ case \"$1\" in
26594 ˇ start)
26595 ˇ echo \"a\"
26596 ˇ ;;
26597 ˇ stop)
26598 ˇ echo \"b\"
26599 ˇ ;;
26600 ˇ *)
26601 ˇ echo \"c\"
26602 ˇ ;;
26603 ˇ esac
26604 ˇ}
26605 "});
26606 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26607 cx.wait_for_autoindent_applied().await;
26608 cx.assert_editor_state(indoc! {"
26609 function handle() {
26610 ˇcase \"$1\" in
26611 ˇstart)
26612 ˇecho \"a\"
26613 ˇ;;
26614 ˇstop)
26615 ˇecho \"b\"
26616 ˇ;;
26617 ˇ*)
26618 ˇecho \"c\"
26619 ˇ;;
26620 ˇesac
26621 ˇ}
26622 "});
26623}
26624
26625#[gpui::test]
26626async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
26627 init_test(cx, |_| {});
26628
26629 let mut cx = EditorTestContext::new(cx).await;
26630 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26631 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26632
26633 // test indents on comment insert
26634 cx.set_state(indoc! {"
26635 function main() {
26636 ˇ for item in $items; do
26637 ˇ while [ -n \"$item\" ]; do
26638 ˇ if [ \"$value\" -gt 10 ]; then
26639 ˇ continue
26640 ˇ elif [ \"$value\" -lt 0 ]; then
26641 ˇ break
26642 ˇ else
26643 ˇ echo \"$item\"
26644 ˇ fi
26645 ˇ done
26646 ˇ done
26647 ˇ}
26648 "});
26649 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
26650 cx.wait_for_autoindent_applied().await;
26651 cx.assert_editor_state(indoc! {"
26652 function main() {
26653 #ˇ for item in $items; do
26654 #ˇ while [ -n \"$item\" ]; do
26655 #ˇ if [ \"$value\" -gt 10 ]; then
26656 #ˇ continue
26657 #ˇ elif [ \"$value\" -lt 0 ]; then
26658 #ˇ break
26659 #ˇ else
26660 #ˇ echo \"$item\"
26661 #ˇ fi
26662 #ˇ done
26663 #ˇ done
26664 #ˇ}
26665 "});
26666}
26667
26668#[gpui::test]
26669async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
26670 init_test(cx, |_| {});
26671
26672 let mut cx = EditorTestContext::new(cx).await;
26673 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26674 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26675
26676 // test `else` auto outdents when typed inside `if` block
26677 cx.set_state(indoc! {"
26678 if [ \"$1\" = \"test\" ]; then
26679 echo \"foo bar\"
26680 ˇ
26681 "});
26682 cx.update_editor(|editor, window, cx| {
26683 editor.handle_input("else", window, cx);
26684 });
26685 cx.wait_for_autoindent_applied().await;
26686 cx.assert_editor_state(indoc! {"
26687 if [ \"$1\" = \"test\" ]; then
26688 echo \"foo bar\"
26689 elseˇ
26690 "});
26691
26692 // test `elif` auto outdents when typed inside `if` block
26693 cx.set_state(indoc! {"
26694 if [ \"$1\" = \"test\" ]; then
26695 echo \"foo bar\"
26696 ˇ
26697 "});
26698 cx.update_editor(|editor, window, cx| {
26699 editor.handle_input("elif", window, cx);
26700 });
26701 cx.wait_for_autoindent_applied().await;
26702 cx.assert_editor_state(indoc! {"
26703 if [ \"$1\" = \"test\" ]; then
26704 echo \"foo bar\"
26705 elifˇ
26706 "});
26707
26708 // test `fi` auto outdents when typed inside `else` block
26709 cx.set_state(indoc! {"
26710 if [ \"$1\" = \"test\" ]; then
26711 echo \"foo bar\"
26712 else
26713 echo \"bar baz\"
26714 ˇ
26715 "});
26716 cx.update_editor(|editor, window, cx| {
26717 editor.handle_input("fi", window, cx);
26718 });
26719 cx.wait_for_autoindent_applied().await;
26720 cx.assert_editor_state(indoc! {"
26721 if [ \"$1\" = \"test\" ]; then
26722 echo \"foo bar\"
26723 else
26724 echo \"bar baz\"
26725 fiˇ
26726 "});
26727
26728 // test `done` auto outdents when typed inside `while` block
26729 cx.set_state(indoc! {"
26730 while read line; do
26731 echo \"$line\"
26732 ˇ
26733 "});
26734 cx.update_editor(|editor, window, cx| {
26735 editor.handle_input("done", window, cx);
26736 });
26737 cx.wait_for_autoindent_applied().await;
26738 cx.assert_editor_state(indoc! {"
26739 while read line; do
26740 echo \"$line\"
26741 doneˇ
26742 "});
26743
26744 // test `done` auto outdents when typed inside `for` block
26745 cx.set_state(indoc! {"
26746 for file in *.txt; do
26747 cat \"$file\"
26748 ˇ
26749 "});
26750 cx.update_editor(|editor, window, cx| {
26751 editor.handle_input("done", window, cx);
26752 });
26753 cx.wait_for_autoindent_applied().await;
26754 cx.assert_editor_state(indoc! {"
26755 for file in *.txt; do
26756 cat \"$file\"
26757 doneˇ
26758 "});
26759
26760 // test `esac` auto outdents when typed inside `case` block
26761 cx.set_state(indoc! {"
26762 case \"$1\" in
26763 start)
26764 echo \"foo bar\"
26765 ;;
26766 stop)
26767 echo \"bar baz\"
26768 ;;
26769 ˇ
26770 "});
26771 cx.update_editor(|editor, window, cx| {
26772 editor.handle_input("esac", window, cx);
26773 });
26774 cx.wait_for_autoindent_applied().await;
26775 cx.assert_editor_state(indoc! {"
26776 case \"$1\" in
26777 start)
26778 echo \"foo bar\"
26779 ;;
26780 stop)
26781 echo \"bar baz\"
26782 ;;
26783 esacˇ
26784 "});
26785
26786 // test `*)` auto outdents when typed inside `case` block
26787 cx.set_state(indoc! {"
26788 case \"$1\" in
26789 start)
26790 echo \"foo bar\"
26791 ;;
26792 ˇ
26793 "});
26794 cx.update_editor(|editor, window, cx| {
26795 editor.handle_input("*)", window, cx);
26796 });
26797 cx.wait_for_autoindent_applied().await;
26798 cx.assert_editor_state(indoc! {"
26799 case \"$1\" in
26800 start)
26801 echo \"foo bar\"
26802 ;;
26803 *)ˇ
26804 "});
26805
26806 // test `fi` outdents to correct level with nested if blocks
26807 cx.set_state(indoc! {"
26808 if [ \"$1\" = \"test\" ]; then
26809 echo \"outer if\"
26810 if [ \"$2\" = \"debug\" ]; then
26811 echo \"inner if\"
26812 ˇ
26813 "});
26814 cx.update_editor(|editor, window, cx| {
26815 editor.handle_input("fi", window, cx);
26816 });
26817 cx.wait_for_autoindent_applied().await;
26818 cx.assert_editor_state(indoc! {"
26819 if [ \"$1\" = \"test\" ]; then
26820 echo \"outer if\"
26821 if [ \"$2\" = \"debug\" ]; then
26822 echo \"inner if\"
26823 fiˇ
26824 "});
26825}
26826
26827#[gpui::test]
26828async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
26829 init_test(cx, |_| {});
26830 update_test_language_settings(cx, |settings| {
26831 settings.defaults.extend_comment_on_newline = Some(false);
26832 });
26833 let mut cx = EditorTestContext::new(cx).await;
26834 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26835 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26836
26837 // test correct indent after newline on comment
26838 cx.set_state(indoc! {"
26839 # COMMENT:ˇ
26840 "});
26841 cx.update_editor(|editor, window, cx| {
26842 editor.newline(&Newline, window, cx);
26843 });
26844 cx.wait_for_autoindent_applied().await;
26845 cx.assert_editor_state(indoc! {"
26846 # COMMENT:
26847 ˇ
26848 "});
26849
26850 // test correct indent after newline after `then`
26851 cx.set_state(indoc! {"
26852
26853 if [ \"$1\" = \"test\" ]; thenˇ
26854 "});
26855 cx.update_editor(|editor, window, cx| {
26856 editor.newline(&Newline, window, cx);
26857 });
26858 cx.wait_for_autoindent_applied().await;
26859 cx.assert_editor_state(indoc! {"
26860
26861 if [ \"$1\" = \"test\" ]; then
26862 ˇ
26863 "});
26864
26865 // test correct indent after newline after `else`
26866 cx.set_state(indoc! {"
26867 if [ \"$1\" = \"test\" ]; then
26868 elseˇ
26869 "});
26870 cx.update_editor(|editor, window, cx| {
26871 editor.newline(&Newline, window, cx);
26872 });
26873 cx.wait_for_autoindent_applied().await;
26874 cx.assert_editor_state(indoc! {"
26875 if [ \"$1\" = \"test\" ]; then
26876 else
26877 ˇ
26878 "});
26879
26880 // test correct indent after newline after `elif`
26881 cx.set_state(indoc! {"
26882 if [ \"$1\" = \"test\" ]; then
26883 elifˇ
26884 "});
26885 cx.update_editor(|editor, window, cx| {
26886 editor.newline(&Newline, window, cx);
26887 });
26888 cx.wait_for_autoindent_applied().await;
26889 cx.assert_editor_state(indoc! {"
26890 if [ \"$1\" = \"test\" ]; then
26891 elif
26892 ˇ
26893 "});
26894
26895 // test correct indent after newline after `do`
26896 cx.set_state(indoc! {"
26897 for file in *.txt; doˇ
26898 "});
26899 cx.update_editor(|editor, window, cx| {
26900 editor.newline(&Newline, window, cx);
26901 });
26902 cx.wait_for_autoindent_applied().await;
26903 cx.assert_editor_state(indoc! {"
26904 for file in *.txt; do
26905 ˇ
26906 "});
26907
26908 // test correct indent after newline after case pattern
26909 cx.set_state(indoc! {"
26910 case \"$1\" in
26911 start)ˇ
26912 "});
26913 cx.update_editor(|editor, window, cx| {
26914 editor.newline(&Newline, window, cx);
26915 });
26916 cx.wait_for_autoindent_applied().await;
26917 cx.assert_editor_state(indoc! {"
26918 case \"$1\" in
26919 start)
26920 ˇ
26921 "});
26922
26923 // test correct indent after newline after case pattern
26924 cx.set_state(indoc! {"
26925 case \"$1\" in
26926 start)
26927 ;;
26928 *)ˇ
26929 "});
26930 cx.update_editor(|editor, window, cx| {
26931 editor.newline(&Newline, window, cx);
26932 });
26933 cx.wait_for_autoindent_applied().await;
26934 cx.assert_editor_state(indoc! {"
26935 case \"$1\" in
26936 start)
26937 ;;
26938 *)
26939 ˇ
26940 "});
26941
26942 // test correct indent after newline after function opening brace
26943 cx.set_state(indoc! {"
26944 function test() {ˇ}
26945 "});
26946 cx.update_editor(|editor, window, cx| {
26947 editor.newline(&Newline, window, cx);
26948 });
26949 cx.wait_for_autoindent_applied().await;
26950 cx.assert_editor_state(indoc! {"
26951 function test() {
26952 ˇ
26953 }
26954 "});
26955
26956 // test no extra indent after semicolon on same line
26957 cx.set_state(indoc! {"
26958 echo \"test\";ˇ
26959 "});
26960 cx.update_editor(|editor, window, cx| {
26961 editor.newline(&Newline, window, cx);
26962 });
26963 cx.wait_for_autoindent_applied().await;
26964 cx.assert_editor_state(indoc! {"
26965 echo \"test\";
26966 ˇ
26967 "});
26968}
26969
26970fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
26971 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
26972 point..point
26973}
26974
26975#[track_caller]
26976fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
26977 let (text, ranges) = marked_text_ranges(marked_text, true);
26978 assert_eq!(editor.text(cx), text);
26979 assert_eq!(
26980 editor.selections.ranges(&editor.display_snapshot(cx)),
26981 ranges
26982 .iter()
26983 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
26984 .collect::<Vec<_>>(),
26985 "Assert selections are {}",
26986 marked_text
26987 );
26988}
26989
26990pub fn handle_signature_help_request(
26991 cx: &mut EditorLspTestContext,
26992 mocked_response: lsp::SignatureHelp,
26993) -> impl Future<Output = ()> + use<> {
26994 let mut request =
26995 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
26996 let mocked_response = mocked_response.clone();
26997 async move { Ok(Some(mocked_response)) }
26998 });
26999
27000 async move {
27001 request.next().await;
27002 }
27003}
27004
27005#[track_caller]
27006pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
27007 cx.update_editor(|editor, _, _| {
27008 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
27009 let entries = menu.entries.borrow();
27010 let entries = entries
27011 .iter()
27012 .map(|entry| entry.string.as_str())
27013 .collect::<Vec<_>>();
27014 assert_eq!(entries, expected);
27015 } else {
27016 panic!("Expected completions menu");
27017 }
27018 });
27019}
27020
27021#[gpui::test]
27022async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
27023 init_test(cx, |_| {});
27024 let mut cx = EditorLspTestContext::new_rust(
27025 lsp::ServerCapabilities {
27026 completion_provider: Some(lsp::CompletionOptions {
27027 ..Default::default()
27028 }),
27029 ..Default::default()
27030 },
27031 cx,
27032 )
27033 .await;
27034 cx.lsp
27035 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
27036 Ok(Some(lsp::CompletionResponse::Array(vec![
27037 lsp::CompletionItem {
27038 label: "unsafe".into(),
27039 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
27040 range: lsp::Range {
27041 start: lsp::Position {
27042 line: 0,
27043 character: 9,
27044 },
27045 end: lsp::Position {
27046 line: 0,
27047 character: 11,
27048 },
27049 },
27050 new_text: "unsafe".to_string(),
27051 })),
27052 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
27053 ..Default::default()
27054 },
27055 ])))
27056 });
27057
27058 cx.update_editor(|editor, _, cx| {
27059 editor.project().unwrap().update(cx, |project, cx| {
27060 project.snippets().update(cx, |snippets, _cx| {
27061 snippets.add_snippet_for_test(
27062 None,
27063 PathBuf::from("test_snippets.json"),
27064 vec![
27065 Arc::new(project::snippet_provider::Snippet {
27066 prefix: vec![
27067 "unlimited word count".to_string(),
27068 "unlimit word count".to_string(),
27069 "unlimited unknown".to_string(),
27070 ],
27071 body: "this is many words".to_string(),
27072 description: Some("description".to_string()),
27073 name: "multi-word snippet test".to_string(),
27074 }),
27075 Arc::new(project::snippet_provider::Snippet {
27076 prefix: vec!["unsnip".to_string(), "@few".to_string()],
27077 body: "fewer words".to_string(),
27078 description: Some("alt description".to_string()),
27079 name: "other name".to_string(),
27080 }),
27081 Arc::new(project::snippet_provider::Snippet {
27082 prefix: vec!["ab aa".to_string()],
27083 body: "abcd".to_string(),
27084 description: None,
27085 name: "alphabet".to_string(),
27086 }),
27087 ],
27088 );
27089 });
27090 })
27091 });
27092
27093 let get_completions = |cx: &mut EditorLspTestContext| {
27094 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
27095 Some(CodeContextMenu::Completions(context_menu)) => {
27096 let entries = context_menu.entries.borrow();
27097 entries
27098 .iter()
27099 .map(|entry| entry.string.clone())
27100 .collect_vec()
27101 }
27102 _ => vec![],
27103 })
27104 };
27105
27106 // snippets:
27107 // @foo
27108 // foo bar
27109 //
27110 // when typing:
27111 //
27112 // when typing:
27113 // - if I type a symbol "open the completions with snippets only"
27114 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
27115 //
27116 // stuff we need:
27117 // - filtering logic change?
27118 // - remember how far back the completion started.
27119
27120 let test_cases: &[(&str, &[&str])] = &[
27121 (
27122 "un",
27123 &[
27124 "unsafe",
27125 "unlimit word count",
27126 "unlimited unknown",
27127 "unlimited word count",
27128 "unsnip",
27129 ],
27130 ),
27131 (
27132 "u ",
27133 &[
27134 "unlimit word count",
27135 "unlimited unknown",
27136 "unlimited word count",
27137 ],
27138 ),
27139 ("u a", &["ab aa", "unsafe"]), // unsAfe
27140 (
27141 "u u",
27142 &[
27143 "unsafe",
27144 "unlimit word count",
27145 "unlimited unknown", // ranked highest among snippets
27146 "unlimited word count",
27147 "unsnip",
27148 ],
27149 ),
27150 ("uw c", &["unlimit word count", "unlimited word count"]),
27151 (
27152 "u w",
27153 &[
27154 "unlimit word count",
27155 "unlimited word count",
27156 "unlimited unknown",
27157 ],
27158 ),
27159 ("u w ", &["unlimit word count", "unlimited word count"]),
27160 (
27161 "u ",
27162 &[
27163 "unlimit word count",
27164 "unlimited unknown",
27165 "unlimited word count",
27166 ],
27167 ),
27168 ("wor", &[]),
27169 ("uf", &["unsafe"]),
27170 ("af", &["unsafe"]),
27171 ("afu", &[]),
27172 (
27173 "ue",
27174 &["unsafe", "unlimited unknown", "unlimited word count"],
27175 ),
27176 ("@", &["@few"]),
27177 ("@few", &["@few"]),
27178 ("@ ", &[]),
27179 ("a@", &["@few"]),
27180 ("a@f", &["@few", "unsafe"]),
27181 ("a@fw", &["@few"]),
27182 ("a", &["ab aa", "unsafe"]),
27183 ("aa", &["ab aa"]),
27184 ("aaa", &["ab aa"]),
27185 ("ab", &["ab aa"]),
27186 ("ab ", &["ab aa"]),
27187 ("ab a", &["ab aa", "unsafe"]),
27188 ("ab ab", &["ab aa"]),
27189 ("ab ab aa", &["ab aa"]),
27190 ];
27191
27192 for &(input_to_simulate, expected_completions) in test_cases {
27193 cx.set_state("fn a() { ˇ }\n");
27194 for c in input_to_simulate.split("") {
27195 cx.simulate_input(c);
27196 cx.run_until_parked();
27197 }
27198 let expected_completions = expected_completions
27199 .iter()
27200 .map(|s| s.to_string())
27201 .collect_vec();
27202 assert_eq!(
27203 get_completions(&mut cx),
27204 expected_completions,
27205 "< actual / expected >, input = {input_to_simulate:?}",
27206 );
27207 }
27208}
27209
27210/// Handle completion request passing a marked string specifying where the completion
27211/// should be triggered from using '|' character, what range should be replaced, and what completions
27212/// should be returned using '<' and '>' to delimit the range.
27213///
27214/// Also see `handle_completion_request_with_insert_and_replace`.
27215#[track_caller]
27216pub fn handle_completion_request(
27217 marked_string: &str,
27218 completions: Vec<&'static str>,
27219 is_incomplete: bool,
27220 counter: Arc<AtomicUsize>,
27221 cx: &mut EditorLspTestContext,
27222) -> impl Future<Output = ()> {
27223 let complete_from_marker: TextRangeMarker = '|'.into();
27224 let replace_range_marker: TextRangeMarker = ('<', '>').into();
27225 let (_, mut marked_ranges) = marked_text_ranges_by(
27226 marked_string,
27227 vec![complete_from_marker.clone(), replace_range_marker.clone()],
27228 );
27229
27230 let complete_from_position = cx.to_lsp(MultiBufferOffset(
27231 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
27232 ));
27233 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
27234 let replace_range =
27235 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
27236
27237 let mut request =
27238 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
27239 let completions = completions.clone();
27240 counter.fetch_add(1, atomic::Ordering::Release);
27241 async move {
27242 assert_eq!(params.text_document_position.text_document.uri, url.clone());
27243 assert_eq!(
27244 params.text_document_position.position,
27245 complete_from_position
27246 );
27247 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
27248 is_incomplete,
27249 item_defaults: None,
27250 items: completions
27251 .iter()
27252 .map(|completion_text| lsp::CompletionItem {
27253 label: completion_text.to_string(),
27254 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
27255 range: replace_range,
27256 new_text: completion_text.to_string(),
27257 })),
27258 ..Default::default()
27259 })
27260 .collect(),
27261 })))
27262 }
27263 });
27264
27265 async move {
27266 request.next().await;
27267 }
27268}
27269
27270/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
27271/// given instead, which also contains an `insert` range.
27272///
27273/// This function uses markers to define ranges:
27274/// - `|` marks the cursor position
27275/// - `<>` marks the replace range
27276/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
27277pub fn handle_completion_request_with_insert_and_replace(
27278 cx: &mut EditorLspTestContext,
27279 marked_string: &str,
27280 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
27281 counter: Arc<AtomicUsize>,
27282) -> impl Future<Output = ()> {
27283 let complete_from_marker: TextRangeMarker = '|'.into();
27284 let replace_range_marker: TextRangeMarker = ('<', '>').into();
27285 let insert_range_marker: TextRangeMarker = ('{', '}').into();
27286
27287 let (_, mut marked_ranges) = marked_text_ranges_by(
27288 marked_string,
27289 vec![
27290 complete_from_marker.clone(),
27291 replace_range_marker.clone(),
27292 insert_range_marker.clone(),
27293 ],
27294 );
27295
27296 let complete_from_position = cx.to_lsp(MultiBufferOffset(
27297 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
27298 ));
27299 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
27300 let replace_range =
27301 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
27302
27303 let insert_range = match marked_ranges.remove(&insert_range_marker) {
27304 Some(ranges) if !ranges.is_empty() => {
27305 let range1 = ranges[0].clone();
27306 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
27307 }
27308 _ => lsp::Range {
27309 start: replace_range.start,
27310 end: complete_from_position,
27311 },
27312 };
27313
27314 let mut request =
27315 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
27316 let completions = completions.clone();
27317 counter.fetch_add(1, atomic::Ordering::Release);
27318 async move {
27319 assert_eq!(params.text_document_position.text_document.uri, url.clone());
27320 assert_eq!(
27321 params.text_document_position.position, complete_from_position,
27322 "marker `|` position doesn't match",
27323 );
27324 Ok(Some(lsp::CompletionResponse::Array(
27325 completions
27326 .iter()
27327 .map(|(label, new_text)| lsp::CompletionItem {
27328 label: label.to_string(),
27329 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
27330 lsp::InsertReplaceEdit {
27331 insert: insert_range,
27332 replace: replace_range,
27333 new_text: new_text.to_string(),
27334 },
27335 )),
27336 ..Default::default()
27337 })
27338 .collect(),
27339 )))
27340 }
27341 });
27342
27343 async move {
27344 request.next().await;
27345 }
27346}
27347
27348fn handle_resolve_completion_request(
27349 cx: &mut EditorLspTestContext,
27350 edits: Option<Vec<(&'static str, &'static str)>>,
27351) -> impl Future<Output = ()> {
27352 let edits = edits.map(|edits| {
27353 edits
27354 .iter()
27355 .map(|(marked_string, new_text)| {
27356 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
27357 let replace_range = cx.to_lsp_range(
27358 MultiBufferOffset(marked_ranges[0].start)
27359 ..MultiBufferOffset(marked_ranges[0].end),
27360 );
27361 lsp::TextEdit::new(replace_range, new_text.to_string())
27362 })
27363 .collect::<Vec<_>>()
27364 });
27365
27366 let mut request =
27367 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
27368 let edits = edits.clone();
27369 async move {
27370 Ok(lsp::CompletionItem {
27371 additional_text_edits: edits,
27372 ..Default::default()
27373 })
27374 }
27375 });
27376
27377 async move {
27378 request.next().await;
27379 }
27380}
27381
27382pub(crate) fn update_test_language_settings(
27383 cx: &mut TestAppContext,
27384 f: impl Fn(&mut AllLanguageSettingsContent),
27385) {
27386 cx.update(|cx| {
27387 SettingsStore::update_global(cx, |store, cx| {
27388 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
27389 });
27390 });
27391}
27392
27393pub(crate) fn update_test_project_settings(
27394 cx: &mut TestAppContext,
27395 f: impl Fn(&mut ProjectSettingsContent),
27396) {
27397 cx.update(|cx| {
27398 SettingsStore::update_global(cx, |store, cx| {
27399 store.update_user_settings(cx, |settings| f(&mut settings.project));
27400 });
27401 });
27402}
27403
27404pub(crate) fn update_test_editor_settings(
27405 cx: &mut TestAppContext,
27406 f: impl Fn(&mut EditorSettingsContent),
27407) {
27408 cx.update(|cx| {
27409 SettingsStore::update_global(cx, |store, cx| {
27410 store.update_user_settings(cx, |settings| f(&mut settings.editor));
27411 })
27412 })
27413}
27414
27415pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
27416 cx.update(|cx| {
27417 assets::Assets.load_test_fonts(cx);
27418 let store = SettingsStore::test(cx);
27419 cx.set_global(store);
27420 theme::init(theme::LoadThemes::JustBase, cx);
27421 release_channel::init(semver::Version::new(0, 0, 0), cx);
27422 crate::init(cx);
27423 });
27424 zlog::init_test();
27425 update_test_language_settings(cx, f);
27426}
27427
27428#[track_caller]
27429fn assert_hunk_revert(
27430 not_reverted_text_with_selections: &str,
27431 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
27432 expected_reverted_text_with_selections: &str,
27433 base_text: &str,
27434 cx: &mut EditorLspTestContext,
27435) {
27436 cx.set_state(not_reverted_text_with_selections);
27437 cx.set_head_text(base_text);
27438 cx.executor().run_until_parked();
27439
27440 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
27441 let snapshot = editor.snapshot(window, cx);
27442 let reverted_hunk_statuses = snapshot
27443 .buffer_snapshot()
27444 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
27445 .map(|hunk| hunk.status().kind)
27446 .collect::<Vec<_>>();
27447
27448 editor.git_restore(&Default::default(), window, cx);
27449 reverted_hunk_statuses
27450 });
27451 cx.executor().run_until_parked();
27452 cx.assert_editor_state(expected_reverted_text_with_selections);
27453 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
27454}
27455
27456#[gpui::test(iterations = 10)]
27457async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
27458 init_test(cx, |_| {});
27459
27460 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
27461 let counter = diagnostic_requests.clone();
27462
27463 let fs = FakeFs::new(cx.executor());
27464 fs.insert_tree(
27465 path!("/a"),
27466 json!({
27467 "first.rs": "fn main() { let a = 5; }",
27468 "second.rs": "// Test file",
27469 }),
27470 )
27471 .await;
27472
27473 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27474 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27475 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27476
27477 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27478 language_registry.add(rust_lang());
27479 let mut fake_servers = language_registry.register_fake_lsp(
27480 "Rust",
27481 FakeLspAdapter {
27482 capabilities: lsp::ServerCapabilities {
27483 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
27484 lsp::DiagnosticOptions {
27485 identifier: None,
27486 inter_file_dependencies: true,
27487 workspace_diagnostics: true,
27488 work_done_progress_options: Default::default(),
27489 },
27490 )),
27491 ..Default::default()
27492 },
27493 ..Default::default()
27494 },
27495 );
27496
27497 let editor = workspace
27498 .update(cx, |workspace, window, cx| {
27499 workspace.open_abs_path(
27500 PathBuf::from(path!("/a/first.rs")),
27501 OpenOptions::default(),
27502 window,
27503 cx,
27504 )
27505 })
27506 .unwrap()
27507 .await
27508 .unwrap()
27509 .downcast::<Editor>()
27510 .unwrap();
27511 let fake_server = fake_servers.next().await.unwrap();
27512 let server_id = fake_server.server.server_id();
27513 let mut first_request = fake_server
27514 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
27515 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
27516 let result_id = Some(new_result_id.to_string());
27517 assert_eq!(
27518 params.text_document.uri,
27519 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27520 );
27521 async move {
27522 Ok(lsp::DocumentDiagnosticReportResult::Report(
27523 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
27524 related_documents: None,
27525 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
27526 items: Vec::new(),
27527 result_id,
27528 },
27529 }),
27530 ))
27531 }
27532 });
27533
27534 let ensure_result_id = |expected: Option<SharedString>, cx: &mut TestAppContext| {
27535 project.update(cx, |project, cx| {
27536 let buffer_id = editor
27537 .read(cx)
27538 .buffer()
27539 .read(cx)
27540 .as_singleton()
27541 .expect("created a singleton buffer")
27542 .read(cx)
27543 .remote_id();
27544 let buffer_result_id = project
27545 .lsp_store()
27546 .read(cx)
27547 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
27548 assert_eq!(expected, buffer_result_id);
27549 });
27550 };
27551
27552 ensure_result_id(None, cx);
27553 cx.executor().advance_clock(Duration::from_millis(60));
27554 cx.executor().run_until_parked();
27555 assert_eq!(
27556 diagnostic_requests.load(atomic::Ordering::Acquire),
27557 1,
27558 "Opening file should trigger diagnostic request"
27559 );
27560 first_request
27561 .next()
27562 .await
27563 .expect("should have sent the first diagnostics pull request");
27564 ensure_result_id(Some(SharedString::new("1")), cx);
27565
27566 // Editing should trigger diagnostics
27567 editor.update_in(cx, |editor, window, cx| {
27568 editor.handle_input("2", window, cx)
27569 });
27570 cx.executor().advance_clock(Duration::from_millis(60));
27571 cx.executor().run_until_parked();
27572 assert_eq!(
27573 diagnostic_requests.load(atomic::Ordering::Acquire),
27574 2,
27575 "Editing should trigger diagnostic request"
27576 );
27577 ensure_result_id(Some(SharedString::new("2")), cx);
27578
27579 // Moving cursor should not trigger diagnostic request
27580 editor.update_in(cx, |editor, window, cx| {
27581 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27582 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
27583 });
27584 });
27585 cx.executor().advance_clock(Duration::from_millis(60));
27586 cx.executor().run_until_parked();
27587 assert_eq!(
27588 diagnostic_requests.load(atomic::Ordering::Acquire),
27589 2,
27590 "Cursor movement should not trigger diagnostic request"
27591 );
27592 ensure_result_id(Some(SharedString::new("2")), cx);
27593 // Multiple rapid edits should be debounced
27594 for _ in 0..5 {
27595 editor.update_in(cx, |editor, window, cx| {
27596 editor.handle_input("x", window, cx)
27597 });
27598 }
27599 cx.executor().advance_clock(Duration::from_millis(60));
27600 cx.executor().run_until_parked();
27601
27602 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
27603 assert!(
27604 final_requests <= 4,
27605 "Multiple rapid edits should be debounced (got {final_requests} requests)",
27606 );
27607 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
27608}
27609
27610#[gpui::test]
27611async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
27612 // Regression test for issue #11671
27613 // Previously, adding a cursor after moving multiple cursors would reset
27614 // the cursor count instead of adding to the existing cursors.
27615 init_test(cx, |_| {});
27616 let mut cx = EditorTestContext::new(cx).await;
27617
27618 // Create a simple buffer with cursor at start
27619 cx.set_state(indoc! {"
27620 ˇaaaa
27621 bbbb
27622 cccc
27623 dddd
27624 eeee
27625 ffff
27626 gggg
27627 hhhh"});
27628
27629 // Add 2 cursors below (so we have 3 total)
27630 cx.update_editor(|editor, window, cx| {
27631 editor.add_selection_below(&Default::default(), window, cx);
27632 editor.add_selection_below(&Default::default(), window, cx);
27633 });
27634
27635 // Verify we have 3 cursors
27636 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
27637 assert_eq!(
27638 initial_count, 3,
27639 "Should have 3 cursors after adding 2 below"
27640 );
27641
27642 // Move down one line
27643 cx.update_editor(|editor, window, cx| {
27644 editor.move_down(&MoveDown, window, cx);
27645 });
27646
27647 // Add another cursor below
27648 cx.update_editor(|editor, window, cx| {
27649 editor.add_selection_below(&Default::default(), window, cx);
27650 });
27651
27652 // Should now have 4 cursors (3 original + 1 new)
27653 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
27654 assert_eq!(
27655 final_count, 4,
27656 "Should have 4 cursors after moving and adding another"
27657 );
27658}
27659
27660#[gpui::test]
27661async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
27662 init_test(cx, |_| {});
27663
27664 let mut cx = EditorTestContext::new(cx).await;
27665
27666 cx.set_state(indoc!(
27667 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
27668 Second line here"#
27669 ));
27670
27671 cx.update_editor(|editor, window, cx| {
27672 // Enable soft wrapping with a narrow width to force soft wrapping and
27673 // confirm that more than 2 rows are being displayed.
27674 editor.set_wrap_width(Some(100.0.into()), cx);
27675 assert!(editor.display_text(cx).lines().count() > 2);
27676
27677 editor.add_selection_below(
27678 &AddSelectionBelow {
27679 skip_soft_wrap: true,
27680 },
27681 window,
27682 cx,
27683 );
27684
27685 assert_eq!(
27686 display_ranges(editor, cx),
27687 &[
27688 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27689 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
27690 ]
27691 );
27692
27693 editor.add_selection_above(
27694 &AddSelectionAbove {
27695 skip_soft_wrap: true,
27696 },
27697 window,
27698 cx,
27699 );
27700
27701 assert_eq!(
27702 display_ranges(editor, cx),
27703 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27704 );
27705
27706 editor.add_selection_below(
27707 &AddSelectionBelow {
27708 skip_soft_wrap: false,
27709 },
27710 window,
27711 cx,
27712 );
27713
27714 assert_eq!(
27715 display_ranges(editor, cx),
27716 &[
27717 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27718 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
27719 ]
27720 );
27721
27722 editor.add_selection_above(
27723 &AddSelectionAbove {
27724 skip_soft_wrap: false,
27725 },
27726 window,
27727 cx,
27728 );
27729
27730 assert_eq!(
27731 display_ranges(editor, cx),
27732 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27733 );
27734 });
27735
27736 // Set up text where selections are in the middle of a soft-wrapped line.
27737 // When adding selection below with `skip_soft_wrap` set to `true`, the new
27738 // selection should be at the same buffer column, not the same pixel
27739 // position.
27740 cx.set_state(indoc!(
27741 r#"1. Very long line to show «howˇ» a wrapped line would look
27742 2. Very long line to show how a wrapped line would look"#
27743 ));
27744
27745 cx.update_editor(|editor, window, cx| {
27746 // Enable soft wrapping with a narrow width to force soft wrapping and
27747 // confirm that more than 2 rows are being displayed.
27748 editor.set_wrap_width(Some(100.0.into()), cx);
27749 assert!(editor.display_text(cx).lines().count() > 2);
27750
27751 editor.add_selection_below(
27752 &AddSelectionBelow {
27753 skip_soft_wrap: true,
27754 },
27755 window,
27756 cx,
27757 );
27758
27759 // Assert that there's now 2 selections, both selecting the same column
27760 // range in the buffer row.
27761 let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
27762 let selections = editor.selections.all::<Point>(&display_map);
27763 assert_eq!(selections.len(), 2);
27764 assert_eq!(selections[0].start.column, selections[1].start.column);
27765 assert_eq!(selections[0].end.column, selections[1].end.column);
27766 });
27767}
27768
27769#[gpui::test]
27770async fn test_insert_snippet(cx: &mut TestAppContext) {
27771 init_test(cx, |_| {});
27772 let mut cx = EditorTestContext::new(cx).await;
27773
27774 cx.update_editor(|editor, _, cx| {
27775 editor.project().unwrap().update(cx, |project, cx| {
27776 project.snippets().update(cx, |snippets, _cx| {
27777 let snippet = project::snippet_provider::Snippet {
27778 prefix: vec![], // no prefix needed!
27779 body: "an Unspecified".to_string(),
27780 description: Some("shhhh it's a secret".to_string()),
27781 name: "super secret snippet".to_string(),
27782 };
27783 snippets.add_snippet_for_test(
27784 None,
27785 PathBuf::from("test_snippets.json"),
27786 vec![Arc::new(snippet)],
27787 );
27788
27789 let snippet = project::snippet_provider::Snippet {
27790 prefix: vec![], // no prefix needed!
27791 body: " Location".to_string(),
27792 description: Some("the word 'location'".to_string()),
27793 name: "location word".to_string(),
27794 };
27795 snippets.add_snippet_for_test(
27796 Some("Markdown".to_string()),
27797 PathBuf::from("test_snippets.json"),
27798 vec![Arc::new(snippet)],
27799 );
27800 });
27801 })
27802 });
27803
27804 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
27805
27806 cx.update_editor(|editor, window, cx| {
27807 editor.insert_snippet_at_selections(
27808 &InsertSnippet {
27809 language: None,
27810 name: Some("super secret snippet".to_string()),
27811 snippet: None,
27812 },
27813 window,
27814 cx,
27815 );
27816
27817 // Language is specified in the action,
27818 // so the buffer language does not need to match
27819 editor.insert_snippet_at_selections(
27820 &InsertSnippet {
27821 language: Some("Markdown".to_string()),
27822 name: Some("location word".to_string()),
27823 snippet: None,
27824 },
27825 window,
27826 cx,
27827 );
27828
27829 editor.insert_snippet_at_selections(
27830 &InsertSnippet {
27831 language: None,
27832 name: None,
27833 snippet: Some("$0 after".to_string()),
27834 },
27835 window,
27836 cx,
27837 );
27838 });
27839
27840 cx.assert_editor_state(
27841 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
27842 );
27843}
27844
27845#[gpui::test(iterations = 10)]
27846async fn test_document_colors(cx: &mut TestAppContext) {
27847 let expected_color = Rgba {
27848 r: 0.33,
27849 g: 0.33,
27850 b: 0.33,
27851 a: 0.33,
27852 };
27853
27854 init_test(cx, |_| {});
27855
27856 let fs = FakeFs::new(cx.executor());
27857 fs.insert_tree(
27858 path!("/a"),
27859 json!({
27860 "first.rs": "fn main() { let a = 5; }",
27861 }),
27862 )
27863 .await;
27864
27865 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27866 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27867 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27868
27869 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27870 language_registry.add(rust_lang());
27871 let mut fake_servers = language_registry.register_fake_lsp(
27872 "Rust",
27873 FakeLspAdapter {
27874 capabilities: lsp::ServerCapabilities {
27875 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
27876 ..lsp::ServerCapabilities::default()
27877 },
27878 name: "rust-analyzer",
27879 ..FakeLspAdapter::default()
27880 },
27881 );
27882 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
27883 "Rust",
27884 FakeLspAdapter {
27885 capabilities: lsp::ServerCapabilities {
27886 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
27887 ..lsp::ServerCapabilities::default()
27888 },
27889 name: "not-rust-analyzer",
27890 ..FakeLspAdapter::default()
27891 },
27892 );
27893
27894 let editor = workspace
27895 .update(cx, |workspace, window, cx| {
27896 workspace.open_abs_path(
27897 PathBuf::from(path!("/a/first.rs")),
27898 OpenOptions::default(),
27899 window,
27900 cx,
27901 )
27902 })
27903 .unwrap()
27904 .await
27905 .unwrap()
27906 .downcast::<Editor>()
27907 .unwrap();
27908 let fake_language_server = fake_servers.next().await.unwrap();
27909 let fake_language_server_without_capabilities =
27910 fake_servers_without_capabilities.next().await.unwrap();
27911 let requests_made = Arc::new(AtomicUsize::new(0));
27912 let closure_requests_made = Arc::clone(&requests_made);
27913 let mut color_request_handle = fake_language_server
27914 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27915 let requests_made = Arc::clone(&closure_requests_made);
27916 async move {
27917 assert_eq!(
27918 params.text_document.uri,
27919 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27920 );
27921 requests_made.fetch_add(1, atomic::Ordering::Release);
27922 Ok(vec![
27923 lsp::ColorInformation {
27924 range: lsp::Range {
27925 start: lsp::Position {
27926 line: 0,
27927 character: 0,
27928 },
27929 end: lsp::Position {
27930 line: 0,
27931 character: 1,
27932 },
27933 },
27934 color: lsp::Color {
27935 red: 0.33,
27936 green: 0.33,
27937 blue: 0.33,
27938 alpha: 0.33,
27939 },
27940 },
27941 lsp::ColorInformation {
27942 range: lsp::Range {
27943 start: lsp::Position {
27944 line: 0,
27945 character: 0,
27946 },
27947 end: lsp::Position {
27948 line: 0,
27949 character: 1,
27950 },
27951 },
27952 color: lsp::Color {
27953 red: 0.33,
27954 green: 0.33,
27955 blue: 0.33,
27956 alpha: 0.33,
27957 },
27958 },
27959 ])
27960 }
27961 });
27962
27963 let _handle = fake_language_server_without_capabilities
27964 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
27965 panic!("Should not be called");
27966 });
27967 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27968 color_request_handle.next().await.unwrap();
27969 cx.run_until_parked();
27970 assert_eq!(
27971 1,
27972 requests_made.load(atomic::Ordering::Acquire),
27973 "Should query for colors once per editor open"
27974 );
27975 editor.update_in(cx, |editor, _, cx| {
27976 assert_eq!(
27977 vec![expected_color],
27978 extract_color_inlays(editor, cx),
27979 "Should have an initial inlay"
27980 );
27981 });
27982
27983 // opening another file in a split should not influence the LSP query counter
27984 workspace
27985 .update(cx, |workspace, window, cx| {
27986 assert_eq!(
27987 workspace.panes().len(),
27988 1,
27989 "Should have one pane with one editor"
27990 );
27991 workspace.move_item_to_pane_in_direction(
27992 &MoveItemToPaneInDirection {
27993 direction: SplitDirection::Right,
27994 focus: false,
27995 clone: true,
27996 },
27997 window,
27998 cx,
27999 );
28000 })
28001 .unwrap();
28002 cx.run_until_parked();
28003 workspace
28004 .update(cx, |workspace, _, cx| {
28005 let panes = workspace.panes();
28006 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
28007 for pane in panes {
28008 let editor = pane
28009 .read(cx)
28010 .active_item()
28011 .and_then(|item| item.downcast::<Editor>())
28012 .expect("Should have opened an editor in each split");
28013 let editor_file = editor
28014 .read(cx)
28015 .buffer()
28016 .read(cx)
28017 .as_singleton()
28018 .expect("test deals with singleton buffers")
28019 .read(cx)
28020 .file()
28021 .expect("test buffese should have a file")
28022 .path();
28023 assert_eq!(
28024 editor_file.as_ref(),
28025 rel_path("first.rs"),
28026 "Both editors should be opened for the same file"
28027 )
28028 }
28029 })
28030 .unwrap();
28031
28032 cx.executor().advance_clock(Duration::from_millis(500));
28033 let save = editor.update_in(cx, |editor, window, cx| {
28034 editor.move_to_end(&MoveToEnd, window, cx);
28035 editor.handle_input("dirty", window, cx);
28036 editor.save(
28037 SaveOptions {
28038 format: true,
28039 autosave: true,
28040 },
28041 project.clone(),
28042 window,
28043 cx,
28044 )
28045 });
28046 save.await.unwrap();
28047
28048 color_request_handle.next().await.unwrap();
28049 cx.run_until_parked();
28050 assert_eq!(
28051 2,
28052 requests_made.load(atomic::Ordering::Acquire),
28053 "Should query for colors once per save (deduplicated) and once per formatting after save"
28054 );
28055
28056 drop(editor);
28057 let close = workspace
28058 .update(cx, |workspace, window, cx| {
28059 workspace.active_pane().update(cx, |pane, cx| {
28060 pane.close_active_item(&CloseActiveItem::default(), window, cx)
28061 })
28062 })
28063 .unwrap();
28064 close.await.unwrap();
28065 let close = workspace
28066 .update(cx, |workspace, window, cx| {
28067 workspace.active_pane().update(cx, |pane, cx| {
28068 pane.close_active_item(&CloseActiveItem::default(), window, cx)
28069 })
28070 })
28071 .unwrap();
28072 close.await.unwrap();
28073 assert_eq!(
28074 2,
28075 requests_made.load(atomic::Ordering::Acquire),
28076 "After saving and closing all editors, no extra requests should be made"
28077 );
28078 workspace
28079 .update(cx, |workspace, _, cx| {
28080 assert!(
28081 workspace.active_item(cx).is_none(),
28082 "Should close all editors"
28083 )
28084 })
28085 .unwrap();
28086
28087 workspace
28088 .update(cx, |workspace, window, cx| {
28089 workspace.active_pane().update(cx, |pane, cx| {
28090 pane.navigate_backward(&workspace::GoBack, window, cx);
28091 })
28092 })
28093 .unwrap();
28094 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
28095 cx.run_until_parked();
28096 let editor = workspace
28097 .update(cx, |workspace, _, cx| {
28098 workspace
28099 .active_item(cx)
28100 .expect("Should have reopened the editor again after navigating back")
28101 .downcast::<Editor>()
28102 .expect("Should be an editor")
28103 })
28104 .unwrap();
28105
28106 assert_eq!(
28107 2,
28108 requests_made.load(atomic::Ordering::Acquire),
28109 "Cache should be reused on buffer close and reopen"
28110 );
28111 editor.update(cx, |editor, cx| {
28112 assert_eq!(
28113 vec![expected_color],
28114 extract_color_inlays(editor, cx),
28115 "Should have an initial inlay"
28116 );
28117 });
28118
28119 drop(color_request_handle);
28120 let closure_requests_made = Arc::clone(&requests_made);
28121 let mut empty_color_request_handle = fake_language_server
28122 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
28123 let requests_made = Arc::clone(&closure_requests_made);
28124 async move {
28125 assert_eq!(
28126 params.text_document.uri,
28127 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
28128 );
28129 requests_made.fetch_add(1, atomic::Ordering::Release);
28130 Ok(Vec::new())
28131 }
28132 });
28133 let save = editor.update_in(cx, |editor, window, cx| {
28134 editor.move_to_end(&MoveToEnd, window, cx);
28135 editor.handle_input("dirty_again", window, cx);
28136 editor.save(
28137 SaveOptions {
28138 format: false,
28139 autosave: true,
28140 },
28141 project.clone(),
28142 window,
28143 cx,
28144 )
28145 });
28146 save.await.unwrap();
28147
28148 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
28149 empty_color_request_handle.next().await.unwrap();
28150 cx.run_until_parked();
28151 assert_eq!(
28152 3,
28153 requests_made.load(atomic::Ordering::Acquire),
28154 "Should query for colors once per save only, as formatting was not requested"
28155 );
28156 editor.update(cx, |editor, cx| {
28157 assert_eq!(
28158 Vec::<Rgba>::new(),
28159 extract_color_inlays(editor, cx),
28160 "Should clear all colors when the server returns an empty response"
28161 );
28162 });
28163}
28164
28165#[gpui::test]
28166async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
28167 init_test(cx, |_| {});
28168 let (editor, cx) = cx.add_window_view(Editor::single_line);
28169 editor.update_in(cx, |editor, window, cx| {
28170 editor.set_text("oops\n\nwow\n", window, cx)
28171 });
28172 cx.run_until_parked();
28173 editor.update(cx, |editor, cx| {
28174 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
28175 });
28176 editor.update(cx, |editor, cx| {
28177 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
28178 });
28179 cx.run_until_parked();
28180 editor.update(cx, |editor, cx| {
28181 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
28182 });
28183}
28184
28185#[gpui::test]
28186async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
28187 init_test(cx, |_| {});
28188
28189 cx.update(|cx| {
28190 register_project_item::<Editor>(cx);
28191 });
28192
28193 let fs = FakeFs::new(cx.executor());
28194 fs.insert_tree("/root1", json!({})).await;
28195 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
28196 .await;
28197
28198 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
28199 let (workspace, cx) =
28200 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
28201
28202 let worktree_id = project.update(cx, |project, cx| {
28203 project.worktrees(cx).next().unwrap().read(cx).id()
28204 });
28205
28206 let handle = workspace
28207 .update_in(cx, |workspace, window, cx| {
28208 let project_path = (worktree_id, rel_path("one.pdf"));
28209 workspace.open_path(project_path, None, true, window, cx)
28210 })
28211 .await
28212 .unwrap();
28213 // The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
28214 // Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
28215 // With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
28216 assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
28217}
28218
28219#[gpui::test]
28220async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
28221 init_test(cx, |_| {});
28222
28223 let language = Arc::new(Language::new(
28224 LanguageConfig::default(),
28225 Some(tree_sitter_rust::LANGUAGE.into()),
28226 ));
28227
28228 // Test hierarchical sibling navigation
28229 let text = r#"
28230 fn outer() {
28231 if condition {
28232 let a = 1;
28233 }
28234 let b = 2;
28235 }
28236
28237 fn another() {
28238 let c = 3;
28239 }
28240 "#;
28241
28242 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
28243 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
28244 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
28245
28246 // Wait for parsing to complete
28247 editor
28248 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
28249 .await;
28250
28251 editor.update_in(cx, |editor, window, cx| {
28252 // Start by selecting "let a = 1;" inside the if block
28253 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28254 s.select_display_ranges([
28255 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
28256 ]);
28257 });
28258
28259 let initial_selection = editor
28260 .selections
28261 .display_ranges(&editor.display_snapshot(cx));
28262 assert_eq!(initial_selection.len(), 1, "Should have one selection");
28263
28264 // Test select next sibling - should move up levels to find the next sibling
28265 // Since "let a = 1;" has no siblings in the if block, it should move up
28266 // to find "let b = 2;" which is a sibling of the if block
28267 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
28268 let next_selection = editor
28269 .selections
28270 .display_ranges(&editor.display_snapshot(cx));
28271
28272 // Should have a selection and it should be different from the initial
28273 assert_eq!(
28274 next_selection.len(),
28275 1,
28276 "Should have one selection after next"
28277 );
28278 assert_ne!(
28279 next_selection[0], initial_selection[0],
28280 "Next sibling selection should be different"
28281 );
28282
28283 // Test hierarchical navigation by going to the end of the current function
28284 // and trying to navigate to the next function
28285 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28286 s.select_display_ranges([
28287 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
28288 ]);
28289 });
28290
28291 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
28292 let function_next_selection = editor
28293 .selections
28294 .display_ranges(&editor.display_snapshot(cx));
28295
28296 // Should move to the next function
28297 assert_eq!(
28298 function_next_selection.len(),
28299 1,
28300 "Should have one selection after function next"
28301 );
28302
28303 // Test select previous sibling navigation
28304 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
28305 let prev_selection = editor
28306 .selections
28307 .display_ranges(&editor.display_snapshot(cx));
28308
28309 // Should have a selection and it should be different
28310 assert_eq!(
28311 prev_selection.len(),
28312 1,
28313 "Should have one selection after prev"
28314 );
28315 assert_ne!(
28316 prev_selection[0], function_next_selection[0],
28317 "Previous sibling selection should be different from next"
28318 );
28319 });
28320}
28321
28322#[gpui::test]
28323async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
28324 init_test(cx, |_| {});
28325
28326 let mut cx = EditorTestContext::new(cx).await;
28327 cx.set_state(
28328 "let ˇvariable = 42;
28329let another = variable + 1;
28330let result = variable * 2;",
28331 );
28332
28333 // Set up document highlights manually (simulating LSP response)
28334 cx.update_editor(|editor, _window, cx| {
28335 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
28336
28337 // Create highlights for "variable" occurrences
28338 let highlight_ranges = [
28339 Point::new(0, 4)..Point::new(0, 12), // First "variable"
28340 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
28341 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
28342 ];
28343
28344 let anchor_ranges: Vec<_> = highlight_ranges
28345 .iter()
28346 .map(|range| range.clone().to_anchors(&buffer_snapshot))
28347 .collect();
28348
28349 editor.highlight_background::<DocumentHighlightRead>(
28350 &anchor_ranges,
28351 |_, theme| theme.colors().editor_document_highlight_read_background,
28352 cx,
28353 );
28354 });
28355
28356 // Go to next highlight - should move to second "variable"
28357 cx.update_editor(|editor, window, cx| {
28358 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28359 });
28360 cx.assert_editor_state(
28361 "let variable = 42;
28362let another = ˇvariable + 1;
28363let result = variable * 2;",
28364 );
28365
28366 // Go to next highlight - should move to third "variable"
28367 cx.update_editor(|editor, window, cx| {
28368 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28369 });
28370 cx.assert_editor_state(
28371 "let variable = 42;
28372let another = variable + 1;
28373let result = ˇvariable * 2;",
28374 );
28375
28376 // Go to next highlight - should stay at third "variable" (no wrap-around)
28377 cx.update_editor(|editor, window, cx| {
28378 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28379 });
28380 cx.assert_editor_state(
28381 "let variable = 42;
28382let another = variable + 1;
28383let result = ˇvariable * 2;",
28384 );
28385
28386 // Now test going backwards from third position
28387 cx.update_editor(|editor, window, cx| {
28388 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28389 });
28390 cx.assert_editor_state(
28391 "let variable = 42;
28392let another = ˇvariable + 1;
28393let result = variable * 2;",
28394 );
28395
28396 // Go to previous highlight - should move to first "variable"
28397 cx.update_editor(|editor, window, cx| {
28398 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28399 });
28400 cx.assert_editor_state(
28401 "let ˇvariable = 42;
28402let another = variable + 1;
28403let result = variable * 2;",
28404 );
28405
28406 // Go to previous highlight - should stay on first "variable"
28407 cx.update_editor(|editor, window, cx| {
28408 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28409 });
28410 cx.assert_editor_state(
28411 "let ˇvariable = 42;
28412let another = variable + 1;
28413let result = variable * 2;",
28414 );
28415}
28416
28417#[gpui::test]
28418async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
28419 cx: &mut gpui::TestAppContext,
28420) {
28421 init_test(cx, |_| {});
28422
28423 let url = "https://zed.dev";
28424
28425 let markdown_language = Arc::new(Language::new(
28426 LanguageConfig {
28427 name: "Markdown".into(),
28428 ..LanguageConfig::default()
28429 },
28430 None,
28431 ));
28432
28433 let mut cx = EditorTestContext::new(cx).await;
28434 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28435 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
28436
28437 cx.update_editor(|editor, window, cx| {
28438 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28439 editor.paste(&Paste, window, cx);
28440 });
28441
28442 cx.assert_editor_state(&format!(
28443 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
28444 ));
28445}
28446
28447#[gpui::test]
28448async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
28449 init_test(cx, |_| {});
28450
28451 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
28452 let mut cx = EditorTestContext::new(cx).await;
28453
28454 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28455
28456 // Case 1: Test if adding a character with multi cursors preserves nested list indents
28457 cx.set_state(&indoc! {"
28458 - [ ] Item 1
28459 - [ ] Item 1.a
28460 - [ˇ] Item 2
28461 - [ˇ] Item 2.a
28462 - [ˇ] Item 2.b
28463 "
28464 });
28465 cx.update_editor(|editor, window, cx| {
28466 editor.handle_input("x", window, cx);
28467 });
28468 cx.run_until_parked();
28469 cx.assert_editor_state(indoc! {"
28470 - [ ] Item 1
28471 - [ ] Item 1.a
28472 - [xˇ] Item 2
28473 - [xˇ] Item 2.a
28474 - [xˇ] Item 2.b
28475 "
28476 });
28477
28478 // Case 2: Test adding new line after nested list continues the list with unchecked task
28479 cx.set_state(&indoc! {"
28480 - [ ] Item 1
28481 - [ ] Item 1.a
28482 - [x] Item 2
28483 - [x] Item 2.a
28484 - [x] Item 2.bˇ"
28485 });
28486 cx.update_editor(|editor, window, cx| {
28487 editor.newline(&Newline, window, cx);
28488 });
28489 cx.assert_editor_state(indoc! {"
28490 - [ ] Item 1
28491 - [ ] Item 1.a
28492 - [x] Item 2
28493 - [x] Item 2.a
28494 - [x] Item 2.b
28495 - [ ] ˇ"
28496 });
28497
28498 // Case 3: Test adding content to continued list item
28499 cx.update_editor(|editor, window, cx| {
28500 editor.handle_input("Item 2.c", window, cx);
28501 });
28502 cx.run_until_parked();
28503 cx.assert_editor_state(indoc! {"
28504 - [ ] Item 1
28505 - [ ] Item 1.a
28506 - [x] Item 2
28507 - [x] Item 2.a
28508 - [x] Item 2.b
28509 - [ ] Item 2.cˇ"
28510 });
28511
28512 // Case 4: Test adding new line after nested ordered list continues with next number
28513 cx.set_state(indoc! {"
28514 1. Item 1
28515 1. Item 1.a
28516 2. Item 2
28517 1. Item 2.a
28518 2. Item 2.bˇ"
28519 });
28520 cx.update_editor(|editor, window, cx| {
28521 editor.newline(&Newline, window, cx);
28522 });
28523 cx.assert_editor_state(indoc! {"
28524 1. Item 1
28525 1. Item 1.a
28526 2. Item 2
28527 1. Item 2.a
28528 2. Item 2.b
28529 3. ˇ"
28530 });
28531
28532 // Case 5: Adding content to continued ordered list item
28533 cx.update_editor(|editor, window, cx| {
28534 editor.handle_input("Item 2.c", window, cx);
28535 });
28536 cx.run_until_parked();
28537 cx.assert_editor_state(indoc! {"
28538 1. Item 1
28539 1. Item 1.a
28540 2. Item 2
28541 1. Item 2.a
28542 2. Item 2.b
28543 3. Item 2.cˇ"
28544 });
28545
28546 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
28547 cx.set_state(indoc! {"
28548 - Item 1
28549 - Item 1.a
28550 - Item 1.a
28551 ˇ"});
28552 cx.update_editor(|editor, window, cx| {
28553 editor.handle_input("-", window, cx);
28554 });
28555 cx.run_until_parked();
28556 cx.assert_editor_state(indoc! {"
28557 - Item 1
28558 - Item 1.a
28559 - Item 1.a
28560 -ˇ"});
28561
28562 // Case 7: Test blockquote newline preserves something
28563 cx.set_state(indoc! {"
28564 > Item 1ˇ"
28565 });
28566 cx.update_editor(|editor, window, cx| {
28567 editor.newline(&Newline, window, cx);
28568 });
28569 cx.assert_editor_state(indoc! {"
28570 > Item 1
28571 ˇ"
28572 });
28573}
28574
28575#[gpui::test]
28576async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
28577 cx: &mut gpui::TestAppContext,
28578) {
28579 init_test(cx, |_| {});
28580
28581 let url = "https://zed.dev";
28582
28583 let markdown_language = Arc::new(Language::new(
28584 LanguageConfig {
28585 name: "Markdown".into(),
28586 ..LanguageConfig::default()
28587 },
28588 None,
28589 ));
28590
28591 let mut cx = EditorTestContext::new(cx).await;
28592 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28593 cx.set_state(&format!(
28594 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
28595 ));
28596
28597 cx.update_editor(|editor, window, cx| {
28598 editor.copy(&Copy, window, cx);
28599 });
28600
28601 cx.set_state(&format!(
28602 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
28603 ));
28604
28605 cx.update_editor(|editor, window, cx| {
28606 editor.paste(&Paste, window, cx);
28607 });
28608
28609 cx.assert_editor_state(&format!(
28610 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
28611 ));
28612}
28613
28614#[gpui::test]
28615async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
28616 cx: &mut gpui::TestAppContext,
28617) {
28618 init_test(cx, |_| {});
28619
28620 let url = "https://zed.dev";
28621
28622 let markdown_language = Arc::new(Language::new(
28623 LanguageConfig {
28624 name: "Markdown".into(),
28625 ..LanguageConfig::default()
28626 },
28627 None,
28628 ));
28629
28630 let mut cx = EditorTestContext::new(cx).await;
28631 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28632 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
28633
28634 cx.update_editor(|editor, window, cx| {
28635 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28636 editor.paste(&Paste, window, cx);
28637 });
28638
28639 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
28640}
28641
28642#[gpui::test]
28643async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
28644 cx: &mut gpui::TestAppContext,
28645) {
28646 init_test(cx, |_| {});
28647
28648 let text = "Awesome";
28649
28650 let markdown_language = Arc::new(Language::new(
28651 LanguageConfig {
28652 name: "Markdown".into(),
28653 ..LanguageConfig::default()
28654 },
28655 None,
28656 ));
28657
28658 let mut cx = EditorTestContext::new(cx).await;
28659 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28660 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
28661
28662 cx.update_editor(|editor, window, cx| {
28663 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
28664 editor.paste(&Paste, window, cx);
28665 });
28666
28667 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
28668}
28669
28670#[gpui::test]
28671async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
28672 cx: &mut gpui::TestAppContext,
28673) {
28674 init_test(cx, |_| {});
28675
28676 let url = "https://zed.dev";
28677
28678 let markdown_language = Arc::new(Language::new(
28679 LanguageConfig {
28680 name: "Rust".into(),
28681 ..LanguageConfig::default()
28682 },
28683 None,
28684 ));
28685
28686 let mut cx = EditorTestContext::new(cx).await;
28687 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28688 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
28689
28690 cx.update_editor(|editor, window, cx| {
28691 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28692 editor.paste(&Paste, window, cx);
28693 });
28694
28695 cx.assert_editor_state(&format!(
28696 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
28697 ));
28698}
28699
28700#[gpui::test]
28701async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
28702 cx: &mut TestAppContext,
28703) {
28704 init_test(cx, |_| {});
28705
28706 let url = "https://zed.dev";
28707
28708 let markdown_language = Arc::new(Language::new(
28709 LanguageConfig {
28710 name: "Markdown".into(),
28711 ..LanguageConfig::default()
28712 },
28713 None,
28714 ));
28715
28716 let (editor, cx) = cx.add_window_view(|window, cx| {
28717 let multi_buffer = MultiBuffer::build_multi(
28718 [
28719 ("this will embed -> link", vec![Point::row_range(0..1)]),
28720 ("this will replace -> link", vec![Point::row_range(0..1)]),
28721 ],
28722 cx,
28723 );
28724 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
28725 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28726 s.select_ranges(vec![
28727 Point::new(0, 19)..Point::new(0, 23),
28728 Point::new(1, 21)..Point::new(1, 25),
28729 ])
28730 });
28731 let first_buffer_id = multi_buffer
28732 .read(cx)
28733 .excerpt_buffer_ids()
28734 .into_iter()
28735 .next()
28736 .unwrap();
28737 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
28738 first_buffer.update(cx, |buffer, cx| {
28739 buffer.set_language(Some(markdown_language.clone()), cx);
28740 });
28741
28742 editor
28743 });
28744 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28745
28746 cx.update_editor(|editor, window, cx| {
28747 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28748 editor.paste(&Paste, window, cx);
28749 });
28750
28751 cx.assert_editor_state(&format!(
28752 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
28753 ));
28754}
28755
28756#[gpui::test]
28757async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
28758 init_test(cx, |_| {});
28759
28760 let fs = FakeFs::new(cx.executor());
28761 fs.insert_tree(
28762 path!("/project"),
28763 json!({
28764 "first.rs": "# First Document\nSome content here.",
28765 "second.rs": "Plain text content for second file.",
28766 }),
28767 )
28768 .await;
28769
28770 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
28771 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
28772 let cx = &mut VisualTestContext::from_window(*workspace, cx);
28773
28774 let language = rust_lang();
28775 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28776 language_registry.add(language.clone());
28777 let mut fake_servers = language_registry.register_fake_lsp(
28778 "Rust",
28779 FakeLspAdapter {
28780 ..FakeLspAdapter::default()
28781 },
28782 );
28783
28784 let buffer1 = project
28785 .update(cx, |project, cx| {
28786 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
28787 })
28788 .await
28789 .unwrap();
28790 let buffer2 = project
28791 .update(cx, |project, cx| {
28792 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
28793 })
28794 .await
28795 .unwrap();
28796
28797 let multi_buffer = cx.new(|cx| {
28798 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
28799 multi_buffer.set_excerpts_for_path(
28800 PathKey::for_buffer(&buffer1, cx),
28801 buffer1.clone(),
28802 [Point::zero()..buffer1.read(cx).max_point()],
28803 3,
28804 cx,
28805 );
28806 multi_buffer.set_excerpts_for_path(
28807 PathKey::for_buffer(&buffer2, cx),
28808 buffer2.clone(),
28809 [Point::zero()..buffer1.read(cx).max_point()],
28810 3,
28811 cx,
28812 );
28813 multi_buffer
28814 });
28815
28816 let (editor, cx) = cx.add_window_view(|window, cx| {
28817 Editor::new(
28818 EditorMode::full(),
28819 multi_buffer,
28820 Some(project.clone()),
28821 window,
28822 cx,
28823 )
28824 });
28825
28826 let fake_language_server = fake_servers.next().await.unwrap();
28827
28828 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
28829
28830 let save = editor.update_in(cx, |editor, window, cx| {
28831 assert!(editor.is_dirty(cx));
28832
28833 editor.save(
28834 SaveOptions {
28835 format: true,
28836 autosave: true,
28837 },
28838 project,
28839 window,
28840 cx,
28841 )
28842 });
28843 let (start_edit_tx, start_edit_rx) = oneshot::channel();
28844 let (done_edit_tx, done_edit_rx) = oneshot::channel();
28845 let mut done_edit_rx = Some(done_edit_rx);
28846 let mut start_edit_tx = Some(start_edit_tx);
28847
28848 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
28849 start_edit_tx.take().unwrap().send(()).unwrap();
28850 let done_edit_rx = done_edit_rx.take().unwrap();
28851 async move {
28852 done_edit_rx.await.unwrap();
28853 Ok(None)
28854 }
28855 });
28856
28857 start_edit_rx.await.unwrap();
28858 buffer2
28859 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
28860 .unwrap();
28861
28862 done_edit_tx.send(()).unwrap();
28863
28864 save.await.unwrap();
28865 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
28866}
28867
28868#[track_caller]
28869fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
28870 editor
28871 .all_inlays(cx)
28872 .into_iter()
28873 .filter_map(|inlay| inlay.get_color())
28874 .map(Rgba::from)
28875 .collect()
28876}
28877
28878#[gpui::test]
28879fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
28880 init_test(cx, |_| {});
28881
28882 let editor = cx.add_window(|window, cx| {
28883 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
28884 build_editor(buffer, window, cx)
28885 });
28886
28887 editor
28888 .update(cx, |editor, window, cx| {
28889 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28890 s.select_display_ranges([
28891 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
28892 ])
28893 });
28894
28895 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
28896
28897 assert_eq!(
28898 editor.display_text(cx),
28899 "line1\nline2\nline2",
28900 "Duplicating last line upward should create duplicate above, not on same line"
28901 );
28902
28903 assert_eq!(
28904 editor
28905 .selections
28906 .display_ranges(&editor.display_snapshot(cx)),
28907 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
28908 "Selection should move to the duplicated line"
28909 );
28910 })
28911 .unwrap();
28912}
28913
28914#[gpui::test]
28915async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
28916 init_test(cx, |_| {});
28917
28918 let mut cx = EditorTestContext::new(cx).await;
28919
28920 cx.set_state("line1\nline2ˇ");
28921
28922 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28923
28924 let clipboard_text = cx
28925 .read_from_clipboard()
28926 .and_then(|item| item.text().as_deref().map(str::to_string));
28927
28928 assert_eq!(
28929 clipboard_text,
28930 Some("line2\n".to_string()),
28931 "Copying a line without trailing newline should include a newline"
28932 );
28933
28934 cx.set_state("line1\nˇ");
28935
28936 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28937
28938 cx.assert_editor_state("line1\nline2\nˇ");
28939}
28940
28941#[gpui::test]
28942async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28943 init_test(cx, |_| {});
28944
28945 let mut cx = EditorTestContext::new(cx).await;
28946
28947 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28948
28949 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28950
28951 let clipboard_text = cx
28952 .read_from_clipboard()
28953 .and_then(|item| item.text().as_deref().map(str::to_string));
28954
28955 assert_eq!(
28956 clipboard_text,
28957 Some("line1\nline2\nline3\n".to_string()),
28958 "Copying multiple lines should include a single newline between lines"
28959 );
28960
28961 cx.set_state("lineA\nˇ");
28962
28963 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28964
28965 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28966}
28967
28968#[gpui::test]
28969async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28970 init_test(cx, |_| {});
28971
28972 let mut cx = EditorTestContext::new(cx).await;
28973
28974 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28975
28976 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
28977
28978 let clipboard_text = cx
28979 .read_from_clipboard()
28980 .and_then(|item| item.text().as_deref().map(str::to_string));
28981
28982 assert_eq!(
28983 clipboard_text,
28984 Some("line1\nline2\nline3\n".to_string()),
28985 "Copying multiple lines should include a single newline between lines"
28986 );
28987
28988 cx.set_state("lineA\nˇ");
28989
28990 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28991
28992 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28993}
28994
28995#[gpui::test]
28996async fn test_end_of_editor_context(cx: &mut TestAppContext) {
28997 init_test(cx, |_| {});
28998
28999 let mut cx = EditorTestContext::new(cx).await;
29000
29001 cx.set_state("line1\nline2ˇ");
29002 cx.update_editor(|e, window, cx| {
29003 e.set_mode(EditorMode::SingleLine);
29004 assert!(e.key_context(window, cx).contains("end_of_input"));
29005 });
29006 cx.set_state("ˇline1\nline2");
29007 cx.update_editor(|e, window, cx| {
29008 assert!(!e.key_context(window, cx).contains("end_of_input"));
29009 });
29010 cx.set_state("line1ˇ\nline2");
29011 cx.update_editor(|e, window, cx| {
29012 assert!(!e.key_context(window, cx).contains("end_of_input"));
29013 });
29014}
29015
29016#[gpui::test]
29017async fn test_sticky_scroll(cx: &mut TestAppContext) {
29018 init_test(cx, |_| {});
29019 let mut cx = EditorTestContext::new(cx).await;
29020
29021 let buffer = indoc! {"
29022 ˇfn foo() {
29023 let abc = 123;
29024 }
29025 struct Bar;
29026 impl Bar {
29027 fn new() -> Self {
29028 Self
29029 }
29030 }
29031 fn baz() {
29032 }
29033 "};
29034 cx.set_state(&buffer);
29035
29036 cx.update_editor(|e, _, cx| {
29037 e.buffer()
29038 .read(cx)
29039 .as_singleton()
29040 .unwrap()
29041 .update(cx, |buffer, cx| {
29042 buffer.set_language(Some(rust_lang()), cx);
29043 })
29044 });
29045
29046 let mut sticky_headers = |offset: ScrollOffset| {
29047 cx.update_editor(|e, window, cx| {
29048 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
29049 let style = e.style(cx).clone();
29050 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), &style, cx)
29051 .into_iter()
29052 .map(
29053 |StickyHeader {
29054 start_point,
29055 offset,
29056 ..
29057 }| { (start_point, offset) },
29058 )
29059 .collect::<Vec<_>>()
29060 })
29061 };
29062
29063 let fn_foo = Point { row: 0, column: 0 };
29064 let impl_bar = Point { row: 4, column: 0 };
29065 let fn_new = Point { row: 5, column: 4 };
29066
29067 assert_eq!(sticky_headers(0.0), vec![]);
29068 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
29069 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
29070 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
29071 assert_eq!(sticky_headers(2.0), vec![]);
29072 assert_eq!(sticky_headers(2.5), vec![]);
29073 assert_eq!(sticky_headers(3.0), vec![]);
29074 assert_eq!(sticky_headers(3.5), vec![]);
29075 assert_eq!(sticky_headers(4.0), vec![]);
29076 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
29077 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
29078 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
29079 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
29080 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
29081 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
29082 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
29083 assert_eq!(sticky_headers(8.0), vec![]);
29084 assert_eq!(sticky_headers(8.5), vec![]);
29085 assert_eq!(sticky_headers(9.0), vec![]);
29086 assert_eq!(sticky_headers(9.5), vec![]);
29087 assert_eq!(sticky_headers(10.0), vec![]);
29088}
29089
29090#[gpui::test]
29091fn test_relative_line_numbers(cx: &mut TestAppContext) {
29092 init_test(cx, |_| {});
29093
29094 let buffer_1 = cx.new(|cx| Buffer::local("aaaaaaaaaa\nbbb\n", cx));
29095 let buffer_2 = cx.new(|cx| Buffer::local("cccccccccc\nddd\n", cx));
29096 let buffer_3 = cx.new(|cx| Buffer::local("eee\nffffffffff\n", cx));
29097
29098 let multibuffer = cx.new(|cx| {
29099 let mut multibuffer = MultiBuffer::new(ReadWrite);
29100 multibuffer.push_excerpts(
29101 buffer_1.clone(),
29102 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
29103 cx,
29104 );
29105 multibuffer.push_excerpts(
29106 buffer_2.clone(),
29107 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
29108 cx,
29109 );
29110 multibuffer.push_excerpts(
29111 buffer_3.clone(),
29112 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
29113 cx,
29114 );
29115 multibuffer
29116 });
29117
29118 // wrapped contents of multibuffer:
29119 // aaa
29120 // aaa
29121 // aaa
29122 // a
29123 // bbb
29124 //
29125 // ccc
29126 // ccc
29127 // ccc
29128 // c
29129 // ddd
29130 //
29131 // eee
29132 // fff
29133 // fff
29134 // fff
29135 // f
29136
29137 let editor = cx.add_window(|window, cx| build_editor(multibuffer, window, cx));
29138 _ = editor.update(cx, |editor, window, cx| {
29139 editor.set_wrap_width(Some(30.0.into()), cx); // every 3 characters
29140
29141 // includes trailing newlines.
29142 let expected_line_numbers = [2, 6, 7, 10, 14, 15, 18, 19, 23];
29143 let expected_wrapped_line_numbers = [
29144 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23,
29145 ];
29146
29147 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
29148 s.select_ranges([
29149 Point::new(7, 0)..Point::new(7, 1), // second row of `ccc`
29150 ]);
29151 });
29152
29153 let snapshot = editor.snapshot(window, cx);
29154
29155 // these are all 0-indexed
29156 let base_display_row = DisplayRow(11);
29157 let base_row = 3;
29158 let wrapped_base_row = 7;
29159
29160 // test not counting wrapped lines
29161 let expected_relative_numbers = expected_line_numbers
29162 .into_iter()
29163 .enumerate()
29164 .map(|(i, row)| (DisplayRow(row), i.abs_diff(base_row) as u32))
29165 .collect_vec();
29166 let actual_relative_numbers = snapshot
29167 .calculate_relative_line_numbers(
29168 &(DisplayRow(0)..DisplayRow(24)),
29169 base_display_row,
29170 false,
29171 )
29172 .into_iter()
29173 .sorted()
29174 .collect_vec();
29175 assert_eq!(expected_relative_numbers, actual_relative_numbers);
29176 // check `calculate_relative_line_numbers()` against `relative_line_delta()` for each line
29177 for (display_row, relative_number) in expected_relative_numbers {
29178 assert_eq!(
29179 relative_number,
29180 snapshot
29181 .relative_line_delta(display_row, base_display_row, false)
29182 .unsigned_abs() as u32,
29183 );
29184 }
29185
29186 // test counting wrapped lines
29187 let expected_wrapped_relative_numbers = expected_wrapped_line_numbers
29188 .into_iter()
29189 .enumerate()
29190 .map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32))
29191 .filter(|(row, _)| *row != base_display_row)
29192 .collect_vec();
29193 let actual_relative_numbers = snapshot
29194 .calculate_relative_line_numbers(
29195 &(DisplayRow(0)..DisplayRow(24)),
29196 base_display_row,
29197 true,
29198 )
29199 .into_iter()
29200 .sorted()
29201 .collect_vec();
29202 assert_eq!(expected_wrapped_relative_numbers, actual_relative_numbers);
29203 // check `calculate_relative_line_numbers()` against `relative_wrapped_line_delta()` for each line
29204 for (display_row, relative_number) in expected_wrapped_relative_numbers {
29205 assert_eq!(
29206 relative_number,
29207 snapshot
29208 .relative_line_delta(display_row, base_display_row, true)
29209 .unsigned_abs() as u32,
29210 );
29211 }
29212 });
29213}
29214
29215#[gpui::test]
29216async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
29217 init_test(cx, |_| {});
29218 cx.update(|cx| {
29219 SettingsStore::update_global(cx, |store, cx| {
29220 store.update_user_settings(cx, |settings| {
29221 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
29222 enabled: Some(true),
29223 })
29224 });
29225 });
29226 });
29227 let mut cx = EditorTestContext::new(cx).await;
29228
29229 let line_height = cx.update_editor(|editor, window, cx| {
29230 editor
29231 .style(cx)
29232 .text
29233 .line_height_in_pixels(window.rem_size())
29234 });
29235
29236 let buffer = indoc! {"
29237 ˇfn foo() {
29238 let abc = 123;
29239 }
29240 struct Bar;
29241 impl Bar {
29242 fn new() -> Self {
29243 Self
29244 }
29245 }
29246 fn baz() {
29247 }
29248 "};
29249 cx.set_state(&buffer);
29250
29251 cx.update_editor(|e, _, cx| {
29252 e.buffer()
29253 .read(cx)
29254 .as_singleton()
29255 .unwrap()
29256 .update(cx, |buffer, cx| {
29257 buffer.set_language(Some(rust_lang()), cx);
29258 })
29259 });
29260
29261 let fn_foo = || empty_range(0, 0);
29262 let impl_bar = || empty_range(4, 0);
29263 let fn_new = || empty_range(5, 4);
29264
29265 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
29266 cx.update_editor(|e, window, cx| {
29267 e.scroll(
29268 gpui::Point {
29269 x: 0.,
29270 y: scroll_offset,
29271 },
29272 None,
29273 window,
29274 cx,
29275 );
29276 });
29277 cx.simulate_click(
29278 gpui::Point {
29279 x: px(0.),
29280 y: click_offset as f32 * line_height,
29281 },
29282 Modifiers::none(),
29283 );
29284 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
29285 };
29286
29287 assert_eq!(
29288 scroll_and_click(
29289 4.5, // impl Bar is halfway off the screen
29290 0.0 // click top of screen
29291 ),
29292 // scrolled to impl Bar
29293 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29294 );
29295
29296 assert_eq!(
29297 scroll_and_click(
29298 4.5, // impl Bar is halfway off the screen
29299 0.25 // click middle of impl Bar
29300 ),
29301 // scrolled to impl Bar
29302 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29303 );
29304
29305 assert_eq!(
29306 scroll_and_click(
29307 4.5, // impl Bar is halfway off the screen
29308 1.5 // click below impl Bar (e.g. fn new())
29309 ),
29310 // scrolled to fn new() - this is below the impl Bar header which has persisted
29311 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
29312 );
29313
29314 assert_eq!(
29315 scroll_and_click(
29316 5.5, // fn new is halfway underneath impl Bar
29317 0.75 // click on the overlap of impl Bar and fn new()
29318 ),
29319 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29320 );
29321
29322 assert_eq!(
29323 scroll_and_click(
29324 5.5, // fn new is halfway underneath impl Bar
29325 1.25 // click on the visible part of fn new()
29326 ),
29327 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
29328 );
29329
29330 assert_eq!(
29331 scroll_and_click(
29332 1.5, // fn foo is halfway off the screen
29333 0.0 // click top of screen
29334 ),
29335 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
29336 );
29337
29338 assert_eq!(
29339 scroll_and_click(
29340 1.5, // fn foo is halfway off the screen
29341 0.75 // click visible part of let abc...
29342 )
29343 .0,
29344 // no change in scroll
29345 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
29346 (gpui::Point { x: 0., y: 1.5 })
29347 );
29348}
29349
29350#[gpui::test]
29351async fn test_next_prev_reference(cx: &mut TestAppContext) {
29352 const CYCLE_POSITIONS: &[&'static str] = &[
29353 indoc! {"
29354 fn foo() {
29355 let ˇabc = 123;
29356 let x = abc + 1;
29357 let y = abc + 2;
29358 let z = abc + 2;
29359 }
29360 "},
29361 indoc! {"
29362 fn foo() {
29363 let abc = 123;
29364 let x = ˇabc + 1;
29365 let y = abc + 2;
29366 let z = abc + 2;
29367 }
29368 "},
29369 indoc! {"
29370 fn foo() {
29371 let abc = 123;
29372 let x = abc + 1;
29373 let y = ˇabc + 2;
29374 let z = abc + 2;
29375 }
29376 "},
29377 indoc! {"
29378 fn foo() {
29379 let abc = 123;
29380 let x = abc + 1;
29381 let y = abc + 2;
29382 let z = ˇabc + 2;
29383 }
29384 "},
29385 ];
29386
29387 init_test(cx, |_| {});
29388
29389 let mut cx = EditorLspTestContext::new_rust(
29390 lsp::ServerCapabilities {
29391 references_provider: Some(lsp::OneOf::Left(true)),
29392 ..Default::default()
29393 },
29394 cx,
29395 )
29396 .await;
29397
29398 // importantly, the cursor is in the middle
29399 cx.set_state(indoc! {"
29400 fn foo() {
29401 let aˇbc = 123;
29402 let x = abc + 1;
29403 let y = abc + 2;
29404 let z = abc + 2;
29405 }
29406 "});
29407
29408 let reference_ranges = [
29409 lsp::Position::new(1, 8),
29410 lsp::Position::new(2, 12),
29411 lsp::Position::new(3, 12),
29412 lsp::Position::new(4, 12),
29413 ]
29414 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
29415
29416 cx.lsp
29417 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
29418 Ok(Some(
29419 reference_ranges
29420 .map(|range| lsp::Location {
29421 uri: params.text_document_position.text_document.uri.clone(),
29422 range,
29423 })
29424 .to_vec(),
29425 ))
29426 });
29427
29428 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
29429 cx.update_editor(|editor, window, cx| {
29430 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
29431 })
29432 .unwrap()
29433 .await
29434 .unwrap()
29435 };
29436
29437 _move(Direction::Next, 1, &mut cx).await;
29438 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29439
29440 _move(Direction::Next, 1, &mut cx).await;
29441 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29442
29443 _move(Direction::Next, 1, &mut cx).await;
29444 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29445
29446 // loops back to the start
29447 _move(Direction::Next, 1, &mut cx).await;
29448 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29449
29450 // loops back to the end
29451 _move(Direction::Prev, 1, &mut cx).await;
29452 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29453
29454 _move(Direction::Prev, 1, &mut cx).await;
29455 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29456
29457 _move(Direction::Prev, 1, &mut cx).await;
29458 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29459
29460 _move(Direction::Prev, 1, &mut cx).await;
29461 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29462
29463 _move(Direction::Next, 3, &mut cx).await;
29464 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29465
29466 _move(Direction::Prev, 2, &mut cx).await;
29467 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29468}
29469
29470#[gpui::test]
29471async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
29472 init_test(cx, |_| {});
29473
29474 let (editor, cx) = cx.add_window_view(|window, cx| {
29475 let multi_buffer = MultiBuffer::build_multi(
29476 [
29477 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29478 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29479 ],
29480 cx,
29481 );
29482 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29483 });
29484
29485 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29486 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
29487
29488 cx.assert_excerpts_with_selections(indoc! {"
29489 [EXCERPT]
29490 ˇ1
29491 2
29492 3
29493 [EXCERPT]
29494 1
29495 2
29496 3
29497 "});
29498
29499 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
29500 cx.update_editor(|editor, window, cx| {
29501 editor.change_selections(None.into(), window, cx, |s| {
29502 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29503 });
29504 });
29505 cx.assert_excerpts_with_selections(indoc! {"
29506 [EXCERPT]
29507 1
29508 2ˇ
29509 3
29510 [EXCERPT]
29511 1
29512 2
29513 3
29514 "});
29515
29516 cx.update_editor(|editor, window, cx| {
29517 editor
29518 .select_all_matches(&SelectAllMatches, window, cx)
29519 .unwrap();
29520 });
29521 cx.assert_excerpts_with_selections(indoc! {"
29522 [EXCERPT]
29523 1
29524 2ˇ
29525 3
29526 [EXCERPT]
29527 1
29528 2ˇ
29529 3
29530 "});
29531
29532 cx.update_editor(|editor, window, cx| {
29533 editor.handle_input("X", window, cx);
29534 });
29535 cx.assert_excerpts_with_selections(indoc! {"
29536 [EXCERPT]
29537 1
29538 Xˇ
29539 3
29540 [EXCERPT]
29541 1
29542 Xˇ
29543 3
29544 "});
29545
29546 // Scenario 2: Select "2", then fold second buffer before insertion
29547 cx.update_multibuffer(|mb, cx| {
29548 for buffer_id in buffer_ids.iter() {
29549 let buffer = mb.buffer(*buffer_id).unwrap();
29550 buffer.update(cx, |buffer, cx| {
29551 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29552 });
29553 }
29554 });
29555
29556 // Select "2" and select all matches
29557 cx.update_editor(|editor, window, cx| {
29558 editor.change_selections(None.into(), window, cx, |s| {
29559 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29560 });
29561 editor
29562 .select_all_matches(&SelectAllMatches, window, cx)
29563 .unwrap();
29564 });
29565
29566 // Fold second buffer - should remove selections from folded buffer
29567 cx.update_editor(|editor, _, cx| {
29568 editor.fold_buffer(buffer_ids[1], cx);
29569 });
29570 cx.assert_excerpts_with_selections(indoc! {"
29571 [EXCERPT]
29572 1
29573 2ˇ
29574 3
29575 [EXCERPT]
29576 [FOLDED]
29577 "});
29578
29579 // Insert text - should only affect first buffer
29580 cx.update_editor(|editor, window, cx| {
29581 editor.handle_input("Y", window, cx);
29582 });
29583 cx.update_editor(|editor, _, cx| {
29584 editor.unfold_buffer(buffer_ids[1], cx);
29585 });
29586 cx.assert_excerpts_with_selections(indoc! {"
29587 [EXCERPT]
29588 1
29589 Yˇ
29590 3
29591 [EXCERPT]
29592 1
29593 2
29594 3
29595 "});
29596
29597 // Scenario 3: Select "2", then fold first buffer before insertion
29598 cx.update_multibuffer(|mb, cx| {
29599 for buffer_id in buffer_ids.iter() {
29600 let buffer = mb.buffer(*buffer_id).unwrap();
29601 buffer.update(cx, |buffer, cx| {
29602 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29603 });
29604 }
29605 });
29606
29607 // Select "2" and select all matches
29608 cx.update_editor(|editor, window, cx| {
29609 editor.change_selections(None.into(), window, cx, |s| {
29610 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29611 });
29612 editor
29613 .select_all_matches(&SelectAllMatches, window, cx)
29614 .unwrap();
29615 });
29616
29617 // Fold first buffer - should remove selections from folded buffer
29618 cx.update_editor(|editor, _, cx| {
29619 editor.fold_buffer(buffer_ids[0], cx);
29620 });
29621 cx.assert_excerpts_with_selections(indoc! {"
29622 [EXCERPT]
29623 [FOLDED]
29624 [EXCERPT]
29625 1
29626 2ˇ
29627 3
29628 "});
29629
29630 // Insert text - should only affect second buffer
29631 cx.update_editor(|editor, window, cx| {
29632 editor.handle_input("Z", window, cx);
29633 });
29634 cx.update_editor(|editor, _, cx| {
29635 editor.unfold_buffer(buffer_ids[0], cx);
29636 });
29637 cx.assert_excerpts_with_selections(indoc! {"
29638 [EXCERPT]
29639 1
29640 2
29641 3
29642 [EXCERPT]
29643 1
29644 Zˇ
29645 3
29646 "});
29647
29648 // Test correct folded header is selected upon fold
29649 cx.update_editor(|editor, _, cx| {
29650 editor.fold_buffer(buffer_ids[0], cx);
29651 editor.fold_buffer(buffer_ids[1], cx);
29652 });
29653 cx.assert_excerpts_with_selections(indoc! {"
29654 [EXCERPT]
29655 [FOLDED]
29656 [EXCERPT]
29657 ˇ[FOLDED]
29658 "});
29659
29660 // Test selection inside folded buffer unfolds it on type
29661 cx.update_editor(|editor, window, cx| {
29662 editor.handle_input("W", window, cx);
29663 });
29664 cx.update_editor(|editor, _, cx| {
29665 editor.unfold_buffer(buffer_ids[0], cx);
29666 });
29667 cx.assert_excerpts_with_selections(indoc! {"
29668 [EXCERPT]
29669 1
29670 2
29671 3
29672 [EXCERPT]
29673 Wˇ1
29674 Z
29675 3
29676 "});
29677}
29678
29679#[gpui::test]
29680async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
29681 init_test(cx, |_| {});
29682
29683 let (editor, cx) = cx.add_window_view(|window, cx| {
29684 let multi_buffer = MultiBuffer::build_multi(
29685 [
29686 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29687 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
29688 ],
29689 cx,
29690 );
29691 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29692 });
29693
29694 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29695
29696 cx.assert_excerpts_with_selections(indoc! {"
29697 [EXCERPT]
29698 ˇ1
29699 2
29700 3
29701 [EXCERPT]
29702 1
29703 2
29704 3
29705 4
29706 5
29707 6
29708 7
29709 8
29710 9
29711 "});
29712
29713 cx.update_editor(|editor, window, cx| {
29714 editor.change_selections(None.into(), window, cx, |s| {
29715 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
29716 });
29717 });
29718
29719 cx.assert_excerpts_with_selections(indoc! {"
29720 [EXCERPT]
29721 1
29722 2
29723 3
29724 [EXCERPT]
29725 1
29726 2
29727 3
29728 4
29729 5
29730 6
29731 ˇ7
29732 8
29733 9
29734 "});
29735
29736 cx.update_editor(|editor, _window, cx| {
29737 editor.set_vertical_scroll_margin(0, cx);
29738 });
29739
29740 cx.update_editor(|editor, window, cx| {
29741 assert_eq!(editor.vertical_scroll_margin(), 0);
29742 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29743 assert_eq!(
29744 editor.snapshot(window, cx).scroll_position(),
29745 gpui::Point::new(0., 12.0)
29746 );
29747 });
29748
29749 cx.update_editor(|editor, _window, cx| {
29750 editor.set_vertical_scroll_margin(3, cx);
29751 });
29752
29753 cx.update_editor(|editor, window, cx| {
29754 assert_eq!(editor.vertical_scroll_margin(), 3);
29755 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29756 assert_eq!(
29757 editor.snapshot(window, cx).scroll_position(),
29758 gpui::Point::new(0., 9.0)
29759 );
29760 });
29761}
29762
29763#[gpui::test]
29764async fn test_find_references_single_case(cx: &mut TestAppContext) {
29765 init_test(cx, |_| {});
29766 let mut cx = EditorLspTestContext::new_rust(
29767 lsp::ServerCapabilities {
29768 references_provider: Some(lsp::OneOf::Left(true)),
29769 ..lsp::ServerCapabilities::default()
29770 },
29771 cx,
29772 )
29773 .await;
29774
29775 let before = indoc!(
29776 r#"
29777 fn main() {
29778 let aˇbc = 123;
29779 let xyz = abc;
29780 }
29781 "#
29782 );
29783 let after = indoc!(
29784 r#"
29785 fn main() {
29786 let abc = 123;
29787 let xyz = ˇabc;
29788 }
29789 "#
29790 );
29791
29792 cx.lsp
29793 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
29794 Ok(Some(vec![
29795 lsp::Location {
29796 uri: params.text_document_position.text_document.uri.clone(),
29797 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
29798 },
29799 lsp::Location {
29800 uri: params.text_document_position.text_document.uri,
29801 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
29802 },
29803 ]))
29804 });
29805
29806 cx.set_state(before);
29807
29808 let action = FindAllReferences {
29809 always_open_multibuffer: false,
29810 };
29811
29812 let navigated = cx
29813 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
29814 .expect("should have spawned a task")
29815 .await
29816 .unwrap();
29817
29818 assert_eq!(navigated, Navigated::No);
29819
29820 cx.run_until_parked();
29821
29822 cx.assert_editor_state(after);
29823}
29824
29825#[gpui::test]
29826async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
29827 init_test(cx, |settings| {
29828 settings.defaults.tab_size = Some(2.try_into().unwrap());
29829 });
29830
29831 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29832 let mut cx = EditorTestContext::new(cx).await;
29833 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29834
29835 // Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
29836 cx.set_state(indoc! {"
29837 - [ ] taskˇ
29838 "});
29839 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29840 cx.wait_for_autoindent_applied().await;
29841 cx.assert_editor_state(indoc! {"
29842 - [ ] task
29843 - [ ] ˇ
29844 "});
29845
29846 // Case 2: Works with checked task items too
29847 cx.set_state(indoc! {"
29848 - [x] completed taskˇ
29849 "});
29850 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29851 cx.wait_for_autoindent_applied().await;
29852 cx.assert_editor_state(indoc! {"
29853 - [x] completed task
29854 - [ ] ˇ
29855 "});
29856
29857 // Case 2.1: Works with uppercase checked marker too
29858 cx.set_state(indoc! {"
29859 - [X] completed taskˇ
29860 "});
29861 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29862 cx.wait_for_autoindent_applied().await;
29863 cx.assert_editor_state(indoc! {"
29864 - [X] completed task
29865 - [ ] ˇ
29866 "});
29867
29868 // Case 3: Cursor position doesn't matter - content after marker is what counts
29869 cx.set_state(indoc! {"
29870 - [ ] taˇsk
29871 "});
29872 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29873 cx.wait_for_autoindent_applied().await;
29874 cx.assert_editor_state(indoc! {"
29875 - [ ] ta
29876 - [ ] ˇsk
29877 "});
29878
29879 // Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
29880 cx.set_state(indoc! {"
29881 - [ ] ˇ
29882 "});
29883 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29884 cx.wait_for_autoindent_applied().await;
29885 cx.assert_editor_state(
29886 indoc! {"
29887 - [ ]$$
29888 ˇ
29889 "}
29890 .replace("$", " ")
29891 .as_str(),
29892 );
29893
29894 // Case 5: Adding newline with content adds marker preserving indentation
29895 cx.set_state(indoc! {"
29896 - [ ] task
29897 - [ ] indentedˇ
29898 "});
29899 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29900 cx.wait_for_autoindent_applied().await;
29901 cx.assert_editor_state(indoc! {"
29902 - [ ] task
29903 - [ ] indented
29904 - [ ] ˇ
29905 "});
29906
29907 // Case 6: Adding newline with cursor right after prefix, unindents
29908 cx.set_state(indoc! {"
29909 - [ ] task
29910 - [ ] sub task
29911 - [ ] ˇ
29912 "});
29913 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29914 cx.wait_for_autoindent_applied().await;
29915 cx.assert_editor_state(indoc! {"
29916 - [ ] task
29917 - [ ] sub task
29918 - [ ] ˇ
29919 "});
29920 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29921 cx.wait_for_autoindent_applied().await;
29922
29923 // Case 7: Adding newline with cursor right after prefix, removes marker
29924 cx.assert_editor_state(indoc! {"
29925 - [ ] task
29926 - [ ] sub task
29927 - [ ] ˇ
29928 "});
29929 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29930 cx.wait_for_autoindent_applied().await;
29931 cx.assert_editor_state(indoc! {"
29932 - [ ] task
29933 - [ ] sub task
29934 ˇ
29935 "});
29936
29937 // Case 8: Cursor before or inside prefix does not add marker
29938 cx.set_state(indoc! {"
29939 ˇ- [ ] task
29940 "});
29941 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29942 cx.wait_for_autoindent_applied().await;
29943 cx.assert_editor_state(indoc! {"
29944
29945 ˇ- [ ] task
29946 "});
29947
29948 cx.set_state(indoc! {"
29949 - [ˇ ] task
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 - [
29955 ˇ
29956 ] task
29957 "});
29958}
29959
29960#[gpui::test]
29961async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
29962 init_test(cx, |settings| {
29963 settings.defaults.tab_size = Some(2.try_into().unwrap());
29964 });
29965
29966 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29967 let mut cx = EditorTestContext::new(cx).await;
29968 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29969
29970 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
29971 cx.set_state(indoc! {"
29972 - itemˇ
29973 "});
29974 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29975 cx.wait_for_autoindent_applied().await;
29976 cx.assert_editor_state(indoc! {"
29977 - item
29978 - ˇ
29979 "});
29980
29981 // Case 2: Works with different markers
29982 cx.set_state(indoc! {"
29983 * starred itemˇ
29984 "});
29985 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29986 cx.wait_for_autoindent_applied().await;
29987 cx.assert_editor_state(indoc! {"
29988 * starred item
29989 * ˇ
29990 "});
29991
29992 cx.set_state(indoc! {"
29993 + plus itemˇ
29994 "});
29995 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29996 cx.wait_for_autoindent_applied().await;
29997 cx.assert_editor_state(indoc! {"
29998 + plus item
29999 + ˇ
30000 "});
30001
30002 // Case 3: Cursor position doesn't matter - content after marker is what counts
30003 cx.set_state(indoc! {"
30004 - itˇem
30005 "});
30006 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30007 cx.wait_for_autoindent_applied().await;
30008 cx.assert_editor_state(indoc! {"
30009 - it
30010 - ˇem
30011 "});
30012
30013 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
30014 cx.set_state(indoc! {"
30015 - ˇ
30016 "});
30017 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30018 cx.wait_for_autoindent_applied().await;
30019 cx.assert_editor_state(
30020 indoc! {"
30021 - $
30022 ˇ
30023 "}
30024 .replace("$", " ")
30025 .as_str(),
30026 );
30027
30028 // Case 5: Adding newline with content adds marker preserving indentation
30029 cx.set_state(indoc! {"
30030 - item
30031 - indentedˇ
30032 "});
30033 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30034 cx.wait_for_autoindent_applied().await;
30035 cx.assert_editor_state(indoc! {"
30036 - item
30037 - indented
30038 - ˇ
30039 "});
30040
30041 // Case 6: Adding newline with cursor right after marker, unindents
30042 cx.set_state(indoc! {"
30043 - item
30044 - sub item
30045 - ˇ
30046 "});
30047 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30048 cx.wait_for_autoindent_applied().await;
30049 cx.assert_editor_state(indoc! {"
30050 - item
30051 - sub item
30052 - ˇ
30053 "});
30054 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30055 cx.wait_for_autoindent_applied().await;
30056
30057 // Case 7: Adding newline with cursor right after marker, removes marker
30058 cx.assert_editor_state(indoc! {"
30059 - item
30060 - sub item
30061 - ˇ
30062 "});
30063 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30064 cx.wait_for_autoindent_applied().await;
30065 cx.assert_editor_state(indoc! {"
30066 - item
30067 - sub item
30068 ˇ
30069 "});
30070
30071 // Case 8: Cursor before or inside prefix does not add marker
30072 cx.set_state(indoc! {"
30073 ˇ- item
30074 "});
30075 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30076 cx.wait_for_autoindent_applied().await;
30077 cx.assert_editor_state(indoc! {"
30078
30079 ˇ- item
30080 "});
30081
30082 cx.set_state(indoc! {"
30083 -ˇ item
30084 "});
30085 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30086 cx.wait_for_autoindent_applied().await;
30087 cx.assert_editor_state(indoc! {"
30088 -
30089 ˇitem
30090 "});
30091}
30092
30093#[gpui::test]
30094async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
30095 init_test(cx, |settings| {
30096 settings.defaults.tab_size = Some(2.try_into().unwrap());
30097 });
30098
30099 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30100 let mut cx = EditorTestContext::new(cx).await;
30101 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30102
30103 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
30104 cx.set_state(indoc! {"
30105 1. first itemˇ
30106 "});
30107 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30108 cx.wait_for_autoindent_applied().await;
30109 cx.assert_editor_state(indoc! {"
30110 1. first item
30111 2. ˇ
30112 "});
30113
30114 // Case 2: Works with larger numbers
30115 cx.set_state(indoc! {"
30116 10. tenth itemˇ
30117 "});
30118 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30119 cx.wait_for_autoindent_applied().await;
30120 cx.assert_editor_state(indoc! {"
30121 10. tenth item
30122 11. ˇ
30123 "});
30124
30125 // Case 3: Cursor position doesn't matter - content after marker is what counts
30126 cx.set_state(indoc! {"
30127 1. itˇem
30128 "});
30129 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30130 cx.wait_for_autoindent_applied().await;
30131 cx.assert_editor_state(indoc! {"
30132 1. it
30133 2. ˇem
30134 "});
30135
30136 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
30137 cx.set_state(indoc! {"
30138 1. ˇ
30139 "});
30140 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30141 cx.wait_for_autoindent_applied().await;
30142 cx.assert_editor_state(
30143 indoc! {"
30144 1. $
30145 ˇ
30146 "}
30147 .replace("$", " ")
30148 .as_str(),
30149 );
30150
30151 // Case 5: Adding newline with content adds marker preserving indentation
30152 cx.set_state(indoc! {"
30153 1. item
30154 2. indentedˇ
30155 "});
30156 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30157 cx.wait_for_autoindent_applied().await;
30158 cx.assert_editor_state(indoc! {"
30159 1. item
30160 2. indented
30161 3. ˇ
30162 "});
30163
30164 // Case 6: Adding newline with cursor right after marker, unindents
30165 cx.set_state(indoc! {"
30166 1. item
30167 2. sub item
30168 3. ˇ
30169 "});
30170 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30171 cx.wait_for_autoindent_applied().await;
30172 cx.assert_editor_state(indoc! {"
30173 1. item
30174 2. sub item
30175 1. ˇ
30176 "});
30177 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30178 cx.wait_for_autoindent_applied().await;
30179
30180 // Case 7: Adding newline with cursor right after marker, removes marker
30181 cx.assert_editor_state(indoc! {"
30182 1. item
30183 2. sub item
30184 1. ˇ
30185 "});
30186 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30187 cx.wait_for_autoindent_applied().await;
30188 cx.assert_editor_state(indoc! {"
30189 1. item
30190 2. sub item
30191 ˇ
30192 "});
30193
30194 // Case 8: Cursor before or inside prefix does not add marker
30195 cx.set_state(indoc! {"
30196 ˇ1. item
30197 "});
30198 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30199 cx.wait_for_autoindent_applied().await;
30200 cx.assert_editor_state(indoc! {"
30201
30202 ˇ1. item
30203 "});
30204
30205 cx.set_state(indoc! {"
30206 1ˇ. item
30207 "});
30208 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30209 cx.wait_for_autoindent_applied().await;
30210 cx.assert_editor_state(indoc! {"
30211 1
30212 ˇ. item
30213 "});
30214}
30215
30216#[gpui::test]
30217async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
30218 init_test(cx, |settings| {
30219 settings.defaults.tab_size = Some(2.try_into().unwrap());
30220 });
30221
30222 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30223 let mut cx = EditorTestContext::new(cx).await;
30224 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30225
30226 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
30227 cx.set_state(indoc! {"
30228 1. first item
30229 1. sub first item
30230 2. sub second item
30231 3. ˇ
30232 "});
30233 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30234 cx.wait_for_autoindent_applied().await;
30235 cx.assert_editor_state(indoc! {"
30236 1. first item
30237 1. sub first item
30238 2. sub second item
30239 1. ˇ
30240 "});
30241}
30242
30243#[gpui::test]
30244async fn test_tab_list_indent(cx: &mut TestAppContext) {
30245 init_test(cx, |settings| {
30246 settings.defaults.tab_size = Some(2.try_into().unwrap());
30247 });
30248
30249 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30250 let mut cx = EditorTestContext::new(cx).await;
30251 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30252
30253 // Case 1: Unordered list - cursor after prefix, adds indent before prefix
30254 cx.set_state(indoc! {"
30255 - ˇitem
30256 "});
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 2: Task list - cursor after prefix
30265 cx.set_state(indoc! {"
30266 - [ ] ˇtask
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 $$- [ ] ˇtask
30272 "};
30273 cx.assert_editor_state(expected.replace("$", " ").as_str());
30274
30275 // Case 3: Ordered list - cursor after prefix
30276 cx.set_state(indoc! {"
30277 1. ˇfirst
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 $$1. ˇfirst
30283 "};
30284 cx.assert_editor_state(expected.replace("$", " ").as_str());
30285
30286 // Case 4: With existing indentation - adds more indent
30287 let initial = indoc! {"
30288 $$- ˇitem
30289 "};
30290 cx.set_state(initial.replace("$", " ").as_str());
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 "};
30296 cx.assert_editor_state(expected.replace("$", " ").as_str());
30297
30298 // Case 5: Empty list item
30299 cx.set_state(indoc! {"
30300 - ˇ
30301 "});
30302 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30303 cx.wait_for_autoindent_applied().await;
30304 let expected = indoc! {"
30305 $$- ˇ
30306 "};
30307 cx.assert_editor_state(expected.replace("$", " ").as_str());
30308
30309 // Case 6: Cursor at end of line with content
30310 cx.set_state(indoc! {"
30311 - itemˇ
30312 "});
30313 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30314 cx.wait_for_autoindent_applied().await;
30315 let expected = indoc! {"
30316 $$- itemˇ
30317 "};
30318 cx.assert_editor_state(expected.replace("$", " ").as_str());
30319
30320 // Case 7: Cursor at start of list item, indents it
30321 cx.set_state(indoc! {"
30322 - item
30323 ˇ - sub item
30324 "});
30325 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30326 cx.wait_for_autoindent_applied().await;
30327 let expected = indoc! {"
30328 - item
30329 ˇ - sub item
30330 "};
30331 cx.assert_editor_state(expected);
30332
30333 // Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
30334 cx.update_editor(|_, _, cx| {
30335 SettingsStore::update_global(cx, |store, cx| {
30336 store.update_user_settings(cx, |settings| {
30337 settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
30338 });
30339 });
30340 });
30341 cx.set_state(indoc! {"
30342 - item
30343 ˇ - sub item
30344 "});
30345 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30346 cx.wait_for_autoindent_applied().await;
30347 let expected = indoc! {"
30348 - item
30349 ˇ- sub item
30350 "};
30351 cx.assert_editor_state(expected);
30352}
30353
30354#[gpui::test]
30355async fn test_local_worktree_trust(cx: &mut TestAppContext) {
30356 init_test(cx, |_| {});
30357 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), cx));
30358
30359 cx.update(|cx| {
30360 SettingsStore::update_global(cx, |store, cx| {
30361 store.update_user_settings(cx, |settings| {
30362 settings.project.all_languages.defaults.inlay_hints =
30363 Some(InlayHintSettingsContent {
30364 enabled: Some(true),
30365 ..InlayHintSettingsContent::default()
30366 });
30367 });
30368 });
30369 });
30370
30371 let fs = FakeFs::new(cx.executor());
30372 fs.insert_tree(
30373 path!("/project"),
30374 json!({
30375 ".zed": {
30376 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
30377 },
30378 "main.rs": "fn main() {}"
30379 }),
30380 )
30381 .await;
30382
30383 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
30384 let server_name = "override-rust-analyzer";
30385 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
30386
30387 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
30388 language_registry.add(rust_lang());
30389
30390 let capabilities = lsp::ServerCapabilities {
30391 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
30392 ..lsp::ServerCapabilities::default()
30393 };
30394 let mut fake_language_servers = language_registry.register_fake_lsp(
30395 "Rust",
30396 FakeLspAdapter {
30397 name: server_name,
30398 capabilities,
30399 initializer: Some(Box::new({
30400 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30401 move |fake_server| {
30402 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30403 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
30404 move |_params, _| {
30405 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
30406 async move {
30407 Ok(Some(vec![lsp::InlayHint {
30408 position: lsp::Position::new(0, 0),
30409 label: lsp::InlayHintLabel::String("hint".to_string()),
30410 kind: None,
30411 text_edits: None,
30412 tooltip: None,
30413 padding_left: None,
30414 padding_right: None,
30415 data: None,
30416 }]))
30417 }
30418 },
30419 );
30420 }
30421 })),
30422 ..FakeLspAdapter::default()
30423 },
30424 );
30425
30426 cx.run_until_parked();
30427
30428 let worktree_id = project.read_with(cx, |project, cx| {
30429 project
30430 .worktrees(cx)
30431 .next()
30432 .map(|wt| wt.read(cx).id())
30433 .expect("should have a worktree")
30434 });
30435 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
30436
30437 let trusted_worktrees =
30438 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
30439
30440 let can_trust = trusted_worktrees.update(cx, |store, cx| {
30441 store.can_trust(&worktree_store, worktree_id, cx)
30442 });
30443 assert!(!can_trust, "worktree should be restricted initially");
30444
30445 let buffer_before_approval = project
30446 .update(cx, |project, cx| {
30447 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
30448 })
30449 .await
30450 .unwrap();
30451
30452 let (editor, cx) = cx.add_window_view(|window, cx| {
30453 Editor::new(
30454 EditorMode::full(),
30455 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
30456 Some(project.clone()),
30457 window,
30458 cx,
30459 )
30460 });
30461 cx.run_until_parked();
30462 let fake_language_server = fake_language_servers.next();
30463
30464 cx.read(|cx| {
30465 let file = buffer_before_approval.read(cx).file();
30466 assert_eq!(
30467 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30468 .language_servers,
30469 ["...".to_string()],
30470 "local .zed/settings.json must not apply before trust approval"
30471 )
30472 });
30473
30474 editor.update_in(cx, |editor, window, cx| {
30475 editor.handle_input("1", window, cx);
30476 });
30477 cx.run_until_parked();
30478 cx.executor()
30479 .advance_clock(std::time::Duration::from_secs(1));
30480 assert_eq!(
30481 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
30482 0,
30483 "inlay hints must not be queried before trust approval"
30484 );
30485
30486 trusted_worktrees.update(cx, |store, cx| {
30487 store.trust(
30488 &worktree_store,
30489 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
30490 cx,
30491 );
30492 });
30493 cx.run_until_parked();
30494
30495 cx.read(|cx| {
30496 let file = buffer_before_approval.read(cx).file();
30497 assert_eq!(
30498 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30499 .language_servers,
30500 ["override-rust-analyzer".to_string()],
30501 "local .zed/settings.json should apply after trust approval"
30502 )
30503 });
30504 let _fake_language_server = fake_language_server.await.unwrap();
30505 editor.update_in(cx, |editor, window, cx| {
30506 editor.handle_input("1", window, cx);
30507 });
30508 cx.run_until_parked();
30509 cx.executor()
30510 .advance_clock(std::time::Duration::from_secs(1));
30511 assert!(
30512 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
30513 "inlay hints should be queried after trust approval"
30514 );
30515
30516 let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
30517 store.can_trust(&worktree_store, worktree_id, cx)
30518 });
30519 assert!(can_trust_after, "worktree should be trusted after trust()");
30520}
30521
30522#[gpui::test]
30523fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
30524 // This test reproduces a bug where drawing an editor at a position above the viewport
30525 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
30526 // causes an infinite loop in blocks_in_range.
30527 //
30528 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
30529 // the content mask intersection produces visible_bounds with origin at the viewport top.
30530 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
30531 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
30532 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
30533 init_test(cx, |_| {});
30534
30535 let window = cx.add_window(|_, _| gpui::Empty);
30536 let mut cx = VisualTestContext::from_window(*window, cx);
30537
30538 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
30539 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
30540
30541 // Simulate a small viewport (500x500 pixels at origin 0,0)
30542 cx.simulate_resize(gpui::size(px(500.), px(500.)));
30543
30544 // Draw the editor at a very negative Y position, simulating an editor that's been
30545 // scrolled way above the visible viewport (like in a List that has scrolled past it).
30546 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
30547 // This should NOT hang - it should just render nothing.
30548 cx.draw(
30549 gpui::point(px(0.), px(-10000.)),
30550 gpui::size(px(500.), px(3000.)),
30551 |_, _| editor.clone(),
30552 );
30553
30554 // If we get here without hanging, the test passes
30555}
30556
30557#[gpui::test]
30558async fn test_diff_review_indicator_created_on_gutter_hover(cx: &mut TestAppContext) {
30559 init_test(cx, |_| {});
30560
30561 let fs = FakeFs::new(cx.executor());
30562 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30563 .await;
30564
30565 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30566 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30567 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30568
30569 let editor = workspace
30570 .update(cx, |workspace, window, cx| {
30571 workspace.open_abs_path(
30572 PathBuf::from(path!("/root/file.txt")),
30573 OpenOptions::default(),
30574 window,
30575 cx,
30576 )
30577 })
30578 .unwrap()
30579 .await
30580 .unwrap()
30581 .downcast::<Editor>()
30582 .unwrap();
30583
30584 // Enable diff review button mode
30585 editor.update(cx, |editor, cx| {
30586 editor.set_show_diff_review_button(true, cx);
30587 });
30588
30589 // Initially, no indicator should be present
30590 editor.update(cx, |editor, _cx| {
30591 assert!(
30592 editor.gutter_diff_review_indicator.0.is_none(),
30593 "Indicator should be None initially"
30594 );
30595 });
30596}
30597
30598#[gpui::test]
30599async fn test_diff_review_button_hidden_when_ai_disabled(cx: &mut TestAppContext) {
30600 init_test(cx, |_| {});
30601
30602 // Register DisableAiSettings and set disable_ai to true
30603 cx.update(|cx| {
30604 project::DisableAiSettings::register(cx);
30605 project::DisableAiSettings::override_global(
30606 project::DisableAiSettings { disable_ai: true },
30607 cx,
30608 );
30609 });
30610
30611 let fs = FakeFs::new(cx.executor());
30612 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30613 .await;
30614
30615 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30616 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30617 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30618
30619 let editor = workspace
30620 .update(cx, |workspace, window, cx| {
30621 workspace.open_abs_path(
30622 PathBuf::from(path!("/root/file.txt")),
30623 OpenOptions::default(),
30624 window,
30625 cx,
30626 )
30627 })
30628 .unwrap()
30629 .await
30630 .unwrap()
30631 .downcast::<Editor>()
30632 .unwrap();
30633
30634 // Enable diff review button mode
30635 editor.update(cx, |editor, cx| {
30636 editor.set_show_diff_review_button(true, cx);
30637 });
30638
30639 // Verify AI is disabled
30640 cx.read(|cx| {
30641 assert!(
30642 project::DisableAiSettings::get_global(cx).disable_ai,
30643 "AI should be disabled"
30644 );
30645 });
30646
30647 // The indicator should not be created when AI is disabled
30648 // (The mouse_moved handler checks DisableAiSettings before creating the indicator)
30649 editor.update(cx, |editor, _cx| {
30650 assert!(
30651 editor.gutter_diff_review_indicator.0.is_none(),
30652 "Indicator should be None when AI is disabled"
30653 );
30654 });
30655}
30656
30657#[gpui::test]
30658async fn test_diff_review_button_shown_when_ai_enabled(cx: &mut TestAppContext) {
30659 init_test(cx, |_| {});
30660
30661 // Register DisableAiSettings and set disable_ai to false
30662 cx.update(|cx| {
30663 project::DisableAiSettings::register(cx);
30664 project::DisableAiSettings::override_global(
30665 project::DisableAiSettings { disable_ai: false },
30666 cx,
30667 );
30668 });
30669
30670 let fs = FakeFs::new(cx.executor());
30671 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30672 .await;
30673
30674 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30675 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30676 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30677
30678 let editor = workspace
30679 .update(cx, |workspace, window, cx| {
30680 workspace.open_abs_path(
30681 PathBuf::from(path!("/root/file.txt")),
30682 OpenOptions::default(),
30683 window,
30684 cx,
30685 )
30686 })
30687 .unwrap()
30688 .await
30689 .unwrap()
30690 .downcast::<Editor>()
30691 .unwrap();
30692
30693 // Enable diff review button mode
30694 editor.update(cx, |editor, cx| {
30695 editor.set_show_diff_review_button(true, cx);
30696 });
30697
30698 // Verify AI is enabled
30699 cx.read(|cx| {
30700 assert!(
30701 !project::DisableAiSettings::get_global(cx).disable_ai,
30702 "AI should be enabled"
30703 );
30704 });
30705
30706 // The show_diff_review_button flag should be true
30707 editor.update(cx, |editor, _cx| {
30708 assert!(
30709 editor.show_diff_review_button(),
30710 "show_diff_review_button should be true"
30711 );
30712 });
30713}
30714
30715/// Helper function to create a DiffHunkKey for testing.
30716/// Uses Anchor::min() as a placeholder anchor since these tests don't need
30717/// real buffer positioning.
30718fn test_hunk_key(file_path: &str) -> DiffHunkKey {
30719 DiffHunkKey {
30720 file_path: if file_path.is_empty() {
30721 Arc::from(util::rel_path::RelPath::empty())
30722 } else {
30723 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
30724 },
30725 hunk_start_anchor: Anchor::min(),
30726 }
30727}
30728
30729/// Helper function to create a DiffHunkKey with a specific anchor for testing.
30730fn test_hunk_key_with_anchor(file_path: &str, anchor: Anchor) -> DiffHunkKey {
30731 DiffHunkKey {
30732 file_path: if file_path.is_empty() {
30733 Arc::from(util::rel_path::RelPath::empty())
30734 } else {
30735 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
30736 },
30737 hunk_start_anchor: anchor,
30738 }
30739}
30740
30741/// Helper function to add a review comment with default anchors for testing.
30742fn add_test_comment(
30743 editor: &mut Editor,
30744 key: DiffHunkKey,
30745 comment: &str,
30746 cx: &mut Context<Editor>,
30747) -> usize {
30748 editor.add_review_comment(key, comment.to_string(), Anchor::min()..Anchor::max(), cx)
30749}
30750
30751#[gpui::test]
30752fn test_review_comment_add_to_hunk(cx: &mut TestAppContext) {
30753 init_test(cx, |_| {});
30754
30755 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30756
30757 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30758 let key = test_hunk_key("");
30759
30760 let id = add_test_comment(editor, key.clone(), "Test comment", cx);
30761
30762 let snapshot = editor.buffer().read(cx).snapshot(cx);
30763 assert_eq!(editor.total_review_comment_count(), 1);
30764 assert_eq!(editor.hunk_comment_count(&key, &snapshot), 1);
30765
30766 let comments = editor.comments_for_hunk(&key, &snapshot);
30767 assert_eq!(comments.len(), 1);
30768 assert_eq!(comments[0].comment, "Test comment");
30769 assert_eq!(comments[0].id, id);
30770 });
30771}
30772
30773#[gpui::test]
30774fn test_review_comments_are_per_hunk(cx: &mut TestAppContext) {
30775 init_test(cx, |_| {});
30776
30777 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30778
30779 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30780 let snapshot = editor.buffer().read(cx).snapshot(cx);
30781 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
30782 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
30783 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
30784 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
30785
30786 add_test_comment(editor, key1.clone(), "Comment for file1", cx);
30787 add_test_comment(editor, key2.clone(), "Comment for file2", cx);
30788
30789 let snapshot = editor.buffer().read(cx).snapshot(cx);
30790 assert_eq!(editor.total_review_comment_count(), 2);
30791 assert_eq!(editor.hunk_comment_count(&key1, &snapshot), 1);
30792 assert_eq!(editor.hunk_comment_count(&key2, &snapshot), 1);
30793
30794 assert_eq!(
30795 editor.comments_for_hunk(&key1, &snapshot)[0].comment,
30796 "Comment for file1"
30797 );
30798 assert_eq!(
30799 editor.comments_for_hunk(&key2, &snapshot)[0].comment,
30800 "Comment for file2"
30801 );
30802 });
30803}
30804
30805#[gpui::test]
30806fn test_review_comment_remove(cx: &mut TestAppContext) {
30807 init_test(cx, |_| {});
30808
30809 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30810
30811 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30812 let key = test_hunk_key("");
30813
30814 let id = add_test_comment(editor, key, "To be removed", cx);
30815
30816 assert_eq!(editor.total_review_comment_count(), 1);
30817
30818 let removed = editor.remove_review_comment(id, cx);
30819 assert!(removed);
30820 assert_eq!(editor.total_review_comment_count(), 0);
30821
30822 // Try to remove again
30823 let removed_again = editor.remove_review_comment(id, cx);
30824 assert!(!removed_again);
30825 });
30826}
30827
30828#[gpui::test]
30829fn test_review_comment_update(cx: &mut TestAppContext) {
30830 init_test(cx, |_| {});
30831
30832 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30833
30834 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30835 let key = test_hunk_key("");
30836
30837 let id = add_test_comment(editor, key.clone(), "Original text", cx);
30838
30839 let updated = editor.update_review_comment(id, "Updated text".to_string(), cx);
30840 assert!(updated);
30841
30842 let snapshot = editor.buffer().read(cx).snapshot(cx);
30843 let comments = editor.comments_for_hunk(&key, &snapshot);
30844 assert_eq!(comments[0].comment, "Updated text");
30845 assert!(!comments[0].is_editing); // Should clear editing flag
30846 });
30847}
30848
30849#[gpui::test]
30850fn test_review_comment_take_all(cx: &mut TestAppContext) {
30851 init_test(cx, |_| {});
30852
30853 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30854
30855 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30856 let snapshot = editor.buffer().read(cx).snapshot(cx);
30857 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
30858 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
30859 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
30860 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
30861
30862 let id1 = add_test_comment(editor, key1.clone(), "Comment 1", cx);
30863 let id2 = add_test_comment(editor, key1.clone(), "Comment 2", cx);
30864 let id3 = add_test_comment(editor, key2.clone(), "Comment 3", cx);
30865
30866 // IDs should be sequential starting from 0
30867 assert_eq!(id1, 0);
30868 assert_eq!(id2, 1);
30869 assert_eq!(id3, 2);
30870
30871 assert_eq!(editor.total_review_comment_count(), 3);
30872
30873 let taken = editor.take_all_review_comments(cx);
30874
30875 // Should have 2 entries (one per hunk)
30876 assert_eq!(taken.len(), 2);
30877
30878 // Total comments should be 3
30879 let total: usize = taken
30880 .iter()
30881 .map(|(_, comments): &(DiffHunkKey, Vec<StoredReviewComment>)| comments.len())
30882 .sum();
30883 assert_eq!(total, 3);
30884
30885 // Storage should be empty
30886 assert_eq!(editor.total_review_comment_count(), 0);
30887
30888 // After taking all comments, ID counter should reset
30889 // New comments should get IDs starting from 0 again
30890 let new_id1 = add_test_comment(editor, key1, "New Comment 1", cx);
30891 let new_id2 = add_test_comment(editor, key2, "New Comment 2", cx);
30892
30893 assert_eq!(new_id1, 0, "ID counter should reset after take_all");
30894 assert_eq!(new_id2, 1, "IDs should be sequential after reset");
30895 });
30896}
30897
30898#[gpui::test]
30899fn test_diff_review_overlay_show_and_dismiss(cx: &mut TestAppContext) {
30900 init_test(cx, |_| {});
30901
30902 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30903
30904 // Show overlay
30905 editor
30906 .update(cx, |editor, window, cx| {
30907 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
30908 })
30909 .unwrap();
30910
30911 // Verify overlay is shown
30912 editor
30913 .update(cx, |editor, _window, cx| {
30914 assert!(!editor.diff_review_overlays.is_empty());
30915 assert_eq!(editor.diff_review_line_range(cx), Some((0, 0)));
30916 assert!(editor.diff_review_prompt_editor().is_some());
30917 })
30918 .unwrap();
30919
30920 // Dismiss overlay
30921 editor
30922 .update(cx, |editor, _window, cx| {
30923 editor.dismiss_all_diff_review_overlays(cx);
30924 })
30925 .unwrap();
30926
30927 // Verify overlay is dismissed
30928 editor
30929 .update(cx, |editor, _window, cx| {
30930 assert!(editor.diff_review_overlays.is_empty());
30931 assert_eq!(editor.diff_review_line_range(cx), None);
30932 assert!(editor.diff_review_prompt_editor().is_none());
30933 })
30934 .unwrap();
30935}
30936
30937#[gpui::test]
30938fn test_diff_review_overlay_dismiss_via_cancel(cx: &mut TestAppContext) {
30939 init_test(cx, |_| {});
30940
30941 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30942
30943 // Show overlay
30944 editor
30945 .update(cx, |editor, window, cx| {
30946 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
30947 })
30948 .unwrap();
30949
30950 // Verify overlay is shown
30951 editor
30952 .update(cx, |editor, _window, _cx| {
30953 assert!(!editor.diff_review_overlays.is_empty());
30954 })
30955 .unwrap();
30956
30957 // Dismiss via dismiss_menus_and_popups (which is called by cancel action)
30958 editor
30959 .update(cx, |editor, window, cx| {
30960 editor.dismiss_menus_and_popups(true, window, cx);
30961 })
30962 .unwrap();
30963
30964 // Verify overlay is dismissed
30965 editor
30966 .update(cx, |editor, _window, _cx| {
30967 assert!(editor.diff_review_overlays.is_empty());
30968 })
30969 .unwrap();
30970}
30971
30972#[gpui::test]
30973fn test_diff_review_empty_comment_not_submitted(cx: &mut TestAppContext) {
30974 init_test(cx, |_| {});
30975
30976 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30977
30978 // Show overlay
30979 editor
30980 .update(cx, |editor, window, cx| {
30981 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
30982 })
30983 .unwrap();
30984
30985 // Try to submit without typing anything (empty comment)
30986 editor
30987 .update(cx, |editor, window, cx| {
30988 editor.submit_diff_review_comment(window, cx);
30989 })
30990 .unwrap();
30991
30992 // Verify no comment was added
30993 editor
30994 .update(cx, |editor, _window, _cx| {
30995 assert_eq!(editor.total_review_comment_count(), 0);
30996 })
30997 .unwrap();
30998
30999 // Try to submit with whitespace-only comment
31000 editor
31001 .update(cx, |editor, window, cx| {
31002 if let Some(prompt_editor) = editor.diff_review_prompt_editor().cloned() {
31003 prompt_editor.update(cx, |pe, cx| {
31004 pe.insert(" \n\t ", window, cx);
31005 });
31006 }
31007 editor.submit_diff_review_comment(window, cx);
31008 })
31009 .unwrap();
31010
31011 // Verify still no comment was added
31012 editor
31013 .update(cx, |editor, _window, _cx| {
31014 assert_eq!(editor.total_review_comment_count(), 0);
31015 })
31016 .unwrap();
31017}
31018
31019#[gpui::test]
31020fn test_diff_review_inline_edit_flow(cx: &mut TestAppContext) {
31021 init_test(cx, |_| {});
31022
31023 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31024
31025 // Add a comment directly
31026 let comment_id = editor
31027 .update(cx, |editor, _window, cx| {
31028 let key = test_hunk_key("");
31029 add_test_comment(editor, key, "Original comment", cx)
31030 })
31031 .unwrap();
31032
31033 // Set comment to editing mode
31034 editor
31035 .update(cx, |editor, _window, cx| {
31036 editor.set_comment_editing(comment_id, true, cx);
31037 })
31038 .unwrap();
31039
31040 // Verify editing flag is set
31041 editor
31042 .update(cx, |editor, _window, cx| {
31043 let key = test_hunk_key("");
31044 let snapshot = editor.buffer().read(cx).snapshot(cx);
31045 let comments = editor.comments_for_hunk(&key, &snapshot);
31046 assert_eq!(comments.len(), 1);
31047 assert!(comments[0].is_editing);
31048 })
31049 .unwrap();
31050
31051 // Update the comment
31052 editor
31053 .update(cx, |editor, _window, cx| {
31054 let updated =
31055 editor.update_review_comment(comment_id, "Updated comment".to_string(), cx);
31056 assert!(updated);
31057 })
31058 .unwrap();
31059
31060 // Verify comment was updated and editing flag is cleared
31061 editor
31062 .update(cx, |editor, _window, cx| {
31063 let key = test_hunk_key("");
31064 let snapshot = editor.buffer().read(cx).snapshot(cx);
31065 let comments = editor.comments_for_hunk(&key, &snapshot);
31066 assert_eq!(comments[0].comment, "Updated comment");
31067 assert!(!comments[0].is_editing);
31068 })
31069 .unwrap();
31070}
31071
31072#[gpui::test]
31073fn test_orphaned_comments_are_cleaned_up(cx: &mut TestAppContext) {
31074 init_test(cx, |_| {});
31075
31076 // Create an editor with some text
31077 let editor = cx.add_window(|window, cx| {
31078 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
31079 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31080 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31081 });
31082
31083 // Add a comment with an anchor on line 2
31084 editor
31085 .update(cx, |editor, _window, cx| {
31086 let snapshot = editor.buffer().read(cx).snapshot(cx);
31087 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
31088 let key = DiffHunkKey {
31089 file_path: Arc::from(util::rel_path::RelPath::empty()),
31090 hunk_start_anchor: anchor,
31091 };
31092 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
31093 assert_eq!(editor.total_review_comment_count(), 1);
31094 })
31095 .unwrap();
31096
31097 // Delete all content (this should orphan the comment's anchor)
31098 editor
31099 .update(cx, |editor, window, cx| {
31100 editor.select_all(&SelectAll, window, cx);
31101 editor.insert("completely new content", window, cx);
31102 })
31103 .unwrap();
31104
31105 // Trigger cleanup
31106 editor
31107 .update(cx, |editor, _window, cx| {
31108 editor.cleanup_orphaned_review_comments(cx);
31109 // Comment should be removed because its anchor is invalid
31110 assert_eq!(editor.total_review_comment_count(), 0);
31111 })
31112 .unwrap();
31113}
31114
31115#[gpui::test]
31116fn test_orphaned_comments_cleanup_called_on_buffer_edit(cx: &mut TestAppContext) {
31117 init_test(cx, |_| {});
31118
31119 // Create an editor with some text
31120 let editor = cx.add_window(|window, cx| {
31121 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
31122 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31123 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31124 });
31125
31126 // Add a comment with an anchor on line 2
31127 editor
31128 .update(cx, |editor, _window, cx| {
31129 let snapshot = editor.buffer().read(cx).snapshot(cx);
31130 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
31131 let key = DiffHunkKey {
31132 file_path: Arc::from(util::rel_path::RelPath::empty()),
31133 hunk_start_anchor: anchor,
31134 };
31135 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
31136 assert_eq!(editor.total_review_comment_count(), 1);
31137 })
31138 .unwrap();
31139
31140 // Edit the buffer - this should trigger cleanup via on_buffer_event
31141 // Delete all content which orphans the anchor
31142 editor
31143 .update(cx, |editor, window, cx| {
31144 editor.select_all(&SelectAll, window, cx);
31145 editor.insert("completely new content", window, cx);
31146 // The cleanup is called automatically in on_buffer_event when Edited fires
31147 })
31148 .unwrap();
31149
31150 // Verify cleanup happened automatically (not manually triggered)
31151 editor
31152 .update(cx, |editor, _window, _cx| {
31153 // Comment should be removed because its anchor became invalid
31154 // and cleanup was called automatically on buffer edit
31155 assert_eq!(editor.total_review_comment_count(), 0);
31156 })
31157 .unwrap();
31158}
31159
31160#[gpui::test]
31161fn test_comments_stored_for_multiple_hunks(cx: &mut TestAppContext) {
31162 init_test(cx, |_| {});
31163
31164 // This test verifies that comments can be stored for multiple different hunks
31165 // and that hunk_comment_count correctly identifies comments per hunk.
31166 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31167
31168 _ = editor.update(cx, |editor, _window, cx| {
31169 let snapshot = editor.buffer().read(cx).snapshot(cx);
31170
31171 // Create two different hunk keys (simulating two different files)
31172 let anchor = snapshot.anchor_before(Point::new(0, 0));
31173 let key1 = DiffHunkKey {
31174 file_path: Arc::from(util::rel_path::RelPath::unix("file1.rs").unwrap()),
31175 hunk_start_anchor: anchor,
31176 };
31177 let key2 = DiffHunkKey {
31178 file_path: Arc::from(util::rel_path::RelPath::unix("file2.rs").unwrap()),
31179 hunk_start_anchor: anchor,
31180 };
31181
31182 // Add comments to first hunk
31183 editor.add_review_comment(
31184 key1.clone(),
31185 "Comment 1 for file1".to_string(),
31186 anchor..anchor,
31187 cx,
31188 );
31189 editor.add_review_comment(
31190 key1.clone(),
31191 "Comment 2 for file1".to_string(),
31192 anchor..anchor,
31193 cx,
31194 );
31195
31196 // Add comment to second hunk
31197 editor.add_review_comment(
31198 key2.clone(),
31199 "Comment for file2".to_string(),
31200 anchor..anchor,
31201 cx,
31202 );
31203
31204 // Verify total count
31205 assert_eq!(editor.total_review_comment_count(), 3);
31206
31207 // Verify per-hunk counts
31208 let snapshot = editor.buffer().read(cx).snapshot(cx);
31209 assert_eq!(
31210 editor.hunk_comment_count(&key1, &snapshot),
31211 2,
31212 "file1 should have 2 comments"
31213 );
31214 assert_eq!(
31215 editor.hunk_comment_count(&key2, &snapshot),
31216 1,
31217 "file2 should have 1 comment"
31218 );
31219
31220 // Verify comments_for_hunk returns correct comments
31221 let file1_comments = editor.comments_for_hunk(&key1, &snapshot);
31222 assert_eq!(file1_comments.len(), 2);
31223 assert_eq!(file1_comments[0].comment, "Comment 1 for file1");
31224 assert_eq!(file1_comments[1].comment, "Comment 2 for file1");
31225
31226 let file2_comments = editor.comments_for_hunk(&key2, &snapshot);
31227 assert_eq!(file2_comments.len(), 1);
31228 assert_eq!(file2_comments[0].comment, "Comment for file2");
31229 });
31230}
31231
31232#[gpui::test]
31233fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
31234 init_test(cx, |_| {});
31235
31236 // This test verifies that hunk_keys_match correctly identifies when two
31237 // DiffHunkKeys refer to the same hunk (same file path and anchor point).
31238 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31239
31240 _ = editor.update(cx, |editor, _window, cx| {
31241 let snapshot = editor.buffer().read(cx).snapshot(cx);
31242 let anchor = snapshot.anchor_before(Point::new(0, 0));
31243
31244 // Create two keys with the same file path and anchor
31245 let key1 = DiffHunkKey {
31246 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
31247 hunk_start_anchor: anchor,
31248 };
31249 let key2 = DiffHunkKey {
31250 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
31251 hunk_start_anchor: anchor,
31252 };
31253
31254 // Add comment to first key
31255 editor.add_review_comment(key1, "Test comment".to_string(), anchor..anchor, cx);
31256
31257 // Verify second key (same hunk) finds the comment
31258 let snapshot = editor.buffer().read(cx).snapshot(cx);
31259 assert_eq!(
31260 editor.hunk_comment_count(&key2, &snapshot),
31261 1,
31262 "Same hunk should find the comment"
31263 );
31264
31265 // Create a key with different file path
31266 let different_file_key = DiffHunkKey {
31267 file_path: Arc::from(util::rel_path::RelPath::unix("other.rs").unwrap()),
31268 hunk_start_anchor: anchor,
31269 };
31270
31271 // Different file should not find the comment
31272 assert_eq!(
31273 editor.hunk_comment_count(&different_file_key, &snapshot),
31274 0,
31275 "Different file should not find the comment"
31276 );
31277 });
31278}
31279
31280#[gpui::test]
31281fn test_overlay_comments_expanded_state(cx: &mut TestAppContext) {
31282 init_test(cx, |_| {});
31283
31284 // This test verifies that set_diff_review_comments_expanded correctly
31285 // updates the expanded state of overlays.
31286 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31287
31288 // Show overlay
31289 editor
31290 .update(cx, |editor, window, cx| {
31291 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
31292 })
31293 .unwrap();
31294
31295 // Verify initially expanded (default)
31296 editor
31297 .update(cx, |editor, _window, _cx| {
31298 assert!(
31299 editor.diff_review_overlays[0].comments_expanded,
31300 "Should be expanded by default"
31301 );
31302 })
31303 .unwrap();
31304
31305 // Set to collapsed using the public method
31306 editor
31307 .update(cx, |editor, _window, cx| {
31308 editor.set_diff_review_comments_expanded(false, cx);
31309 })
31310 .unwrap();
31311
31312 // Verify collapsed
31313 editor
31314 .update(cx, |editor, _window, _cx| {
31315 assert!(
31316 !editor.diff_review_overlays[0].comments_expanded,
31317 "Should be collapsed after setting to false"
31318 );
31319 })
31320 .unwrap();
31321
31322 // Set back to expanded
31323 editor
31324 .update(cx, |editor, _window, cx| {
31325 editor.set_diff_review_comments_expanded(true, cx);
31326 })
31327 .unwrap();
31328
31329 // Verify expanded again
31330 editor
31331 .update(cx, |editor, _window, _cx| {
31332 assert!(
31333 editor.diff_review_overlays[0].comments_expanded,
31334 "Should be expanded after setting to true"
31335 );
31336 })
31337 .unwrap();
31338}
31339
31340#[gpui::test]
31341fn test_diff_review_multiline_selection(cx: &mut TestAppContext) {
31342 init_test(cx, |_| {});
31343
31344 // Create an editor with multiple lines of text
31345 let editor = cx.add_window(|window, cx| {
31346 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\nline 4\nline 5\n", cx));
31347 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31348 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31349 });
31350
31351 // Test showing overlay with a multi-line selection (lines 1-3, which are rows 0-2)
31352 editor
31353 .update(cx, |editor, window, cx| {
31354 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(2), window, cx);
31355 })
31356 .unwrap();
31357
31358 // Verify line range
31359 editor
31360 .update(cx, |editor, _window, cx| {
31361 assert!(!editor.diff_review_overlays.is_empty());
31362 assert_eq!(editor.diff_review_line_range(cx), Some((0, 2)));
31363 })
31364 .unwrap();
31365
31366 // Dismiss and test with reversed range (end < start)
31367 editor
31368 .update(cx, |editor, _window, cx| {
31369 editor.dismiss_all_diff_review_overlays(cx);
31370 })
31371 .unwrap();
31372
31373 // Show overlay with reversed range - should normalize it
31374 editor
31375 .update(cx, |editor, window, cx| {
31376 editor.show_diff_review_overlay(DisplayRow(3)..DisplayRow(1), window, cx);
31377 })
31378 .unwrap();
31379
31380 // Verify range is normalized (start <= end)
31381 editor
31382 .update(cx, |editor, _window, cx| {
31383 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
31384 })
31385 .unwrap();
31386}
31387
31388#[gpui::test]
31389fn test_diff_review_drag_state(cx: &mut TestAppContext) {
31390 init_test(cx, |_| {});
31391
31392 let editor = cx.add_window(|window, cx| {
31393 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
31394 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31395 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31396 });
31397
31398 // Initially no drag state
31399 editor
31400 .update(cx, |editor, _window, _cx| {
31401 assert!(editor.diff_review_drag_state.is_none());
31402 })
31403 .unwrap();
31404
31405 // Start drag at row 1
31406 editor
31407 .update(cx, |editor, window, cx| {
31408 editor.start_diff_review_drag(DisplayRow(1), window, cx);
31409 })
31410 .unwrap();
31411
31412 // Verify drag state is set
31413 editor
31414 .update(cx, |editor, window, cx| {
31415 assert!(editor.diff_review_drag_state.is_some());
31416 let snapshot = editor.snapshot(window, cx);
31417 let range = editor
31418 .diff_review_drag_state
31419 .as_ref()
31420 .unwrap()
31421 .row_range(&snapshot.display_snapshot);
31422 assert_eq!(*range.start(), DisplayRow(1));
31423 assert_eq!(*range.end(), DisplayRow(1));
31424 })
31425 .unwrap();
31426
31427 // Update drag to row 3
31428 editor
31429 .update(cx, |editor, window, cx| {
31430 editor.update_diff_review_drag(DisplayRow(3), window, cx);
31431 })
31432 .unwrap();
31433
31434 // Verify drag state is updated
31435 editor
31436 .update(cx, |editor, window, cx| {
31437 assert!(editor.diff_review_drag_state.is_some());
31438 let snapshot = editor.snapshot(window, cx);
31439 let range = editor
31440 .diff_review_drag_state
31441 .as_ref()
31442 .unwrap()
31443 .row_range(&snapshot.display_snapshot);
31444 assert_eq!(*range.start(), DisplayRow(1));
31445 assert_eq!(*range.end(), DisplayRow(3));
31446 })
31447 .unwrap();
31448
31449 // End drag - should show overlay
31450 editor
31451 .update(cx, |editor, window, cx| {
31452 editor.end_diff_review_drag(window, cx);
31453 })
31454 .unwrap();
31455
31456 // Verify drag state is cleared and overlay is shown
31457 editor
31458 .update(cx, |editor, _window, cx| {
31459 assert!(editor.diff_review_drag_state.is_none());
31460 assert!(!editor.diff_review_overlays.is_empty());
31461 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
31462 })
31463 .unwrap();
31464}
31465
31466#[gpui::test]
31467fn test_diff_review_drag_cancel(cx: &mut TestAppContext) {
31468 init_test(cx, |_| {});
31469
31470 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31471
31472 // Start drag
31473 editor
31474 .update(cx, |editor, window, cx| {
31475 editor.start_diff_review_drag(DisplayRow(0), window, cx);
31476 })
31477 .unwrap();
31478
31479 // Verify drag state is set
31480 editor
31481 .update(cx, |editor, _window, _cx| {
31482 assert!(editor.diff_review_drag_state.is_some());
31483 })
31484 .unwrap();
31485
31486 // Cancel drag
31487 editor
31488 .update(cx, |editor, _window, cx| {
31489 editor.cancel_diff_review_drag(cx);
31490 })
31491 .unwrap();
31492
31493 // Verify drag state is cleared and no overlay was created
31494 editor
31495 .update(cx, |editor, _window, _cx| {
31496 assert!(editor.diff_review_drag_state.is_none());
31497 assert!(editor.diff_review_overlays.is_empty());
31498 })
31499 .unwrap();
31500}
31501
31502#[gpui::test]
31503fn test_calculate_overlay_height(cx: &mut TestAppContext) {
31504 init_test(cx, |_| {});
31505
31506 // This test verifies that calculate_overlay_height returns correct heights
31507 // based on comment count and expanded state.
31508 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31509
31510 _ = editor.update(cx, |editor, _window, cx| {
31511 let snapshot = editor.buffer().read(cx).snapshot(cx);
31512 let anchor = snapshot.anchor_before(Point::new(0, 0));
31513 let key = DiffHunkKey {
31514 file_path: Arc::from(util::rel_path::RelPath::empty()),
31515 hunk_start_anchor: anchor,
31516 };
31517
31518 // No comments: base height of 2
31519 let height_no_comments = editor.calculate_overlay_height(&key, true, &snapshot);
31520 assert_eq!(
31521 height_no_comments, 2,
31522 "Base height should be 2 with no comments"
31523 );
31524
31525 // Add one comment
31526 editor.add_review_comment(key.clone(), "Comment 1".to_string(), anchor..anchor, cx);
31527
31528 let snapshot = editor.buffer().read(cx).snapshot(cx);
31529
31530 // With comments expanded: base (2) + header (1) + 2 per comment
31531 let height_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
31532 assert_eq!(
31533 height_expanded,
31534 2 + 1 + 2, // base + header + 1 comment * 2
31535 "Height with 1 comment expanded"
31536 );
31537
31538 // With comments collapsed: base (2) + header (1)
31539 let height_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
31540 assert_eq!(
31541 height_collapsed,
31542 2 + 1, // base + header only
31543 "Height with comments collapsed"
31544 );
31545
31546 // Add more comments
31547 editor.add_review_comment(key.clone(), "Comment 2".to_string(), anchor..anchor, cx);
31548 editor.add_review_comment(key.clone(), "Comment 3".to_string(), anchor..anchor, cx);
31549
31550 let snapshot = editor.buffer().read(cx).snapshot(cx);
31551
31552 // With 3 comments expanded
31553 let height_3_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
31554 assert_eq!(
31555 height_3_expanded,
31556 2 + 1 + (3 * 2), // base + header + 3 comments * 2
31557 "Height with 3 comments expanded"
31558 );
31559
31560 // Collapsed height stays the same regardless of comment count
31561 let height_3_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
31562 assert_eq!(
31563 height_3_collapsed,
31564 2 + 1, // base + header only
31565 "Height with 3 comments collapsed should be same as 1 comment collapsed"
31566 );
31567 });
31568}
31569
31570#[gpui::test]
31571async fn test_move_to_start_end_of_larger_syntax_node_single_cursor(cx: &mut TestAppContext) {
31572 init_test(cx, |_| {});
31573
31574 let language = Arc::new(Language::new(
31575 LanguageConfig::default(),
31576 Some(tree_sitter_rust::LANGUAGE.into()),
31577 ));
31578
31579 let text = r#"
31580 fn main() {
31581 let x = foo(1, 2);
31582 }
31583 "#
31584 .unindent();
31585
31586 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
31587 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31588 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
31589
31590 editor
31591 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
31592 .await;
31593
31594 // Test case 1: Move to end of syntax nodes
31595 editor.update_in(cx, |editor, window, cx| {
31596 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31597 s.select_display_ranges([
31598 DisplayPoint::new(DisplayRow(1), 16)..DisplayPoint::new(DisplayRow(1), 16)
31599 ]);
31600 });
31601 });
31602 editor.update(cx, |editor, cx| {
31603 assert_text_with_selections(
31604 editor,
31605 indoc! {r#"
31606 fn main() {
31607 let x = foo(ˇ1, 2);
31608 }
31609 "#},
31610 cx,
31611 );
31612 });
31613 editor.update_in(cx, |editor, window, cx| {
31614 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31615 });
31616 editor.update(cx, |editor, cx| {
31617 assert_text_with_selections(
31618 editor,
31619 indoc! {r#"
31620 fn main() {
31621 let x = foo(1ˇ, 2);
31622 }
31623 "#},
31624 cx,
31625 );
31626 });
31627 editor.update_in(cx, |editor, window, cx| {
31628 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31629 });
31630 editor.update(cx, |editor, cx| {
31631 assert_text_with_selections(
31632 editor,
31633 indoc! {r#"
31634 fn main() {
31635 let x = foo(1, 2)ˇ;
31636 }
31637 "#},
31638 cx,
31639 );
31640 });
31641 editor.update_in(cx, |editor, window, cx| {
31642 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
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_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, 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
31670 // Test case 2: Move to start of syntax nodes
31671 editor.update_in(cx, |editor, window, cx| {
31672 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31673 s.select_display_ranges([
31674 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20)
31675 ]);
31676 });
31677 });
31678 editor.update(cx, |editor, cx| {
31679 assert_text_with_selections(
31680 editor,
31681 indoc! {r#"
31682 fn main() {
31683 let x = foo(1, 2ˇ);
31684 }
31685 "#},
31686 cx,
31687 );
31688 });
31689 editor.update_in(cx, |editor, window, cx| {
31690 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31691 });
31692 editor.update(cx, |editor, cx| {
31693 assert_text_with_selections(
31694 editor,
31695 indoc! {r#"
31696 fn main() {
31697 let x = fooˇ(1, 2);
31698 }
31699 "#},
31700 cx,
31701 );
31702 });
31703 editor.update_in(cx, |editor, window, cx| {
31704 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31705 });
31706 editor.update(cx, |editor, cx| {
31707 assert_text_with_selections(
31708 editor,
31709 indoc! {r#"
31710 fn main() {
31711 let x = ˇfoo(1, 2);
31712 }
31713 "#},
31714 cx,
31715 );
31716 });
31717 editor.update_in(cx, |editor, window, cx| {
31718 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31719 });
31720 editor.update(cx, |editor, cx| {
31721 assert_text_with_selections(
31722 editor,
31723 indoc! {r#"
31724 fn main() {
31725 ˇlet x = foo(1, 2);
31726 }
31727 "#},
31728 cx,
31729 );
31730 });
31731 editor.update_in(cx, |editor, window, cx| {
31732 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31733 });
31734 editor.update(cx, |editor, cx| {
31735 assert_text_with_selections(
31736 editor,
31737 indoc! {r#"
31738 fn main() ˇ{
31739 let x = foo(1, 2);
31740 }
31741 "#},
31742 cx,
31743 );
31744 });
31745 editor.update_in(cx, |editor, window, cx| {
31746 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31747 });
31748 editor.update(cx, |editor, cx| {
31749 assert_text_with_selections(
31750 editor,
31751 indoc! {r#"
31752 ˇfn main() {
31753 let x = foo(1, 2);
31754 }
31755 "#},
31756 cx,
31757 );
31758 });
31759}
31760
31761#[gpui::test]
31762async fn test_move_to_start_end_of_larger_syntax_node_two_cursors(cx: &mut TestAppContext) {
31763 init_test(cx, |_| {});
31764
31765 let language = Arc::new(Language::new(
31766 LanguageConfig::default(),
31767 Some(tree_sitter_rust::LANGUAGE.into()),
31768 ));
31769
31770 let text = r#"
31771 fn main() {
31772 let x = foo(1, 2);
31773 let y = bar(3, 4);
31774 }
31775 "#
31776 .unindent();
31777
31778 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
31779 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31780 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
31781
31782 editor
31783 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
31784 .await;
31785
31786 // Test case 1: Move to end of syntax nodes with two cursors
31787 editor.update_in(cx, |editor, window, cx| {
31788 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31789 s.select_display_ranges([
31790 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20),
31791 DisplayPoint::new(DisplayRow(2), 20)..DisplayPoint::new(DisplayRow(2), 20),
31792 ]);
31793 });
31794 });
31795 editor.update(cx, |editor, cx| {
31796 assert_text_with_selections(
31797 editor,
31798 indoc! {r#"
31799 fn main() {
31800 let x = foo(1, 2ˇ);
31801 let y = bar(3, 4ˇ);
31802 }
31803 "#},
31804 cx,
31805 );
31806 });
31807 editor.update_in(cx, |editor, window, cx| {
31808 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31809 });
31810 editor.update(cx, |editor, cx| {
31811 assert_text_with_selections(
31812 editor,
31813 indoc! {r#"
31814 fn main() {
31815 let x = foo(1, 2)ˇ;
31816 let y = bar(3, 4)ˇ;
31817 }
31818 "#},
31819 cx,
31820 );
31821 });
31822 editor.update_in(cx, |editor, window, cx| {
31823 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31824 });
31825 editor.update(cx, |editor, cx| {
31826 assert_text_with_selections(
31827 editor,
31828 indoc! {r#"
31829 fn main() {
31830 let x = foo(1, 2);ˇ
31831 let y = bar(3, 4);ˇ
31832 }
31833 "#},
31834 cx,
31835 );
31836 });
31837
31838 // Test case 2: Move to start of syntax nodes with two cursors
31839 editor.update_in(cx, |editor, window, cx| {
31840 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31841 s.select_display_ranges([
31842 DisplayPoint::new(DisplayRow(1), 19)..DisplayPoint::new(DisplayRow(1), 19),
31843 DisplayPoint::new(DisplayRow(2), 19)..DisplayPoint::new(DisplayRow(2), 19),
31844 ]);
31845 });
31846 });
31847 editor.update(cx, |editor, cx| {
31848 assert_text_with_selections(
31849 editor,
31850 indoc! {r#"
31851 fn main() {
31852 let x = foo(1, ˇ2);
31853 let y = bar(3, ˇ4);
31854 }
31855 "#},
31856 cx,
31857 );
31858 });
31859 editor.update_in(cx, |editor, window, cx| {
31860 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31861 });
31862 editor.update(cx, |editor, cx| {
31863 assert_text_with_selections(
31864 editor,
31865 indoc! {r#"
31866 fn main() {
31867 let x = fooˇ(1, 2);
31868 let y = barˇ(3, 4);
31869 }
31870 "#},
31871 cx,
31872 );
31873 });
31874 editor.update_in(cx, |editor, window, cx| {
31875 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31876 });
31877 editor.update(cx, |editor, cx| {
31878 assert_text_with_selections(
31879 editor,
31880 indoc! {r#"
31881 fn main() {
31882 let x = ˇfoo(1, 2);
31883 let y = ˇbar(3, 4);
31884 }
31885 "#},
31886 cx,
31887 );
31888 });
31889 editor.update_in(cx, |editor, window, cx| {
31890 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31891 });
31892 editor.update(cx, |editor, cx| {
31893 assert_text_with_selections(
31894 editor,
31895 indoc! {r#"
31896 fn main() {
31897 ˇlet x = foo(1, 2);
31898 ˇlet y = bar(3, 4);
31899 }
31900 "#},
31901 cx,
31902 );
31903 });
31904}
31905
31906#[gpui::test]
31907async fn test_move_to_start_end_of_larger_syntax_node_with_selections_and_strings(
31908 cx: &mut TestAppContext,
31909) {
31910 init_test(cx, |_| {});
31911
31912 let language = Arc::new(Language::new(
31913 LanguageConfig::default(),
31914 Some(tree_sitter_rust::LANGUAGE.into()),
31915 ));
31916
31917 let text = r#"
31918 fn main() {
31919 let x = foo(1, 2);
31920 let msg = "hello world";
31921 }
31922 "#
31923 .unindent();
31924
31925 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
31926 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31927 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
31928
31929 editor
31930 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
31931 .await;
31932
31933 // Test case 1: With existing selection, move_to_end keeps selection
31934 editor.update_in(cx, |editor, window, cx| {
31935 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31936 s.select_display_ranges([
31937 DisplayPoint::new(DisplayRow(1), 12)..DisplayPoint::new(DisplayRow(1), 21)
31938 ]);
31939 });
31940 });
31941 editor.update(cx, |editor, cx| {
31942 assert_text_with_selections(
31943 editor,
31944 indoc! {r#"
31945 fn main() {
31946 let x = «foo(1, 2)ˇ»;
31947 let msg = "hello world";
31948 }
31949 "#},
31950 cx,
31951 );
31952 });
31953 editor.update_in(cx, |editor, window, cx| {
31954 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31955 });
31956 editor.update(cx, |editor, cx| {
31957 assert_text_with_selections(
31958 editor,
31959 indoc! {r#"
31960 fn main() {
31961 let x = «foo(1, 2)ˇ»;
31962 let msg = "hello world";
31963 }
31964 "#},
31965 cx,
31966 );
31967 });
31968
31969 // Test case 2: Move to end within a string
31970 editor.update_in(cx, |editor, window, cx| {
31971 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31972 s.select_display_ranges([
31973 DisplayPoint::new(DisplayRow(2), 15)..DisplayPoint::new(DisplayRow(2), 15)
31974 ]);
31975 });
31976 });
31977 editor.update(cx, |editor, cx| {
31978 assert_text_with_selections(
31979 editor,
31980 indoc! {r#"
31981 fn main() {
31982 let x = foo(1, 2);
31983 let msg = "ˇhello world";
31984 }
31985 "#},
31986 cx,
31987 );
31988 });
31989 editor.update_in(cx, |editor, window, cx| {
31990 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31991 });
31992 editor.update(cx, |editor, cx| {
31993 assert_text_with_selections(
31994 editor,
31995 indoc! {r#"
31996 fn main() {
31997 let x = foo(1, 2);
31998 let msg = "hello worldˇ";
31999 }
32000 "#},
32001 cx,
32002 );
32003 });
32004
32005 // Test case 3: Move to start within a string
32006 editor.update_in(cx, |editor, window, cx| {
32007 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32008 s.select_display_ranges([
32009 DisplayPoint::new(DisplayRow(2), 21)..DisplayPoint::new(DisplayRow(2), 21)
32010 ]);
32011 });
32012 });
32013 editor.update(cx, |editor, cx| {
32014 assert_text_with_selections(
32015 editor,
32016 indoc! {r#"
32017 fn main() {
32018 let x = foo(1, 2);
32019 let msg = "hello ˇworld";
32020 }
32021 "#},
32022 cx,
32023 );
32024 });
32025 editor.update_in(cx, |editor, window, cx| {
32026 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
32027 });
32028 editor.update(cx, |editor, cx| {
32029 assert_text_with_selections(
32030 editor,
32031 indoc! {r#"
32032 fn main() {
32033 let x = foo(1, 2);
32034 let msg = "ˇhello world";
32035 }
32036 "#},
32037 cx,
32038 );
32039 });
32040}