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, 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 });
13548 });
13549 });
13550
13551 let mut cx = EditorLspTestContext::new_rust(
13552 lsp::ServerCapabilities {
13553 signature_help_provider: Some(lsp::SignatureHelpOptions {
13554 ..Default::default()
13555 }),
13556 ..Default::default()
13557 },
13558 cx,
13559 )
13560 .await;
13561
13562 let language = Language::new(
13563 LanguageConfig {
13564 name: "Rust".into(),
13565 brackets: BracketPairConfig {
13566 pairs: vec![
13567 BracketPair {
13568 start: "{".to_string(),
13569 end: "}".to_string(),
13570 close: true,
13571 surround: true,
13572 newline: true,
13573 },
13574 BracketPair {
13575 start: "(".to_string(),
13576 end: ")".to_string(),
13577 close: true,
13578 surround: true,
13579 newline: true,
13580 },
13581 BracketPair {
13582 start: "/*".to_string(),
13583 end: " */".to_string(),
13584 close: true,
13585 surround: true,
13586 newline: true,
13587 },
13588 BracketPair {
13589 start: "[".to_string(),
13590 end: "]".to_string(),
13591 close: false,
13592 surround: false,
13593 newline: true,
13594 },
13595 BracketPair {
13596 start: "\"".to_string(),
13597 end: "\"".to_string(),
13598 close: true,
13599 surround: true,
13600 newline: false,
13601 },
13602 BracketPair {
13603 start: "<".to_string(),
13604 end: ">".to_string(),
13605 close: false,
13606 surround: true,
13607 newline: true,
13608 },
13609 ],
13610 ..Default::default()
13611 },
13612 autoclose_before: "})]".to_string(),
13613 ..Default::default()
13614 },
13615 Some(tree_sitter_rust::LANGUAGE.into()),
13616 );
13617 let language = Arc::new(language);
13618
13619 cx.language_registry().add(language.clone());
13620 cx.update_buffer(|buffer, cx| {
13621 buffer.set_language(Some(language), cx);
13622 });
13623
13624 cx.set_state(
13625 &r#"
13626 fn main() {
13627 sampleˇ
13628 }
13629 "#
13630 .unindent(),
13631 );
13632
13633 cx.update_editor(|editor, window, cx| {
13634 editor.handle_input("(", window, cx);
13635 });
13636 cx.assert_editor_state(
13637 &"
13638 fn main() {
13639 sample(ˇ)
13640 }
13641 "
13642 .unindent(),
13643 );
13644
13645 let mocked_response = lsp::SignatureHelp {
13646 signatures: vec![lsp::SignatureInformation {
13647 label: "fn sample(param1: u8, param2: u8)".to_string(),
13648 documentation: None,
13649 parameters: Some(vec![
13650 lsp::ParameterInformation {
13651 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13652 documentation: None,
13653 },
13654 lsp::ParameterInformation {
13655 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13656 documentation: None,
13657 },
13658 ]),
13659 active_parameter: None,
13660 }],
13661 active_signature: Some(0),
13662 active_parameter: Some(0),
13663 };
13664 handle_signature_help_request(&mut cx, mocked_response).await;
13665
13666 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13667 .await;
13668
13669 cx.editor(|editor, _, _| {
13670 let signature_help_state = editor.signature_help_state.popover().cloned();
13671 let signature = signature_help_state.unwrap();
13672 assert_eq!(
13673 signature.signatures[signature.current_signature].label,
13674 "fn sample(param1: u8, param2: u8)"
13675 );
13676 });
13677}
13678
13679#[gpui::test]
13680async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13681 init_test(cx, |_| {});
13682
13683 cx.update(|cx| {
13684 cx.update_global::<SettingsStore, _>(|settings, cx| {
13685 settings.update_user_settings(cx, |settings| {
13686 settings.editor.auto_signature_help = Some(false);
13687 settings.editor.show_signature_help_after_edits = Some(false);
13688 });
13689 });
13690 });
13691
13692 let mut cx = EditorLspTestContext::new_rust(
13693 lsp::ServerCapabilities {
13694 signature_help_provider: Some(lsp::SignatureHelpOptions {
13695 ..Default::default()
13696 }),
13697 ..Default::default()
13698 },
13699 cx,
13700 )
13701 .await;
13702
13703 let language = Language::new(
13704 LanguageConfig {
13705 name: "Rust".into(),
13706 brackets: BracketPairConfig {
13707 pairs: vec![
13708 BracketPair {
13709 start: "{".to_string(),
13710 end: "}".to_string(),
13711 close: true,
13712 surround: true,
13713 newline: true,
13714 },
13715 BracketPair {
13716 start: "(".to_string(),
13717 end: ")".to_string(),
13718 close: true,
13719 surround: true,
13720 newline: true,
13721 },
13722 BracketPair {
13723 start: "/*".to_string(),
13724 end: " */".to_string(),
13725 close: true,
13726 surround: true,
13727 newline: true,
13728 },
13729 BracketPair {
13730 start: "[".to_string(),
13731 end: "]".to_string(),
13732 close: false,
13733 surround: false,
13734 newline: true,
13735 },
13736 BracketPair {
13737 start: "\"".to_string(),
13738 end: "\"".to_string(),
13739 close: true,
13740 surround: true,
13741 newline: false,
13742 },
13743 BracketPair {
13744 start: "<".to_string(),
13745 end: ">".to_string(),
13746 close: false,
13747 surround: true,
13748 newline: true,
13749 },
13750 ],
13751 ..Default::default()
13752 },
13753 autoclose_before: "})]".to_string(),
13754 ..Default::default()
13755 },
13756 Some(tree_sitter_rust::LANGUAGE.into()),
13757 );
13758 let language = Arc::new(language);
13759
13760 cx.language_registry().add(language.clone());
13761 cx.update_buffer(|buffer, cx| {
13762 buffer.set_language(Some(language), cx);
13763 });
13764
13765 // Ensure that signature_help is not called when no signature help is enabled.
13766 cx.set_state(
13767 &r#"
13768 fn main() {
13769 sampleˇ
13770 }
13771 "#
13772 .unindent(),
13773 );
13774 cx.update_editor(|editor, window, cx| {
13775 editor.handle_input("(", window, cx);
13776 });
13777 cx.assert_editor_state(
13778 &"
13779 fn main() {
13780 sample(ˇ)
13781 }
13782 "
13783 .unindent(),
13784 );
13785 cx.editor(|editor, _, _| {
13786 assert!(editor.signature_help_state.task().is_none());
13787 });
13788
13789 let mocked_response = lsp::SignatureHelp {
13790 signatures: vec![lsp::SignatureInformation {
13791 label: "fn sample(param1: u8, param2: u8)".to_string(),
13792 documentation: None,
13793 parameters: Some(vec![
13794 lsp::ParameterInformation {
13795 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13796 documentation: None,
13797 },
13798 lsp::ParameterInformation {
13799 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13800 documentation: None,
13801 },
13802 ]),
13803 active_parameter: None,
13804 }],
13805 active_signature: Some(0),
13806 active_parameter: Some(0),
13807 };
13808
13809 // Ensure that signature_help is called when enabled afte edits
13810 cx.update(|_, cx| {
13811 cx.update_global::<SettingsStore, _>(|settings, cx| {
13812 settings.update_user_settings(cx, |settings| {
13813 settings.editor.auto_signature_help = Some(false);
13814 settings.editor.show_signature_help_after_edits = Some(true);
13815 });
13816 });
13817 });
13818 cx.set_state(
13819 &r#"
13820 fn main() {
13821 sampleˇ
13822 }
13823 "#
13824 .unindent(),
13825 );
13826 cx.update_editor(|editor, window, cx| {
13827 editor.handle_input("(", window, cx);
13828 });
13829 cx.assert_editor_state(
13830 &"
13831 fn main() {
13832 sample(ˇ)
13833 }
13834 "
13835 .unindent(),
13836 );
13837 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13838 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13839 .await;
13840 cx.update_editor(|editor, _, _| {
13841 let signature_help_state = editor.signature_help_state.popover().cloned();
13842 assert!(signature_help_state.is_some());
13843 let signature = signature_help_state.unwrap();
13844 assert_eq!(
13845 signature.signatures[signature.current_signature].label,
13846 "fn sample(param1: u8, param2: u8)"
13847 );
13848 editor.signature_help_state = SignatureHelpState::default();
13849 });
13850
13851 // Ensure that signature_help is called when auto signature help override is enabled
13852 cx.update(|_, cx| {
13853 cx.update_global::<SettingsStore, _>(|settings, cx| {
13854 settings.update_user_settings(cx, |settings| {
13855 settings.editor.auto_signature_help = Some(true);
13856 settings.editor.show_signature_help_after_edits = Some(false);
13857 });
13858 });
13859 });
13860 cx.set_state(
13861 &r#"
13862 fn main() {
13863 sampleˇ
13864 }
13865 "#
13866 .unindent(),
13867 );
13868 cx.update_editor(|editor, window, cx| {
13869 editor.handle_input("(", window, cx);
13870 });
13871 cx.assert_editor_state(
13872 &"
13873 fn main() {
13874 sample(ˇ)
13875 }
13876 "
13877 .unindent(),
13878 );
13879 handle_signature_help_request(&mut cx, mocked_response).await;
13880 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13881 .await;
13882 cx.editor(|editor, _, _| {
13883 let signature_help_state = editor.signature_help_state.popover().cloned();
13884 assert!(signature_help_state.is_some());
13885 let signature = signature_help_state.unwrap();
13886 assert_eq!(
13887 signature.signatures[signature.current_signature].label,
13888 "fn sample(param1: u8, param2: u8)"
13889 );
13890 });
13891}
13892
13893#[gpui::test]
13894async fn test_signature_help(cx: &mut TestAppContext) {
13895 init_test(cx, |_| {});
13896 cx.update(|cx| {
13897 cx.update_global::<SettingsStore, _>(|settings, cx| {
13898 settings.update_user_settings(cx, |settings| {
13899 settings.editor.auto_signature_help = Some(true);
13900 });
13901 });
13902 });
13903
13904 let mut cx = EditorLspTestContext::new_rust(
13905 lsp::ServerCapabilities {
13906 signature_help_provider: Some(lsp::SignatureHelpOptions {
13907 ..Default::default()
13908 }),
13909 ..Default::default()
13910 },
13911 cx,
13912 )
13913 .await;
13914
13915 // A test that directly calls `show_signature_help`
13916 cx.update_editor(|editor, window, cx| {
13917 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13918 });
13919
13920 let mocked_response = lsp::SignatureHelp {
13921 signatures: vec![lsp::SignatureInformation {
13922 label: "fn sample(param1: u8, param2: u8)".to_string(),
13923 documentation: None,
13924 parameters: Some(vec![
13925 lsp::ParameterInformation {
13926 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13927 documentation: None,
13928 },
13929 lsp::ParameterInformation {
13930 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13931 documentation: None,
13932 },
13933 ]),
13934 active_parameter: None,
13935 }],
13936 active_signature: Some(0),
13937 active_parameter: Some(0),
13938 };
13939 handle_signature_help_request(&mut cx, mocked_response).await;
13940
13941 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13942 .await;
13943
13944 cx.editor(|editor, _, _| {
13945 let signature_help_state = editor.signature_help_state.popover().cloned();
13946 assert!(signature_help_state.is_some());
13947 let signature = signature_help_state.unwrap();
13948 assert_eq!(
13949 signature.signatures[signature.current_signature].label,
13950 "fn sample(param1: u8, param2: u8)"
13951 );
13952 });
13953
13954 // When exiting outside from inside the brackets, `signature_help` is closed.
13955 cx.set_state(indoc! {"
13956 fn main() {
13957 sample(ˇ);
13958 }
13959
13960 fn sample(param1: u8, param2: u8) {}
13961 "});
13962
13963 cx.update_editor(|editor, window, cx| {
13964 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13965 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13966 });
13967 });
13968
13969 let mocked_response = lsp::SignatureHelp {
13970 signatures: Vec::new(),
13971 active_signature: None,
13972 active_parameter: None,
13973 };
13974 handle_signature_help_request(&mut cx, mocked_response).await;
13975
13976 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13977 .await;
13978
13979 cx.editor(|editor, _, _| {
13980 assert!(!editor.signature_help_state.is_shown());
13981 });
13982
13983 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13984 cx.set_state(indoc! {"
13985 fn main() {
13986 sample(ˇ);
13987 }
13988
13989 fn sample(param1: u8, param2: u8) {}
13990 "});
13991
13992 let mocked_response = lsp::SignatureHelp {
13993 signatures: vec![lsp::SignatureInformation {
13994 label: "fn sample(param1: u8, param2: u8)".to_string(),
13995 documentation: None,
13996 parameters: Some(vec![
13997 lsp::ParameterInformation {
13998 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13999 documentation: None,
14000 },
14001 lsp::ParameterInformation {
14002 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14003 documentation: None,
14004 },
14005 ]),
14006 active_parameter: None,
14007 }],
14008 active_signature: Some(0),
14009 active_parameter: Some(0),
14010 };
14011 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14012 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14013 .await;
14014 cx.editor(|editor, _, _| {
14015 assert!(editor.signature_help_state.is_shown());
14016 });
14017
14018 // Restore the popover with more parameter input
14019 cx.set_state(indoc! {"
14020 fn main() {
14021 sample(param1, param2ˇ);
14022 }
14023
14024 fn sample(param1: u8, param2: u8) {}
14025 "});
14026
14027 let mocked_response = lsp::SignatureHelp {
14028 signatures: vec![lsp::SignatureInformation {
14029 label: "fn sample(param1: u8, param2: u8)".to_string(),
14030 documentation: None,
14031 parameters: Some(vec![
14032 lsp::ParameterInformation {
14033 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14034 documentation: None,
14035 },
14036 lsp::ParameterInformation {
14037 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14038 documentation: None,
14039 },
14040 ]),
14041 active_parameter: None,
14042 }],
14043 active_signature: Some(0),
14044 active_parameter: Some(1),
14045 };
14046 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14047 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14048 .await;
14049
14050 // When selecting a range, the popover is gone.
14051 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
14052 cx.update_editor(|editor, window, cx| {
14053 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14054 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
14055 })
14056 });
14057 cx.assert_editor_state(indoc! {"
14058 fn main() {
14059 sample(param1, «ˇparam2»);
14060 }
14061
14062 fn sample(param1: u8, param2: u8) {}
14063 "});
14064 cx.editor(|editor, _, _| {
14065 assert!(!editor.signature_help_state.is_shown());
14066 });
14067
14068 // When unselecting again, the popover is back if within the brackets.
14069 cx.update_editor(|editor, window, cx| {
14070 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14071 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14072 })
14073 });
14074 cx.assert_editor_state(indoc! {"
14075 fn main() {
14076 sample(param1, ˇparam2);
14077 }
14078
14079 fn sample(param1: u8, param2: u8) {}
14080 "});
14081 handle_signature_help_request(&mut cx, mocked_response).await;
14082 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14083 .await;
14084 cx.editor(|editor, _, _| {
14085 assert!(editor.signature_help_state.is_shown());
14086 });
14087
14088 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
14089 cx.update_editor(|editor, window, cx| {
14090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14091 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
14092 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14093 })
14094 });
14095 cx.assert_editor_state(indoc! {"
14096 fn main() {
14097 sample(param1, ˇparam2);
14098 }
14099
14100 fn sample(param1: u8, param2: u8) {}
14101 "});
14102
14103 let mocked_response = lsp::SignatureHelp {
14104 signatures: vec![lsp::SignatureInformation {
14105 label: "fn sample(param1: u8, param2: u8)".to_string(),
14106 documentation: None,
14107 parameters: Some(vec![
14108 lsp::ParameterInformation {
14109 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14110 documentation: None,
14111 },
14112 lsp::ParameterInformation {
14113 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14114 documentation: None,
14115 },
14116 ]),
14117 active_parameter: None,
14118 }],
14119 active_signature: Some(0),
14120 active_parameter: Some(1),
14121 };
14122 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14123 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14124 .await;
14125 cx.update_editor(|editor, _, cx| {
14126 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
14127 });
14128 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
14129 .await;
14130 cx.update_editor(|editor, window, cx| {
14131 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14132 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
14133 })
14134 });
14135 cx.assert_editor_state(indoc! {"
14136 fn main() {
14137 sample(param1, «ˇparam2»);
14138 }
14139
14140 fn sample(param1: u8, param2: u8) {}
14141 "});
14142 cx.update_editor(|editor, window, cx| {
14143 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14144 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14145 })
14146 });
14147 cx.assert_editor_state(indoc! {"
14148 fn main() {
14149 sample(param1, ˇparam2);
14150 }
14151
14152 fn sample(param1: u8, param2: u8) {}
14153 "});
14154 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
14155 .await;
14156}
14157
14158#[gpui::test]
14159async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
14160 init_test(cx, |_| {});
14161
14162 let mut cx = EditorLspTestContext::new_rust(
14163 lsp::ServerCapabilities {
14164 signature_help_provider: Some(lsp::SignatureHelpOptions {
14165 ..Default::default()
14166 }),
14167 ..Default::default()
14168 },
14169 cx,
14170 )
14171 .await;
14172
14173 cx.set_state(indoc! {"
14174 fn main() {
14175 overloadedˇ
14176 }
14177 "});
14178
14179 cx.update_editor(|editor, window, cx| {
14180 editor.handle_input("(", window, cx);
14181 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14182 });
14183
14184 // Mock response with 3 signatures
14185 let mocked_response = lsp::SignatureHelp {
14186 signatures: vec![
14187 lsp::SignatureInformation {
14188 label: "fn overloaded(x: i32)".to_string(),
14189 documentation: None,
14190 parameters: Some(vec![lsp::ParameterInformation {
14191 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14192 documentation: None,
14193 }]),
14194 active_parameter: None,
14195 },
14196 lsp::SignatureInformation {
14197 label: "fn overloaded(x: i32, y: i32)".to_string(),
14198 documentation: None,
14199 parameters: Some(vec![
14200 lsp::ParameterInformation {
14201 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14202 documentation: None,
14203 },
14204 lsp::ParameterInformation {
14205 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14206 documentation: None,
14207 },
14208 ]),
14209 active_parameter: None,
14210 },
14211 lsp::SignatureInformation {
14212 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
14213 documentation: None,
14214 parameters: Some(vec![
14215 lsp::ParameterInformation {
14216 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14217 documentation: None,
14218 },
14219 lsp::ParameterInformation {
14220 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14221 documentation: None,
14222 },
14223 lsp::ParameterInformation {
14224 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
14225 documentation: None,
14226 },
14227 ]),
14228 active_parameter: None,
14229 },
14230 ],
14231 active_signature: Some(1),
14232 active_parameter: Some(0),
14233 };
14234 handle_signature_help_request(&mut cx, mocked_response).await;
14235
14236 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14237 .await;
14238
14239 // Verify we have multiple signatures and the right one is selected
14240 cx.editor(|editor, _, _| {
14241 let popover = editor.signature_help_state.popover().cloned().unwrap();
14242 assert_eq!(popover.signatures.len(), 3);
14243 // active_signature was 1, so that should be the current
14244 assert_eq!(popover.current_signature, 1);
14245 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
14246 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
14247 assert_eq!(
14248 popover.signatures[2].label,
14249 "fn overloaded(x: i32, y: i32, z: i32)"
14250 );
14251 });
14252
14253 // Test navigation functionality
14254 cx.update_editor(|editor, window, cx| {
14255 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14256 });
14257
14258 cx.editor(|editor, _, _| {
14259 let popover = editor.signature_help_state.popover().cloned().unwrap();
14260 assert_eq!(popover.current_signature, 2);
14261 });
14262
14263 // Test wrap around
14264 cx.update_editor(|editor, window, cx| {
14265 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14266 });
14267
14268 cx.editor(|editor, _, _| {
14269 let popover = editor.signature_help_state.popover().cloned().unwrap();
14270 assert_eq!(popover.current_signature, 0);
14271 });
14272
14273 // Test previous navigation
14274 cx.update_editor(|editor, window, cx| {
14275 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
14276 });
14277
14278 cx.editor(|editor, _, _| {
14279 let popover = editor.signature_help_state.popover().cloned().unwrap();
14280 assert_eq!(popover.current_signature, 2);
14281 });
14282}
14283
14284#[gpui::test]
14285async fn test_completion_mode(cx: &mut TestAppContext) {
14286 init_test(cx, |_| {});
14287 let mut cx = EditorLspTestContext::new_rust(
14288 lsp::ServerCapabilities {
14289 completion_provider: Some(lsp::CompletionOptions {
14290 resolve_provider: Some(true),
14291 ..Default::default()
14292 }),
14293 ..Default::default()
14294 },
14295 cx,
14296 )
14297 .await;
14298
14299 struct Run {
14300 run_description: &'static str,
14301 initial_state: String,
14302 buffer_marked_text: String,
14303 completion_label: &'static str,
14304 completion_text: &'static str,
14305 expected_with_insert_mode: String,
14306 expected_with_replace_mode: String,
14307 expected_with_replace_subsequence_mode: String,
14308 expected_with_replace_suffix_mode: String,
14309 }
14310
14311 let runs = [
14312 Run {
14313 run_description: "Start of word matches completion text",
14314 initial_state: "before ediˇ after".into(),
14315 buffer_marked_text: "before <edi|> after".into(),
14316 completion_label: "editor",
14317 completion_text: "editor",
14318 expected_with_insert_mode: "before editorˇ after".into(),
14319 expected_with_replace_mode: "before editorˇ after".into(),
14320 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14321 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14322 },
14323 Run {
14324 run_description: "Accept same text at the middle of the word",
14325 initial_state: "before ediˇtor after".into(),
14326 buffer_marked_text: "before <edi|tor> after".into(),
14327 completion_label: "editor",
14328 completion_text: "editor",
14329 expected_with_insert_mode: "before editorˇtor after".into(),
14330 expected_with_replace_mode: "before editorˇ after".into(),
14331 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14332 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14333 },
14334 Run {
14335 run_description: "End of word matches completion text -- cursor at end",
14336 initial_state: "before torˇ after".into(),
14337 buffer_marked_text: "before <tor|> after".into(),
14338 completion_label: "editor",
14339 completion_text: "editor",
14340 expected_with_insert_mode: "before editorˇ after".into(),
14341 expected_with_replace_mode: "before editorˇ after".into(),
14342 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14343 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14344 },
14345 Run {
14346 run_description: "End of word matches completion text -- cursor at start",
14347 initial_state: "before ˇtor after".into(),
14348 buffer_marked_text: "before <|tor> after".into(),
14349 completion_label: "editor",
14350 completion_text: "editor",
14351 expected_with_insert_mode: "before editorˇtor after".into(),
14352 expected_with_replace_mode: "before editorˇ after".into(),
14353 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14354 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14355 },
14356 Run {
14357 run_description: "Prepend text containing whitespace",
14358 initial_state: "pˇfield: bool".into(),
14359 buffer_marked_text: "<p|field>: bool".into(),
14360 completion_label: "pub ",
14361 completion_text: "pub ",
14362 expected_with_insert_mode: "pub ˇfield: bool".into(),
14363 expected_with_replace_mode: "pub ˇ: bool".into(),
14364 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
14365 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
14366 },
14367 Run {
14368 run_description: "Add element to start of list",
14369 initial_state: "[element_ˇelement_2]".into(),
14370 buffer_marked_text: "[<element_|element_2>]".into(),
14371 completion_label: "element_1",
14372 completion_text: "element_1",
14373 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
14374 expected_with_replace_mode: "[element_1ˇ]".into(),
14375 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
14376 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
14377 },
14378 Run {
14379 run_description: "Add element to start of list -- first and second elements are equal",
14380 initial_state: "[elˇelement]".into(),
14381 buffer_marked_text: "[<el|element>]".into(),
14382 completion_label: "element",
14383 completion_text: "element",
14384 expected_with_insert_mode: "[elementˇelement]".into(),
14385 expected_with_replace_mode: "[elementˇ]".into(),
14386 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
14387 expected_with_replace_suffix_mode: "[elementˇ]".into(),
14388 },
14389 Run {
14390 run_description: "Ends with matching suffix",
14391 initial_state: "SubˇError".into(),
14392 buffer_marked_text: "<Sub|Error>".into(),
14393 completion_label: "SubscriptionError",
14394 completion_text: "SubscriptionError",
14395 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
14396 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14397 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14398 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
14399 },
14400 Run {
14401 run_description: "Suffix is a subsequence -- contiguous",
14402 initial_state: "SubˇErr".into(),
14403 buffer_marked_text: "<Sub|Err>".into(),
14404 completion_label: "SubscriptionError",
14405 completion_text: "SubscriptionError",
14406 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
14407 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14408 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14409 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
14410 },
14411 Run {
14412 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
14413 initial_state: "Suˇscrirr".into(),
14414 buffer_marked_text: "<Su|scrirr>".into(),
14415 completion_label: "SubscriptionError",
14416 completion_text: "SubscriptionError",
14417 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
14418 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14419 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14420 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
14421 },
14422 Run {
14423 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
14424 initial_state: "foo(indˇix)".into(),
14425 buffer_marked_text: "foo(<ind|ix>)".into(),
14426 completion_label: "node_index",
14427 completion_text: "node_index",
14428 expected_with_insert_mode: "foo(node_indexˇix)".into(),
14429 expected_with_replace_mode: "foo(node_indexˇ)".into(),
14430 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
14431 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
14432 },
14433 Run {
14434 run_description: "Replace range ends before cursor - should extend to cursor",
14435 initial_state: "before editˇo after".into(),
14436 buffer_marked_text: "before <{ed}>it|o after".into(),
14437 completion_label: "editor",
14438 completion_text: "editor",
14439 expected_with_insert_mode: "before editorˇo after".into(),
14440 expected_with_replace_mode: "before editorˇo after".into(),
14441 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
14442 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14443 },
14444 Run {
14445 run_description: "Uses label for suffix matching",
14446 initial_state: "before ediˇtor after".into(),
14447 buffer_marked_text: "before <edi|tor> after".into(),
14448 completion_label: "editor",
14449 completion_text: "editor()",
14450 expected_with_insert_mode: "before editor()ˇtor after".into(),
14451 expected_with_replace_mode: "before editor()ˇ after".into(),
14452 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14453 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14454 },
14455 Run {
14456 run_description: "Case insensitive subsequence and suffix matching",
14457 initial_state: "before EDiˇtoR after".into(),
14458 buffer_marked_text: "before <EDi|toR> after".into(),
14459 completion_label: "editor",
14460 completion_text: "editor",
14461 expected_with_insert_mode: "before editorˇtoR after".into(),
14462 expected_with_replace_mode: "before editorˇ after".into(),
14463 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14464 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14465 },
14466 ];
14467
14468 for run in runs {
14469 let run_variations = [
14470 (LspInsertMode::Insert, run.expected_with_insert_mode),
14471 (LspInsertMode::Replace, run.expected_with_replace_mode),
14472 (
14473 LspInsertMode::ReplaceSubsequence,
14474 run.expected_with_replace_subsequence_mode,
14475 ),
14476 (
14477 LspInsertMode::ReplaceSuffix,
14478 run.expected_with_replace_suffix_mode,
14479 ),
14480 ];
14481
14482 for (lsp_insert_mode, expected_text) in run_variations {
14483 eprintln!(
14484 "run = {:?}, mode = {lsp_insert_mode:.?}",
14485 run.run_description,
14486 );
14487
14488 update_test_language_settings(&mut cx, |settings| {
14489 settings.defaults.completions = Some(CompletionSettingsContent {
14490 lsp_insert_mode: Some(lsp_insert_mode),
14491 words: Some(WordsCompletionMode::Disabled),
14492 words_min_length: Some(0),
14493 ..Default::default()
14494 });
14495 });
14496
14497 cx.set_state(&run.initial_state);
14498
14499 // Set up resolve handler before showing completions, since resolve may be
14500 // triggered when menu becomes visible (for documentation), not just on confirm.
14501 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(
14502 move |_, _, _| async move {
14503 Ok(lsp::CompletionItem {
14504 additional_text_edits: None,
14505 ..Default::default()
14506 })
14507 },
14508 );
14509
14510 cx.update_editor(|editor, window, cx| {
14511 editor.show_completions(&ShowCompletions, window, cx);
14512 });
14513
14514 let counter = Arc::new(AtomicUsize::new(0));
14515 handle_completion_request_with_insert_and_replace(
14516 &mut cx,
14517 &run.buffer_marked_text,
14518 vec![(run.completion_label, run.completion_text)],
14519 counter.clone(),
14520 )
14521 .await;
14522 cx.condition(|editor, _| editor.context_menu_visible())
14523 .await;
14524 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14525
14526 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14527 editor
14528 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14529 .unwrap()
14530 });
14531 cx.assert_editor_state(&expected_text);
14532 apply_additional_edits.await.unwrap();
14533 }
14534 }
14535}
14536
14537#[gpui::test]
14538async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14539 init_test(cx, |_| {});
14540 let mut cx = EditorLspTestContext::new_rust(
14541 lsp::ServerCapabilities {
14542 completion_provider: Some(lsp::CompletionOptions {
14543 resolve_provider: Some(true),
14544 ..Default::default()
14545 }),
14546 ..Default::default()
14547 },
14548 cx,
14549 )
14550 .await;
14551
14552 let initial_state = "SubˇError";
14553 let buffer_marked_text = "<Sub|Error>";
14554 let completion_text = "SubscriptionError";
14555 let expected_with_insert_mode = "SubscriptionErrorˇError";
14556 let expected_with_replace_mode = "SubscriptionErrorˇ";
14557
14558 update_test_language_settings(&mut cx, |settings| {
14559 settings.defaults.completions = Some(CompletionSettingsContent {
14560 words: Some(WordsCompletionMode::Disabled),
14561 words_min_length: Some(0),
14562 // set the opposite here to ensure that the action is overriding the default behavior
14563 lsp_insert_mode: Some(LspInsertMode::Insert),
14564 ..Default::default()
14565 });
14566 });
14567
14568 cx.set_state(initial_state);
14569 cx.update_editor(|editor, window, cx| {
14570 editor.show_completions(&ShowCompletions, window, cx);
14571 });
14572
14573 let counter = Arc::new(AtomicUsize::new(0));
14574 handle_completion_request_with_insert_and_replace(
14575 &mut cx,
14576 buffer_marked_text,
14577 vec![(completion_text, completion_text)],
14578 counter.clone(),
14579 )
14580 .await;
14581 cx.condition(|editor, _| editor.context_menu_visible())
14582 .await;
14583 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14584
14585 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14586 editor
14587 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14588 .unwrap()
14589 });
14590 cx.assert_editor_state(expected_with_replace_mode);
14591 handle_resolve_completion_request(&mut cx, None).await;
14592 apply_additional_edits.await.unwrap();
14593
14594 update_test_language_settings(&mut cx, |settings| {
14595 settings.defaults.completions = Some(CompletionSettingsContent {
14596 words: Some(WordsCompletionMode::Disabled),
14597 words_min_length: Some(0),
14598 // set the opposite here to ensure that the action is overriding the default behavior
14599 lsp_insert_mode: Some(LspInsertMode::Replace),
14600 ..Default::default()
14601 });
14602 });
14603
14604 cx.set_state(initial_state);
14605 cx.update_editor(|editor, window, cx| {
14606 editor.show_completions(&ShowCompletions, window, cx);
14607 });
14608 handle_completion_request_with_insert_and_replace(
14609 &mut cx,
14610 buffer_marked_text,
14611 vec![(completion_text, completion_text)],
14612 counter.clone(),
14613 )
14614 .await;
14615 cx.condition(|editor, _| editor.context_menu_visible())
14616 .await;
14617 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14618
14619 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14620 editor
14621 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14622 .unwrap()
14623 });
14624 cx.assert_editor_state(expected_with_insert_mode);
14625 handle_resolve_completion_request(&mut cx, None).await;
14626 apply_additional_edits.await.unwrap();
14627}
14628
14629#[gpui::test]
14630async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14631 init_test(cx, |_| {});
14632 let mut cx = EditorLspTestContext::new_rust(
14633 lsp::ServerCapabilities {
14634 completion_provider: Some(lsp::CompletionOptions {
14635 resolve_provider: Some(true),
14636 ..Default::default()
14637 }),
14638 ..Default::default()
14639 },
14640 cx,
14641 )
14642 .await;
14643
14644 // scenario: surrounding text matches completion text
14645 let completion_text = "to_offset";
14646 let initial_state = indoc! {"
14647 1. buf.to_offˇsuffix
14648 2. buf.to_offˇsuf
14649 3. buf.to_offˇfix
14650 4. buf.to_offˇ
14651 5. into_offˇensive
14652 6. ˇsuffix
14653 7. let ˇ //
14654 8. aaˇzz
14655 9. buf.to_off«zzzzzˇ»suffix
14656 10. buf.«ˇzzzzz»suffix
14657 11. to_off«ˇzzzzz»
14658
14659 buf.to_offˇsuffix // newest cursor
14660 "};
14661 let completion_marked_buffer = indoc! {"
14662 1. buf.to_offsuffix
14663 2. buf.to_offsuf
14664 3. buf.to_offfix
14665 4. buf.to_off
14666 5. into_offensive
14667 6. suffix
14668 7. let //
14669 8. aazz
14670 9. buf.to_offzzzzzsuffix
14671 10. buf.zzzzzsuffix
14672 11. to_offzzzzz
14673
14674 buf.<to_off|suffix> // newest cursor
14675 "};
14676 let expected = indoc! {"
14677 1. buf.to_offsetˇ
14678 2. buf.to_offsetˇsuf
14679 3. buf.to_offsetˇfix
14680 4. buf.to_offsetˇ
14681 5. into_offsetˇensive
14682 6. to_offsetˇsuffix
14683 7. let to_offsetˇ //
14684 8. aato_offsetˇzz
14685 9. buf.to_offsetˇ
14686 10. buf.to_offsetˇsuffix
14687 11. to_offsetˇ
14688
14689 buf.to_offsetˇ // newest cursor
14690 "};
14691 cx.set_state(initial_state);
14692 cx.update_editor(|editor, window, cx| {
14693 editor.show_completions(&ShowCompletions, window, cx);
14694 });
14695 handle_completion_request_with_insert_and_replace(
14696 &mut cx,
14697 completion_marked_buffer,
14698 vec![(completion_text, completion_text)],
14699 Arc::new(AtomicUsize::new(0)),
14700 )
14701 .await;
14702 cx.condition(|editor, _| editor.context_menu_visible())
14703 .await;
14704 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14705 editor
14706 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14707 .unwrap()
14708 });
14709 cx.assert_editor_state(expected);
14710 handle_resolve_completion_request(&mut cx, None).await;
14711 apply_additional_edits.await.unwrap();
14712
14713 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14714 let completion_text = "foo_and_bar";
14715 let initial_state = indoc! {"
14716 1. ooanbˇ
14717 2. zooanbˇ
14718 3. ooanbˇz
14719 4. zooanbˇz
14720 5. ooanˇ
14721 6. oanbˇ
14722
14723 ooanbˇ
14724 "};
14725 let completion_marked_buffer = indoc! {"
14726 1. ooanb
14727 2. zooanb
14728 3. ooanbz
14729 4. zooanbz
14730 5. ooan
14731 6. oanb
14732
14733 <ooanb|>
14734 "};
14735 let expected = indoc! {"
14736 1. foo_and_barˇ
14737 2. zfoo_and_barˇ
14738 3. foo_and_barˇz
14739 4. zfoo_and_barˇz
14740 5. ooanfoo_and_barˇ
14741 6. oanbfoo_and_barˇ
14742
14743 foo_and_barˇ
14744 "};
14745 cx.set_state(initial_state);
14746 cx.update_editor(|editor, window, cx| {
14747 editor.show_completions(&ShowCompletions, window, cx);
14748 });
14749 handle_completion_request_with_insert_and_replace(
14750 &mut cx,
14751 completion_marked_buffer,
14752 vec![(completion_text, completion_text)],
14753 Arc::new(AtomicUsize::new(0)),
14754 )
14755 .await;
14756 cx.condition(|editor, _| editor.context_menu_visible())
14757 .await;
14758 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14759 editor
14760 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14761 .unwrap()
14762 });
14763 cx.assert_editor_state(expected);
14764 handle_resolve_completion_request(&mut cx, None).await;
14765 apply_additional_edits.await.unwrap();
14766
14767 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14768 // (expects the same as if it was inserted at the end)
14769 let completion_text = "foo_and_bar";
14770 let initial_state = indoc! {"
14771 1. ooˇanb
14772 2. zooˇanb
14773 3. ooˇanbz
14774 4. zooˇanbz
14775
14776 ooˇanb
14777 "};
14778 let completion_marked_buffer = indoc! {"
14779 1. ooanb
14780 2. zooanb
14781 3. ooanbz
14782 4. zooanbz
14783
14784 <oo|anb>
14785 "};
14786 let expected = indoc! {"
14787 1. foo_and_barˇ
14788 2. zfoo_and_barˇ
14789 3. foo_and_barˇz
14790 4. zfoo_and_barˇz
14791
14792 foo_and_barˇ
14793 "};
14794 cx.set_state(initial_state);
14795 cx.update_editor(|editor, window, cx| {
14796 editor.show_completions(&ShowCompletions, window, cx);
14797 });
14798 handle_completion_request_with_insert_and_replace(
14799 &mut cx,
14800 completion_marked_buffer,
14801 vec![(completion_text, completion_text)],
14802 Arc::new(AtomicUsize::new(0)),
14803 )
14804 .await;
14805 cx.condition(|editor, _| editor.context_menu_visible())
14806 .await;
14807 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14808 editor
14809 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14810 .unwrap()
14811 });
14812 cx.assert_editor_state(expected);
14813 handle_resolve_completion_request(&mut cx, None).await;
14814 apply_additional_edits.await.unwrap();
14815}
14816
14817// This used to crash
14818#[gpui::test]
14819async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14820 init_test(cx, |_| {});
14821
14822 let buffer_text = indoc! {"
14823 fn main() {
14824 10.satu;
14825
14826 //
14827 // separate cursors so they open in different excerpts (manually reproducible)
14828 //
14829
14830 10.satu20;
14831 }
14832 "};
14833 let multibuffer_text_with_selections = indoc! {"
14834 fn main() {
14835 10.satuˇ;
14836
14837 //
14838
14839 //
14840
14841 10.satuˇ20;
14842 }
14843 "};
14844 let expected_multibuffer = indoc! {"
14845 fn main() {
14846 10.saturating_sub()ˇ;
14847
14848 //
14849
14850 //
14851
14852 10.saturating_sub()ˇ;
14853 }
14854 "};
14855
14856 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14857 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14858
14859 let fs = FakeFs::new(cx.executor());
14860 fs.insert_tree(
14861 path!("/a"),
14862 json!({
14863 "main.rs": buffer_text,
14864 }),
14865 )
14866 .await;
14867
14868 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14869 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14870 language_registry.add(rust_lang());
14871 let mut fake_servers = language_registry.register_fake_lsp(
14872 "Rust",
14873 FakeLspAdapter {
14874 capabilities: lsp::ServerCapabilities {
14875 completion_provider: Some(lsp::CompletionOptions {
14876 resolve_provider: None,
14877 ..lsp::CompletionOptions::default()
14878 }),
14879 ..lsp::ServerCapabilities::default()
14880 },
14881 ..FakeLspAdapter::default()
14882 },
14883 );
14884 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14885 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14886 let buffer = project
14887 .update(cx, |project, cx| {
14888 project.open_local_buffer(path!("/a/main.rs"), cx)
14889 })
14890 .await
14891 .unwrap();
14892
14893 let multi_buffer = cx.new(|cx| {
14894 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14895 multi_buffer.push_excerpts(
14896 buffer.clone(),
14897 [ExcerptRange::new(0..first_excerpt_end)],
14898 cx,
14899 );
14900 multi_buffer.push_excerpts(
14901 buffer.clone(),
14902 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14903 cx,
14904 );
14905 multi_buffer
14906 });
14907
14908 let editor = workspace
14909 .update(cx, |_, window, cx| {
14910 cx.new(|cx| {
14911 Editor::new(
14912 EditorMode::Full {
14913 scale_ui_elements_with_buffer_font_size: false,
14914 show_active_line_background: false,
14915 sizing_behavior: SizingBehavior::Default,
14916 },
14917 multi_buffer.clone(),
14918 Some(project.clone()),
14919 window,
14920 cx,
14921 )
14922 })
14923 })
14924 .unwrap();
14925
14926 let pane = workspace
14927 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14928 .unwrap();
14929 pane.update_in(cx, |pane, window, cx| {
14930 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14931 });
14932
14933 let fake_server = fake_servers.next().await.unwrap();
14934 cx.run_until_parked();
14935
14936 editor.update_in(cx, |editor, window, cx| {
14937 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14938 s.select_ranges([
14939 Point::new(1, 11)..Point::new(1, 11),
14940 Point::new(7, 11)..Point::new(7, 11),
14941 ])
14942 });
14943
14944 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14945 });
14946
14947 editor.update_in(cx, |editor, window, cx| {
14948 editor.show_completions(&ShowCompletions, window, cx);
14949 });
14950
14951 fake_server
14952 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14953 let completion_item = lsp::CompletionItem {
14954 label: "saturating_sub()".into(),
14955 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14956 lsp::InsertReplaceEdit {
14957 new_text: "saturating_sub()".to_owned(),
14958 insert: lsp::Range::new(
14959 lsp::Position::new(7, 7),
14960 lsp::Position::new(7, 11),
14961 ),
14962 replace: lsp::Range::new(
14963 lsp::Position::new(7, 7),
14964 lsp::Position::new(7, 13),
14965 ),
14966 },
14967 )),
14968 ..lsp::CompletionItem::default()
14969 };
14970
14971 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14972 })
14973 .next()
14974 .await
14975 .unwrap();
14976
14977 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14978 .await;
14979
14980 editor
14981 .update_in(cx, |editor, window, cx| {
14982 editor
14983 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14984 .unwrap()
14985 })
14986 .await
14987 .unwrap();
14988
14989 editor.update(cx, |editor, cx| {
14990 assert_text_with_selections(editor, expected_multibuffer, cx);
14991 })
14992}
14993
14994#[gpui::test]
14995async fn test_completion(cx: &mut TestAppContext) {
14996 init_test(cx, |_| {});
14997
14998 let mut cx = EditorLspTestContext::new_rust(
14999 lsp::ServerCapabilities {
15000 completion_provider: Some(lsp::CompletionOptions {
15001 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15002 resolve_provider: Some(true),
15003 ..Default::default()
15004 }),
15005 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15006 ..Default::default()
15007 },
15008 cx,
15009 )
15010 .await;
15011 let counter = Arc::new(AtomicUsize::new(0));
15012
15013 cx.set_state(indoc! {"
15014 oneˇ
15015 two
15016 three
15017 "});
15018 cx.simulate_keystroke(".");
15019 handle_completion_request(
15020 indoc! {"
15021 one.|<>
15022 two
15023 three
15024 "},
15025 vec!["first_completion", "second_completion"],
15026 true,
15027 counter.clone(),
15028 &mut cx,
15029 )
15030 .await;
15031 cx.condition(|editor, _| editor.context_menu_visible())
15032 .await;
15033 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15034
15035 let _handler = handle_signature_help_request(
15036 &mut cx,
15037 lsp::SignatureHelp {
15038 signatures: vec![lsp::SignatureInformation {
15039 label: "test signature".to_string(),
15040 documentation: None,
15041 parameters: Some(vec![lsp::ParameterInformation {
15042 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
15043 documentation: None,
15044 }]),
15045 active_parameter: None,
15046 }],
15047 active_signature: None,
15048 active_parameter: None,
15049 },
15050 );
15051 cx.update_editor(|editor, window, cx| {
15052 assert!(
15053 !editor.signature_help_state.is_shown(),
15054 "No signature help was called for"
15055 );
15056 editor.show_signature_help(&ShowSignatureHelp, window, cx);
15057 });
15058 cx.run_until_parked();
15059 cx.update_editor(|editor, _, _| {
15060 assert!(
15061 !editor.signature_help_state.is_shown(),
15062 "No signature help should be shown when completions menu is open"
15063 );
15064 });
15065
15066 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15067 editor.context_menu_next(&Default::default(), window, cx);
15068 editor
15069 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15070 .unwrap()
15071 });
15072 cx.assert_editor_state(indoc! {"
15073 one.second_completionˇ
15074 two
15075 three
15076 "});
15077
15078 handle_resolve_completion_request(
15079 &mut cx,
15080 Some(vec![
15081 (
15082 //This overlaps with the primary completion edit which is
15083 //misbehavior from the LSP spec, test that we filter it out
15084 indoc! {"
15085 one.second_ˇcompletion
15086 two
15087 threeˇ
15088 "},
15089 "overlapping additional edit",
15090 ),
15091 (
15092 indoc! {"
15093 one.second_completion
15094 two
15095 threeˇ
15096 "},
15097 "\nadditional edit",
15098 ),
15099 ]),
15100 )
15101 .await;
15102 apply_additional_edits.await.unwrap();
15103 cx.assert_editor_state(indoc! {"
15104 one.second_completionˇ
15105 two
15106 three
15107 additional edit
15108 "});
15109
15110 cx.set_state(indoc! {"
15111 one.second_completion
15112 twoˇ
15113 threeˇ
15114 additional edit
15115 "});
15116 cx.simulate_keystroke(" ");
15117 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15118 cx.simulate_keystroke("s");
15119 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15120
15121 cx.assert_editor_state(indoc! {"
15122 one.second_completion
15123 two sˇ
15124 three sˇ
15125 additional edit
15126 "});
15127 handle_completion_request(
15128 indoc! {"
15129 one.second_completion
15130 two s
15131 three <s|>
15132 additional edit
15133 "},
15134 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
15135 true,
15136 counter.clone(),
15137 &mut cx,
15138 )
15139 .await;
15140 cx.condition(|editor, _| editor.context_menu_visible())
15141 .await;
15142 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15143
15144 cx.simulate_keystroke("i");
15145
15146 handle_completion_request(
15147 indoc! {"
15148 one.second_completion
15149 two si
15150 three <si|>
15151 additional edit
15152 "},
15153 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
15154 true,
15155 counter.clone(),
15156 &mut cx,
15157 )
15158 .await;
15159 cx.condition(|editor, _| editor.context_menu_visible())
15160 .await;
15161 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15162
15163 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15164 editor
15165 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15166 .unwrap()
15167 });
15168 cx.assert_editor_state(indoc! {"
15169 one.second_completion
15170 two sixth_completionˇ
15171 three sixth_completionˇ
15172 additional edit
15173 "});
15174
15175 apply_additional_edits.await.unwrap();
15176
15177 update_test_language_settings(&mut cx, |settings| {
15178 settings.defaults.show_completions_on_input = Some(false);
15179 });
15180 cx.set_state("editorˇ");
15181 cx.simulate_keystroke(".");
15182 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15183 cx.simulate_keystrokes("c l o");
15184 cx.assert_editor_state("editor.cloˇ");
15185 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15186 cx.update_editor(|editor, window, cx| {
15187 editor.show_completions(&ShowCompletions, window, cx);
15188 });
15189 handle_completion_request(
15190 "editor.<clo|>",
15191 vec!["close", "clobber"],
15192 true,
15193 counter.clone(),
15194 &mut cx,
15195 )
15196 .await;
15197 cx.condition(|editor, _| editor.context_menu_visible())
15198 .await;
15199 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
15200
15201 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15202 editor
15203 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15204 .unwrap()
15205 });
15206 cx.assert_editor_state("editor.clobberˇ");
15207 handle_resolve_completion_request(&mut cx, None).await;
15208 apply_additional_edits.await.unwrap();
15209}
15210
15211#[gpui::test]
15212async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
15213 init_test(cx, |_| {});
15214
15215 let fs = FakeFs::new(cx.executor());
15216 fs.insert_tree(
15217 path!("/a"),
15218 json!({
15219 "main.rs": "",
15220 }),
15221 )
15222 .await;
15223
15224 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15225 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15226 language_registry.add(rust_lang());
15227 let command_calls = Arc::new(AtomicUsize::new(0));
15228 let registered_command = "_the/command";
15229
15230 let closure_command_calls = command_calls.clone();
15231 let mut fake_servers = language_registry.register_fake_lsp(
15232 "Rust",
15233 FakeLspAdapter {
15234 capabilities: lsp::ServerCapabilities {
15235 completion_provider: Some(lsp::CompletionOptions {
15236 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15237 ..lsp::CompletionOptions::default()
15238 }),
15239 execute_command_provider: Some(lsp::ExecuteCommandOptions {
15240 commands: vec![registered_command.to_owned()],
15241 ..lsp::ExecuteCommandOptions::default()
15242 }),
15243 ..lsp::ServerCapabilities::default()
15244 },
15245 initializer: Some(Box::new(move |fake_server| {
15246 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15247 move |params, _| async move {
15248 Ok(Some(lsp::CompletionResponse::Array(vec![
15249 lsp::CompletionItem {
15250 label: "registered_command".to_owned(),
15251 text_edit: gen_text_edit(¶ms, ""),
15252 command: Some(lsp::Command {
15253 title: registered_command.to_owned(),
15254 command: "_the/command".to_owned(),
15255 arguments: Some(vec![serde_json::Value::Bool(true)]),
15256 }),
15257 ..lsp::CompletionItem::default()
15258 },
15259 lsp::CompletionItem {
15260 label: "unregistered_command".to_owned(),
15261 text_edit: gen_text_edit(¶ms, ""),
15262 command: Some(lsp::Command {
15263 title: "????????????".to_owned(),
15264 command: "????????????".to_owned(),
15265 arguments: Some(vec![serde_json::Value::Null]),
15266 }),
15267 ..lsp::CompletionItem::default()
15268 },
15269 ])))
15270 },
15271 );
15272 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
15273 let command_calls = closure_command_calls.clone();
15274 move |params, _| {
15275 assert_eq!(params.command, registered_command);
15276 let command_calls = command_calls.clone();
15277 async move {
15278 command_calls.fetch_add(1, atomic::Ordering::Release);
15279 Ok(Some(json!(null)))
15280 }
15281 }
15282 });
15283 })),
15284 ..FakeLspAdapter::default()
15285 },
15286 );
15287 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15288 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15289 let editor = workspace
15290 .update(cx, |workspace, window, cx| {
15291 workspace.open_abs_path(
15292 PathBuf::from(path!("/a/main.rs")),
15293 OpenOptions::default(),
15294 window,
15295 cx,
15296 )
15297 })
15298 .unwrap()
15299 .await
15300 .unwrap()
15301 .downcast::<Editor>()
15302 .unwrap();
15303 let _fake_server = fake_servers.next().await.unwrap();
15304 cx.run_until_parked();
15305
15306 editor.update_in(cx, |editor, window, cx| {
15307 cx.focus_self(window);
15308 editor.move_to_end(&MoveToEnd, window, cx);
15309 editor.handle_input(".", window, cx);
15310 });
15311 cx.run_until_parked();
15312 editor.update(cx, |editor, _| {
15313 assert!(editor.context_menu_visible());
15314 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15315 {
15316 let completion_labels = menu
15317 .completions
15318 .borrow()
15319 .iter()
15320 .map(|c| c.label.text.clone())
15321 .collect::<Vec<_>>();
15322 assert_eq!(
15323 completion_labels,
15324 &["registered_command", "unregistered_command",],
15325 );
15326 } else {
15327 panic!("expected completion menu to be open");
15328 }
15329 });
15330
15331 editor
15332 .update_in(cx, |editor, window, cx| {
15333 editor
15334 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15335 .unwrap()
15336 })
15337 .await
15338 .unwrap();
15339 cx.run_until_parked();
15340 assert_eq!(
15341 command_calls.load(atomic::Ordering::Acquire),
15342 1,
15343 "For completion with a registered command, Zed should send a command execution request",
15344 );
15345
15346 editor.update_in(cx, |editor, window, cx| {
15347 cx.focus_self(window);
15348 editor.handle_input(".", window, cx);
15349 });
15350 cx.run_until_parked();
15351 editor.update(cx, |editor, _| {
15352 assert!(editor.context_menu_visible());
15353 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15354 {
15355 let completion_labels = menu
15356 .completions
15357 .borrow()
15358 .iter()
15359 .map(|c| c.label.text.clone())
15360 .collect::<Vec<_>>();
15361 assert_eq!(
15362 completion_labels,
15363 &["registered_command", "unregistered_command",],
15364 );
15365 } else {
15366 panic!("expected completion menu to be open");
15367 }
15368 });
15369 editor
15370 .update_in(cx, |editor, window, cx| {
15371 editor.context_menu_next(&Default::default(), window, cx);
15372 editor
15373 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15374 .unwrap()
15375 })
15376 .await
15377 .unwrap();
15378 cx.run_until_parked();
15379 assert_eq!(
15380 command_calls.load(atomic::Ordering::Acquire),
15381 1,
15382 "For completion with an unregistered command, Zed should not send a command execution request",
15383 );
15384}
15385
15386#[gpui::test]
15387async fn test_completion_reuse(cx: &mut TestAppContext) {
15388 init_test(cx, |_| {});
15389
15390 let mut cx = EditorLspTestContext::new_rust(
15391 lsp::ServerCapabilities {
15392 completion_provider: Some(lsp::CompletionOptions {
15393 trigger_characters: Some(vec![".".to_string()]),
15394 ..Default::default()
15395 }),
15396 ..Default::default()
15397 },
15398 cx,
15399 )
15400 .await;
15401
15402 let counter = Arc::new(AtomicUsize::new(0));
15403 cx.set_state("objˇ");
15404 cx.simulate_keystroke(".");
15405
15406 // Initial completion request returns complete results
15407 let is_incomplete = false;
15408 handle_completion_request(
15409 "obj.|<>",
15410 vec!["a", "ab", "abc"],
15411 is_incomplete,
15412 counter.clone(),
15413 &mut cx,
15414 )
15415 .await;
15416 cx.run_until_parked();
15417 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15418 cx.assert_editor_state("obj.ˇ");
15419 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15420
15421 // Type "a" - filters existing completions
15422 cx.simulate_keystroke("a");
15423 cx.run_until_parked();
15424 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15425 cx.assert_editor_state("obj.aˇ");
15426 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15427
15428 // Type "b" - filters existing completions
15429 cx.simulate_keystroke("b");
15430 cx.run_until_parked();
15431 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15432 cx.assert_editor_state("obj.abˇ");
15433 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15434
15435 // Type "c" - filters existing completions
15436 cx.simulate_keystroke("c");
15437 cx.run_until_parked();
15438 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15439 cx.assert_editor_state("obj.abcˇ");
15440 check_displayed_completions(vec!["abc"], &mut cx);
15441
15442 // Backspace to delete "c" - filters existing completions
15443 cx.update_editor(|editor, window, cx| {
15444 editor.backspace(&Backspace, window, cx);
15445 });
15446 cx.run_until_parked();
15447 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15448 cx.assert_editor_state("obj.abˇ");
15449 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15450
15451 // Moving cursor to the left dismisses menu.
15452 cx.update_editor(|editor, window, cx| {
15453 editor.move_left(&MoveLeft, window, cx);
15454 });
15455 cx.run_until_parked();
15456 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15457 cx.assert_editor_state("obj.aˇb");
15458 cx.update_editor(|editor, _, _| {
15459 assert_eq!(editor.context_menu_visible(), false);
15460 });
15461
15462 // Type "b" - new request
15463 cx.simulate_keystroke("b");
15464 let is_incomplete = false;
15465 handle_completion_request(
15466 "obj.<ab|>a",
15467 vec!["ab", "abc"],
15468 is_incomplete,
15469 counter.clone(),
15470 &mut cx,
15471 )
15472 .await;
15473 cx.run_until_parked();
15474 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15475 cx.assert_editor_state("obj.abˇb");
15476 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15477
15478 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
15479 cx.update_editor(|editor, window, cx| {
15480 editor.backspace(&Backspace, window, cx);
15481 });
15482 let is_incomplete = false;
15483 handle_completion_request(
15484 "obj.<a|>b",
15485 vec!["a", "ab", "abc"],
15486 is_incomplete,
15487 counter.clone(),
15488 &mut cx,
15489 )
15490 .await;
15491 cx.run_until_parked();
15492 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15493 cx.assert_editor_state("obj.aˇb");
15494 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15495
15496 // Backspace to delete "a" - dismisses menu.
15497 cx.update_editor(|editor, window, cx| {
15498 editor.backspace(&Backspace, window, cx);
15499 });
15500 cx.run_until_parked();
15501 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15502 cx.assert_editor_state("obj.ˇb");
15503 cx.update_editor(|editor, _, _| {
15504 assert_eq!(editor.context_menu_visible(), false);
15505 });
15506}
15507
15508#[gpui::test]
15509async fn test_word_completion(cx: &mut TestAppContext) {
15510 let lsp_fetch_timeout_ms = 10;
15511 init_test(cx, |language_settings| {
15512 language_settings.defaults.completions = Some(CompletionSettingsContent {
15513 words_min_length: Some(0),
15514 lsp_fetch_timeout_ms: Some(10),
15515 lsp_insert_mode: Some(LspInsertMode::Insert),
15516 ..Default::default()
15517 });
15518 });
15519
15520 let mut cx = EditorLspTestContext::new_rust(
15521 lsp::ServerCapabilities {
15522 completion_provider: Some(lsp::CompletionOptions {
15523 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15524 ..lsp::CompletionOptions::default()
15525 }),
15526 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15527 ..lsp::ServerCapabilities::default()
15528 },
15529 cx,
15530 )
15531 .await;
15532
15533 let throttle_completions = Arc::new(AtomicBool::new(false));
15534
15535 let lsp_throttle_completions = throttle_completions.clone();
15536 let _completion_requests_handler =
15537 cx.lsp
15538 .server
15539 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
15540 let lsp_throttle_completions = lsp_throttle_completions.clone();
15541 let cx = cx.clone();
15542 async move {
15543 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
15544 cx.background_executor()
15545 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
15546 .await;
15547 }
15548 Ok(Some(lsp::CompletionResponse::Array(vec![
15549 lsp::CompletionItem {
15550 label: "first".into(),
15551 ..lsp::CompletionItem::default()
15552 },
15553 lsp::CompletionItem {
15554 label: "last".into(),
15555 ..lsp::CompletionItem::default()
15556 },
15557 ])))
15558 }
15559 });
15560
15561 cx.set_state(indoc! {"
15562 oneˇ
15563 two
15564 three
15565 "});
15566 cx.simulate_keystroke(".");
15567 cx.executor().run_until_parked();
15568 cx.condition(|editor, _| editor.context_menu_visible())
15569 .await;
15570 cx.update_editor(|editor, window, cx| {
15571 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15572 {
15573 assert_eq!(
15574 completion_menu_entries(menu),
15575 &["first", "last"],
15576 "When LSP server is fast to reply, no fallback word completions are used"
15577 );
15578 } else {
15579 panic!("expected completion menu to be open");
15580 }
15581 editor.cancel(&Cancel, window, cx);
15582 });
15583 cx.executor().run_until_parked();
15584 cx.condition(|editor, _| !editor.context_menu_visible())
15585 .await;
15586
15587 throttle_completions.store(true, atomic::Ordering::Release);
15588 cx.simulate_keystroke(".");
15589 cx.executor()
15590 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
15591 cx.executor().run_until_parked();
15592 cx.condition(|editor, _| editor.context_menu_visible())
15593 .await;
15594 cx.update_editor(|editor, _, _| {
15595 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15596 {
15597 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
15598 "When LSP server is slow, document words can be shown instead, if configured accordingly");
15599 } else {
15600 panic!("expected completion menu to be open");
15601 }
15602 });
15603}
15604
15605#[gpui::test]
15606async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
15607 init_test(cx, |language_settings| {
15608 language_settings.defaults.completions = Some(CompletionSettingsContent {
15609 words: Some(WordsCompletionMode::Enabled),
15610 words_min_length: Some(0),
15611 lsp_insert_mode: Some(LspInsertMode::Insert),
15612 ..Default::default()
15613 });
15614 });
15615
15616 let mut cx = EditorLspTestContext::new_rust(
15617 lsp::ServerCapabilities {
15618 completion_provider: Some(lsp::CompletionOptions {
15619 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15620 ..lsp::CompletionOptions::default()
15621 }),
15622 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15623 ..lsp::ServerCapabilities::default()
15624 },
15625 cx,
15626 )
15627 .await;
15628
15629 let _completion_requests_handler =
15630 cx.lsp
15631 .server
15632 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15633 Ok(Some(lsp::CompletionResponse::Array(vec![
15634 lsp::CompletionItem {
15635 label: "first".into(),
15636 ..lsp::CompletionItem::default()
15637 },
15638 lsp::CompletionItem {
15639 label: "last".into(),
15640 ..lsp::CompletionItem::default()
15641 },
15642 ])))
15643 });
15644
15645 cx.set_state(indoc! {"ˇ
15646 first
15647 last
15648 second
15649 "});
15650 cx.simulate_keystroke(".");
15651 cx.executor().run_until_parked();
15652 cx.condition(|editor, _| editor.context_menu_visible())
15653 .await;
15654 cx.update_editor(|editor, _, _| {
15655 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15656 {
15657 assert_eq!(
15658 completion_menu_entries(menu),
15659 &["first", "last", "second"],
15660 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15661 );
15662 } else {
15663 panic!("expected completion menu to be open");
15664 }
15665 });
15666}
15667
15668#[gpui::test]
15669async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15670 init_test(cx, |language_settings| {
15671 language_settings.defaults.completions = Some(CompletionSettingsContent {
15672 words: Some(WordsCompletionMode::Disabled),
15673 words_min_length: Some(0),
15674 lsp_insert_mode: Some(LspInsertMode::Insert),
15675 ..Default::default()
15676 });
15677 });
15678
15679 let mut cx = EditorLspTestContext::new_rust(
15680 lsp::ServerCapabilities {
15681 completion_provider: Some(lsp::CompletionOptions {
15682 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15683 ..lsp::CompletionOptions::default()
15684 }),
15685 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15686 ..lsp::ServerCapabilities::default()
15687 },
15688 cx,
15689 )
15690 .await;
15691
15692 let _completion_requests_handler =
15693 cx.lsp
15694 .server
15695 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15696 panic!("LSP completions should not be queried when dealing with word completions")
15697 });
15698
15699 cx.set_state(indoc! {"ˇ
15700 first
15701 last
15702 second
15703 "});
15704 cx.update_editor(|editor, window, cx| {
15705 editor.show_word_completions(&ShowWordCompletions, window, cx);
15706 });
15707 cx.executor().run_until_parked();
15708 cx.condition(|editor, _| editor.context_menu_visible())
15709 .await;
15710 cx.update_editor(|editor, _, _| {
15711 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15712 {
15713 assert_eq!(
15714 completion_menu_entries(menu),
15715 &["first", "last", "second"],
15716 "`ShowWordCompletions` action should show word completions"
15717 );
15718 } else {
15719 panic!("expected completion menu to be open");
15720 }
15721 });
15722
15723 cx.simulate_keystroke("l");
15724 cx.executor().run_until_parked();
15725 cx.condition(|editor, _| editor.context_menu_visible())
15726 .await;
15727 cx.update_editor(|editor, _, _| {
15728 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15729 {
15730 assert_eq!(
15731 completion_menu_entries(menu),
15732 &["last"],
15733 "After showing word completions, further editing should filter them and not query the LSP"
15734 );
15735 } else {
15736 panic!("expected completion menu to be open");
15737 }
15738 });
15739}
15740
15741#[gpui::test]
15742async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15743 init_test(cx, |language_settings| {
15744 language_settings.defaults.completions = Some(CompletionSettingsContent {
15745 words_min_length: Some(0),
15746 lsp: Some(false),
15747 lsp_insert_mode: Some(LspInsertMode::Insert),
15748 ..Default::default()
15749 });
15750 });
15751
15752 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15753
15754 cx.set_state(indoc! {"ˇ
15755 0_usize
15756 let
15757 33
15758 4.5f32
15759 "});
15760 cx.update_editor(|editor, window, cx| {
15761 editor.show_completions(&ShowCompletions, window, cx);
15762 });
15763 cx.executor().run_until_parked();
15764 cx.condition(|editor, _| editor.context_menu_visible())
15765 .await;
15766 cx.update_editor(|editor, window, cx| {
15767 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15768 {
15769 assert_eq!(
15770 completion_menu_entries(menu),
15771 &["let"],
15772 "With no digits in the completion query, no digits should be in the word completions"
15773 );
15774 } else {
15775 panic!("expected completion menu to be open");
15776 }
15777 editor.cancel(&Cancel, window, cx);
15778 });
15779
15780 cx.set_state(indoc! {"3ˇ
15781 0_usize
15782 let
15783 3
15784 33.35f32
15785 "});
15786 cx.update_editor(|editor, window, cx| {
15787 editor.show_completions(&ShowCompletions, window, cx);
15788 });
15789 cx.executor().run_until_parked();
15790 cx.condition(|editor, _| editor.context_menu_visible())
15791 .await;
15792 cx.update_editor(|editor, _, _| {
15793 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15794 {
15795 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15796 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15797 } else {
15798 panic!("expected completion menu to be open");
15799 }
15800 });
15801}
15802
15803#[gpui::test]
15804async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15805 init_test(cx, |language_settings| {
15806 language_settings.defaults.completions = Some(CompletionSettingsContent {
15807 words: Some(WordsCompletionMode::Enabled),
15808 words_min_length: Some(3),
15809 lsp_insert_mode: Some(LspInsertMode::Insert),
15810 ..Default::default()
15811 });
15812 });
15813
15814 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15815 cx.set_state(indoc! {"ˇ
15816 wow
15817 wowen
15818 wowser
15819 "});
15820 cx.simulate_keystroke("w");
15821 cx.executor().run_until_parked();
15822 cx.update_editor(|editor, _, _| {
15823 if editor.context_menu.borrow_mut().is_some() {
15824 panic!(
15825 "expected completion menu to be hidden, as words completion threshold is not met"
15826 );
15827 }
15828 });
15829
15830 cx.update_editor(|editor, window, cx| {
15831 editor.show_word_completions(&ShowWordCompletions, window, cx);
15832 });
15833 cx.executor().run_until_parked();
15834 cx.update_editor(|editor, window, cx| {
15835 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15836 {
15837 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");
15838 } else {
15839 panic!("expected completion menu to be open after the word completions are called with an action");
15840 }
15841
15842 editor.cancel(&Cancel, window, cx);
15843 });
15844 cx.update_editor(|editor, _, _| {
15845 if editor.context_menu.borrow_mut().is_some() {
15846 panic!("expected completion menu to be hidden after canceling");
15847 }
15848 });
15849
15850 cx.simulate_keystroke("o");
15851 cx.executor().run_until_parked();
15852 cx.update_editor(|editor, _, _| {
15853 if editor.context_menu.borrow_mut().is_some() {
15854 panic!(
15855 "expected completion menu to be hidden, as words completion threshold is not met still"
15856 );
15857 }
15858 });
15859
15860 cx.simulate_keystroke("w");
15861 cx.executor().run_until_parked();
15862 cx.update_editor(|editor, _, _| {
15863 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15864 {
15865 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15866 } else {
15867 panic!("expected completion menu to be open after the word completions threshold is met");
15868 }
15869 });
15870}
15871
15872#[gpui::test]
15873async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15874 init_test(cx, |language_settings| {
15875 language_settings.defaults.completions = Some(CompletionSettingsContent {
15876 words: Some(WordsCompletionMode::Enabled),
15877 words_min_length: Some(0),
15878 lsp_insert_mode: Some(LspInsertMode::Insert),
15879 ..Default::default()
15880 });
15881 });
15882
15883 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15884 cx.update_editor(|editor, _, _| {
15885 editor.disable_word_completions();
15886 });
15887 cx.set_state(indoc! {"ˇ
15888 wow
15889 wowen
15890 wowser
15891 "});
15892 cx.simulate_keystroke("w");
15893 cx.executor().run_until_parked();
15894 cx.update_editor(|editor, _, _| {
15895 if editor.context_menu.borrow_mut().is_some() {
15896 panic!(
15897 "expected completion menu to be hidden, as words completion are disabled for this editor"
15898 );
15899 }
15900 });
15901
15902 cx.update_editor(|editor, window, cx| {
15903 editor.show_word_completions(&ShowWordCompletions, window, cx);
15904 });
15905 cx.executor().run_until_parked();
15906 cx.update_editor(|editor, _, _| {
15907 if editor.context_menu.borrow_mut().is_some() {
15908 panic!(
15909 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15910 );
15911 }
15912 });
15913}
15914
15915#[gpui::test]
15916async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15917 init_test(cx, |language_settings| {
15918 language_settings.defaults.completions = Some(CompletionSettingsContent {
15919 words: Some(WordsCompletionMode::Disabled),
15920 words_min_length: Some(0),
15921 lsp_insert_mode: Some(LspInsertMode::Insert),
15922 ..Default::default()
15923 });
15924 });
15925
15926 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15927 cx.update_editor(|editor, _, _| {
15928 editor.set_completion_provider(None);
15929 });
15930 cx.set_state(indoc! {"ˇ
15931 wow
15932 wowen
15933 wowser
15934 "});
15935 cx.simulate_keystroke("w");
15936 cx.executor().run_until_parked();
15937 cx.update_editor(|editor, _, _| {
15938 if editor.context_menu.borrow_mut().is_some() {
15939 panic!("expected completion menu to be hidden, as disabled in settings");
15940 }
15941 });
15942}
15943
15944fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15945 let position = || lsp::Position {
15946 line: params.text_document_position.position.line,
15947 character: params.text_document_position.position.character,
15948 };
15949 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15950 range: lsp::Range {
15951 start: position(),
15952 end: position(),
15953 },
15954 new_text: text.to_string(),
15955 }))
15956}
15957
15958#[gpui::test]
15959async fn test_multiline_completion(cx: &mut TestAppContext) {
15960 init_test(cx, |_| {});
15961
15962 let fs = FakeFs::new(cx.executor());
15963 fs.insert_tree(
15964 path!("/a"),
15965 json!({
15966 "main.ts": "a",
15967 }),
15968 )
15969 .await;
15970
15971 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15972 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15973 let typescript_language = Arc::new(Language::new(
15974 LanguageConfig {
15975 name: "TypeScript".into(),
15976 matcher: LanguageMatcher {
15977 path_suffixes: vec!["ts".to_string()],
15978 ..LanguageMatcher::default()
15979 },
15980 line_comments: vec!["// ".into()],
15981 ..LanguageConfig::default()
15982 },
15983 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15984 ));
15985 language_registry.add(typescript_language.clone());
15986 let mut fake_servers = language_registry.register_fake_lsp(
15987 "TypeScript",
15988 FakeLspAdapter {
15989 capabilities: lsp::ServerCapabilities {
15990 completion_provider: Some(lsp::CompletionOptions {
15991 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15992 ..lsp::CompletionOptions::default()
15993 }),
15994 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15995 ..lsp::ServerCapabilities::default()
15996 },
15997 // Emulate vtsls label generation
15998 label_for_completion: Some(Box::new(|item, _| {
15999 let text = if let Some(description) = item
16000 .label_details
16001 .as_ref()
16002 .and_then(|label_details| label_details.description.as_ref())
16003 {
16004 format!("{} {}", item.label, description)
16005 } else if let Some(detail) = &item.detail {
16006 format!("{} {}", item.label, detail)
16007 } else {
16008 item.label.clone()
16009 };
16010 Some(language::CodeLabel::plain(text, None))
16011 })),
16012 ..FakeLspAdapter::default()
16013 },
16014 );
16015 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16016 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16017 let worktree_id = workspace
16018 .update(cx, |workspace, _window, cx| {
16019 workspace.project().update(cx, |project, cx| {
16020 project.worktrees(cx).next().unwrap().read(cx).id()
16021 })
16022 })
16023 .unwrap();
16024 let _buffer = project
16025 .update(cx, |project, cx| {
16026 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
16027 })
16028 .await
16029 .unwrap();
16030 let editor = workspace
16031 .update(cx, |workspace, window, cx| {
16032 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
16033 })
16034 .unwrap()
16035 .await
16036 .unwrap()
16037 .downcast::<Editor>()
16038 .unwrap();
16039 let fake_server = fake_servers.next().await.unwrap();
16040 cx.run_until_parked();
16041
16042 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
16043 let multiline_label_2 = "a\nb\nc\n";
16044 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
16045 let multiline_description = "d\ne\nf\n";
16046 let multiline_detail_2 = "g\nh\ni\n";
16047
16048 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
16049 move |params, _| async move {
16050 Ok(Some(lsp::CompletionResponse::Array(vec![
16051 lsp::CompletionItem {
16052 label: multiline_label.to_string(),
16053 text_edit: gen_text_edit(¶ms, "new_text_1"),
16054 ..lsp::CompletionItem::default()
16055 },
16056 lsp::CompletionItem {
16057 label: "single line label 1".to_string(),
16058 detail: Some(multiline_detail.to_string()),
16059 text_edit: gen_text_edit(¶ms, "new_text_2"),
16060 ..lsp::CompletionItem::default()
16061 },
16062 lsp::CompletionItem {
16063 label: "single line label 2".to_string(),
16064 label_details: Some(lsp::CompletionItemLabelDetails {
16065 description: Some(multiline_description.to_string()),
16066 detail: None,
16067 }),
16068 text_edit: gen_text_edit(¶ms, "new_text_2"),
16069 ..lsp::CompletionItem::default()
16070 },
16071 lsp::CompletionItem {
16072 label: multiline_label_2.to_string(),
16073 detail: Some(multiline_detail_2.to_string()),
16074 text_edit: gen_text_edit(¶ms, "new_text_3"),
16075 ..lsp::CompletionItem::default()
16076 },
16077 lsp::CompletionItem {
16078 label: "Label with many spaces and \t but without newlines".to_string(),
16079 detail: Some(
16080 "Details with many spaces and \t but without newlines".to_string(),
16081 ),
16082 text_edit: gen_text_edit(¶ms, "new_text_4"),
16083 ..lsp::CompletionItem::default()
16084 },
16085 ])))
16086 },
16087 );
16088
16089 editor.update_in(cx, |editor, window, cx| {
16090 cx.focus_self(window);
16091 editor.move_to_end(&MoveToEnd, window, cx);
16092 editor.handle_input(".", window, cx);
16093 });
16094 cx.run_until_parked();
16095 completion_handle.next().await.unwrap();
16096
16097 editor.update(cx, |editor, _| {
16098 assert!(editor.context_menu_visible());
16099 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16100 {
16101 let completion_labels = menu
16102 .completions
16103 .borrow()
16104 .iter()
16105 .map(|c| c.label.text.clone())
16106 .collect::<Vec<_>>();
16107 assert_eq!(
16108 completion_labels,
16109 &[
16110 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
16111 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
16112 "single line label 2 d e f ",
16113 "a b c g h i ",
16114 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
16115 ],
16116 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
16117 );
16118
16119 for completion in menu
16120 .completions
16121 .borrow()
16122 .iter() {
16123 assert_eq!(
16124 completion.label.filter_range,
16125 0..completion.label.text.len(),
16126 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
16127 );
16128 }
16129 } else {
16130 panic!("expected completion menu to be open");
16131 }
16132 });
16133}
16134
16135#[gpui::test]
16136async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
16137 init_test(cx, |_| {});
16138 let mut cx = EditorLspTestContext::new_rust(
16139 lsp::ServerCapabilities {
16140 completion_provider: Some(lsp::CompletionOptions {
16141 trigger_characters: Some(vec![".".to_string()]),
16142 ..Default::default()
16143 }),
16144 ..Default::default()
16145 },
16146 cx,
16147 )
16148 .await;
16149 cx.lsp
16150 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16151 Ok(Some(lsp::CompletionResponse::Array(vec![
16152 lsp::CompletionItem {
16153 label: "first".into(),
16154 ..Default::default()
16155 },
16156 lsp::CompletionItem {
16157 label: "last".into(),
16158 ..Default::default()
16159 },
16160 ])))
16161 });
16162 cx.set_state("variableˇ");
16163 cx.simulate_keystroke(".");
16164 cx.executor().run_until_parked();
16165
16166 cx.update_editor(|editor, _, _| {
16167 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16168 {
16169 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
16170 } else {
16171 panic!("expected completion menu to be open");
16172 }
16173 });
16174
16175 cx.update_editor(|editor, window, cx| {
16176 editor.move_page_down(&MovePageDown::default(), window, cx);
16177 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16178 {
16179 assert!(
16180 menu.selected_item == 1,
16181 "expected PageDown to select the last item from the context menu"
16182 );
16183 } else {
16184 panic!("expected completion menu to stay open after PageDown");
16185 }
16186 });
16187
16188 cx.update_editor(|editor, window, cx| {
16189 editor.move_page_up(&MovePageUp::default(), window, cx);
16190 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16191 {
16192 assert!(
16193 menu.selected_item == 0,
16194 "expected PageUp to select the first item from the context menu"
16195 );
16196 } else {
16197 panic!("expected completion menu to stay open after PageUp");
16198 }
16199 });
16200}
16201
16202#[gpui::test]
16203async fn test_as_is_completions(cx: &mut TestAppContext) {
16204 init_test(cx, |_| {});
16205 let mut cx = EditorLspTestContext::new_rust(
16206 lsp::ServerCapabilities {
16207 completion_provider: Some(lsp::CompletionOptions {
16208 ..Default::default()
16209 }),
16210 ..Default::default()
16211 },
16212 cx,
16213 )
16214 .await;
16215 cx.lsp
16216 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16217 Ok(Some(lsp::CompletionResponse::Array(vec![
16218 lsp::CompletionItem {
16219 label: "unsafe".into(),
16220 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16221 range: lsp::Range {
16222 start: lsp::Position {
16223 line: 1,
16224 character: 2,
16225 },
16226 end: lsp::Position {
16227 line: 1,
16228 character: 3,
16229 },
16230 },
16231 new_text: "unsafe".to_string(),
16232 })),
16233 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
16234 ..Default::default()
16235 },
16236 ])))
16237 });
16238 cx.set_state("fn a() {}\n nˇ");
16239 cx.executor().run_until_parked();
16240 cx.update_editor(|editor, window, cx| {
16241 editor.trigger_completion_on_input("n", true, window, cx)
16242 });
16243 cx.executor().run_until_parked();
16244
16245 cx.update_editor(|editor, window, cx| {
16246 editor.confirm_completion(&Default::default(), window, cx)
16247 });
16248 cx.executor().run_until_parked();
16249 cx.assert_editor_state("fn a() {}\n unsafeˇ");
16250}
16251
16252#[gpui::test]
16253async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
16254 init_test(cx, |_| {});
16255 let language =
16256 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
16257 let mut cx = EditorLspTestContext::new(
16258 language,
16259 lsp::ServerCapabilities {
16260 completion_provider: Some(lsp::CompletionOptions {
16261 ..lsp::CompletionOptions::default()
16262 }),
16263 ..lsp::ServerCapabilities::default()
16264 },
16265 cx,
16266 )
16267 .await;
16268
16269 cx.set_state(
16270 "#ifndef BAR_H
16271#define BAR_H
16272
16273#include <stdbool.h>
16274
16275int fn_branch(bool do_branch1, bool do_branch2);
16276
16277#endif // BAR_H
16278ˇ",
16279 );
16280 cx.executor().run_until_parked();
16281 cx.update_editor(|editor, window, cx| {
16282 editor.handle_input("#", window, cx);
16283 });
16284 cx.executor().run_until_parked();
16285 cx.update_editor(|editor, window, cx| {
16286 editor.handle_input("i", window, cx);
16287 });
16288 cx.executor().run_until_parked();
16289 cx.update_editor(|editor, window, cx| {
16290 editor.handle_input("n", window, cx);
16291 });
16292 cx.executor().run_until_parked();
16293 cx.assert_editor_state(
16294 "#ifndef BAR_H
16295#define BAR_H
16296
16297#include <stdbool.h>
16298
16299int fn_branch(bool do_branch1, bool do_branch2);
16300
16301#endif // BAR_H
16302#inˇ",
16303 );
16304
16305 cx.lsp
16306 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16307 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16308 is_incomplete: false,
16309 item_defaults: None,
16310 items: vec![lsp::CompletionItem {
16311 kind: Some(lsp::CompletionItemKind::SNIPPET),
16312 label_details: Some(lsp::CompletionItemLabelDetails {
16313 detail: Some("header".to_string()),
16314 description: None,
16315 }),
16316 label: " include".to_string(),
16317 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16318 range: lsp::Range {
16319 start: lsp::Position {
16320 line: 8,
16321 character: 1,
16322 },
16323 end: lsp::Position {
16324 line: 8,
16325 character: 1,
16326 },
16327 },
16328 new_text: "include \"$0\"".to_string(),
16329 })),
16330 sort_text: Some("40b67681include".to_string()),
16331 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16332 filter_text: Some("include".to_string()),
16333 insert_text: Some("include \"$0\"".to_string()),
16334 ..lsp::CompletionItem::default()
16335 }],
16336 })))
16337 });
16338 cx.update_editor(|editor, window, cx| {
16339 editor.show_completions(&ShowCompletions, window, cx);
16340 });
16341 cx.executor().run_until_parked();
16342 cx.update_editor(|editor, window, cx| {
16343 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16344 });
16345 cx.executor().run_until_parked();
16346 cx.assert_editor_state(
16347 "#ifndef BAR_H
16348#define BAR_H
16349
16350#include <stdbool.h>
16351
16352int fn_branch(bool do_branch1, bool do_branch2);
16353
16354#endif // BAR_H
16355#include \"ˇ\"",
16356 );
16357
16358 cx.lsp
16359 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16360 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16361 is_incomplete: true,
16362 item_defaults: None,
16363 items: vec![lsp::CompletionItem {
16364 kind: Some(lsp::CompletionItemKind::FILE),
16365 label: "AGL/".to_string(),
16366 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16367 range: lsp::Range {
16368 start: lsp::Position {
16369 line: 8,
16370 character: 10,
16371 },
16372 end: lsp::Position {
16373 line: 8,
16374 character: 11,
16375 },
16376 },
16377 new_text: "AGL/".to_string(),
16378 })),
16379 sort_text: Some("40b67681AGL/".to_string()),
16380 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16381 filter_text: Some("AGL/".to_string()),
16382 insert_text: Some("AGL/".to_string()),
16383 ..lsp::CompletionItem::default()
16384 }],
16385 })))
16386 });
16387 cx.update_editor(|editor, window, cx| {
16388 editor.show_completions(&ShowCompletions, window, cx);
16389 });
16390 cx.executor().run_until_parked();
16391 cx.update_editor(|editor, window, cx| {
16392 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16393 });
16394 cx.executor().run_until_parked();
16395 cx.assert_editor_state(
16396 r##"#ifndef BAR_H
16397#define BAR_H
16398
16399#include <stdbool.h>
16400
16401int fn_branch(bool do_branch1, bool do_branch2);
16402
16403#endif // BAR_H
16404#include "AGL/ˇ"##,
16405 );
16406
16407 cx.update_editor(|editor, window, cx| {
16408 editor.handle_input("\"", window, cx);
16409 });
16410 cx.executor().run_until_parked();
16411 cx.assert_editor_state(
16412 r##"#ifndef BAR_H
16413#define BAR_H
16414
16415#include <stdbool.h>
16416
16417int fn_branch(bool do_branch1, bool do_branch2);
16418
16419#endif // BAR_H
16420#include "AGL/"ˇ"##,
16421 );
16422}
16423
16424#[gpui::test]
16425async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
16426 init_test(cx, |_| {});
16427
16428 let mut cx = EditorLspTestContext::new_rust(
16429 lsp::ServerCapabilities {
16430 completion_provider: Some(lsp::CompletionOptions {
16431 trigger_characters: Some(vec![".".to_string()]),
16432 resolve_provider: Some(true),
16433 ..Default::default()
16434 }),
16435 ..Default::default()
16436 },
16437 cx,
16438 )
16439 .await;
16440
16441 cx.set_state("fn main() { let a = 2ˇ; }");
16442 cx.simulate_keystroke(".");
16443 let completion_item = lsp::CompletionItem {
16444 label: "Some".into(),
16445 kind: Some(lsp::CompletionItemKind::SNIPPET),
16446 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16447 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16448 kind: lsp::MarkupKind::Markdown,
16449 value: "```rust\nSome(2)\n```".to_string(),
16450 })),
16451 deprecated: Some(false),
16452 sort_text: Some("Some".to_string()),
16453 filter_text: Some("Some".to_string()),
16454 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16455 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16456 range: lsp::Range {
16457 start: lsp::Position {
16458 line: 0,
16459 character: 22,
16460 },
16461 end: lsp::Position {
16462 line: 0,
16463 character: 22,
16464 },
16465 },
16466 new_text: "Some(2)".to_string(),
16467 })),
16468 additional_text_edits: Some(vec![lsp::TextEdit {
16469 range: lsp::Range {
16470 start: lsp::Position {
16471 line: 0,
16472 character: 20,
16473 },
16474 end: lsp::Position {
16475 line: 0,
16476 character: 22,
16477 },
16478 },
16479 new_text: "".to_string(),
16480 }]),
16481 ..Default::default()
16482 };
16483
16484 let closure_completion_item = completion_item.clone();
16485 let counter = Arc::new(AtomicUsize::new(0));
16486 let counter_clone = counter.clone();
16487 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16488 let task_completion_item = closure_completion_item.clone();
16489 counter_clone.fetch_add(1, atomic::Ordering::Release);
16490 async move {
16491 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16492 is_incomplete: true,
16493 item_defaults: None,
16494 items: vec![task_completion_item],
16495 })))
16496 }
16497 });
16498
16499 cx.condition(|editor, _| editor.context_menu_visible())
16500 .await;
16501 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
16502 assert!(request.next().await.is_some());
16503 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16504
16505 cx.simulate_keystrokes("S o m");
16506 cx.condition(|editor, _| editor.context_menu_visible())
16507 .await;
16508 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
16509 assert!(request.next().await.is_some());
16510 assert!(request.next().await.is_some());
16511 assert!(request.next().await.is_some());
16512 request.close();
16513 assert!(request.next().await.is_none());
16514 assert_eq!(
16515 counter.load(atomic::Ordering::Acquire),
16516 4,
16517 "With the completions menu open, only one LSP request should happen per input"
16518 );
16519}
16520
16521#[gpui::test]
16522async fn test_toggle_comment(cx: &mut TestAppContext) {
16523 init_test(cx, |_| {});
16524 let mut cx = EditorTestContext::new(cx).await;
16525 let language = Arc::new(Language::new(
16526 LanguageConfig {
16527 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16528 ..Default::default()
16529 },
16530 Some(tree_sitter_rust::LANGUAGE.into()),
16531 ));
16532 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16533
16534 // If multiple selections intersect a line, the line is only toggled once.
16535 cx.set_state(indoc! {"
16536 fn a() {
16537 «//b();
16538 ˇ»// «c();
16539 //ˇ» d();
16540 }
16541 "});
16542
16543 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16544
16545 cx.assert_editor_state(indoc! {"
16546 fn a() {
16547 «b();
16548 ˇ»«c();
16549 ˇ» d();
16550 }
16551 "});
16552
16553 // The comment prefix is inserted at the same column for every line in a
16554 // selection.
16555 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16556
16557 cx.assert_editor_state(indoc! {"
16558 fn a() {
16559 // «b();
16560 ˇ»// «c();
16561 ˇ» // d();
16562 }
16563 "});
16564
16565 // If a selection ends at the beginning of a line, that line is not toggled.
16566 cx.set_selections_state(indoc! {"
16567 fn a() {
16568 // b();
16569 «// c();
16570 ˇ» // d();
16571 }
16572 "});
16573
16574 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16575
16576 cx.assert_editor_state(indoc! {"
16577 fn a() {
16578 // b();
16579 «c();
16580 ˇ» // d();
16581 }
16582 "});
16583
16584 // If a selection span a single line and is empty, the line is toggled.
16585 cx.set_state(indoc! {"
16586 fn a() {
16587 a();
16588 b();
16589 ˇ
16590 }
16591 "});
16592
16593 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16594
16595 cx.assert_editor_state(indoc! {"
16596 fn a() {
16597 a();
16598 b();
16599 //•ˇ
16600 }
16601 "});
16602
16603 // If a selection span multiple lines, empty lines are not toggled.
16604 cx.set_state(indoc! {"
16605 fn a() {
16606 «a();
16607
16608 c();ˇ»
16609 }
16610 "});
16611
16612 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16613
16614 cx.assert_editor_state(indoc! {"
16615 fn a() {
16616 // «a();
16617
16618 // c();ˇ»
16619 }
16620 "});
16621
16622 // If a selection includes multiple comment prefixes, all lines are uncommented.
16623 cx.set_state(indoc! {"
16624 fn a() {
16625 «// a();
16626 /// b();
16627 //! c();ˇ»
16628 }
16629 "});
16630
16631 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16632
16633 cx.assert_editor_state(indoc! {"
16634 fn a() {
16635 «a();
16636 b();
16637 c();ˇ»
16638 }
16639 "});
16640}
16641
16642#[gpui::test]
16643async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16644 init_test(cx, |_| {});
16645 let mut cx = EditorTestContext::new(cx).await;
16646 let language = Arc::new(Language::new(
16647 LanguageConfig {
16648 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16649 ..Default::default()
16650 },
16651 Some(tree_sitter_rust::LANGUAGE.into()),
16652 ));
16653 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16654
16655 let toggle_comments = &ToggleComments {
16656 advance_downwards: false,
16657 ignore_indent: true,
16658 };
16659
16660 // If multiple selections intersect a line, the line is only toggled once.
16661 cx.set_state(indoc! {"
16662 fn a() {
16663 // «b();
16664 // c();
16665 // ˇ» d();
16666 }
16667 "});
16668
16669 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16670
16671 cx.assert_editor_state(indoc! {"
16672 fn a() {
16673 «b();
16674 c();
16675 ˇ» d();
16676 }
16677 "});
16678
16679 // The comment prefix is inserted at the beginning of each line
16680 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16681
16682 cx.assert_editor_state(indoc! {"
16683 fn a() {
16684 // «b();
16685 // c();
16686 // ˇ» d();
16687 }
16688 "});
16689
16690 // If a selection ends at the beginning of a line, that line is not toggled.
16691 cx.set_selections_state(indoc! {"
16692 fn a() {
16693 // b();
16694 // «c();
16695 ˇ»// d();
16696 }
16697 "});
16698
16699 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16700
16701 cx.assert_editor_state(indoc! {"
16702 fn a() {
16703 // b();
16704 «c();
16705 ˇ»// d();
16706 }
16707 "});
16708
16709 // If a selection span a single line and is empty, the line is toggled.
16710 cx.set_state(indoc! {"
16711 fn a() {
16712 a();
16713 b();
16714 ˇ
16715 }
16716 "});
16717
16718 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16719
16720 cx.assert_editor_state(indoc! {"
16721 fn a() {
16722 a();
16723 b();
16724 //ˇ
16725 }
16726 "});
16727
16728 // If a selection span multiple lines, empty lines are not toggled.
16729 cx.set_state(indoc! {"
16730 fn a() {
16731 «a();
16732
16733 c();ˇ»
16734 }
16735 "});
16736
16737 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16738
16739 cx.assert_editor_state(indoc! {"
16740 fn a() {
16741 // «a();
16742
16743 // c();ˇ»
16744 }
16745 "});
16746
16747 // If a selection includes multiple comment prefixes, all lines are uncommented.
16748 cx.set_state(indoc! {"
16749 fn a() {
16750 // «a();
16751 /// b();
16752 //! c();ˇ»
16753 }
16754 "});
16755
16756 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16757
16758 cx.assert_editor_state(indoc! {"
16759 fn a() {
16760 «a();
16761 b();
16762 c();ˇ»
16763 }
16764 "});
16765}
16766
16767#[gpui::test]
16768async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16769 init_test(cx, |_| {});
16770
16771 let language = Arc::new(Language::new(
16772 LanguageConfig {
16773 line_comments: vec!["// ".into()],
16774 ..Default::default()
16775 },
16776 Some(tree_sitter_rust::LANGUAGE.into()),
16777 ));
16778
16779 let mut cx = EditorTestContext::new(cx).await;
16780
16781 cx.language_registry().add(language.clone());
16782 cx.update_buffer(|buffer, cx| {
16783 buffer.set_language(Some(language), cx);
16784 });
16785
16786 let toggle_comments = &ToggleComments {
16787 advance_downwards: true,
16788 ignore_indent: false,
16789 };
16790
16791 // Single cursor on one line -> advance
16792 // Cursor moves horizontally 3 characters as well on non-blank line
16793 cx.set_state(indoc!(
16794 "fn a() {
16795 ˇdog();
16796 cat();
16797 }"
16798 ));
16799 cx.update_editor(|editor, window, cx| {
16800 editor.toggle_comments(toggle_comments, window, cx);
16801 });
16802 cx.assert_editor_state(indoc!(
16803 "fn a() {
16804 // dog();
16805 catˇ();
16806 }"
16807 ));
16808
16809 // Single selection on one line -> don't advance
16810 cx.set_state(indoc!(
16811 "fn a() {
16812 «dog()ˇ»;
16813 cat();
16814 }"
16815 ));
16816 cx.update_editor(|editor, window, cx| {
16817 editor.toggle_comments(toggle_comments, window, cx);
16818 });
16819 cx.assert_editor_state(indoc!(
16820 "fn a() {
16821 // «dog()ˇ»;
16822 cat();
16823 }"
16824 ));
16825
16826 // Multiple cursors on one line -> advance
16827 cx.set_state(indoc!(
16828 "fn a() {
16829 ˇdˇog();
16830 cat();
16831 }"
16832 ));
16833 cx.update_editor(|editor, window, cx| {
16834 editor.toggle_comments(toggle_comments, window, cx);
16835 });
16836 cx.assert_editor_state(indoc!(
16837 "fn a() {
16838 // dog();
16839 catˇ(ˇ);
16840 }"
16841 ));
16842
16843 // Multiple cursors on one line, with selection -> don't advance
16844 cx.set_state(indoc!(
16845 "fn a() {
16846 ˇdˇog«()ˇ»;
16847 cat();
16848 }"
16849 ));
16850 cx.update_editor(|editor, window, cx| {
16851 editor.toggle_comments(toggle_comments, window, cx);
16852 });
16853 cx.assert_editor_state(indoc!(
16854 "fn a() {
16855 // ˇdˇog«()ˇ»;
16856 cat();
16857 }"
16858 ));
16859
16860 // Single cursor on one line -> advance
16861 // Cursor moves to column 0 on blank line
16862 cx.set_state(indoc!(
16863 "fn a() {
16864 ˇdog();
16865
16866 cat();
16867 }"
16868 ));
16869 cx.update_editor(|editor, window, cx| {
16870 editor.toggle_comments(toggle_comments, window, cx);
16871 });
16872 cx.assert_editor_state(indoc!(
16873 "fn a() {
16874 // dog();
16875 ˇ
16876 cat();
16877 }"
16878 ));
16879
16880 // Single cursor on one line -> advance
16881 // Cursor starts and ends at column 0
16882 cx.set_state(indoc!(
16883 "fn a() {
16884 ˇ dog();
16885 cat();
16886 }"
16887 ));
16888 cx.update_editor(|editor, window, cx| {
16889 editor.toggle_comments(toggle_comments, window, cx);
16890 });
16891 cx.assert_editor_state(indoc!(
16892 "fn a() {
16893 // dog();
16894 ˇ cat();
16895 }"
16896 ));
16897}
16898
16899#[gpui::test]
16900async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16901 init_test(cx, |_| {});
16902
16903 let mut cx = EditorTestContext::new(cx).await;
16904
16905 let html_language = Arc::new(
16906 Language::new(
16907 LanguageConfig {
16908 name: "HTML".into(),
16909 block_comment: Some(BlockCommentConfig {
16910 start: "<!-- ".into(),
16911 prefix: "".into(),
16912 end: " -->".into(),
16913 tab_size: 0,
16914 }),
16915 ..Default::default()
16916 },
16917 Some(tree_sitter_html::LANGUAGE.into()),
16918 )
16919 .with_injection_query(
16920 r#"
16921 (script_element
16922 (raw_text) @injection.content
16923 (#set! injection.language "javascript"))
16924 "#,
16925 )
16926 .unwrap(),
16927 );
16928
16929 let javascript_language = Arc::new(Language::new(
16930 LanguageConfig {
16931 name: "JavaScript".into(),
16932 line_comments: vec!["// ".into()],
16933 ..Default::default()
16934 },
16935 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16936 ));
16937
16938 cx.language_registry().add(html_language.clone());
16939 cx.language_registry().add(javascript_language);
16940 cx.update_buffer(|buffer, cx| {
16941 buffer.set_language(Some(html_language), cx);
16942 });
16943
16944 // Toggle comments for empty selections
16945 cx.set_state(
16946 &r#"
16947 <p>A</p>ˇ
16948 <p>B</p>ˇ
16949 <p>C</p>ˇ
16950 "#
16951 .unindent(),
16952 );
16953 cx.update_editor(|editor, window, cx| {
16954 editor.toggle_comments(&ToggleComments::default(), window, cx)
16955 });
16956 cx.assert_editor_state(
16957 &r#"
16958 <!-- <p>A</p>ˇ -->
16959 <!-- <p>B</p>ˇ -->
16960 <!-- <p>C</p>ˇ -->
16961 "#
16962 .unindent(),
16963 );
16964 cx.update_editor(|editor, window, cx| {
16965 editor.toggle_comments(&ToggleComments::default(), window, cx)
16966 });
16967 cx.assert_editor_state(
16968 &r#"
16969 <p>A</p>ˇ
16970 <p>B</p>ˇ
16971 <p>C</p>ˇ
16972 "#
16973 .unindent(),
16974 );
16975
16976 // Toggle comments for mixture of empty and non-empty selections, where
16977 // multiple selections occupy a given line.
16978 cx.set_state(
16979 &r#"
16980 <p>A«</p>
16981 <p>ˇ»B</p>ˇ
16982 <p>C«</p>
16983 <p>ˇ»D</p>ˇ
16984 "#
16985 .unindent(),
16986 );
16987
16988 cx.update_editor(|editor, window, cx| {
16989 editor.toggle_comments(&ToggleComments::default(), window, cx)
16990 });
16991 cx.assert_editor_state(
16992 &r#"
16993 <!-- <p>A«</p>
16994 <p>ˇ»B</p>ˇ -->
16995 <!-- <p>C«</p>
16996 <p>ˇ»D</p>ˇ -->
16997 "#
16998 .unindent(),
16999 );
17000 cx.update_editor(|editor, window, cx| {
17001 editor.toggle_comments(&ToggleComments::default(), window, cx)
17002 });
17003 cx.assert_editor_state(
17004 &r#"
17005 <p>A«</p>
17006 <p>ˇ»B</p>ˇ
17007 <p>C«</p>
17008 <p>ˇ»D</p>ˇ
17009 "#
17010 .unindent(),
17011 );
17012
17013 // Toggle comments when different languages are active for different
17014 // selections.
17015 cx.set_state(
17016 &r#"
17017 ˇ<script>
17018 ˇvar x = new Y();
17019 ˇ</script>
17020 "#
17021 .unindent(),
17022 );
17023 cx.executor().run_until_parked();
17024 cx.update_editor(|editor, window, cx| {
17025 editor.toggle_comments(&ToggleComments::default(), window, cx)
17026 });
17027 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
17028 // Uncommenting and commenting from this position brings in even more wrong artifacts.
17029 cx.assert_editor_state(
17030 &r#"
17031 <!-- ˇ<script> -->
17032 // ˇvar x = new Y();
17033 <!-- ˇ</script> -->
17034 "#
17035 .unindent(),
17036 );
17037}
17038
17039#[gpui::test]
17040fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
17041 init_test(cx, |_| {});
17042
17043 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17044 let multibuffer = cx.new(|cx| {
17045 let mut multibuffer = MultiBuffer::new(ReadWrite);
17046 multibuffer.push_excerpts(
17047 buffer.clone(),
17048 [
17049 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
17050 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
17051 ],
17052 cx,
17053 );
17054 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
17055 multibuffer
17056 });
17057
17058 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
17059 editor.update_in(cx, |editor, window, cx| {
17060 assert_eq!(editor.text(cx), "aaaa\nbbbb");
17061 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17062 s.select_ranges([
17063 Point::new(0, 0)..Point::new(0, 0),
17064 Point::new(1, 0)..Point::new(1, 0),
17065 ])
17066 });
17067
17068 editor.handle_input("X", window, cx);
17069 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
17070 assert_eq!(
17071 editor.selections.ranges(&editor.display_snapshot(cx)),
17072 [
17073 Point::new(0, 1)..Point::new(0, 1),
17074 Point::new(1, 1)..Point::new(1, 1),
17075 ]
17076 );
17077
17078 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
17079 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17080 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
17081 });
17082 editor.backspace(&Default::default(), window, cx);
17083 assert_eq!(editor.text(cx), "Xa\nbbb");
17084 assert_eq!(
17085 editor.selections.ranges(&editor.display_snapshot(cx)),
17086 [Point::new(1, 0)..Point::new(1, 0)]
17087 );
17088
17089 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17090 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
17091 });
17092 editor.backspace(&Default::default(), window, cx);
17093 assert_eq!(editor.text(cx), "X\nbb");
17094 assert_eq!(
17095 editor.selections.ranges(&editor.display_snapshot(cx)),
17096 [Point::new(0, 1)..Point::new(0, 1)]
17097 );
17098 });
17099}
17100
17101#[gpui::test]
17102fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
17103 init_test(cx, |_| {});
17104
17105 let markers = vec![('[', ']').into(), ('(', ')').into()];
17106 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
17107 indoc! {"
17108 [aaaa
17109 (bbbb]
17110 cccc)",
17111 },
17112 markers.clone(),
17113 );
17114 let excerpt_ranges = markers.into_iter().map(|marker| {
17115 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
17116 ExcerptRange::new(context)
17117 });
17118 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
17119 let multibuffer = cx.new(|cx| {
17120 let mut multibuffer = MultiBuffer::new(ReadWrite);
17121 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
17122 multibuffer
17123 });
17124
17125 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
17126 editor.update_in(cx, |editor, window, cx| {
17127 let (expected_text, selection_ranges) = marked_text_ranges(
17128 indoc! {"
17129 aaaa
17130 bˇbbb
17131 bˇbbˇb
17132 cccc"
17133 },
17134 true,
17135 );
17136 assert_eq!(editor.text(cx), expected_text);
17137 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17138 s.select_ranges(
17139 selection_ranges
17140 .iter()
17141 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
17142 )
17143 });
17144
17145 editor.handle_input("X", window, cx);
17146
17147 let (expected_text, expected_selections) = marked_text_ranges(
17148 indoc! {"
17149 aaaa
17150 bXˇbbXb
17151 bXˇbbXˇb
17152 cccc"
17153 },
17154 false,
17155 );
17156 assert_eq!(editor.text(cx), expected_text);
17157 assert_eq!(
17158 editor.selections.ranges(&editor.display_snapshot(cx)),
17159 expected_selections
17160 .iter()
17161 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
17162 .collect::<Vec<_>>()
17163 );
17164
17165 editor.newline(&Newline, window, cx);
17166 let (expected_text, expected_selections) = marked_text_ranges(
17167 indoc! {"
17168 aaaa
17169 bX
17170 ˇbbX
17171 b
17172 bX
17173 ˇbbX
17174 ˇb
17175 cccc"
17176 },
17177 false,
17178 );
17179 assert_eq!(editor.text(cx), expected_text);
17180 assert_eq!(
17181 editor.selections.ranges(&editor.display_snapshot(cx)),
17182 expected_selections
17183 .iter()
17184 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
17185 .collect::<Vec<_>>()
17186 );
17187 });
17188}
17189
17190#[gpui::test]
17191fn test_refresh_selections(cx: &mut TestAppContext) {
17192 init_test(cx, |_| {});
17193
17194 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17195 let mut excerpt1_id = None;
17196 let multibuffer = cx.new(|cx| {
17197 let mut multibuffer = MultiBuffer::new(ReadWrite);
17198 excerpt1_id = multibuffer
17199 .push_excerpts(
17200 buffer.clone(),
17201 [
17202 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17203 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17204 ],
17205 cx,
17206 )
17207 .into_iter()
17208 .next();
17209 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17210 multibuffer
17211 });
17212
17213 let editor = cx.add_window(|window, cx| {
17214 let mut editor = build_editor(multibuffer.clone(), window, cx);
17215 let snapshot = editor.snapshot(window, cx);
17216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17217 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
17218 });
17219 editor.begin_selection(
17220 Point::new(2, 1).to_display_point(&snapshot),
17221 true,
17222 1,
17223 window,
17224 cx,
17225 );
17226 assert_eq!(
17227 editor.selections.ranges(&editor.display_snapshot(cx)),
17228 [
17229 Point::new(1, 3)..Point::new(1, 3),
17230 Point::new(2, 1)..Point::new(2, 1),
17231 ]
17232 );
17233 editor
17234 });
17235
17236 // Refreshing selections is a no-op when excerpts haven't changed.
17237 _ = editor.update(cx, |editor, window, cx| {
17238 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17239 assert_eq!(
17240 editor.selections.ranges(&editor.display_snapshot(cx)),
17241 [
17242 Point::new(1, 3)..Point::new(1, 3),
17243 Point::new(2, 1)..Point::new(2, 1),
17244 ]
17245 );
17246 });
17247
17248 multibuffer.update(cx, |multibuffer, cx| {
17249 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17250 });
17251 _ = editor.update(cx, |editor, window, cx| {
17252 // Removing an excerpt causes the first selection to become degenerate.
17253 assert_eq!(
17254 editor.selections.ranges(&editor.display_snapshot(cx)),
17255 [
17256 Point::new(0, 0)..Point::new(0, 0),
17257 Point::new(0, 1)..Point::new(0, 1)
17258 ]
17259 );
17260
17261 // Refreshing selections will relocate the first selection to the original buffer
17262 // location.
17263 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17264 assert_eq!(
17265 editor.selections.ranges(&editor.display_snapshot(cx)),
17266 [
17267 Point::new(0, 1)..Point::new(0, 1),
17268 Point::new(0, 3)..Point::new(0, 3)
17269 ]
17270 );
17271 assert!(editor.selections.pending_anchor().is_some());
17272 });
17273}
17274
17275#[gpui::test]
17276fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
17277 init_test(cx, |_| {});
17278
17279 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17280 let mut excerpt1_id = None;
17281 let multibuffer = cx.new(|cx| {
17282 let mut multibuffer = MultiBuffer::new(ReadWrite);
17283 excerpt1_id = multibuffer
17284 .push_excerpts(
17285 buffer.clone(),
17286 [
17287 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17288 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17289 ],
17290 cx,
17291 )
17292 .into_iter()
17293 .next();
17294 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17295 multibuffer
17296 });
17297
17298 let editor = cx.add_window(|window, cx| {
17299 let mut editor = build_editor(multibuffer.clone(), window, cx);
17300 let snapshot = editor.snapshot(window, cx);
17301 editor.begin_selection(
17302 Point::new(1, 3).to_display_point(&snapshot),
17303 false,
17304 1,
17305 window,
17306 cx,
17307 );
17308 assert_eq!(
17309 editor.selections.ranges(&editor.display_snapshot(cx)),
17310 [Point::new(1, 3)..Point::new(1, 3)]
17311 );
17312 editor
17313 });
17314
17315 multibuffer.update(cx, |multibuffer, cx| {
17316 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17317 });
17318 _ = editor.update(cx, |editor, window, cx| {
17319 assert_eq!(
17320 editor.selections.ranges(&editor.display_snapshot(cx)),
17321 [Point::new(0, 0)..Point::new(0, 0)]
17322 );
17323
17324 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
17325 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17326 assert_eq!(
17327 editor.selections.ranges(&editor.display_snapshot(cx)),
17328 [Point::new(0, 3)..Point::new(0, 3)]
17329 );
17330 assert!(editor.selections.pending_anchor().is_some());
17331 });
17332}
17333
17334#[gpui::test]
17335async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
17336 init_test(cx, |_| {});
17337
17338 let language = Arc::new(
17339 Language::new(
17340 LanguageConfig {
17341 brackets: BracketPairConfig {
17342 pairs: vec![
17343 BracketPair {
17344 start: "{".to_string(),
17345 end: "}".to_string(),
17346 close: true,
17347 surround: true,
17348 newline: true,
17349 },
17350 BracketPair {
17351 start: "/* ".to_string(),
17352 end: " */".to_string(),
17353 close: true,
17354 surround: true,
17355 newline: true,
17356 },
17357 ],
17358 ..Default::default()
17359 },
17360 ..Default::default()
17361 },
17362 Some(tree_sitter_rust::LANGUAGE.into()),
17363 )
17364 .with_indents_query("")
17365 .unwrap(),
17366 );
17367
17368 let text = concat!(
17369 "{ }\n", //
17370 " x\n", //
17371 " /* */\n", //
17372 "x\n", //
17373 "{{} }\n", //
17374 );
17375
17376 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17377 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17378 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17379 editor
17380 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
17381 .await;
17382
17383 editor.update_in(cx, |editor, window, cx| {
17384 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17385 s.select_display_ranges([
17386 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
17387 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
17388 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
17389 ])
17390 });
17391 editor.newline(&Newline, window, cx);
17392
17393 assert_eq!(
17394 editor.buffer().read(cx).read(cx).text(),
17395 concat!(
17396 "{ \n", // Suppress rustfmt
17397 "\n", //
17398 "}\n", //
17399 " x\n", //
17400 " /* \n", //
17401 " \n", //
17402 " */\n", //
17403 "x\n", //
17404 "{{} \n", //
17405 "}\n", //
17406 )
17407 );
17408 });
17409}
17410
17411#[gpui::test]
17412fn test_highlighted_ranges(cx: &mut TestAppContext) {
17413 init_test(cx, |_| {});
17414
17415 let editor = cx.add_window(|window, cx| {
17416 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
17417 build_editor(buffer, window, cx)
17418 });
17419
17420 _ = editor.update(cx, |editor, window, cx| {
17421 struct Type1;
17422 struct Type2;
17423
17424 let buffer = editor.buffer.read(cx).snapshot(cx);
17425
17426 let anchor_range =
17427 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
17428
17429 editor.highlight_background::<Type1>(
17430 &[
17431 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
17432 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
17433 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
17434 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
17435 ],
17436 |_, _| Hsla::red(),
17437 cx,
17438 );
17439 editor.highlight_background::<Type2>(
17440 &[
17441 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
17442 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
17443 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
17444 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
17445 ],
17446 |_, _| Hsla::green(),
17447 cx,
17448 );
17449
17450 let snapshot = editor.snapshot(window, cx);
17451 let highlighted_ranges = editor.sorted_background_highlights_in_range(
17452 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
17453 &snapshot,
17454 cx.theme(),
17455 );
17456 assert_eq!(
17457 highlighted_ranges,
17458 &[
17459 (
17460 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
17461 Hsla::green(),
17462 ),
17463 (
17464 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
17465 Hsla::red(),
17466 ),
17467 (
17468 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
17469 Hsla::green(),
17470 ),
17471 (
17472 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17473 Hsla::red(),
17474 ),
17475 ]
17476 );
17477 assert_eq!(
17478 editor.sorted_background_highlights_in_range(
17479 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
17480 &snapshot,
17481 cx.theme(),
17482 ),
17483 &[(
17484 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17485 Hsla::red(),
17486 )]
17487 );
17488 });
17489}
17490
17491#[gpui::test]
17492async fn test_following(cx: &mut TestAppContext) {
17493 init_test(cx, |_| {});
17494
17495 let fs = FakeFs::new(cx.executor());
17496 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17497
17498 let buffer = project.update(cx, |project, cx| {
17499 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
17500 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
17501 });
17502 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17503 let follower = cx.update(|cx| {
17504 cx.open_window(
17505 WindowOptions {
17506 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
17507 gpui::Point::new(px(0.), px(0.)),
17508 gpui::Point::new(px(10.), px(80.)),
17509 ))),
17510 ..Default::default()
17511 },
17512 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
17513 )
17514 .unwrap()
17515 });
17516
17517 let is_still_following = Rc::new(RefCell::new(true));
17518 let follower_edit_event_count = Rc::new(RefCell::new(0));
17519 let pending_update = Rc::new(RefCell::new(None));
17520 let leader_entity = leader.root(cx).unwrap();
17521 let follower_entity = follower.root(cx).unwrap();
17522 _ = follower.update(cx, {
17523 let update = pending_update.clone();
17524 let is_still_following = is_still_following.clone();
17525 let follower_edit_event_count = follower_edit_event_count.clone();
17526 |_, window, cx| {
17527 cx.subscribe_in(
17528 &leader_entity,
17529 window,
17530 move |_, leader, event, window, cx| {
17531 leader.read(cx).add_event_to_update_proto(
17532 event,
17533 &mut update.borrow_mut(),
17534 window,
17535 cx,
17536 );
17537 },
17538 )
17539 .detach();
17540
17541 cx.subscribe_in(
17542 &follower_entity,
17543 window,
17544 move |_, _, event: &EditorEvent, _window, _cx| {
17545 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
17546 *is_still_following.borrow_mut() = false;
17547 }
17548
17549 if let EditorEvent::BufferEdited = event {
17550 *follower_edit_event_count.borrow_mut() += 1;
17551 }
17552 },
17553 )
17554 .detach();
17555 }
17556 });
17557
17558 // Update the selections only
17559 _ = leader.update(cx, |leader, window, cx| {
17560 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17561 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17562 });
17563 });
17564 follower
17565 .update(cx, |follower, window, cx| {
17566 follower.apply_update_proto(
17567 &project,
17568 pending_update.borrow_mut().take().unwrap(),
17569 window,
17570 cx,
17571 )
17572 })
17573 .unwrap()
17574 .await
17575 .unwrap();
17576 _ = follower.update(cx, |follower, _, cx| {
17577 assert_eq!(
17578 follower.selections.ranges(&follower.display_snapshot(cx)),
17579 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
17580 );
17581 });
17582 assert!(*is_still_following.borrow());
17583 assert_eq!(*follower_edit_event_count.borrow(), 0);
17584
17585 // Update the scroll position only
17586 _ = leader.update(cx, |leader, window, cx| {
17587 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17588 });
17589 follower
17590 .update(cx, |follower, window, cx| {
17591 follower.apply_update_proto(
17592 &project,
17593 pending_update.borrow_mut().take().unwrap(),
17594 window,
17595 cx,
17596 )
17597 })
17598 .unwrap()
17599 .await
17600 .unwrap();
17601 assert_eq!(
17602 follower
17603 .update(cx, |follower, _, cx| follower.scroll_position(cx))
17604 .unwrap(),
17605 gpui::Point::new(1.5, 3.5)
17606 );
17607 assert!(*is_still_following.borrow());
17608 assert_eq!(*follower_edit_event_count.borrow(), 0);
17609
17610 // Update the selections and scroll position. The follower's scroll position is updated
17611 // via autoscroll, not via the leader's exact scroll position.
17612 _ = leader.update(cx, |leader, window, cx| {
17613 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17614 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
17615 });
17616 leader.request_autoscroll(Autoscroll::newest(), cx);
17617 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17618 });
17619 follower
17620 .update(cx, |follower, window, cx| {
17621 follower.apply_update_proto(
17622 &project,
17623 pending_update.borrow_mut().take().unwrap(),
17624 window,
17625 cx,
17626 )
17627 })
17628 .unwrap()
17629 .await
17630 .unwrap();
17631 _ = follower.update(cx, |follower, _, cx| {
17632 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17633 assert_eq!(
17634 follower.selections.ranges(&follower.display_snapshot(cx)),
17635 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17636 );
17637 });
17638 assert!(*is_still_following.borrow());
17639
17640 // Creating a pending selection that precedes another selection
17641 _ = leader.update(cx, |leader, window, cx| {
17642 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17643 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17644 });
17645 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17646 });
17647 follower
17648 .update(cx, |follower, window, cx| {
17649 follower.apply_update_proto(
17650 &project,
17651 pending_update.borrow_mut().take().unwrap(),
17652 window,
17653 cx,
17654 )
17655 })
17656 .unwrap()
17657 .await
17658 .unwrap();
17659 _ = follower.update(cx, |follower, _, cx| {
17660 assert_eq!(
17661 follower.selections.ranges(&follower.display_snapshot(cx)),
17662 vec![
17663 MultiBufferOffset(0)..MultiBufferOffset(0),
17664 MultiBufferOffset(1)..MultiBufferOffset(1)
17665 ]
17666 );
17667 });
17668 assert!(*is_still_following.borrow());
17669
17670 // Extend the pending selection so that it surrounds another selection
17671 _ = leader.update(cx, |leader, window, cx| {
17672 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17673 });
17674 follower
17675 .update(cx, |follower, window, cx| {
17676 follower.apply_update_proto(
17677 &project,
17678 pending_update.borrow_mut().take().unwrap(),
17679 window,
17680 cx,
17681 )
17682 })
17683 .unwrap()
17684 .await
17685 .unwrap();
17686 _ = follower.update(cx, |follower, _, cx| {
17687 assert_eq!(
17688 follower.selections.ranges(&follower.display_snapshot(cx)),
17689 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17690 );
17691 });
17692
17693 // Scrolling locally breaks the follow
17694 _ = follower.update(cx, |follower, window, cx| {
17695 let top_anchor = follower
17696 .buffer()
17697 .read(cx)
17698 .read(cx)
17699 .anchor_after(MultiBufferOffset(0));
17700 follower.set_scroll_anchor(
17701 ScrollAnchor {
17702 anchor: top_anchor,
17703 offset: gpui::Point::new(0.0, 0.5),
17704 },
17705 window,
17706 cx,
17707 );
17708 });
17709 assert!(!(*is_still_following.borrow()));
17710}
17711
17712#[gpui::test]
17713async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17714 init_test(cx, |_| {});
17715
17716 let fs = FakeFs::new(cx.executor());
17717 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17718 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17719 let pane = workspace
17720 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17721 .unwrap();
17722
17723 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17724
17725 let leader = pane.update_in(cx, |_, window, cx| {
17726 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17727 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17728 });
17729
17730 // Start following the editor when it has no excerpts.
17731 let mut state_message =
17732 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17733 let workspace_entity = workspace.root(cx).unwrap();
17734 let follower_1 = cx
17735 .update_window(*workspace.deref(), |_, window, cx| {
17736 Editor::from_state_proto(
17737 workspace_entity,
17738 ViewId {
17739 creator: CollaboratorId::PeerId(PeerId::default()),
17740 id: 0,
17741 },
17742 &mut state_message,
17743 window,
17744 cx,
17745 )
17746 })
17747 .unwrap()
17748 .unwrap()
17749 .await
17750 .unwrap();
17751
17752 let update_message = Rc::new(RefCell::new(None));
17753 follower_1.update_in(cx, {
17754 let update = update_message.clone();
17755 |_, window, cx| {
17756 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17757 leader.read(cx).add_event_to_update_proto(
17758 event,
17759 &mut update.borrow_mut(),
17760 window,
17761 cx,
17762 );
17763 })
17764 .detach();
17765 }
17766 });
17767
17768 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17769 (
17770 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17771 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17772 )
17773 });
17774
17775 // Insert some excerpts.
17776 leader.update(cx, |leader, cx| {
17777 leader.buffer.update(cx, |multibuffer, cx| {
17778 multibuffer.set_excerpts_for_path(
17779 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17780 buffer_1.clone(),
17781 vec![
17782 Point::row_range(0..3),
17783 Point::row_range(1..6),
17784 Point::row_range(12..15),
17785 ],
17786 0,
17787 cx,
17788 );
17789 multibuffer.set_excerpts_for_path(
17790 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17791 buffer_2.clone(),
17792 vec![Point::row_range(0..6), Point::row_range(8..12)],
17793 0,
17794 cx,
17795 );
17796 });
17797 });
17798
17799 // Apply the update of adding the excerpts.
17800 follower_1
17801 .update_in(cx, |follower, window, cx| {
17802 follower.apply_update_proto(
17803 &project,
17804 update_message.borrow().clone().unwrap(),
17805 window,
17806 cx,
17807 )
17808 })
17809 .await
17810 .unwrap();
17811 assert_eq!(
17812 follower_1.update(cx, |editor, cx| editor.text(cx)),
17813 leader.update(cx, |editor, cx| editor.text(cx))
17814 );
17815 update_message.borrow_mut().take();
17816
17817 // Start following separately after it already has excerpts.
17818 let mut state_message =
17819 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17820 let workspace_entity = workspace.root(cx).unwrap();
17821 let follower_2 = cx
17822 .update_window(*workspace.deref(), |_, window, cx| {
17823 Editor::from_state_proto(
17824 workspace_entity,
17825 ViewId {
17826 creator: CollaboratorId::PeerId(PeerId::default()),
17827 id: 0,
17828 },
17829 &mut state_message,
17830 window,
17831 cx,
17832 )
17833 })
17834 .unwrap()
17835 .unwrap()
17836 .await
17837 .unwrap();
17838 assert_eq!(
17839 follower_2.update(cx, |editor, cx| editor.text(cx)),
17840 leader.update(cx, |editor, cx| editor.text(cx))
17841 );
17842
17843 // Remove some excerpts.
17844 leader.update(cx, |leader, cx| {
17845 leader.buffer.update(cx, |multibuffer, cx| {
17846 let excerpt_ids = multibuffer.excerpt_ids();
17847 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17848 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17849 });
17850 });
17851
17852 // Apply the update of removing the excerpts.
17853 follower_1
17854 .update_in(cx, |follower, window, cx| {
17855 follower.apply_update_proto(
17856 &project,
17857 update_message.borrow().clone().unwrap(),
17858 window,
17859 cx,
17860 )
17861 })
17862 .await
17863 .unwrap();
17864 follower_2
17865 .update_in(cx, |follower, window, cx| {
17866 follower.apply_update_proto(
17867 &project,
17868 update_message.borrow().clone().unwrap(),
17869 window,
17870 cx,
17871 )
17872 })
17873 .await
17874 .unwrap();
17875 update_message.borrow_mut().take();
17876 assert_eq!(
17877 follower_1.update(cx, |editor, cx| editor.text(cx)),
17878 leader.update(cx, |editor, cx| editor.text(cx))
17879 );
17880}
17881
17882#[gpui::test]
17883async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17884 init_test(cx, |_| {});
17885
17886 let mut cx = EditorTestContext::new(cx).await;
17887 let lsp_store =
17888 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17889
17890 cx.set_state(indoc! {"
17891 ˇfn func(abc def: i32) -> u32 {
17892 }
17893 "});
17894
17895 cx.update(|_, cx| {
17896 lsp_store.update(cx, |lsp_store, cx| {
17897 lsp_store
17898 .update_diagnostics(
17899 LanguageServerId(0),
17900 lsp::PublishDiagnosticsParams {
17901 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17902 version: None,
17903 diagnostics: vec![
17904 lsp::Diagnostic {
17905 range: lsp::Range::new(
17906 lsp::Position::new(0, 11),
17907 lsp::Position::new(0, 12),
17908 ),
17909 severity: Some(lsp::DiagnosticSeverity::ERROR),
17910 ..Default::default()
17911 },
17912 lsp::Diagnostic {
17913 range: lsp::Range::new(
17914 lsp::Position::new(0, 12),
17915 lsp::Position::new(0, 15),
17916 ),
17917 severity: Some(lsp::DiagnosticSeverity::ERROR),
17918 ..Default::default()
17919 },
17920 lsp::Diagnostic {
17921 range: lsp::Range::new(
17922 lsp::Position::new(0, 25),
17923 lsp::Position::new(0, 28),
17924 ),
17925 severity: Some(lsp::DiagnosticSeverity::ERROR),
17926 ..Default::default()
17927 },
17928 ],
17929 },
17930 None,
17931 DiagnosticSourceKind::Pushed,
17932 &[],
17933 cx,
17934 )
17935 .unwrap()
17936 });
17937 });
17938
17939 executor.run_until_parked();
17940
17941 cx.update_editor(|editor, window, cx| {
17942 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17943 });
17944
17945 cx.assert_editor_state(indoc! {"
17946 fn func(abc def: i32) -> ˇu32 {
17947 }
17948 "});
17949
17950 cx.update_editor(|editor, window, cx| {
17951 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17952 });
17953
17954 cx.assert_editor_state(indoc! {"
17955 fn func(abc ˇdef: i32) -> u32 {
17956 }
17957 "});
17958
17959 cx.update_editor(|editor, window, cx| {
17960 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17961 });
17962
17963 cx.assert_editor_state(indoc! {"
17964 fn func(abcˇ def: i32) -> u32 {
17965 }
17966 "});
17967
17968 cx.update_editor(|editor, window, cx| {
17969 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17970 });
17971
17972 cx.assert_editor_state(indoc! {"
17973 fn func(abc def: i32) -> ˇu32 {
17974 }
17975 "});
17976}
17977
17978#[gpui::test]
17979async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17980 init_test(cx, |_| {});
17981
17982 let mut cx = EditorTestContext::new(cx).await;
17983
17984 let diff_base = r#"
17985 use some::mod;
17986
17987 const A: u32 = 42;
17988
17989 fn main() {
17990 println!("hello");
17991
17992 println!("world");
17993 }
17994 "#
17995 .unindent();
17996
17997 // Edits are modified, removed, modified, added
17998 cx.set_state(
17999 &r#"
18000 use some::modified;
18001
18002 ˇ
18003 fn main() {
18004 println!("hello there");
18005
18006 println!("around the");
18007 println!("world");
18008 }
18009 "#
18010 .unindent(),
18011 );
18012
18013 cx.set_head_text(&diff_base);
18014 executor.run_until_parked();
18015
18016 cx.update_editor(|editor, window, cx| {
18017 //Wrap around the bottom of the buffer
18018 for _ in 0..3 {
18019 editor.go_to_next_hunk(&GoToHunk, window, cx);
18020 }
18021 });
18022
18023 cx.assert_editor_state(
18024 &r#"
18025 ˇuse some::modified;
18026
18027
18028 fn main() {
18029 println!("hello there");
18030
18031 println!("around the");
18032 println!("world");
18033 }
18034 "#
18035 .unindent(),
18036 );
18037
18038 cx.update_editor(|editor, window, cx| {
18039 //Wrap around the top of the buffer
18040 for _ in 0..2 {
18041 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18042 }
18043 });
18044
18045 cx.assert_editor_state(
18046 &r#"
18047 use some::modified;
18048
18049
18050 fn main() {
18051 ˇ println!("hello there");
18052
18053 println!("around the");
18054 println!("world");
18055 }
18056 "#
18057 .unindent(),
18058 );
18059
18060 cx.update_editor(|editor, window, cx| {
18061 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18062 });
18063
18064 cx.assert_editor_state(
18065 &r#"
18066 use some::modified;
18067
18068 ˇ
18069 fn main() {
18070 println!("hello there");
18071
18072 println!("around the");
18073 println!("world");
18074 }
18075 "#
18076 .unindent(),
18077 );
18078
18079 cx.update_editor(|editor, window, cx| {
18080 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18081 });
18082
18083 cx.assert_editor_state(
18084 &r#"
18085 ˇuse some::modified;
18086
18087
18088 fn main() {
18089 println!("hello there");
18090
18091 println!("around the");
18092 println!("world");
18093 }
18094 "#
18095 .unindent(),
18096 );
18097
18098 cx.update_editor(|editor, window, cx| {
18099 for _ in 0..2 {
18100 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18101 }
18102 });
18103
18104 cx.assert_editor_state(
18105 &r#"
18106 use some::modified;
18107
18108
18109 fn main() {
18110 ˇ println!("hello there");
18111
18112 println!("around the");
18113 println!("world");
18114 }
18115 "#
18116 .unindent(),
18117 );
18118
18119 cx.update_editor(|editor, window, cx| {
18120 editor.fold(&Fold, window, cx);
18121 });
18122
18123 cx.update_editor(|editor, window, cx| {
18124 editor.go_to_next_hunk(&GoToHunk, window, cx);
18125 });
18126
18127 cx.assert_editor_state(
18128 &r#"
18129 ˇuse some::modified;
18130
18131
18132 fn main() {
18133 println!("hello there");
18134
18135 println!("around the");
18136 println!("world");
18137 }
18138 "#
18139 .unindent(),
18140 );
18141}
18142
18143#[test]
18144fn test_split_words() {
18145 fn split(text: &str) -> Vec<&str> {
18146 split_words(text).collect()
18147 }
18148
18149 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
18150 assert_eq!(split("hello_world"), &["hello_", "world"]);
18151 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
18152 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
18153 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
18154 assert_eq!(split("helloworld"), &["helloworld"]);
18155
18156 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
18157}
18158
18159#[test]
18160fn test_split_words_for_snippet_prefix() {
18161 fn split(text: &str) -> Vec<&str> {
18162 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
18163 }
18164
18165 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
18166 assert_eq!(split("hello_world"), &["hello_world"]);
18167 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
18168 assert_eq!(split("Hello_World"), &["Hello_World"]);
18169 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
18170 assert_eq!(split("helloworld"), &["helloworld"]);
18171 assert_eq!(
18172 split("this@is!@#$^many . symbols"),
18173 &[
18174 "symbols",
18175 " symbols",
18176 ". symbols",
18177 " . symbols",
18178 " . symbols",
18179 " . symbols",
18180 "many . symbols",
18181 "^many . symbols",
18182 "$^many . symbols",
18183 "#$^many . symbols",
18184 "@#$^many . symbols",
18185 "!@#$^many . symbols",
18186 "is!@#$^many . symbols",
18187 "@is!@#$^many . symbols",
18188 "this@is!@#$^many . symbols",
18189 ],
18190 );
18191 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
18192}
18193
18194#[gpui::test]
18195async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
18196 init_test(cx, |_| {});
18197
18198 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
18199
18200 #[track_caller]
18201 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
18202 let _state_context = cx.set_state(before);
18203 cx.run_until_parked();
18204 cx.update_editor(|editor, window, cx| {
18205 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
18206 });
18207 cx.run_until_parked();
18208 cx.assert_editor_state(after);
18209 }
18210
18211 // Outside bracket jumps to outside of matching bracket
18212 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
18213 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
18214
18215 // Inside bracket jumps to inside of matching bracket
18216 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
18217 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
18218
18219 // When outside a bracket and inside, favor jumping to the inside bracket
18220 assert(
18221 "console.log('foo', [1, 2, 3]ˇ);",
18222 "console.log('foo', ˇ[1, 2, 3]);",
18223 &mut cx,
18224 );
18225 assert(
18226 "console.log(ˇ'foo', [1, 2, 3]);",
18227 "console.log('foo'ˇ, [1, 2, 3]);",
18228 &mut cx,
18229 );
18230
18231 // Bias forward if two options are equally likely
18232 assert(
18233 "let result = curried_fun()ˇ();",
18234 "let result = curried_fun()()ˇ;",
18235 &mut cx,
18236 );
18237
18238 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
18239 assert(
18240 indoc! {"
18241 function test() {
18242 console.log('test')ˇ
18243 }"},
18244 indoc! {"
18245 function test() {
18246 console.logˇ('test')
18247 }"},
18248 &mut cx,
18249 );
18250}
18251
18252#[gpui::test]
18253async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
18254 init_test(cx, |_| {});
18255 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
18256 language_registry.add(markdown_lang());
18257 language_registry.add(rust_lang());
18258 let buffer = cx.new(|cx| {
18259 let mut buffer = language::Buffer::local(
18260 indoc! {"
18261 ```rs
18262 impl Worktree {
18263 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18264 }
18265 }
18266 ```
18267 "},
18268 cx,
18269 );
18270 buffer.set_language_registry(language_registry.clone());
18271 buffer.set_language(Some(markdown_lang()), cx);
18272 buffer
18273 });
18274 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18275 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
18276 cx.executor().run_until_parked();
18277 _ = editor.update(cx, |editor, window, cx| {
18278 // Case 1: Test outer enclosing brackets
18279 select_ranges(
18280 editor,
18281 &indoc! {"
18282 ```rs
18283 impl Worktree {
18284 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18285 }
18286 }ˇ
18287 ```
18288 "},
18289 window,
18290 cx,
18291 );
18292 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18293 assert_text_with_selections(
18294 editor,
18295 &indoc! {"
18296 ```rs
18297 impl Worktree ˇ{
18298 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18299 }
18300 }
18301 ```
18302 "},
18303 cx,
18304 );
18305 // Case 2: Test inner enclosing brackets
18306 select_ranges(
18307 editor,
18308 &indoc! {"
18309 ```rs
18310 impl Worktree {
18311 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18312 }ˇ
18313 }
18314 ```
18315 "},
18316 window,
18317 cx,
18318 );
18319 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18320 assert_text_with_selections(
18321 editor,
18322 &indoc! {"
18323 ```rs
18324 impl Worktree {
18325 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
18326 }
18327 }
18328 ```
18329 "},
18330 cx,
18331 );
18332 });
18333}
18334
18335#[gpui::test]
18336async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
18337 init_test(cx, |_| {});
18338
18339 let fs = FakeFs::new(cx.executor());
18340 fs.insert_tree(
18341 path!("/a"),
18342 json!({
18343 "main.rs": "fn main() { let a = 5; }",
18344 "other.rs": "// Test file",
18345 }),
18346 )
18347 .await;
18348 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18349
18350 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18351 language_registry.add(Arc::new(Language::new(
18352 LanguageConfig {
18353 name: "Rust".into(),
18354 matcher: LanguageMatcher {
18355 path_suffixes: vec!["rs".to_string()],
18356 ..Default::default()
18357 },
18358 brackets: BracketPairConfig {
18359 pairs: vec![BracketPair {
18360 start: "{".to_string(),
18361 end: "}".to_string(),
18362 close: true,
18363 surround: true,
18364 newline: true,
18365 }],
18366 disabled_scopes_by_bracket_ix: Vec::new(),
18367 },
18368 ..Default::default()
18369 },
18370 Some(tree_sitter_rust::LANGUAGE.into()),
18371 )));
18372 let mut fake_servers = language_registry.register_fake_lsp(
18373 "Rust",
18374 FakeLspAdapter {
18375 capabilities: lsp::ServerCapabilities {
18376 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18377 first_trigger_character: "{".to_string(),
18378 more_trigger_character: None,
18379 }),
18380 ..Default::default()
18381 },
18382 ..Default::default()
18383 },
18384 );
18385
18386 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18387
18388 let cx = &mut VisualTestContext::from_window(*workspace, cx);
18389
18390 let worktree_id = workspace
18391 .update(cx, |workspace, _, cx| {
18392 workspace.project().update(cx, |project, cx| {
18393 project.worktrees(cx).next().unwrap().read(cx).id()
18394 })
18395 })
18396 .unwrap();
18397
18398 let buffer = project
18399 .update(cx, |project, cx| {
18400 project.open_local_buffer(path!("/a/main.rs"), cx)
18401 })
18402 .await
18403 .unwrap();
18404 let editor_handle = workspace
18405 .update(cx, |workspace, window, cx| {
18406 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
18407 })
18408 .unwrap()
18409 .await
18410 .unwrap()
18411 .downcast::<Editor>()
18412 .unwrap();
18413
18414 let fake_server = fake_servers.next().await.unwrap();
18415
18416 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
18417 |params, _| async move {
18418 assert_eq!(
18419 params.text_document_position.text_document.uri,
18420 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
18421 );
18422 assert_eq!(
18423 params.text_document_position.position,
18424 lsp::Position::new(0, 21),
18425 );
18426
18427 Ok(Some(vec![lsp::TextEdit {
18428 new_text: "]".to_string(),
18429 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18430 }]))
18431 },
18432 );
18433
18434 editor_handle.update_in(cx, |editor, window, cx| {
18435 window.focus(&editor.focus_handle(cx), cx);
18436 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18437 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
18438 });
18439 editor.handle_input("{", window, cx);
18440 });
18441
18442 cx.executor().run_until_parked();
18443
18444 buffer.update(cx, |buffer, _| {
18445 assert_eq!(
18446 buffer.text(),
18447 "fn main() { let a = {5}; }",
18448 "No extra braces from on type formatting should appear in the buffer"
18449 )
18450 });
18451}
18452
18453#[gpui::test(iterations = 20, seeds(31))]
18454async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
18455 init_test(cx, |_| {});
18456
18457 let mut cx = EditorLspTestContext::new_rust(
18458 lsp::ServerCapabilities {
18459 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18460 first_trigger_character: ".".to_string(),
18461 more_trigger_character: None,
18462 }),
18463 ..Default::default()
18464 },
18465 cx,
18466 )
18467 .await;
18468
18469 cx.update_buffer(|buffer, _| {
18470 // This causes autoindent to be async.
18471 buffer.set_sync_parse_timeout(None)
18472 });
18473
18474 cx.set_state("fn c() {\n d()ˇ\n}\n");
18475 cx.simulate_keystroke("\n");
18476 cx.run_until_parked();
18477
18478 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
18479 let mut request =
18480 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
18481 let buffer_cloned = buffer_cloned.clone();
18482 async move {
18483 buffer_cloned.update(&mut cx, |buffer, _| {
18484 assert_eq!(
18485 buffer.text(),
18486 "fn c() {\n d()\n .\n}\n",
18487 "OnTypeFormatting should triggered after autoindent applied"
18488 )
18489 });
18490
18491 Ok(Some(vec![]))
18492 }
18493 });
18494
18495 cx.simulate_keystroke(".");
18496 cx.run_until_parked();
18497
18498 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
18499 assert!(request.next().await.is_some());
18500 request.close();
18501 assert!(request.next().await.is_none());
18502}
18503
18504#[gpui::test]
18505async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
18506 init_test(cx, |_| {});
18507
18508 let fs = FakeFs::new(cx.executor());
18509 fs.insert_tree(
18510 path!("/a"),
18511 json!({
18512 "main.rs": "fn main() { let a = 5; }",
18513 "other.rs": "// Test file",
18514 }),
18515 )
18516 .await;
18517
18518 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18519
18520 let server_restarts = Arc::new(AtomicUsize::new(0));
18521 let closure_restarts = Arc::clone(&server_restarts);
18522 let language_server_name = "test language server";
18523 let language_name: LanguageName = "Rust".into();
18524
18525 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18526 language_registry.add(Arc::new(Language::new(
18527 LanguageConfig {
18528 name: language_name.clone(),
18529 matcher: LanguageMatcher {
18530 path_suffixes: vec!["rs".to_string()],
18531 ..Default::default()
18532 },
18533 ..Default::default()
18534 },
18535 Some(tree_sitter_rust::LANGUAGE.into()),
18536 )));
18537 let mut fake_servers = language_registry.register_fake_lsp(
18538 "Rust",
18539 FakeLspAdapter {
18540 name: language_server_name,
18541 initialization_options: Some(json!({
18542 "testOptionValue": true
18543 })),
18544 initializer: Some(Box::new(move |fake_server| {
18545 let task_restarts = Arc::clone(&closure_restarts);
18546 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
18547 task_restarts.fetch_add(1, atomic::Ordering::Release);
18548 futures::future::ready(Ok(()))
18549 });
18550 })),
18551 ..Default::default()
18552 },
18553 );
18554
18555 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18556 let _buffer = project
18557 .update(cx, |project, cx| {
18558 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
18559 })
18560 .await
18561 .unwrap();
18562 let _fake_server = fake_servers.next().await.unwrap();
18563 update_test_language_settings(cx, |language_settings| {
18564 language_settings.languages.0.insert(
18565 language_name.clone().0.to_string(),
18566 LanguageSettingsContent {
18567 tab_size: NonZeroU32::new(8),
18568 ..Default::default()
18569 },
18570 );
18571 });
18572 cx.executor().run_until_parked();
18573 assert_eq!(
18574 server_restarts.load(atomic::Ordering::Acquire),
18575 0,
18576 "Should not restart LSP server on an unrelated change"
18577 );
18578
18579 update_test_project_settings(cx, |project_settings| {
18580 project_settings.lsp.0.insert(
18581 "Some other server name".into(),
18582 LspSettings {
18583 binary: None,
18584 settings: None,
18585 initialization_options: Some(json!({
18586 "some other init value": false
18587 })),
18588 enable_lsp_tasks: false,
18589 fetch: None,
18590 },
18591 );
18592 });
18593 cx.executor().run_until_parked();
18594 assert_eq!(
18595 server_restarts.load(atomic::Ordering::Acquire),
18596 0,
18597 "Should not restart LSP server on an unrelated LSP settings change"
18598 );
18599
18600 update_test_project_settings(cx, |project_settings| {
18601 project_settings.lsp.0.insert(
18602 language_server_name.into(),
18603 LspSettings {
18604 binary: None,
18605 settings: None,
18606 initialization_options: Some(json!({
18607 "anotherInitValue": false
18608 })),
18609 enable_lsp_tasks: false,
18610 fetch: None,
18611 },
18612 );
18613 });
18614 cx.executor().run_until_parked();
18615 assert_eq!(
18616 server_restarts.load(atomic::Ordering::Acquire),
18617 1,
18618 "Should restart LSP server on a related LSP settings change"
18619 );
18620
18621 update_test_project_settings(cx, |project_settings| {
18622 project_settings.lsp.0.insert(
18623 language_server_name.into(),
18624 LspSettings {
18625 binary: None,
18626 settings: None,
18627 initialization_options: Some(json!({
18628 "anotherInitValue": false
18629 })),
18630 enable_lsp_tasks: false,
18631 fetch: None,
18632 },
18633 );
18634 });
18635 cx.executor().run_until_parked();
18636 assert_eq!(
18637 server_restarts.load(atomic::Ordering::Acquire),
18638 1,
18639 "Should not restart LSP server on a related LSP settings change that is the same"
18640 );
18641
18642 update_test_project_settings(cx, |project_settings| {
18643 project_settings.lsp.0.insert(
18644 language_server_name.into(),
18645 LspSettings {
18646 binary: None,
18647 settings: None,
18648 initialization_options: None,
18649 enable_lsp_tasks: false,
18650 fetch: None,
18651 },
18652 );
18653 });
18654 cx.executor().run_until_parked();
18655 assert_eq!(
18656 server_restarts.load(atomic::Ordering::Acquire),
18657 2,
18658 "Should restart LSP server on another related LSP settings change"
18659 );
18660}
18661
18662#[gpui::test]
18663async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18664 init_test(cx, |_| {});
18665
18666 let mut cx = EditorLspTestContext::new_rust(
18667 lsp::ServerCapabilities {
18668 completion_provider: Some(lsp::CompletionOptions {
18669 trigger_characters: Some(vec![".".to_string()]),
18670 resolve_provider: Some(true),
18671 ..Default::default()
18672 }),
18673 ..Default::default()
18674 },
18675 cx,
18676 )
18677 .await;
18678
18679 cx.set_state("fn main() { let a = 2ˇ; }");
18680 cx.simulate_keystroke(".");
18681 let completion_item = lsp::CompletionItem {
18682 label: "some".into(),
18683 kind: Some(lsp::CompletionItemKind::SNIPPET),
18684 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18685 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18686 kind: lsp::MarkupKind::Markdown,
18687 value: "```rust\nSome(2)\n```".to_string(),
18688 })),
18689 deprecated: Some(false),
18690 sort_text: Some("fffffff2".to_string()),
18691 filter_text: Some("some".to_string()),
18692 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18693 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18694 range: lsp::Range {
18695 start: lsp::Position {
18696 line: 0,
18697 character: 22,
18698 },
18699 end: lsp::Position {
18700 line: 0,
18701 character: 22,
18702 },
18703 },
18704 new_text: "Some(2)".to_string(),
18705 })),
18706 additional_text_edits: Some(vec![lsp::TextEdit {
18707 range: lsp::Range {
18708 start: lsp::Position {
18709 line: 0,
18710 character: 20,
18711 },
18712 end: lsp::Position {
18713 line: 0,
18714 character: 22,
18715 },
18716 },
18717 new_text: "".to_string(),
18718 }]),
18719 ..Default::default()
18720 };
18721
18722 let closure_completion_item = completion_item.clone();
18723 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18724 let task_completion_item = closure_completion_item.clone();
18725 async move {
18726 Ok(Some(lsp::CompletionResponse::Array(vec![
18727 task_completion_item,
18728 ])))
18729 }
18730 });
18731
18732 request.next().await;
18733
18734 cx.condition(|editor, _| editor.context_menu_visible())
18735 .await;
18736 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18737 editor
18738 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18739 .unwrap()
18740 });
18741 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18742
18743 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18744 let task_completion_item = completion_item.clone();
18745 async move { Ok(task_completion_item) }
18746 })
18747 .next()
18748 .await
18749 .unwrap();
18750 apply_additional_edits.await.unwrap();
18751 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18752}
18753
18754#[gpui::test]
18755async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18756 init_test(cx, |_| {});
18757
18758 let mut cx = EditorLspTestContext::new_rust(
18759 lsp::ServerCapabilities {
18760 completion_provider: Some(lsp::CompletionOptions {
18761 trigger_characters: Some(vec![".".to_string()]),
18762 resolve_provider: Some(true),
18763 ..Default::default()
18764 }),
18765 ..Default::default()
18766 },
18767 cx,
18768 )
18769 .await;
18770
18771 cx.set_state("fn main() { let a = 2ˇ; }");
18772 cx.simulate_keystroke(".");
18773
18774 let item1 = lsp::CompletionItem {
18775 label: "method id()".to_string(),
18776 filter_text: Some("id".to_string()),
18777 detail: None,
18778 documentation: None,
18779 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18780 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18781 new_text: ".id".to_string(),
18782 })),
18783 ..lsp::CompletionItem::default()
18784 };
18785
18786 let item2 = lsp::CompletionItem {
18787 label: "other".to_string(),
18788 filter_text: Some("other".to_string()),
18789 detail: None,
18790 documentation: None,
18791 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18792 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18793 new_text: ".other".to_string(),
18794 })),
18795 ..lsp::CompletionItem::default()
18796 };
18797
18798 let item1 = item1.clone();
18799 cx.set_request_handler::<lsp::request::Completion, _, _>({
18800 let item1 = item1.clone();
18801 move |_, _, _| {
18802 let item1 = item1.clone();
18803 let item2 = item2.clone();
18804 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
18805 }
18806 })
18807 .next()
18808 .await;
18809
18810 cx.condition(|editor, _| editor.context_menu_visible())
18811 .await;
18812 cx.update_editor(|editor, _, _| {
18813 let context_menu = editor.context_menu.borrow_mut();
18814 let context_menu = context_menu
18815 .as_ref()
18816 .expect("Should have the context menu deployed");
18817 match context_menu {
18818 CodeContextMenu::Completions(completions_menu) => {
18819 let completions = completions_menu.completions.borrow_mut();
18820 assert_eq!(
18821 completions
18822 .iter()
18823 .map(|completion| &completion.label.text)
18824 .collect::<Vec<_>>(),
18825 vec!["method id()", "other"]
18826 )
18827 }
18828 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18829 }
18830 });
18831
18832 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
18833 let item1 = item1.clone();
18834 move |_, item_to_resolve, _| {
18835 let item1 = item1.clone();
18836 async move {
18837 if item1 == item_to_resolve {
18838 Ok(lsp::CompletionItem {
18839 label: "method id()".to_string(),
18840 filter_text: Some("id".to_string()),
18841 detail: Some("Now resolved!".to_string()),
18842 documentation: Some(lsp::Documentation::String("Docs".to_string())),
18843 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18844 range: lsp::Range::new(
18845 lsp::Position::new(0, 22),
18846 lsp::Position::new(0, 22),
18847 ),
18848 new_text: ".id".to_string(),
18849 })),
18850 ..lsp::CompletionItem::default()
18851 })
18852 } else {
18853 Ok(item_to_resolve)
18854 }
18855 }
18856 }
18857 })
18858 .next()
18859 .await
18860 .unwrap();
18861 cx.run_until_parked();
18862
18863 cx.update_editor(|editor, window, cx| {
18864 editor.context_menu_next(&Default::default(), window, cx);
18865 });
18866 cx.run_until_parked();
18867
18868 cx.update_editor(|editor, _, _| {
18869 let context_menu = editor.context_menu.borrow_mut();
18870 let context_menu = context_menu
18871 .as_ref()
18872 .expect("Should have the context menu deployed");
18873 match context_menu {
18874 CodeContextMenu::Completions(completions_menu) => {
18875 let completions = completions_menu.completions.borrow_mut();
18876 assert_eq!(
18877 completions
18878 .iter()
18879 .map(|completion| &completion.label.text)
18880 .collect::<Vec<_>>(),
18881 vec!["method id() Now resolved!", "other"],
18882 "Should update first completion label, but not second as the filter text did not match."
18883 );
18884 }
18885 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18886 }
18887 });
18888}
18889
18890#[gpui::test]
18891async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
18892 init_test(cx, |_| {});
18893 let mut cx = EditorLspTestContext::new_rust(
18894 lsp::ServerCapabilities {
18895 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
18896 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
18897 completion_provider: Some(lsp::CompletionOptions {
18898 resolve_provider: Some(true),
18899 ..Default::default()
18900 }),
18901 ..Default::default()
18902 },
18903 cx,
18904 )
18905 .await;
18906 cx.set_state(indoc! {"
18907 struct TestStruct {
18908 field: i32
18909 }
18910
18911 fn mainˇ() {
18912 let unused_var = 42;
18913 let test_struct = TestStruct { field: 42 };
18914 }
18915 "});
18916 let symbol_range = cx.lsp_range(indoc! {"
18917 struct TestStruct {
18918 field: i32
18919 }
18920
18921 «fn main»() {
18922 let unused_var = 42;
18923 let test_struct = TestStruct { field: 42 };
18924 }
18925 "});
18926 let mut hover_requests =
18927 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18928 Ok(Some(lsp::Hover {
18929 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18930 kind: lsp::MarkupKind::Markdown,
18931 value: "Function documentation".to_string(),
18932 }),
18933 range: Some(symbol_range),
18934 }))
18935 });
18936
18937 // Case 1: Test that code action menu hide hover popover
18938 cx.dispatch_action(Hover);
18939 hover_requests.next().await;
18940 cx.condition(|editor, _| editor.hover_state.visible()).await;
18941 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18942 move |_, _, _| async move {
18943 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18944 lsp::CodeAction {
18945 title: "Remove unused variable".to_string(),
18946 kind: Some(CodeActionKind::QUICKFIX),
18947 edit: Some(lsp::WorkspaceEdit {
18948 changes: Some(
18949 [(
18950 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18951 vec![lsp::TextEdit {
18952 range: lsp::Range::new(
18953 lsp::Position::new(5, 4),
18954 lsp::Position::new(5, 27),
18955 ),
18956 new_text: "".to_string(),
18957 }],
18958 )]
18959 .into_iter()
18960 .collect(),
18961 ),
18962 ..Default::default()
18963 }),
18964 ..Default::default()
18965 },
18966 )]))
18967 },
18968 );
18969 cx.update_editor(|editor, window, cx| {
18970 editor.toggle_code_actions(
18971 &ToggleCodeActions {
18972 deployed_from: None,
18973 quick_launch: false,
18974 },
18975 window,
18976 cx,
18977 );
18978 });
18979 code_action_requests.next().await;
18980 cx.run_until_parked();
18981 cx.condition(|editor, _| editor.context_menu_visible())
18982 .await;
18983 cx.update_editor(|editor, _, _| {
18984 assert!(
18985 !editor.hover_state.visible(),
18986 "Hover popover should be hidden when code action menu is shown"
18987 );
18988 // Hide code actions
18989 editor.context_menu.take();
18990 });
18991
18992 // Case 2: Test that code completions hide hover popover
18993 cx.dispatch_action(Hover);
18994 hover_requests.next().await;
18995 cx.condition(|editor, _| editor.hover_state.visible()).await;
18996 let counter = Arc::new(AtomicUsize::new(0));
18997 let mut completion_requests =
18998 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18999 let counter = counter.clone();
19000 async move {
19001 counter.fetch_add(1, atomic::Ordering::Release);
19002 Ok(Some(lsp::CompletionResponse::Array(vec![
19003 lsp::CompletionItem {
19004 label: "main".into(),
19005 kind: Some(lsp::CompletionItemKind::FUNCTION),
19006 detail: Some("() -> ()".to_string()),
19007 ..Default::default()
19008 },
19009 lsp::CompletionItem {
19010 label: "TestStruct".into(),
19011 kind: Some(lsp::CompletionItemKind::STRUCT),
19012 detail: Some("struct TestStruct".to_string()),
19013 ..Default::default()
19014 },
19015 ])))
19016 }
19017 });
19018 cx.update_editor(|editor, window, cx| {
19019 editor.show_completions(&ShowCompletions, window, cx);
19020 });
19021 completion_requests.next().await;
19022 cx.condition(|editor, _| editor.context_menu_visible())
19023 .await;
19024 cx.update_editor(|editor, _, _| {
19025 assert!(
19026 !editor.hover_state.visible(),
19027 "Hover popover should be hidden when completion menu is shown"
19028 );
19029 });
19030}
19031
19032#[gpui::test]
19033async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
19034 init_test(cx, |_| {});
19035
19036 let mut cx = EditorLspTestContext::new_rust(
19037 lsp::ServerCapabilities {
19038 completion_provider: Some(lsp::CompletionOptions {
19039 trigger_characters: Some(vec![".".to_string()]),
19040 resolve_provider: Some(true),
19041 ..Default::default()
19042 }),
19043 ..Default::default()
19044 },
19045 cx,
19046 )
19047 .await;
19048
19049 cx.set_state("fn main() { let a = 2ˇ; }");
19050 cx.simulate_keystroke(".");
19051
19052 let unresolved_item_1 = lsp::CompletionItem {
19053 label: "id".to_string(),
19054 filter_text: Some("id".to_string()),
19055 detail: None,
19056 documentation: None,
19057 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19058 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19059 new_text: ".id".to_string(),
19060 })),
19061 ..lsp::CompletionItem::default()
19062 };
19063 let resolved_item_1 = lsp::CompletionItem {
19064 additional_text_edits: Some(vec![lsp::TextEdit {
19065 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
19066 new_text: "!!".to_string(),
19067 }]),
19068 ..unresolved_item_1.clone()
19069 };
19070 let unresolved_item_2 = lsp::CompletionItem {
19071 label: "other".to_string(),
19072 filter_text: Some("other".to_string()),
19073 detail: None,
19074 documentation: None,
19075 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19076 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19077 new_text: ".other".to_string(),
19078 })),
19079 ..lsp::CompletionItem::default()
19080 };
19081 let resolved_item_2 = lsp::CompletionItem {
19082 additional_text_edits: Some(vec![lsp::TextEdit {
19083 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
19084 new_text: "??".to_string(),
19085 }]),
19086 ..unresolved_item_2.clone()
19087 };
19088
19089 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
19090 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
19091 cx.lsp
19092 .server
19093 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19094 let unresolved_item_1 = unresolved_item_1.clone();
19095 let resolved_item_1 = resolved_item_1.clone();
19096 let unresolved_item_2 = unresolved_item_2.clone();
19097 let resolved_item_2 = resolved_item_2.clone();
19098 let resolve_requests_1 = resolve_requests_1.clone();
19099 let resolve_requests_2 = resolve_requests_2.clone();
19100 move |unresolved_request, _| {
19101 let unresolved_item_1 = unresolved_item_1.clone();
19102 let resolved_item_1 = resolved_item_1.clone();
19103 let unresolved_item_2 = unresolved_item_2.clone();
19104 let resolved_item_2 = resolved_item_2.clone();
19105 let resolve_requests_1 = resolve_requests_1.clone();
19106 let resolve_requests_2 = resolve_requests_2.clone();
19107 async move {
19108 if unresolved_request == unresolved_item_1 {
19109 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
19110 Ok(resolved_item_1.clone())
19111 } else if unresolved_request == unresolved_item_2 {
19112 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
19113 Ok(resolved_item_2.clone())
19114 } else {
19115 panic!("Unexpected completion item {unresolved_request:?}")
19116 }
19117 }
19118 }
19119 })
19120 .detach();
19121
19122 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19123 let unresolved_item_1 = unresolved_item_1.clone();
19124 let unresolved_item_2 = unresolved_item_2.clone();
19125 async move {
19126 Ok(Some(lsp::CompletionResponse::Array(vec![
19127 unresolved_item_1,
19128 unresolved_item_2,
19129 ])))
19130 }
19131 })
19132 .next()
19133 .await;
19134
19135 cx.condition(|editor, _| editor.context_menu_visible())
19136 .await;
19137 cx.update_editor(|editor, _, _| {
19138 let context_menu = editor.context_menu.borrow_mut();
19139 let context_menu = context_menu
19140 .as_ref()
19141 .expect("Should have the context menu deployed");
19142 match context_menu {
19143 CodeContextMenu::Completions(completions_menu) => {
19144 let completions = completions_menu.completions.borrow_mut();
19145 assert_eq!(
19146 completions
19147 .iter()
19148 .map(|completion| &completion.label.text)
19149 .collect::<Vec<_>>(),
19150 vec!["id", "other"]
19151 )
19152 }
19153 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
19154 }
19155 });
19156 cx.run_until_parked();
19157
19158 cx.update_editor(|editor, window, cx| {
19159 editor.context_menu_next(&ContextMenuNext, window, cx);
19160 });
19161 cx.run_until_parked();
19162 cx.update_editor(|editor, window, cx| {
19163 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19164 });
19165 cx.run_until_parked();
19166 cx.update_editor(|editor, window, cx| {
19167 editor.context_menu_next(&ContextMenuNext, window, cx);
19168 });
19169 cx.run_until_parked();
19170 cx.update_editor(|editor, window, cx| {
19171 editor
19172 .compose_completion(&ComposeCompletion::default(), window, cx)
19173 .expect("No task returned")
19174 })
19175 .await
19176 .expect("Completion failed");
19177 cx.run_until_parked();
19178
19179 cx.update_editor(|editor, _, cx| {
19180 assert_eq!(
19181 resolve_requests_1.load(atomic::Ordering::Acquire),
19182 1,
19183 "Should always resolve once despite multiple selections"
19184 );
19185 assert_eq!(
19186 resolve_requests_2.load(atomic::Ordering::Acquire),
19187 1,
19188 "Should always resolve once after multiple selections and applying the completion"
19189 );
19190 assert_eq!(
19191 editor.text(cx),
19192 "fn main() { let a = ??.other; }",
19193 "Should use resolved data when applying the completion"
19194 );
19195 });
19196}
19197
19198#[gpui::test]
19199async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
19200 init_test(cx, |_| {});
19201
19202 let item_0 = lsp::CompletionItem {
19203 label: "abs".into(),
19204 insert_text: Some("abs".into()),
19205 data: Some(json!({ "very": "special"})),
19206 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
19207 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19208 lsp::InsertReplaceEdit {
19209 new_text: "abs".to_string(),
19210 insert: lsp::Range::default(),
19211 replace: lsp::Range::default(),
19212 },
19213 )),
19214 ..lsp::CompletionItem::default()
19215 };
19216 let items = iter::once(item_0.clone())
19217 .chain((11..51).map(|i| lsp::CompletionItem {
19218 label: format!("item_{}", i),
19219 insert_text: Some(format!("item_{}", i)),
19220 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
19221 ..lsp::CompletionItem::default()
19222 }))
19223 .collect::<Vec<_>>();
19224
19225 let default_commit_characters = vec!["?".to_string()];
19226 let default_data = json!({ "default": "data"});
19227 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
19228 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
19229 let default_edit_range = lsp::Range {
19230 start: lsp::Position {
19231 line: 0,
19232 character: 5,
19233 },
19234 end: lsp::Position {
19235 line: 0,
19236 character: 5,
19237 },
19238 };
19239
19240 let mut cx = EditorLspTestContext::new_rust(
19241 lsp::ServerCapabilities {
19242 completion_provider: Some(lsp::CompletionOptions {
19243 trigger_characters: Some(vec![".".to_string()]),
19244 resolve_provider: Some(true),
19245 ..Default::default()
19246 }),
19247 ..Default::default()
19248 },
19249 cx,
19250 )
19251 .await;
19252
19253 cx.set_state("fn main() { let a = 2ˇ; }");
19254 cx.simulate_keystroke(".");
19255
19256 let completion_data = default_data.clone();
19257 let completion_characters = default_commit_characters.clone();
19258 let completion_items = items.clone();
19259 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19260 let default_data = completion_data.clone();
19261 let default_commit_characters = completion_characters.clone();
19262 let items = completion_items.clone();
19263 async move {
19264 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
19265 items,
19266 item_defaults: Some(lsp::CompletionListItemDefaults {
19267 data: Some(default_data.clone()),
19268 commit_characters: Some(default_commit_characters.clone()),
19269 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
19270 default_edit_range,
19271 )),
19272 insert_text_format: Some(default_insert_text_format),
19273 insert_text_mode: Some(default_insert_text_mode),
19274 }),
19275 ..lsp::CompletionList::default()
19276 })))
19277 }
19278 })
19279 .next()
19280 .await;
19281
19282 let resolved_items = Arc::new(Mutex::new(Vec::new()));
19283 cx.lsp
19284 .server
19285 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19286 let closure_resolved_items = resolved_items.clone();
19287 move |item_to_resolve, _| {
19288 let closure_resolved_items = closure_resolved_items.clone();
19289 async move {
19290 closure_resolved_items.lock().push(item_to_resolve.clone());
19291 Ok(item_to_resolve)
19292 }
19293 }
19294 })
19295 .detach();
19296
19297 cx.condition(|editor, _| editor.context_menu_visible())
19298 .await;
19299 cx.run_until_parked();
19300 cx.update_editor(|editor, _, _| {
19301 let menu = editor.context_menu.borrow_mut();
19302 match menu.as_ref().expect("should have the completions menu") {
19303 CodeContextMenu::Completions(completions_menu) => {
19304 assert_eq!(
19305 completions_menu
19306 .entries
19307 .borrow()
19308 .iter()
19309 .map(|mat| mat.string.clone())
19310 .collect::<Vec<String>>(),
19311 items
19312 .iter()
19313 .map(|completion| completion.label.clone())
19314 .collect::<Vec<String>>()
19315 );
19316 }
19317 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
19318 }
19319 });
19320 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
19321 // with 4 from the end.
19322 assert_eq!(
19323 *resolved_items.lock(),
19324 [&items[0..16], &items[items.len() - 4..items.len()]]
19325 .concat()
19326 .iter()
19327 .cloned()
19328 .map(|mut item| {
19329 if item.data.is_none() {
19330 item.data = Some(default_data.clone());
19331 }
19332 item
19333 })
19334 .collect::<Vec<lsp::CompletionItem>>(),
19335 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
19336 );
19337 resolved_items.lock().clear();
19338
19339 cx.update_editor(|editor, window, cx| {
19340 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19341 });
19342 cx.run_until_parked();
19343 // Completions that have already been resolved are skipped.
19344 assert_eq!(
19345 *resolved_items.lock(),
19346 items[items.len() - 17..items.len() - 4]
19347 .iter()
19348 .cloned()
19349 .map(|mut item| {
19350 if item.data.is_none() {
19351 item.data = Some(default_data.clone());
19352 }
19353 item
19354 })
19355 .collect::<Vec<lsp::CompletionItem>>()
19356 );
19357 resolved_items.lock().clear();
19358}
19359
19360#[gpui::test]
19361async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
19362 init_test(cx, |_| {});
19363
19364 let mut cx = EditorLspTestContext::new(
19365 Language::new(
19366 LanguageConfig {
19367 matcher: LanguageMatcher {
19368 path_suffixes: vec!["jsx".into()],
19369 ..Default::default()
19370 },
19371 overrides: [(
19372 "element".into(),
19373 LanguageConfigOverride {
19374 completion_query_characters: Override::Set(['-'].into_iter().collect()),
19375 ..Default::default()
19376 },
19377 )]
19378 .into_iter()
19379 .collect(),
19380 ..Default::default()
19381 },
19382 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
19383 )
19384 .with_override_query("(jsx_self_closing_element) @element")
19385 .unwrap(),
19386 lsp::ServerCapabilities {
19387 completion_provider: Some(lsp::CompletionOptions {
19388 trigger_characters: Some(vec![":".to_string()]),
19389 ..Default::default()
19390 }),
19391 ..Default::default()
19392 },
19393 cx,
19394 )
19395 .await;
19396
19397 cx.lsp
19398 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19399 Ok(Some(lsp::CompletionResponse::Array(vec![
19400 lsp::CompletionItem {
19401 label: "bg-blue".into(),
19402 ..Default::default()
19403 },
19404 lsp::CompletionItem {
19405 label: "bg-red".into(),
19406 ..Default::default()
19407 },
19408 lsp::CompletionItem {
19409 label: "bg-yellow".into(),
19410 ..Default::default()
19411 },
19412 ])))
19413 });
19414
19415 cx.set_state(r#"<p class="bgˇ" />"#);
19416
19417 // Trigger completion when typing a dash, because the dash is an extra
19418 // word character in the 'element' scope, which contains the cursor.
19419 cx.simulate_keystroke("-");
19420 cx.executor().run_until_parked();
19421 cx.update_editor(|editor, _, _| {
19422 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19423 {
19424 assert_eq!(
19425 completion_menu_entries(menu),
19426 &["bg-blue", "bg-red", "bg-yellow"]
19427 );
19428 } else {
19429 panic!("expected completion menu to be open");
19430 }
19431 });
19432
19433 cx.simulate_keystroke("l");
19434 cx.executor().run_until_parked();
19435 cx.update_editor(|editor, _, _| {
19436 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19437 {
19438 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
19439 } else {
19440 panic!("expected completion menu to be open");
19441 }
19442 });
19443
19444 // When filtering completions, consider the character after the '-' to
19445 // be the start of a subword.
19446 cx.set_state(r#"<p class="yelˇ" />"#);
19447 cx.simulate_keystroke("l");
19448 cx.executor().run_until_parked();
19449 cx.update_editor(|editor, _, _| {
19450 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19451 {
19452 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
19453 } else {
19454 panic!("expected completion menu to be open");
19455 }
19456 });
19457}
19458
19459fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
19460 let entries = menu.entries.borrow();
19461 entries.iter().map(|mat| mat.string.clone()).collect()
19462}
19463
19464#[gpui::test]
19465async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
19466 init_test(cx, |settings| {
19467 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19468 });
19469
19470 let fs = FakeFs::new(cx.executor());
19471 fs.insert_file(path!("/file.ts"), Default::default()).await;
19472
19473 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
19474 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19475
19476 language_registry.add(Arc::new(Language::new(
19477 LanguageConfig {
19478 name: "TypeScript".into(),
19479 matcher: LanguageMatcher {
19480 path_suffixes: vec!["ts".to_string()],
19481 ..Default::default()
19482 },
19483 ..Default::default()
19484 },
19485 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19486 )));
19487 update_test_language_settings(cx, |settings| {
19488 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19489 });
19490
19491 let test_plugin = "test_plugin";
19492 let _ = language_registry.register_fake_lsp(
19493 "TypeScript",
19494 FakeLspAdapter {
19495 prettier_plugins: vec![test_plugin],
19496 ..Default::default()
19497 },
19498 );
19499
19500 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19501 let buffer = project
19502 .update(cx, |project, cx| {
19503 project.open_local_buffer(path!("/file.ts"), cx)
19504 })
19505 .await
19506 .unwrap();
19507
19508 let buffer_text = "one\ntwo\nthree\n";
19509 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19510 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19511 editor.update_in(cx, |editor, window, cx| {
19512 editor.set_text(buffer_text, window, cx)
19513 });
19514
19515 editor
19516 .update_in(cx, |editor, window, cx| {
19517 editor.perform_format(
19518 project.clone(),
19519 FormatTrigger::Manual,
19520 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19521 window,
19522 cx,
19523 )
19524 })
19525 .unwrap()
19526 .await;
19527 assert_eq!(
19528 editor.update(cx, |editor, cx| editor.text(cx)),
19529 buffer_text.to_string() + prettier_format_suffix,
19530 "Test prettier formatting was not applied to the original buffer text",
19531 );
19532
19533 update_test_language_settings(cx, |settings| {
19534 settings.defaults.formatter = Some(FormatterList::default())
19535 });
19536 let format = editor.update_in(cx, |editor, window, cx| {
19537 editor.perform_format(
19538 project.clone(),
19539 FormatTrigger::Manual,
19540 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19541 window,
19542 cx,
19543 )
19544 });
19545 format.await.unwrap();
19546 assert_eq!(
19547 editor.update(cx, |editor, cx| editor.text(cx)),
19548 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
19549 "Autoformatting (via test prettier) was not applied to the original buffer text",
19550 );
19551}
19552
19553#[gpui::test]
19554async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
19555 init_test(cx, |settings| {
19556 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19557 });
19558
19559 let fs = FakeFs::new(cx.executor());
19560 fs.insert_file(path!("/file.settings"), Default::default())
19561 .await;
19562
19563 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
19564 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19565
19566 let ts_lang = Arc::new(Language::new(
19567 LanguageConfig {
19568 name: "TypeScript".into(),
19569 matcher: LanguageMatcher {
19570 path_suffixes: vec!["ts".to_string()],
19571 ..LanguageMatcher::default()
19572 },
19573 prettier_parser_name: Some("typescript".to_string()),
19574 ..LanguageConfig::default()
19575 },
19576 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19577 ));
19578
19579 language_registry.add(ts_lang.clone());
19580
19581 update_test_language_settings(cx, |settings| {
19582 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19583 });
19584
19585 let test_plugin = "test_plugin";
19586 let _ = language_registry.register_fake_lsp(
19587 "TypeScript",
19588 FakeLspAdapter {
19589 prettier_plugins: vec![test_plugin],
19590 ..Default::default()
19591 },
19592 );
19593
19594 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19595 let buffer = project
19596 .update(cx, |project, cx| {
19597 project.open_local_buffer(path!("/file.settings"), cx)
19598 })
19599 .await
19600 .unwrap();
19601
19602 project.update(cx, |project, cx| {
19603 project.set_language_for_buffer(&buffer, ts_lang, cx)
19604 });
19605
19606 let buffer_text = "one\ntwo\nthree\n";
19607 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19608 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19609 editor.update_in(cx, |editor, window, cx| {
19610 editor.set_text(buffer_text, window, cx)
19611 });
19612
19613 editor
19614 .update_in(cx, |editor, window, cx| {
19615 editor.perform_format(
19616 project.clone(),
19617 FormatTrigger::Manual,
19618 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19619 window,
19620 cx,
19621 )
19622 })
19623 .unwrap()
19624 .await;
19625 assert_eq!(
19626 editor.update(cx, |editor, cx| editor.text(cx)),
19627 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
19628 "Test prettier formatting was not applied to the original buffer text",
19629 );
19630
19631 update_test_language_settings(cx, |settings| {
19632 settings.defaults.formatter = Some(FormatterList::default())
19633 });
19634 let format = editor.update_in(cx, |editor, window, cx| {
19635 editor.perform_format(
19636 project.clone(),
19637 FormatTrigger::Manual,
19638 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19639 window,
19640 cx,
19641 )
19642 });
19643 format.await.unwrap();
19644
19645 assert_eq!(
19646 editor.update(cx, |editor, cx| editor.text(cx)),
19647 buffer_text.to_string()
19648 + prettier_format_suffix
19649 + "\ntypescript\n"
19650 + prettier_format_suffix
19651 + "\ntypescript",
19652 "Autoformatting (via test prettier) was not applied to the original buffer text",
19653 );
19654}
19655
19656#[gpui::test]
19657async fn test_addition_reverts(cx: &mut TestAppContext) {
19658 init_test(cx, |_| {});
19659 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19660 let base_text = indoc! {r#"
19661 struct Row;
19662 struct Row1;
19663 struct Row2;
19664
19665 struct Row4;
19666 struct Row5;
19667 struct Row6;
19668
19669 struct Row8;
19670 struct Row9;
19671 struct Row10;"#};
19672
19673 // When addition hunks are not adjacent to carets, no hunk revert is performed
19674 assert_hunk_revert(
19675 indoc! {r#"struct Row;
19676 struct Row1;
19677 struct Row1.1;
19678 struct Row1.2;
19679 struct Row2;ˇ
19680
19681 struct Row4;
19682 struct Row5;
19683 struct Row6;
19684
19685 struct Row8;
19686 ˇstruct Row9;
19687 struct Row9.1;
19688 struct Row9.2;
19689 struct Row9.3;
19690 struct Row10;"#},
19691 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19692 indoc! {r#"struct Row;
19693 struct Row1;
19694 struct Row1.1;
19695 struct Row1.2;
19696 struct Row2;ˇ
19697
19698 struct Row4;
19699 struct Row5;
19700 struct Row6;
19701
19702 struct Row8;
19703 ˇstruct Row9;
19704 struct Row9.1;
19705 struct Row9.2;
19706 struct Row9.3;
19707 struct Row10;"#},
19708 base_text,
19709 &mut cx,
19710 );
19711 // Same for selections
19712 assert_hunk_revert(
19713 indoc! {r#"struct Row;
19714 struct Row1;
19715 struct Row2;
19716 struct Row2.1;
19717 struct Row2.2;
19718 «ˇ
19719 struct Row4;
19720 struct» Row5;
19721 «struct Row6;
19722 ˇ»
19723 struct Row9.1;
19724 struct Row9.2;
19725 struct Row9.3;
19726 struct Row8;
19727 struct Row9;
19728 struct Row10;"#},
19729 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19730 indoc! {r#"struct Row;
19731 struct Row1;
19732 struct Row2;
19733 struct Row2.1;
19734 struct Row2.2;
19735 «ˇ
19736 struct Row4;
19737 struct» Row5;
19738 «struct Row6;
19739 ˇ»
19740 struct Row9.1;
19741 struct Row9.2;
19742 struct Row9.3;
19743 struct Row8;
19744 struct Row9;
19745 struct Row10;"#},
19746 base_text,
19747 &mut cx,
19748 );
19749
19750 // When carets and selections intersect the addition hunks, those are reverted.
19751 // Adjacent carets got merged.
19752 assert_hunk_revert(
19753 indoc! {r#"struct Row;
19754 ˇ// something on the top
19755 struct Row1;
19756 struct Row2;
19757 struct Roˇw3.1;
19758 struct Row2.2;
19759 struct Row2.3;ˇ
19760
19761 struct Row4;
19762 struct ˇRow5.1;
19763 struct Row5.2;
19764 struct «Rowˇ»5.3;
19765 struct Row5;
19766 struct Row6;
19767 ˇ
19768 struct Row9.1;
19769 struct «Rowˇ»9.2;
19770 struct «ˇRow»9.3;
19771 struct Row8;
19772 struct Row9;
19773 «ˇ// something on bottom»
19774 struct Row10;"#},
19775 vec![
19776 DiffHunkStatusKind::Added,
19777 DiffHunkStatusKind::Added,
19778 DiffHunkStatusKind::Added,
19779 DiffHunkStatusKind::Added,
19780 DiffHunkStatusKind::Added,
19781 ],
19782 indoc! {r#"struct Row;
19783 ˇstruct Row1;
19784 struct Row2;
19785 ˇ
19786 struct Row4;
19787 ˇstruct Row5;
19788 struct Row6;
19789 ˇ
19790 ˇstruct Row8;
19791 struct Row9;
19792 ˇstruct Row10;"#},
19793 base_text,
19794 &mut cx,
19795 );
19796}
19797
19798#[gpui::test]
19799async fn test_modification_reverts(cx: &mut TestAppContext) {
19800 init_test(cx, |_| {});
19801 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19802 let base_text = indoc! {r#"
19803 struct Row;
19804 struct Row1;
19805 struct Row2;
19806
19807 struct Row4;
19808 struct Row5;
19809 struct Row6;
19810
19811 struct Row8;
19812 struct Row9;
19813 struct Row10;"#};
19814
19815 // Modification hunks behave the same as the addition ones.
19816 assert_hunk_revert(
19817 indoc! {r#"struct Row;
19818 struct Row1;
19819 struct Row33;
19820 ˇ
19821 struct Row4;
19822 struct Row5;
19823 struct Row6;
19824 ˇ
19825 struct Row99;
19826 struct Row9;
19827 struct Row10;"#},
19828 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19829 indoc! {r#"struct Row;
19830 struct Row1;
19831 struct Row33;
19832 ˇ
19833 struct Row4;
19834 struct Row5;
19835 struct Row6;
19836 ˇ
19837 struct Row99;
19838 struct Row9;
19839 struct Row10;"#},
19840 base_text,
19841 &mut cx,
19842 );
19843 assert_hunk_revert(
19844 indoc! {r#"struct Row;
19845 struct Row1;
19846 struct Row33;
19847 «ˇ
19848 struct Row4;
19849 struct» Row5;
19850 «struct Row6;
19851 ˇ»
19852 struct Row99;
19853 struct Row9;
19854 struct Row10;"#},
19855 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19856 indoc! {r#"struct Row;
19857 struct Row1;
19858 struct Row33;
19859 «ˇ
19860 struct Row4;
19861 struct» Row5;
19862 «struct Row6;
19863 ˇ»
19864 struct Row99;
19865 struct Row9;
19866 struct Row10;"#},
19867 base_text,
19868 &mut cx,
19869 );
19870
19871 assert_hunk_revert(
19872 indoc! {r#"ˇstruct Row1.1;
19873 struct Row1;
19874 «ˇstr»uct Row22;
19875
19876 struct ˇRow44;
19877 struct Row5;
19878 struct «Rˇ»ow66;ˇ
19879
19880 «struˇ»ct Row88;
19881 struct Row9;
19882 struct Row1011;ˇ"#},
19883 vec![
19884 DiffHunkStatusKind::Modified,
19885 DiffHunkStatusKind::Modified,
19886 DiffHunkStatusKind::Modified,
19887 DiffHunkStatusKind::Modified,
19888 DiffHunkStatusKind::Modified,
19889 DiffHunkStatusKind::Modified,
19890 ],
19891 indoc! {r#"struct Row;
19892 ˇstruct Row1;
19893 struct Row2;
19894 ˇ
19895 struct Row4;
19896 ˇstruct Row5;
19897 struct Row6;
19898 ˇ
19899 struct Row8;
19900 ˇstruct Row9;
19901 struct Row10;ˇ"#},
19902 base_text,
19903 &mut cx,
19904 );
19905}
19906
19907#[gpui::test]
19908async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
19909 init_test(cx, |_| {});
19910 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19911 let base_text = indoc! {r#"
19912 one
19913
19914 two
19915 three
19916 "#};
19917
19918 cx.set_head_text(base_text);
19919 cx.set_state("\nˇ\n");
19920 cx.executor().run_until_parked();
19921 cx.update_editor(|editor, _window, cx| {
19922 editor.expand_selected_diff_hunks(cx);
19923 });
19924 cx.executor().run_until_parked();
19925 cx.update_editor(|editor, window, cx| {
19926 editor.backspace(&Default::default(), window, cx);
19927 });
19928 cx.run_until_parked();
19929 cx.assert_state_with_diff(
19930 indoc! {r#"
19931
19932 - two
19933 - threeˇ
19934 +
19935 "#}
19936 .to_string(),
19937 );
19938}
19939
19940#[gpui::test]
19941async fn test_deletion_reverts(cx: &mut TestAppContext) {
19942 init_test(cx, |_| {});
19943 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19944 let base_text = indoc! {r#"struct Row;
19945struct Row1;
19946struct Row2;
19947
19948struct Row4;
19949struct Row5;
19950struct Row6;
19951
19952struct Row8;
19953struct Row9;
19954struct Row10;"#};
19955
19956 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
19957 assert_hunk_revert(
19958 indoc! {r#"struct Row;
19959 struct Row2;
19960
19961 ˇstruct Row4;
19962 struct Row5;
19963 struct Row6;
19964 ˇ
19965 struct Row8;
19966 struct Row10;"#},
19967 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19968 indoc! {r#"struct Row;
19969 struct Row2;
19970
19971 ˇstruct Row4;
19972 struct Row5;
19973 struct Row6;
19974 ˇ
19975 struct Row8;
19976 struct Row10;"#},
19977 base_text,
19978 &mut cx,
19979 );
19980 assert_hunk_revert(
19981 indoc! {r#"struct Row;
19982 struct Row2;
19983
19984 «ˇstruct Row4;
19985 struct» Row5;
19986 «struct Row6;
19987 ˇ»
19988 struct Row8;
19989 struct Row10;"#},
19990 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19991 indoc! {r#"struct Row;
19992 struct Row2;
19993
19994 «ˇstruct Row4;
19995 struct» Row5;
19996 «struct Row6;
19997 ˇ»
19998 struct Row8;
19999 struct Row10;"#},
20000 base_text,
20001 &mut cx,
20002 );
20003
20004 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
20005 assert_hunk_revert(
20006 indoc! {r#"struct Row;
20007 ˇstruct Row2;
20008
20009 struct Row4;
20010 struct Row5;
20011 struct Row6;
20012
20013 struct Row8;ˇ
20014 struct Row10;"#},
20015 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
20016 indoc! {r#"struct Row;
20017 struct Row1;
20018 ˇstruct Row2;
20019
20020 struct Row4;
20021 struct Row5;
20022 struct Row6;
20023
20024 struct Row8;ˇ
20025 struct Row9;
20026 struct Row10;"#},
20027 base_text,
20028 &mut cx,
20029 );
20030 assert_hunk_revert(
20031 indoc! {r#"struct Row;
20032 struct Row2«ˇ;
20033 struct Row4;
20034 struct» Row5;
20035 «struct Row6;
20036
20037 struct Row8;ˇ»
20038 struct Row10;"#},
20039 vec![
20040 DiffHunkStatusKind::Deleted,
20041 DiffHunkStatusKind::Deleted,
20042 DiffHunkStatusKind::Deleted,
20043 ],
20044 indoc! {r#"struct Row;
20045 struct Row1;
20046 struct Row2«ˇ;
20047
20048 struct Row4;
20049 struct» Row5;
20050 «struct Row6;
20051
20052 struct Row8;ˇ»
20053 struct Row9;
20054 struct Row10;"#},
20055 base_text,
20056 &mut cx,
20057 );
20058}
20059
20060#[gpui::test]
20061async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
20062 init_test(cx, |_| {});
20063
20064 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
20065 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
20066 let base_text_3 =
20067 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
20068
20069 let text_1 = edit_first_char_of_every_line(base_text_1);
20070 let text_2 = edit_first_char_of_every_line(base_text_2);
20071 let text_3 = edit_first_char_of_every_line(base_text_3);
20072
20073 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
20074 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
20075 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
20076
20077 let multibuffer = cx.new(|cx| {
20078 let mut multibuffer = MultiBuffer::new(ReadWrite);
20079 multibuffer.push_excerpts(
20080 buffer_1.clone(),
20081 [
20082 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20083 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20084 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20085 ],
20086 cx,
20087 );
20088 multibuffer.push_excerpts(
20089 buffer_2.clone(),
20090 [
20091 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20092 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20093 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20094 ],
20095 cx,
20096 );
20097 multibuffer.push_excerpts(
20098 buffer_3.clone(),
20099 [
20100 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20101 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20102 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20103 ],
20104 cx,
20105 );
20106 multibuffer
20107 });
20108
20109 let fs = FakeFs::new(cx.executor());
20110 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20111 let (editor, cx) = cx
20112 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
20113 editor.update_in(cx, |editor, _window, cx| {
20114 for (buffer, diff_base) in [
20115 (buffer_1.clone(), base_text_1),
20116 (buffer_2.clone(), base_text_2),
20117 (buffer_3.clone(), base_text_3),
20118 ] {
20119 let diff = cx.new(|cx| {
20120 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
20121 });
20122 editor
20123 .buffer
20124 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20125 }
20126 });
20127 cx.executor().run_until_parked();
20128
20129 editor.update_in(cx, |editor, window, cx| {
20130 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}");
20131 editor.select_all(&SelectAll, window, cx);
20132 editor.git_restore(&Default::default(), window, cx);
20133 });
20134 cx.executor().run_until_parked();
20135
20136 // When all ranges are selected, all buffer hunks are reverted.
20137 editor.update(cx, |editor, cx| {
20138 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");
20139 });
20140 buffer_1.update(cx, |buffer, _| {
20141 assert_eq!(buffer.text(), base_text_1);
20142 });
20143 buffer_2.update(cx, |buffer, _| {
20144 assert_eq!(buffer.text(), base_text_2);
20145 });
20146 buffer_3.update(cx, |buffer, _| {
20147 assert_eq!(buffer.text(), base_text_3);
20148 });
20149
20150 editor.update_in(cx, |editor, window, cx| {
20151 editor.undo(&Default::default(), window, cx);
20152 });
20153
20154 editor.update_in(cx, |editor, window, cx| {
20155 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20156 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
20157 });
20158 editor.git_restore(&Default::default(), window, cx);
20159 });
20160
20161 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
20162 // but not affect buffer_2 and its related excerpts.
20163 editor.update(cx, |editor, cx| {
20164 assert_eq!(
20165 editor.text(cx),
20166 "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}"
20167 );
20168 });
20169 buffer_1.update(cx, |buffer, _| {
20170 assert_eq!(buffer.text(), base_text_1);
20171 });
20172 buffer_2.update(cx, |buffer, _| {
20173 assert_eq!(
20174 buffer.text(),
20175 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
20176 );
20177 });
20178 buffer_3.update(cx, |buffer, _| {
20179 assert_eq!(
20180 buffer.text(),
20181 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
20182 );
20183 });
20184
20185 fn edit_first_char_of_every_line(text: &str) -> String {
20186 text.split('\n')
20187 .map(|line| format!("X{}", &line[1..]))
20188 .collect::<Vec<_>>()
20189 .join("\n")
20190 }
20191}
20192
20193#[gpui::test]
20194async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
20195 init_test(cx, |_| {});
20196
20197 let cols = 4;
20198 let rows = 10;
20199 let sample_text_1 = sample_text(rows, cols, 'a');
20200 assert_eq!(
20201 sample_text_1,
20202 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
20203 );
20204 let sample_text_2 = sample_text(rows, cols, 'l');
20205 assert_eq!(
20206 sample_text_2,
20207 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
20208 );
20209 let sample_text_3 = sample_text(rows, cols, 'v');
20210 assert_eq!(
20211 sample_text_3,
20212 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
20213 );
20214
20215 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
20216 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
20217 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
20218
20219 let multi_buffer = cx.new(|cx| {
20220 let mut multibuffer = MultiBuffer::new(ReadWrite);
20221 multibuffer.push_excerpts(
20222 buffer_1.clone(),
20223 [
20224 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20225 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20226 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20227 ],
20228 cx,
20229 );
20230 multibuffer.push_excerpts(
20231 buffer_2.clone(),
20232 [
20233 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20234 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20235 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20236 ],
20237 cx,
20238 );
20239 multibuffer.push_excerpts(
20240 buffer_3.clone(),
20241 [
20242 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20243 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20244 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20245 ],
20246 cx,
20247 );
20248 multibuffer
20249 });
20250
20251 let fs = FakeFs::new(cx.executor());
20252 fs.insert_tree(
20253 "/a",
20254 json!({
20255 "main.rs": sample_text_1,
20256 "other.rs": sample_text_2,
20257 "lib.rs": sample_text_3,
20258 }),
20259 )
20260 .await;
20261 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20262 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20263 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20264 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20265 Editor::new(
20266 EditorMode::full(),
20267 multi_buffer,
20268 Some(project.clone()),
20269 window,
20270 cx,
20271 )
20272 });
20273 let multibuffer_item_id = workspace
20274 .update(cx, |workspace, window, cx| {
20275 assert!(
20276 workspace.active_item(cx).is_none(),
20277 "active item should be None before the first item is added"
20278 );
20279 workspace.add_item_to_active_pane(
20280 Box::new(multi_buffer_editor.clone()),
20281 None,
20282 true,
20283 window,
20284 cx,
20285 );
20286 let active_item = workspace
20287 .active_item(cx)
20288 .expect("should have an active item after adding the multi buffer");
20289 assert_eq!(
20290 active_item.buffer_kind(cx),
20291 ItemBufferKind::Multibuffer,
20292 "A multi buffer was expected to active after adding"
20293 );
20294 active_item.item_id()
20295 })
20296 .unwrap();
20297 cx.executor().run_until_parked();
20298
20299 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20300 editor.change_selections(
20301 SelectionEffects::scroll(Autoscroll::Next),
20302 window,
20303 cx,
20304 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
20305 );
20306 editor.open_excerpts(&OpenExcerpts, window, cx);
20307 });
20308 cx.executor().run_until_parked();
20309 let first_item_id = workspace
20310 .update(cx, |workspace, window, cx| {
20311 let active_item = workspace
20312 .active_item(cx)
20313 .expect("should have an active item after navigating into the 1st buffer");
20314 let first_item_id = active_item.item_id();
20315 assert_ne!(
20316 first_item_id, multibuffer_item_id,
20317 "Should navigate into the 1st buffer and activate it"
20318 );
20319 assert_eq!(
20320 active_item.buffer_kind(cx),
20321 ItemBufferKind::Singleton,
20322 "New active item should be a singleton buffer"
20323 );
20324 assert_eq!(
20325 active_item
20326 .act_as::<Editor>(cx)
20327 .expect("should have navigated into an editor for the 1st buffer")
20328 .read(cx)
20329 .text(cx),
20330 sample_text_1
20331 );
20332
20333 workspace
20334 .go_back(workspace.active_pane().downgrade(), window, cx)
20335 .detach_and_log_err(cx);
20336
20337 first_item_id
20338 })
20339 .unwrap();
20340 cx.executor().run_until_parked();
20341 workspace
20342 .update(cx, |workspace, _, cx| {
20343 let active_item = workspace
20344 .active_item(cx)
20345 .expect("should have an active item after navigating back");
20346 assert_eq!(
20347 active_item.item_id(),
20348 multibuffer_item_id,
20349 "Should navigate back to the multi buffer"
20350 );
20351 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20352 })
20353 .unwrap();
20354
20355 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20356 editor.change_selections(
20357 SelectionEffects::scroll(Autoscroll::Next),
20358 window,
20359 cx,
20360 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
20361 );
20362 editor.open_excerpts(&OpenExcerpts, window, cx);
20363 });
20364 cx.executor().run_until_parked();
20365 let second_item_id = workspace
20366 .update(cx, |workspace, window, cx| {
20367 let active_item = workspace
20368 .active_item(cx)
20369 .expect("should have an active item after navigating into the 2nd buffer");
20370 let second_item_id = active_item.item_id();
20371 assert_ne!(
20372 second_item_id, multibuffer_item_id,
20373 "Should navigate away from the multibuffer"
20374 );
20375 assert_ne!(
20376 second_item_id, first_item_id,
20377 "Should navigate into the 2nd buffer and activate it"
20378 );
20379 assert_eq!(
20380 active_item.buffer_kind(cx),
20381 ItemBufferKind::Singleton,
20382 "New active item should be a singleton buffer"
20383 );
20384 assert_eq!(
20385 active_item
20386 .act_as::<Editor>(cx)
20387 .expect("should have navigated into an editor")
20388 .read(cx)
20389 .text(cx),
20390 sample_text_2
20391 );
20392
20393 workspace
20394 .go_back(workspace.active_pane().downgrade(), window, cx)
20395 .detach_and_log_err(cx);
20396
20397 second_item_id
20398 })
20399 .unwrap();
20400 cx.executor().run_until_parked();
20401 workspace
20402 .update(cx, |workspace, _, cx| {
20403 let active_item = workspace
20404 .active_item(cx)
20405 .expect("should have an active item after navigating back from the 2nd buffer");
20406 assert_eq!(
20407 active_item.item_id(),
20408 multibuffer_item_id,
20409 "Should navigate back from the 2nd buffer to the multi buffer"
20410 );
20411 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20412 })
20413 .unwrap();
20414
20415 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20416 editor.change_selections(
20417 SelectionEffects::scroll(Autoscroll::Next),
20418 window,
20419 cx,
20420 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
20421 );
20422 editor.open_excerpts(&OpenExcerpts, window, cx);
20423 });
20424 cx.executor().run_until_parked();
20425 workspace
20426 .update(cx, |workspace, window, cx| {
20427 let active_item = workspace
20428 .active_item(cx)
20429 .expect("should have an active item after navigating into the 3rd buffer");
20430 let third_item_id = active_item.item_id();
20431 assert_ne!(
20432 third_item_id, multibuffer_item_id,
20433 "Should navigate into the 3rd buffer and activate it"
20434 );
20435 assert_ne!(third_item_id, first_item_id);
20436 assert_ne!(third_item_id, second_item_id);
20437 assert_eq!(
20438 active_item.buffer_kind(cx),
20439 ItemBufferKind::Singleton,
20440 "New active item should be a singleton buffer"
20441 );
20442 assert_eq!(
20443 active_item
20444 .act_as::<Editor>(cx)
20445 .expect("should have navigated into an editor")
20446 .read(cx)
20447 .text(cx),
20448 sample_text_3
20449 );
20450
20451 workspace
20452 .go_back(workspace.active_pane().downgrade(), window, cx)
20453 .detach_and_log_err(cx);
20454 })
20455 .unwrap();
20456 cx.executor().run_until_parked();
20457 workspace
20458 .update(cx, |workspace, _, cx| {
20459 let active_item = workspace
20460 .active_item(cx)
20461 .expect("should have an active item after navigating back from the 3rd buffer");
20462 assert_eq!(
20463 active_item.item_id(),
20464 multibuffer_item_id,
20465 "Should navigate back from the 3rd buffer to the multi buffer"
20466 );
20467 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20468 })
20469 .unwrap();
20470}
20471
20472#[gpui::test]
20473async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20474 init_test(cx, |_| {});
20475
20476 let mut cx = EditorTestContext::new(cx).await;
20477
20478 let diff_base = r#"
20479 use some::mod;
20480
20481 const A: u32 = 42;
20482
20483 fn main() {
20484 println!("hello");
20485
20486 println!("world");
20487 }
20488 "#
20489 .unindent();
20490
20491 cx.set_state(
20492 &r#"
20493 use some::modified;
20494
20495 ˇ
20496 fn main() {
20497 println!("hello there");
20498
20499 println!("around the");
20500 println!("world");
20501 }
20502 "#
20503 .unindent(),
20504 );
20505
20506 cx.set_head_text(&diff_base);
20507 executor.run_until_parked();
20508
20509 cx.update_editor(|editor, window, cx| {
20510 editor.go_to_next_hunk(&GoToHunk, window, cx);
20511 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20512 });
20513 executor.run_until_parked();
20514 cx.assert_state_with_diff(
20515 r#"
20516 use some::modified;
20517
20518
20519 fn main() {
20520 - println!("hello");
20521 + ˇ println!("hello there");
20522
20523 println!("around the");
20524 println!("world");
20525 }
20526 "#
20527 .unindent(),
20528 );
20529
20530 cx.update_editor(|editor, window, cx| {
20531 for _ in 0..2 {
20532 editor.go_to_next_hunk(&GoToHunk, window, cx);
20533 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20534 }
20535 });
20536 executor.run_until_parked();
20537 cx.assert_state_with_diff(
20538 r#"
20539 - use some::mod;
20540 + ˇuse some::modified;
20541
20542
20543 fn main() {
20544 - println!("hello");
20545 + println!("hello there");
20546
20547 + println!("around the");
20548 println!("world");
20549 }
20550 "#
20551 .unindent(),
20552 );
20553
20554 cx.update_editor(|editor, window, cx| {
20555 editor.go_to_next_hunk(&GoToHunk, window, cx);
20556 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20557 });
20558 executor.run_until_parked();
20559 cx.assert_state_with_diff(
20560 r#"
20561 - use some::mod;
20562 + use some::modified;
20563
20564 - const A: u32 = 42;
20565 ˇ
20566 fn main() {
20567 - println!("hello");
20568 + println!("hello there");
20569
20570 + println!("around the");
20571 println!("world");
20572 }
20573 "#
20574 .unindent(),
20575 );
20576
20577 cx.update_editor(|editor, window, cx| {
20578 editor.cancel(&Cancel, window, cx);
20579 });
20580
20581 cx.assert_state_with_diff(
20582 r#"
20583 use some::modified;
20584
20585 ˇ
20586 fn main() {
20587 println!("hello there");
20588
20589 println!("around the");
20590 println!("world");
20591 }
20592 "#
20593 .unindent(),
20594 );
20595}
20596
20597#[gpui::test]
20598async fn test_diff_base_change_with_expanded_diff_hunks(
20599 executor: BackgroundExecutor,
20600 cx: &mut TestAppContext,
20601) {
20602 init_test(cx, |_| {});
20603
20604 let mut cx = EditorTestContext::new(cx).await;
20605
20606 let diff_base = r#"
20607 use some::mod1;
20608 use some::mod2;
20609
20610 const A: u32 = 42;
20611 const B: u32 = 42;
20612 const C: u32 = 42;
20613
20614 fn main() {
20615 println!("hello");
20616
20617 println!("world");
20618 }
20619 "#
20620 .unindent();
20621
20622 cx.set_state(
20623 &r#"
20624 use some::mod2;
20625
20626 const A: u32 = 42;
20627 const C: u32 = 42;
20628
20629 fn main(ˇ) {
20630 //println!("hello");
20631
20632 println!("world");
20633 //
20634 //
20635 }
20636 "#
20637 .unindent(),
20638 );
20639
20640 cx.set_head_text(&diff_base);
20641 executor.run_until_parked();
20642
20643 cx.update_editor(|editor, window, cx| {
20644 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20645 });
20646 executor.run_until_parked();
20647 cx.assert_state_with_diff(
20648 r#"
20649 - use some::mod1;
20650 use some::mod2;
20651
20652 const A: u32 = 42;
20653 - const B: u32 = 42;
20654 const C: u32 = 42;
20655
20656 fn main(ˇ) {
20657 - println!("hello");
20658 + //println!("hello");
20659
20660 println!("world");
20661 + //
20662 + //
20663 }
20664 "#
20665 .unindent(),
20666 );
20667
20668 cx.set_head_text("new diff base!");
20669 executor.run_until_parked();
20670 cx.assert_state_with_diff(
20671 r#"
20672 - new diff base!
20673 + use some::mod2;
20674 +
20675 + const A: u32 = 42;
20676 + const C: u32 = 42;
20677 +
20678 + fn main(ˇ) {
20679 + //println!("hello");
20680 +
20681 + println!("world");
20682 + //
20683 + //
20684 + }
20685 "#
20686 .unindent(),
20687 );
20688}
20689
20690#[gpui::test]
20691async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
20692 init_test(cx, |_| {});
20693
20694 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20695 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20696 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20697 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20698 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
20699 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
20700
20701 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
20702 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
20703 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
20704
20705 let multi_buffer = cx.new(|cx| {
20706 let mut multibuffer = MultiBuffer::new(ReadWrite);
20707 multibuffer.push_excerpts(
20708 buffer_1.clone(),
20709 [
20710 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20711 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20712 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20713 ],
20714 cx,
20715 );
20716 multibuffer.push_excerpts(
20717 buffer_2.clone(),
20718 [
20719 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20720 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20721 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20722 ],
20723 cx,
20724 );
20725 multibuffer.push_excerpts(
20726 buffer_3.clone(),
20727 [
20728 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20729 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20730 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20731 ],
20732 cx,
20733 );
20734 multibuffer
20735 });
20736
20737 let editor =
20738 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20739 editor
20740 .update(cx, |editor, _window, cx| {
20741 for (buffer, diff_base) in [
20742 (buffer_1.clone(), file_1_old),
20743 (buffer_2.clone(), file_2_old),
20744 (buffer_3.clone(), file_3_old),
20745 ] {
20746 let diff = cx.new(|cx| {
20747 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
20748 });
20749 editor
20750 .buffer
20751 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20752 }
20753 })
20754 .unwrap();
20755
20756 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20757 cx.run_until_parked();
20758
20759 cx.assert_editor_state(
20760 &"
20761 ˇaaa
20762 ccc
20763 ddd
20764
20765 ggg
20766 hhh
20767
20768
20769 lll
20770 mmm
20771 NNN
20772
20773 qqq
20774 rrr
20775
20776 uuu
20777 111
20778 222
20779 333
20780
20781 666
20782 777
20783
20784 000
20785 !!!"
20786 .unindent(),
20787 );
20788
20789 cx.update_editor(|editor, window, cx| {
20790 editor.select_all(&SelectAll, window, cx);
20791 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20792 });
20793 cx.executor().run_until_parked();
20794
20795 cx.assert_state_with_diff(
20796 "
20797 «aaa
20798 - bbb
20799 ccc
20800 ddd
20801
20802 ggg
20803 hhh
20804
20805
20806 lll
20807 mmm
20808 - nnn
20809 + NNN
20810
20811 qqq
20812 rrr
20813
20814 uuu
20815 111
20816 222
20817 333
20818
20819 + 666
20820 777
20821
20822 000
20823 !!!ˇ»"
20824 .unindent(),
20825 );
20826}
20827
20828#[gpui::test]
20829async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
20830 init_test(cx, |_| {});
20831
20832 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
20833 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
20834
20835 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
20836 let multi_buffer = cx.new(|cx| {
20837 let mut multibuffer = MultiBuffer::new(ReadWrite);
20838 multibuffer.push_excerpts(
20839 buffer.clone(),
20840 [
20841 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
20842 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
20843 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
20844 ],
20845 cx,
20846 );
20847 multibuffer
20848 });
20849
20850 let editor =
20851 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20852 editor
20853 .update(cx, |editor, _window, cx| {
20854 let diff = cx.new(|cx| {
20855 BufferDiff::new_with_base_text(base, &buffer.read(cx).text_snapshot(), cx)
20856 });
20857 editor
20858 .buffer
20859 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
20860 })
20861 .unwrap();
20862
20863 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20864 cx.run_until_parked();
20865
20866 cx.update_editor(|editor, window, cx| {
20867 editor.expand_all_diff_hunks(&Default::default(), window, cx)
20868 });
20869 cx.executor().run_until_parked();
20870
20871 // When the start of a hunk coincides with the start of its excerpt,
20872 // the hunk is expanded. When the start of a hunk is earlier than
20873 // the start of its excerpt, the hunk is not expanded.
20874 cx.assert_state_with_diff(
20875 "
20876 ˇaaa
20877 - bbb
20878 + BBB
20879
20880 - ddd
20881 - eee
20882 + DDD
20883 + EEE
20884 fff
20885
20886 iii
20887 "
20888 .unindent(),
20889 );
20890}
20891
20892#[gpui::test]
20893async fn test_edits_around_expanded_insertion_hunks(
20894 executor: BackgroundExecutor,
20895 cx: &mut TestAppContext,
20896) {
20897 init_test(cx, |_| {});
20898
20899 let mut cx = EditorTestContext::new(cx).await;
20900
20901 let diff_base = r#"
20902 use some::mod1;
20903 use some::mod2;
20904
20905 const A: u32 = 42;
20906
20907 fn main() {
20908 println!("hello");
20909
20910 println!("world");
20911 }
20912 "#
20913 .unindent();
20914 executor.run_until_parked();
20915 cx.set_state(
20916 &r#"
20917 use some::mod1;
20918 use some::mod2;
20919
20920 const A: u32 = 42;
20921 const B: u32 = 42;
20922 const C: u32 = 42;
20923 ˇ
20924
20925 fn main() {
20926 println!("hello");
20927
20928 println!("world");
20929 }
20930 "#
20931 .unindent(),
20932 );
20933
20934 cx.set_head_text(&diff_base);
20935 executor.run_until_parked();
20936
20937 cx.update_editor(|editor, window, cx| {
20938 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20939 });
20940 executor.run_until_parked();
20941
20942 cx.assert_state_with_diff(
20943 r#"
20944 use some::mod1;
20945 use some::mod2;
20946
20947 const A: u32 = 42;
20948 + const B: u32 = 42;
20949 + const C: u32 = 42;
20950 + ˇ
20951
20952 fn main() {
20953 println!("hello");
20954
20955 println!("world");
20956 }
20957 "#
20958 .unindent(),
20959 );
20960
20961 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
20962 executor.run_until_parked();
20963
20964 cx.assert_state_with_diff(
20965 r#"
20966 use some::mod1;
20967 use some::mod2;
20968
20969 const A: u32 = 42;
20970 + const B: u32 = 42;
20971 + const C: u32 = 42;
20972 + const D: u32 = 42;
20973 + ˇ
20974
20975 fn main() {
20976 println!("hello");
20977
20978 println!("world");
20979 }
20980 "#
20981 .unindent(),
20982 );
20983
20984 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
20985 executor.run_until_parked();
20986
20987 cx.assert_state_with_diff(
20988 r#"
20989 use some::mod1;
20990 use some::mod2;
20991
20992 const A: u32 = 42;
20993 + const B: u32 = 42;
20994 + const C: u32 = 42;
20995 + const D: u32 = 42;
20996 + const E: u32 = 42;
20997 + ˇ
20998
20999 fn main() {
21000 println!("hello");
21001
21002 println!("world");
21003 }
21004 "#
21005 .unindent(),
21006 );
21007
21008 cx.update_editor(|editor, window, cx| {
21009 editor.delete_line(&DeleteLine, window, cx);
21010 });
21011 executor.run_until_parked();
21012
21013 cx.assert_state_with_diff(
21014 r#"
21015 use some::mod1;
21016 use some::mod2;
21017
21018 const A: u32 = 42;
21019 + const B: u32 = 42;
21020 + const C: u32 = 42;
21021 + const D: u32 = 42;
21022 + const E: u32 = 42;
21023 ˇ
21024 fn main() {
21025 println!("hello");
21026
21027 println!("world");
21028 }
21029 "#
21030 .unindent(),
21031 );
21032
21033 cx.update_editor(|editor, window, cx| {
21034 editor.move_up(&MoveUp, window, cx);
21035 editor.delete_line(&DeleteLine, window, cx);
21036 editor.move_up(&MoveUp, window, cx);
21037 editor.delete_line(&DeleteLine, window, cx);
21038 editor.move_up(&MoveUp, window, cx);
21039 editor.delete_line(&DeleteLine, window, cx);
21040 });
21041 executor.run_until_parked();
21042 cx.assert_state_with_diff(
21043 r#"
21044 use some::mod1;
21045 use some::mod2;
21046
21047 const A: u32 = 42;
21048 + const B: u32 = 42;
21049 ˇ
21050 fn main() {
21051 println!("hello");
21052
21053 println!("world");
21054 }
21055 "#
21056 .unindent(),
21057 );
21058
21059 cx.update_editor(|editor, window, cx| {
21060 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
21061 editor.delete_line(&DeleteLine, window, cx);
21062 });
21063 executor.run_until_parked();
21064 cx.assert_state_with_diff(
21065 r#"
21066 ˇ
21067 fn main() {
21068 println!("hello");
21069
21070 println!("world");
21071 }
21072 "#
21073 .unindent(),
21074 );
21075}
21076
21077#[gpui::test]
21078async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
21079 init_test(cx, |_| {});
21080
21081 let mut cx = EditorTestContext::new(cx).await;
21082 cx.set_head_text(indoc! { "
21083 one
21084 two
21085 three
21086 four
21087 five
21088 "
21089 });
21090 cx.set_state(indoc! { "
21091 one
21092 ˇthree
21093 five
21094 "});
21095 cx.run_until_parked();
21096 cx.update_editor(|editor, window, cx| {
21097 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21098 });
21099 cx.assert_state_with_diff(
21100 indoc! { "
21101 one
21102 - two
21103 ˇthree
21104 - four
21105 five
21106 "}
21107 .to_string(),
21108 );
21109 cx.update_editor(|editor, window, cx| {
21110 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21111 });
21112
21113 cx.assert_state_with_diff(
21114 indoc! { "
21115 one
21116 ˇthree
21117 five
21118 "}
21119 .to_string(),
21120 );
21121
21122 cx.update_editor(|editor, window, cx| {
21123 editor.move_up(&MoveUp, window, cx);
21124 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21125 });
21126 cx.assert_state_with_diff(
21127 indoc! { "
21128 ˇone
21129 - two
21130 three
21131 five
21132 "}
21133 .to_string(),
21134 );
21135
21136 cx.update_editor(|editor, window, cx| {
21137 editor.move_down(&MoveDown, window, cx);
21138 editor.move_down(&MoveDown, window, cx);
21139 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21140 });
21141 cx.assert_state_with_diff(
21142 indoc! { "
21143 one
21144 - two
21145 ˇthree
21146 - four
21147 five
21148 "}
21149 .to_string(),
21150 );
21151
21152 cx.set_state(indoc! { "
21153 one
21154 ˇTWO
21155 three
21156 four
21157 five
21158 "});
21159 cx.run_until_parked();
21160 cx.update_editor(|editor, window, cx| {
21161 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21162 });
21163
21164 cx.assert_state_with_diff(
21165 indoc! { "
21166 one
21167 - two
21168 + ˇTWO
21169 three
21170 four
21171 five
21172 "}
21173 .to_string(),
21174 );
21175 cx.update_editor(|editor, window, cx| {
21176 editor.move_up(&Default::default(), window, cx);
21177 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21178 });
21179 cx.assert_state_with_diff(
21180 indoc! { "
21181 one
21182 ˇTWO
21183 three
21184 four
21185 five
21186 "}
21187 .to_string(),
21188 );
21189}
21190
21191#[gpui::test]
21192async fn test_toggling_adjacent_diff_hunks_2(
21193 executor: BackgroundExecutor,
21194 cx: &mut TestAppContext,
21195) {
21196 init_test(cx, |_| {});
21197
21198 let mut cx = EditorTestContext::new(cx).await;
21199
21200 let diff_base = r#"
21201 lineA
21202 lineB
21203 lineC
21204 lineD
21205 "#
21206 .unindent();
21207
21208 cx.set_state(
21209 &r#"
21210 ˇlineA1
21211 lineB
21212 lineD
21213 "#
21214 .unindent(),
21215 );
21216 cx.set_head_text(&diff_base);
21217 executor.run_until_parked();
21218
21219 cx.update_editor(|editor, window, cx| {
21220 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21221 });
21222 executor.run_until_parked();
21223 cx.assert_state_with_diff(
21224 r#"
21225 - lineA
21226 + ˇlineA1
21227 lineB
21228 lineD
21229 "#
21230 .unindent(),
21231 );
21232
21233 cx.update_editor(|editor, window, cx| {
21234 editor.move_down(&MoveDown, window, cx);
21235 editor.move_right(&MoveRight, window, cx);
21236 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21237 });
21238 executor.run_until_parked();
21239 cx.assert_state_with_diff(
21240 r#"
21241 - lineA
21242 + lineA1
21243 lˇineB
21244 - lineC
21245 lineD
21246 "#
21247 .unindent(),
21248 );
21249}
21250
21251#[gpui::test]
21252async fn test_edits_around_expanded_deletion_hunks(
21253 executor: BackgroundExecutor,
21254 cx: &mut TestAppContext,
21255) {
21256 init_test(cx, |_| {});
21257
21258 let mut cx = EditorTestContext::new(cx).await;
21259
21260 let diff_base = r#"
21261 use some::mod1;
21262 use some::mod2;
21263
21264 const A: u32 = 42;
21265 const B: u32 = 42;
21266 const C: u32 = 42;
21267
21268
21269 fn main() {
21270 println!("hello");
21271
21272 println!("world");
21273 }
21274 "#
21275 .unindent();
21276 executor.run_until_parked();
21277 cx.set_state(
21278 &r#"
21279 use some::mod1;
21280 use some::mod2;
21281
21282 ˇconst B: u32 = 42;
21283 const C: u32 = 42;
21284
21285
21286 fn main() {
21287 println!("hello");
21288
21289 println!("world");
21290 }
21291 "#
21292 .unindent(),
21293 );
21294
21295 cx.set_head_text(&diff_base);
21296 executor.run_until_parked();
21297
21298 cx.update_editor(|editor, window, cx| {
21299 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21300 });
21301 executor.run_until_parked();
21302
21303 cx.assert_state_with_diff(
21304 r#"
21305 use some::mod1;
21306 use some::mod2;
21307
21308 - const A: u32 = 42;
21309 ˇconst B: u32 = 42;
21310 const C: u32 = 42;
21311
21312
21313 fn main() {
21314 println!("hello");
21315
21316 println!("world");
21317 }
21318 "#
21319 .unindent(),
21320 );
21321
21322 cx.update_editor(|editor, window, cx| {
21323 editor.delete_line(&DeleteLine, window, cx);
21324 });
21325 executor.run_until_parked();
21326 cx.assert_state_with_diff(
21327 r#"
21328 use some::mod1;
21329 use some::mod2;
21330
21331 - const A: u32 = 42;
21332 - const B: u32 = 42;
21333 ˇconst C: u32 = 42;
21334
21335
21336 fn main() {
21337 println!("hello");
21338
21339 println!("world");
21340 }
21341 "#
21342 .unindent(),
21343 );
21344
21345 cx.update_editor(|editor, window, cx| {
21346 editor.delete_line(&DeleteLine, window, cx);
21347 });
21348 executor.run_until_parked();
21349 cx.assert_state_with_diff(
21350 r#"
21351 use some::mod1;
21352 use some::mod2;
21353
21354 - const A: u32 = 42;
21355 - const B: u32 = 42;
21356 - const C: u32 = 42;
21357 ˇ
21358
21359 fn main() {
21360 println!("hello");
21361
21362 println!("world");
21363 }
21364 "#
21365 .unindent(),
21366 );
21367
21368 cx.update_editor(|editor, window, cx| {
21369 editor.handle_input("replacement", window, cx);
21370 });
21371 executor.run_until_parked();
21372 cx.assert_state_with_diff(
21373 r#"
21374 use some::mod1;
21375 use some::mod2;
21376
21377 - const A: u32 = 42;
21378 - const B: u32 = 42;
21379 - const C: u32 = 42;
21380 -
21381 + replacementˇ
21382
21383 fn main() {
21384 println!("hello");
21385
21386 println!("world");
21387 }
21388 "#
21389 .unindent(),
21390 );
21391}
21392
21393#[gpui::test]
21394async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21395 init_test(cx, |_| {});
21396
21397 let mut cx = EditorTestContext::new(cx).await;
21398
21399 let base_text = r#"
21400 one
21401 two
21402 three
21403 four
21404 five
21405 "#
21406 .unindent();
21407 executor.run_until_parked();
21408 cx.set_state(
21409 &r#"
21410 one
21411 two
21412 fˇour
21413 five
21414 "#
21415 .unindent(),
21416 );
21417
21418 cx.set_head_text(&base_text);
21419 executor.run_until_parked();
21420
21421 cx.update_editor(|editor, window, cx| {
21422 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21423 });
21424 executor.run_until_parked();
21425
21426 cx.assert_state_with_diff(
21427 r#"
21428 one
21429 two
21430 - three
21431 fˇour
21432 five
21433 "#
21434 .unindent(),
21435 );
21436
21437 cx.update_editor(|editor, window, cx| {
21438 editor.backspace(&Backspace, window, cx);
21439 editor.backspace(&Backspace, window, cx);
21440 });
21441 executor.run_until_parked();
21442 cx.assert_state_with_diff(
21443 r#"
21444 one
21445 two
21446 - threeˇ
21447 - four
21448 + our
21449 five
21450 "#
21451 .unindent(),
21452 );
21453}
21454
21455#[gpui::test]
21456async fn test_edit_after_expanded_modification_hunk(
21457 executor: BackgroundExecutor,
21458 cx: &mut TestAppContext,
21459) {
21460 init_test(cx, |_| {});
21461
21462 let mut cx = EditorTestContext::new(cx).await;
21463
21464 let diff_base = r#"
21465 use some::mod1;
21466 use some::mod2;
21467
21468 const A: u32 = 42;
21469 const B: u32 = 42;
21470 const C: u32 = 42;
21471 const D: u32 = 42;
21472
21473
21474 fn main() {
21475 println!("hello");
21476
21477 println!("world");
21478 }"#
21479 .unindent();
21480
21481 cx.set_state(
21482 &r#"
21483 use some::mod1;
21484 use some::mod2;
21485
21486 const A: u32 = 42;
21487 const B: u32 = 42;
21488 const C: u32 = 43ˇ
21489 const D: u32 = 42;
21490
21491
21492 fn main() {
21493 println!("hello");
21494
21495 println!("world");
21496 }"#
21497 .unindent(),
21498 );
21499
21500 cx.set_head_text(&diff_base);
21501 executor.run_until_parked();
21502 cx.update_editor(|editor, window, cx| {
21503 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21504 });
21505 executor.run_until_parked();
21506
21507 cx.assert_state_with_diff(
21508 r#"
21509 use some::mod1;
21510 use some::mod2;
21511
21512 const A: u32 = 42;
21513 const B: u32 = 42;
21514 - const C: u32 = 42;
21515 + const C: u32 = 43ˇ
21516 const D: u32 = 42;
21517
21518
21519 fn main() {
21520 println!("hello");
21521
21522 println!("world");
21523 }"#
21524 .unindent(),
21525 );
21526
21527 cx.update_editor(|editor, window, cx| {
21528 editor.handle_input("\nnew_line\n", window, cx);
21529 });
21530 executor.run_until_parked();
21531
21532 cx.assert_state_with_diff(
21533 r#"
21534 use some::mod1;
21535 use some::mod2;
21536
21537 const A: u32 = 42;
21538 const B: u32 = 42;
21539 - const C: u32 = 42;
21540 + const C: u32 = 43
21541 + new_line
21542 + ˇ
21543 const D: u32 = 42;
21544
21545
21546 fn main() {
21547 println!("hello");
21548
21549 println!("world");
21550 }"#
21551 .unindent(),
21552 );
21553}
21554
21555#[gpui::test]
21556async fn test_stage_and_unstage_added_file_hunk(
21557 executor: BackgroundExecutor,
21558 cx: &mut TestAppContext,
21559) {
21560 init_test(cx, |_| {});
21561
21562 let mut cx = EditorTestContext::new(cx).await;
21563 cx.update_editor(|editor, _, cx| {
21564 editor.set_expand_all_diff_hunks(cx);
21565 });
21566
21567 let working_copy = r#"
21568 ˇfn main() {
21569 println!("hello, world!");
21570 }
21571 "#
21572 .unindent();
21573
21574 cx.set_state(&working_copy);
21575 executor.run_until_parked();
21576
21577 cx.assert_state_with_diff(
21578 r#"
21579 + ˇfn main() {
21580 + println!("hello, world!");
21581 + }
21582 "#
21583 .unindent(),
21584 );
21585 cx.assert_index_text(None);
21586
21587 cx.update_editor(|editor, window, cx| {
21588 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21589 });
21590 executor.run_until_parked();
21591 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
21592 cx.assert_state_with_diff(
21593 r#"
21594 + ˇfn main() {
21595 + println!("hello, world!");
21596 + }
21597 "#
21598 .unindent(),
21599 );
21600
21601 cx.update_editor(|editor, window, cx| {
21602 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21603 });
21604 executor.run_until_parked();
21605 cx.assert_index_text(None);
21606}
21607
21608async fn setup_indent_guides_editor(
21609 text: &str,
21610 cx: &mut TestAppContext,
21611) -> (BufferId, EditorTestContext) {
21612 init_test(cx, |_| {});
21613
21614 let mut cx = EditorTestContext::new(cx).await;
21615
21616 let buffer_id = cx.update_editor(|editor, window, cx| {
21617 editor.set_text(text, window, cx);
21618 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
21619
21620 buffer_ids[0]
21621 });
21622
21623 (buffer_id, cx)
21624}
21625
21626fn assert_indent_guides(
21627 range: Range<u32>,
21628 expected: Vec<IndentGuide>,
21629 active_indices: Option<Vec<usize>>,
21630 cx: &mut EditorTestContext,
21631) {
21632 let indent_guides = cx.update_editor(|editor, window, cx| {
21633 let snapshot = editor.snapshot(window, cx).display_snapshot;
21634 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
21635 editor,
21636 MultiBufferRow(range.start)..MultiBufferRow(range.end),
21637 true,
21638 &snapshot,
21639 cx,
21640 );
21641
21642 indent_guides.sort_by(|a, b| {
21643 a.depth.cmp(&b.depth).then(
21644 a.start_row
21645 .cmp(&b.start_row)
21646 .then(a.end_row.cmp(&b.end_row)),
21647 )
21648 });
21649 indent_guides
21650 });
21651
21652 if let Some(expected) = active_indices {
21653 let active_indices = cx.update_editor(|editor, window, cx| {
21654 let snapshot = editor.snapshot(window, cx).display_snapshot;
21655 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
21656 });
21657
21658 assert_eq!(
21659 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
21660 expected,
21661 "Active indent guide indices do not match"
21662 );
21663 }
21664
21665 assert_eq!(indent_guides, expected, "Indent guides do not match");
21666}
21667
21668fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
21669 IndentGuide {
21670 buffer_id,
21671 start_row: MultiBufferRow(start_row),
21672 end_row: MultiBufferRow(end_row),
21673 depth,
21674 tab_size: 4,
21675 settings: IndentGuideSettings {
21676 enabled: true,
21677 line_width: 1,
21678 active_line_width: 1,
21679 coloring: IndentGuideColoring::default(),
21680 background_coloring: IndentGuideBackgroundColoring::default(),
21681 },
21682 }
21683}
21684
21685#[gpui::test]
21686async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
21687 let (buffer_id, mut cx) = setup_indent_guides_editor(
21688 &"
21689 fn main() {
21690 let a = 1;
21691 }"
21692 .unindent(),
21693 cx,
21694 )
21695 .await;
21696
21697 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21698}
21699
21700#[gpui::test]
21701async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
21702 let (buffer_id, mut cx) = setup_indent_guides_editor(
21703 &"
21704 fn main() {
21705 let a = 1;
21706 let b = 2;
21707 }"
21708 .unindent(),
21709 cx,
21710 )
21711 .await;
21712
21713 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
21714}
21715
21716#[gpui::test]
21717async fn test_indent_guide_nested(cx: &mut TestAppContext) {
21718 let (buffer_id, mut cx) = setup_indent_guides_editor(
21719 &"
21720 fn main() {
21721 let a = 1;
21722 if a == 3 {
21723 let b = 2;
21724 } else {
21725 let c = 3;
21726 }
21727 }"
21728 .unindent(),
21729 cx,
21730 )
21731 .await;
21732
21733 assert_indent_guides(
21734 0..8,
21735 vec![
21736 indent_guide(buffer_id, 1, 6, 0),
21737 indent_guide(buffer_id, 3, 3, 1),
21738 indent_guide(buffer_id, 5, 5, 1),
21739 ],
21740 None,
21741 &mut cx,
21742 );
21743}
21744
21745#[gpui::test]
21746async fn test_indent_guide_tab(cx: &mut TestAppContext) {
21747 let (buffer_id, mut cx) = setup_indent_guides_editor(
21748 &"
21749 fn main() {
21750 let a = 1;
21751 let b = 2;
21752 let c = 3;
21753 }"
21754 .unindent(),
21755 cx,
21756 )
21757 .await;
21758
21759 assert_indent_guides(
21760 0..5,
21761 vec![
21762 indent_guide(buffer_id, 1, 3, 0),
21763 indent_guide(buffer_id, 2, 2, 1),
21764 ],
21765 None,
21766 &mut cx,
21767 );
21768}
21769
21770#[gpui::test]
21771async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
21772 let (buffer_id, mut cx) = setup_indent_guides_editor(
21773 &"
21774 fn main() {
21775 let a = 1;
21776
21777 let c = 3;
21778 }"
21779 .unindent(),
21780 cx,
21781 )
21782 .await;
21783
21784 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
21785}
21786
21787#[gpui::test]
21788async fn test_indent_guide_complex(cx: &mut TestAppContext) {
21789 let (buffer_id, mut cx) = setup_indent_guides_editor(
21790 &"
21791 fn main() {
21792 let a = 1;
21793
21794 let c = 3;
21795
21796 if a == 3 {
21797 let b = 2;
21798 } else {
21799 let c = 3;
21800 }
21801 }"
21802 .unindent(),
21803 cx,
21804 )
21805 .await;
21806
21807 assert_indent_guides(
21808 0..11,
21809 vec![
21810 indent_guide(buffer_id, 1, 9, 0),
21811 indent_guide(buffer_id, 6, 6, 1),
21812 indent_guide(buffer_id, 8, 8, 1),
21813 ],
21814 None,
21815 &mut cx,
21816 );
21817}
21818
21819#[gpui::test]
21820async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
21821 let (buffer_id, mut cx) = setup_indent_guides_editor(
21822 &"
21823 fn main() {
21824 let a = 1;
21825
21826 let c = 3;
21827
21828 if a == 3 {
21829 let b = 2;
21830 } else {
21831 let c = 3;
21832 }
21833 }"
21834 .unindent(),
21835 cx,
21836 )
21837 .await;
21838
21839 assert_indent_guides(
21840 1..11,
21841 vec![
21842 indent_guide(buffer_id, 1, 9, 0),
21843 indent_guide(buffer_id, 6, 6, 1),
21844 indent_guide(buffer_id, 8, 8, 1),
21845 ],
21846 None,
21847 &mut cx,
21848 );
21849}
21850
21851#[gpui::test]
21852async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
21853 let (buffer_id, mut cx) = setup_indent_guides_editor(
21854 &"
21855 fn main() {
21856 let a = 1;
21857
21858 let c = 3;
21859
21860 if a == 3 {
21861 let b = 2;
21862 } else {
21863 let c = 3;
21864 }
21865 }"
21866 .unindent(),
21867 cx,
21868 )
21869 .await;
21870
21871 assert_indent_guides(
21872 1..10,
21873 vec![
21874 indent_guide(buffer_id, 1, 9, 0),
21875 indent_guide(buffer_id, 6, 6, 1),
21876 indent_guide(buffer_id, 8, 8, 1),
21877 ],
21878 None,
21879 &mut cx,
21880 );
21881}
21882
21883#[gpui::test]
21884async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
21885 let (buffer_id, mut cx) = setup_indent_guides_editor(
21886 &"
21887 fn main() {
21888 if a {
21889 b(
21890 c,
21891 d,
21892 )
21893 } else {
21894 e(
21895 f
21896 )
21897 }
21898 }"
21899 .unindent(),
21900 cx,
21901 )
21902 .await;
21903
21904 assert_indent_guides(
21905 0..11,
21906 vec![
21907 indent_guide(buffer_id, 1, 10, 0),
21908 indent_guide(buffer_id, 2, 5, 1),
21909 indent_guide(buffer_id, 7, 9, 1),
21910 indent_guide(buffer_id, 3, 4, 2),
21911 indent_guide(buffer_id, 8, 8, 2),
21912 ],
21913 None,
21914 &mut cx,
21915 );
21916
21917 cx.update_editor(|editor, window, cx| {
21918 editor.fold_at(MultiBufferRow(2), window, cx);
21919 assert_eq!(
21920 editor.display_text(cx),
21921 "
21922 fn main() {
21923 if a {
21924 b(⋯
21925 )
21926 } else {
21927 e(
21928 f
21929 )
21930 }
21931 }"
21932 .unindent()
21933 );
21934 });
21935
21936 assert_indent_guides(
21937 0..11,
21938 vec![
21939 indent_guide(buffer_id, 1, 10, 0),
21940 indent_guide(buffer_id, 2, 5, 1),
21941 indent_guide(buffer_id, 7, 9, 1),
21942 indent_guide(buffer_id, 8, 8, 2),
21943 ],
21944 None,
21945 &mut cx,
21946 );
21947}
21948
21949#[gpui::test]
21950async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
21951 let (buffer_id, mut cx) = setup_indent_guides_editor(
21952 &"
21953 block1
21954 block2
21955 block3
21956 block4
21957 block2
21958 block1
21959 block1"
21960 .unindent(),
21961 cx,
21962 )
21963 .await;
21964
21965 assert_indent_guides(
21966 1..10,
21967 vec![
21968 indent_guide(buffer_id, 1, 4, 0),
21969 indent_guide(buffer_id, 2, 3, 1),
21970 indent_guide(buffer_id, 3, 3, 2),
21971 ],
21972 None,
21973 &mut cx,
21974 );
21975}
21976
21977#[gpui::test]
21978async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
21979 let (buffer_id, mut cx) = setup_indent_guides_editor(
21980 &"
21981 block1
21982 block2
21983 block3
21984
21985 block1
21986 block1"
21987 .unindent(),
21988 cx,
21989 )
21990 .await;
21991
21992 assert_indent_guides(
21993 0..6,
21994 vec![
21995 indent_guide(buffer_id, 1, 2, 0),
21996 indent_guide(buffer_id, 2, 2, 1),
21997 ],
21998 None,
21999 &mut cx,
22000 );
22001}
22002
22003#[gpui::test]
22004async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
22005 let (buffer_id, mut cx) = setup_indent_guides_editor(
22006 &"
22007 function component() {
22008 \treturn (
22009 \t\t\t
22010 \t\t<div>
22011 \t\t\t<abc></abc>
22012 \t\t</div>
22013 \t)
22014 }"
22015 .unindent(),
22016 cx,
22017 )
22018 .await;
22019
22020 assert_indent_guides(
22021 0..8,
22022 vec![
22023 indent_guide(buffer_id, 1, 6, 0),
22024 indent_guide(buffer_id, 2, 5, 1),
22025 indent_guide(buffer_id, 4, 4, 2),
22026 ],
22027 None,
22028 &mut cx,
22029 );
22030}
22031
22032#[gpui::test]
22033async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
22034 let (buffer_id, mut cx) = setup_indent_guides_editor(
22035 &"
22036 function component() {
22037 \treturn (
22038 \t
22039 \t\t<div>
22040 \t\t\t<abc></abc>
22041 \t\t</div>
22042 \t)
22043 }"
22044 .unindent(),
22045 cx,
22046 )
22047 .await;
22048
22049 assert_indent_guides(
22050 0..8,
22051 vec![
22052 indent_guide(buffer_id, 1, 6, 0),
22053 indent_guide(buffer_id, 2, 5, 1),
22054 indent_guide(buffer_id, 4, 4, 2),
22055 ],
22056 None,
22057 &mut cx,
22058 );
22059}
22060
22061#[gpui::test]
22062async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
22063 let (buffer_id, mut cx) = setup_indent_guides_editor(
22064 &"
22065 block1
22066
22067
22068
22069 block2
22070 "
22071 .unindent(),
22072 cx,
22073 )
22074 .await;
22075
22076 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
22077}
22078
22079#[gpui::test]
22080async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
22081 let (buffer_id, mut cx) = setup_indent_guides_editor(
22082 &"
22083 def a:
22084 \tb = 3
22085 \tif True:
22086 \t\tc = 4
22087 \t\td = 5
22088 \tprint(b)
22089 "
22090 .unindent(),
22091 cx,
22092 )
22093 .await;
22094
22095 assert_indent_guides(
22096 0..6,
22097 vec![
22098 indent_guide(buffer_id, 1, 5, 0),
22099 indent_guide(buffer_id, 3, 4, 1),
22100 ],
22101 None,
22102 &mut cx,
22103 );
22104}
22105
22106#[gpui::test]
22107async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
22108 let (buffer_id, mut cx) = setup_indent_guides_editor(
22109 &"
22110 fn main() {
22111 let a = 1;
22112 }"
22113 .unindent(),
22114 cx,
22115 )
22116 .await;
22117
22118 cx.update_editor(|editor, window, cx| {
22119 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22120 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22121 });
22122 });
22123
22124 assert_indent_guides(
22125 0..3,
22126 vec![indent_guide(buffer_id, 1, 1, 0)],
22127 Some(vec![0]),
22128 &mut cx,
22129 );
22130}
22131
22132#[gpui::test]
22133async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
22134 let (buffer_id, mut cx) = setup_indent_guides_editor(
22135 &"
22136 fn main() {
22137 if 1 == 2 {
22138 let a = 1;
22139 }
22140 }"
22141 .unindent(),
22142 cx,
22143 )
22144 .await;
22145
22146 cx.update_editor(|editor, window, cx| {
22147 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22148 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22149 });
22150 });
22151
22152 assert_indent_guides(
22153 0..4,
22154 vec![
22155 indent_guide(buffer_id, 1, 3, 0),
22156 indent_guide(buffer_id, 2, 2, 1),
22157 ],
22158 Some(vec![1]),
22159 &mut cx,
22160 );
22161
22162 cx.update_editor(|editor, window, cx| {
22163 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22164 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
22165 });
22166 });
22167
22168 assert_indent_guides(
22169 0..4,
22170 vec![
22171 indent_guide(buffer_id, 1, 3, 0),
22172 indent_guide(buffer_id, 2, 2, 1),
22173 ],
22174 Some(vec![1]),
22175 &mut cx,
22176 );
22177
22178 cx.update_editor(|editor, window, cx| {
22179 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22180 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
22181 });
22182 });
22183
22184 assert_indent_guides(
22185 0..4,
22186 vec![
22187 indent_guide(buffer_id, 1, 3, 0),
22188 indent_guide(buffer_id, 2, 2, 1),
22189 ],
22190 Some(vec![0]),
22191 &mut cx,
22192 );
22193}
22194
22195#[gpui::test]
22196async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
22197 let (buffer_id, mut cx) = setup_indent_guides_editor(
22198 &"
22199 fn main() {
22200 let a = 1;
22201
22202 let b = 2;
22203 }"
22204 .unindent(),
22205 cx,
22206 )
22207 .await;
22208
22209 cx.update_editor(|editor, window, cx| {
22210 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22211 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
22212 });
22213 });
22214
22215 assert_indent_guides(
22216 0..5,
22217 vec![indent_guide(buffer_id, 1, 3, 0)],
22218 Some(vec![0]),
22219 &mut cx,
22220 );
22221}
22222
22223#[gpui::test]
22224async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
22225 let (buffer_id, mut cx) = setup_indent_guides_editor(
22226 &"
22227 def m:
22228 a = 1
22229 pass"
22230 .unindent(),
22231 cx,
22232 )
22233 .await;
22234
22235 cx.update_editor(|editor, window, cx| {
22236 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22237 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22238 });
22239 });
22240
22241 assert_indent_guides(
22242 0..3,
22243 vec![indent_guide(buffer_id, 1, 2, 0)],
22244 Some(vec![0]),
22245 &mut cx,
22246 );
22247}
22248
22249#[gpui::test]
22250async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
22251 init_test(cx, |_| {});
22252 let mut cx = EditorTestContext::new(cx).await;
22253 let text = indoc! {
22254 "
22255 impl A {
22256 fn b() {
22257 0;
22258 3;
22259 5;
22260 6;
22261 7;
22262 }
22263 }
22264 "
22265 };
22266 let base_text = indoc! {
22267 "
22268 impl A {
22269 fn b() {
22270 0;
22271 1;
22272 2;
22273 3;
22274 4;
22275 }
22276 fn c() {
22277 5;
22278 6;
22279 7;
22280 }
22281 }
22282 "
22283 };
22284
22285 cx.update_editor(|editor, window, cx| {
22286 editor.set_text(text, window, cx);
22287
22288 editor.buffer().update(cx, |multibuffer, cx| {
22289 let buffer = multibuffer.as_singleton().unwrap();
22290 let diff = cx.new(|cx| {
22291 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
22292 });
22293
22294 multibuffer.set_all_diff_hunks_expanded(cx);
22295 multibuffer.add_diff(diff, cx);
22296
22297 buffer.read(cx).remote_id()
22298 })
22299 });
22300 cx.run_until_parked();
22301
22302 cx.assert_state_with_diff(
22303 indoc! { "
22304 impl A {
22305 fn b() {
22306 0;
22307 - 1;
22308 - 2;
22309 3;
22310 - 4;
22311 - }
22312 - fn c() {
22313 5;
22314 6;
22315 7;
22316 }
22317 }
22318 ˇ"
22319 }
22320 .to_string(),
22321 );
22322
22323 let mut actual_guides = cx.update_editor(|editor, window, cx| {
22324 editor
22325 .snapshot(window, cx)
22326 .buffer_snapshot()
22327 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
22328 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
22329 .collect::<Vec<_>>()
22330 });
22331 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
22332 assert_eq!(
22333 actual_guides,
22334 vec![
22335 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
22336 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
22337 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
22338 ]
22339 );
22340}
22341
22342#[gpui::test]
22343async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22344 init_test(cx, |_| {});
22345 let mut cx = EditorTestContext::new(cx).await;
22346
22347 let diff_base = r#"
22348 a
22349 b
22350 c
22351 "#
22352 .unindent();
22353
22354 cx.set_state(
22355 &r#"
22356 ˇA
22357 b
22358 C
22359 "#
22360 .unindent(),
22361 );
22362 cx.set_head_text(&diff_base);
22363 cx.update_editor(|editor, window, cx| {
22364 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22365 });
22366 executor.run_until_parked();
22367
22368 let both_hunks_expanded = r#"
22369 - a
22370 + ˇA
22371 b
22372 - c
22373 + C
22374 "#
22375 .unindent();
22376
22377 cx.assert_state_with_diff(both_hunks_expanded.clone());
22378
22379 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22380 let snapshot = editor.snapshot(window, cx);
22381 let hunks = editor
22382 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22383 .collect::<Vec<_>>();
22384 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22385 hunks
22386 .into_iter()
22387 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22388 .collect::<Vec<_>>()
22389 });
22390 assert_eq!(hunk_ranges.len(), 2);
22391
22392 cx.update_editor(|editor, _, cx| {
22393 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22394 });
22395 executor.run_until_parked();
22396
22397 let second_hunk_expanded = r#"
22398 ˇA
22399 b
22400 - c
22401 + C
22402 "#
22403 .unindent();
22404
22405 cx.assert_state_with_diff(second_hunk_expanded);
22406
22407 cx.update_editor(|editor, _, cx| {
22408 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22409 });
22410 executor.run_until_parked();
22411
22412 cx.assert_state_with_diff(both_hunks_expanded.clone());
22413
22414 cx.update_editor(|editor, _, cx| {
22415 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22416 });
22417 executor.run_until_parked();
22418
22419 let first_hunk_expanded = r#"
22420 - a
22421 + ˇA
22422 b
22423 C
22424 "#
22425 .unindent();
22426
22427 cx.assert_state_with_diff(first_hunk_expanded);
22428
22429 cx.update_editor(|editor, _, cx| {
22430 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22431 });
22432 executor.run_until_parked();
22433
22434 cx.assert_state_with_diff(both_hunks_expanded);
22435
22436 cx.set_state(
22437 &r#"
22438 ˇA
22439 b
22440 "#
22441 .unindent(),
22442 );
22443 cx.run_until_parked();
22444
22445 // TODO this cursor position seems bad
22446 cx.assert_state_with_diff(
22447 r#"
22448 - ˇa
22449 + A
22450 b
22451 "#
22452 .unindent(),
22453 );
22454
22455 cx.update_editor(|editor, window, cx| {
22456 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22457 });
22458
22459 cx.assert_state_with_diff(
22460 r#"
22461 - ˇa
22462 + A
22463 b
22464 - c
22465 "#
22466 .unindent(),
22467 );
22468
22469 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22470 let snapshot = editor.snapshot(window, cx);
22471 let hunks = editor
22472 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22473 .collect::<Vec<_>>();
22474 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22475 hunks
22476 .into_iter()
22477 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22478 .collect::<Vec<_>>()
22479 });
22480 assert_eq!(hunk_ranges.len(), 2);
22481
22482 cx.update_editor(|editor, _, cx| {
22483 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22484 });
22485 executor.run_until_parked();
22486
22487 cx.assert_state_with_diff(
22488 r#"
22489 - ˇa
22490 + A
22491 b
22492 "#
22493 .unindent(),
22494 );
22495}
22496
22497#[gpui::test]
22498async fn test_toggle_deletion_hunk_at_start_of_file(
22499 executor: BackgroundExecutor,
22500 cx: &mut TestAppContext,
22501) {
22502 init_test(cx, |_| {});
22503 let mut cx = EditorTestContext::new(cx).await;
22504
22505 let diff_base = r#"
22506 a
22507 b
22508 c
22509 "#
22510 .unindent();
22511
22512 cx.set_state(
22513 &r#"
22514 ˇb
22515 c
22516 "#
22517 .unindent(),
22518 );
22519 cx.set_head_text(&diff_base);
22520 cx.update_editor(|editor, window, cx| {
22521 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22522 });
22523 executor.run_until_parked();
22524
22525 let hunk_expanded = r#"
22526 - a
22527 ˇb
22528 c
22529 "#
22530 .unindent();
22531
22532 cx.assert_state_with_diff(hunk_expanded.clone());
22533
22534 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22535 let snapshot = editor.snapshot(window, cx);
22536 let hunks = editor
22537 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22538 .collect::<Vec<_>>();
22539 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22540 hunks
22541 .into_iter()
22542 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22543 .collect::<Vec<_>>()
22544 });
22545 assert_eq!(hunk_ranges.len(), 1);
22546
22547 cx.update_editor(|editor, _, cx| {
22548 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22549 });
22550 executor.run_until_parked();
22551
22552 let hunk_collapsed = r#"
22553 ˇb
22554 c
22555 "#
22556 .unindent();
22557
22558 cx.assert_state_with_diff(hunk_collapsed);
22559
22560 cx.update_editor(|editor, _, cx| {
22561 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22562 });
22563 executor.run_until_parked();
22564
22565 cx.assert_state_with_diff(hunk_expanded);
22566}
22567
22568#[gpui::test]
22569async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
22570 executor: BackgroundExecutor,
22571 cx: &mut TestAppContext,
22572) {
22573 init_test(cx, |_| {});
22574 let mut cx = EditorTestContext::new(cx).await;
22575
22576 cx.set_state("ˇnew\nsecond\nthird\n");
22577 cx.set_head_text("old\nsecond\nthird\n");
22578 cx.update_editor(|editor, window, cx| {
22579 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
22580 });
22581 executor.run_until_parked();
22582 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22583
22584 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
22585 cx.update_editor(|editor, window, cx| {
22586 let snapshot = editor.snapshot(window, cx);
22587 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22588 let hunks = editor
22589 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22590 .collect::<Vec<_>>();
22591 assert_eq!(hunks.len(), 1);
22592 let hunk_range = Anchor::range_in_buffer(excerpt_id, hunks[0].buffer_range.clone());
22593 editor.toggle_single_diff_hunk(hunk_range, cx)
22594 });
22595 executor.run_until_parked();
22596 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
22597
22598 // Keep the editor scrolled to the top so the full hunk remains visible.
22599 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22600}
22601
22602#[gpui::test]
22603async fn test_display_diff_hunks(cx: &mut TestAppContext) {
22604 init_test(cx, |_| {});
22605
22606 let fs = FakeFs::new(cx.executor());
22607 fs.insert_tree(
22608 path!("/test"),
22609 json!({
22610 ".git": {},
22611 "file-1": "ONE\n",
22612 "file-2": "TWO\n",
22613 "file-3": "THREE\n",
22614 }),
22615 )
22616 .await;
22617
22618 fs.set_head_for_repo(
22619 path!("/test/.git").as_ref(),
22620 &[
22621 ("file-1", "one\n".into()),
22622 ("file-2", "two\n".into()),
22623 ("file-3", "three\n".into()),
22624 ],
22625 "deadbeef",
22626 );
22627
22628 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
22629 let mut buffers = vec![];
22630 for i in 1..=3 {
22631 let buffer = project
22632 .update(cx, |project, cx| {
22633 let path = format!(path!("/test/file-{}"), i);
22634 project.open_local_buffer(path, cx)
22635 })
22636 .await
22637 .unwrap();
22638 buffers.push(buffer);
22639 }
22640
22641 let multibuffer = cx.new(|cx| {
22642 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
22643 multibuffer.set_all_diff_hunks_expanded(cx);
22644 for buffer in &buffers {
22645 let snapshot = buffer.read(cx).snapshot();
22646 multibuffer.set_excerpts_for_path(
22647 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
22648 buffer.clone(),
22649 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
22650 2,
22651 cx,
22652 );
22653 }
22654 multibuffer
22655 });
22656
22657 let editor = cx.add_window(|window, cx| {
22658 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
22659 });
22660 cx.run_until_parked();
22661
22662 let snapshot = editor
22663 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22664 .unwrap();
22665 let hunks = snapshot
22666 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
22667 .map(|hunk| match hunk {
22668 DisplayDiffHunk::Unfolded {
22669 display_row_range, ..
22670 } => display_row_range,
22671 DisplayDiffHunk::Folded { .. } => unreachable!(),
22672 })
22673 .collect::<Vec<_>>();
22674 assert_eq!(
22675 hunks,
22676 [
22677 DisplayRow(2)..DisplayRow(4),
22678 DisplayRow(7)..DisplayRow(9),
22679 DisplayRow(12)..DisplayRow(14),
22680 ]
22681 );
22682}
22683
22684#[gpui::test]
22685async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
22686 init_test(cx, |_| {});
22687
22688 let mut cx = EditorTestContext::new(cx).await;
22689 cx.set_head_text(indoc! { "
22690 one
22691 two
22692 three
22693 four
22694 five
22695 "
22696 });
22697 cx.set_index_text(indoc! { "
22698 one
22699 two
22700 three
22701 four
22702 five
22703 "
22704 });
22705 cx.set_state(indoc! {"
22706 one
22707 TWO
22708 ˇTHREE
22709 FOUR
22710 five
22711 "});
22712 cx.run_until_parked();
22713 cx.update_editor(|editor, window, cx| {
22714 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22715 });
22716 cx.run_until_parked();
22717 cx.assert_index_text(Some(indoc! {"
22718 one
22719 TWO
22720 THREE
22721 FOUR
22722 five
22723 "}));
22724 cx.set_state(indoc! { "
22725 one
22726 TWO
22727 ˇTHREE-HUNDRED
22728 FOUR
22729 five
22730 "});
22731 cx.run_until_parked();
22732 cx.update_editor(|editor, window, cx| {
22733 let snapshot = editor.snapshot(window, cx);
22734 let hunks = editor
22735 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22736 .collect::<Vec<_>>();
22737 assert_eq!(hunks.len(), 1);
22738 assert_eq!(
22739 hunks[0].status(),
22740 DiffHunkStatus {
22741 kind: DiffHunkStatusKind::Modified,
22742 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
22743 }
22744 );
22745
22746 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22747 });
22748 cx.run_until_parked();
22749 cx.assert_index_text(Some(indoc! {"
22750 one
22751 TWO
22752 THREE-HUNDRED
22753 FOUR
22754 five
22755 "}));
22756}
22757
22758#[gpui::test]
22759fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
22760 init_test(cx, |_| {});
22761
22762 let editor = cx.add_window(|window, cx| {
22763 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
22764 build_editor(buffer, window, cx)
22765 });
22766
22767 let render_args = Arc::new(Mutex::new(None));
22768 let snapshot = editor
22769 .update(cx, |editor, window, cx| {
22770 let snapshot = editor.buffer().read(cx).snapshot(cx);
22771 let range =
22772 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
22773
22774 struct RenderArgs {
22775 row: MultiBufferRow,
22776 folded: bool,
22777 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
22778 }
22779
22780 let crease = Crease::inline(
22781 range,
22782 FoldPlaceholder::test(),
22783 {
22784 let toggle_callback = render_args.clone();
22785 move |row, folded, callback, _window, _cx| {
22786 *toggle_callback.lock() = Some(RenderArgs {
22787 row,
22788 folded,
22789 callback,
22790 });
22791 div()
22792 }
22793 },
22794 |_row, _folded, _window, _cx| div(),
22795 );
22796
22797 editor.insert_creases(Some(crease), cx);
22798 let snapshot = editor.snapshot(window, cx);
22799 let _div =
22800 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
22801 snapshot
22802 })
22803 .unwrap();
22804
22805 let render_args = render_args.lock().take().unwrap();
22806 assert_eq!(render_args.row, MultiBufferRow(1));
22807 assert!(!render_args.folded);
22808 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22809
22810 cx.update_window(*editor, |_, window, cx| {
22811 (render_args.callback)(true, window, cx)
22812 })
22813 .unwrap();
22814 let snapshot = editor
22815 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22816 .unwrap();
22817 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
22818
22819 cx.update_window(*editor, |_, window, cx| {
22820 (render_args.callback)(false, window, cx)
22821 })
22822 .unwrap();
22823 let snapshot = editor
22824 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22825 .unwrap();
22826 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22827}
22828
22829#[gpui::test]
22830async fn test_input_text(cx: &mut TestAppContext) {
22831 init_test(cx, |_| {});
22832 let mut cx = EditorTestContext::new(cx).await;
22833
22834 cx.set_state(
22835 &r#"ˇone
22836 two
22837
22838 three
22839 fourˇ
22840 five
22841
22842 siˇx"#
22843 .unindent(),
22844 );
22845
22846 cx.dispatch_action(HandleInput(String::new()));
22847 cx.assert_editor_state(
22848 &r#"ˇone
22849 two
22850
22851 three
22852 fourˇ
22853 five
22854
22855 siˇx"#
22856 .unindent(),
22857 );
22858
22859 cx.dispatch_action(HandleInput("AAAA".to_string()));
22860 cx.assert_editor_state(
22861 &r#"AAAAˇone
22862 two
22863
22864 three
22865 fourAAAAˇ
22866 five
22867
22868 siAAAAˇx"#
22869 .unindent(),
22870 );
22871}
22872
22873#[gpui::test]
22874async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22875 init_test(cx, |_| {});
22876
22877 let mut cx = EditorTestContext::new(cx).await;
22878 cx.set_state(
22879 r#"let foo = 1;
22880let foo = 2;
22881let foo = 3;
22882let fooˇ = 4;
22883let foo = 5;
22884let foo = 6;
22885let foo = 7;
22886let foo = 8;
22887let foo = 9;
22888let foo = 10;
22889let foo = 11;
22890let foo = 12;
22891let foo = 13;
22892let foo = 14;
22893let foo = 15;"#,
22894 );
22895
22896 cx.update_editor(|e, window, cx| {
22897 assert_eq!(
22898 e.next_scroll_position,
22899 NextScrollCursorCenterTopBottom::Center,
22900 "Default next scroll direction is center",
22901 );
22902
22903 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22904 assert_eq!(
22905 e.next_scroll_position,
22906 NextScrollCursorCenterTopBottom::Top,
22907 "After center, next scroll direction should be top",
22908 );
22909
22910 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22911 assert_eq!(
22912 e.next_scroll_position,
22913 NextScrollCursorCenterTopBottom::Bottom,
22914 "After top, next scroll direction should be bottom",
22915 );
22916
22917 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22918 assert_eq!(
22919 e.next_scroll_position,
22920 NextScrollCursorCenterTopBottom::Center,
22921 "After bottom, scrolling should start over",
22922 );
22923
22924 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22925 assert_eq!(
22926 e.next_scroll_position,
22927 NextScrollCursorCenterTopBottom::Top,
22928 "Scrolling continues if retriggered fast enough"
22929 );
22930 });
22931
22932 cx.executor()
22933 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22934 cx.executor().run_until_parked();
22935 cx.update_editor(|e, _, _| {
22936 assert_eq!(
22937 e.next_scroll_position,
22938 NextScrollCursorCenterTopBottom::Center,
22939 "If scrolling is not triggered fast enough, it should reset"
22940 );
22941 });
22942}
22943
22944#[gpui::test]
22945async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22946 init_test(cx, |_| {});
22947 let mut cx = EditorLspTestContext::new_rust(
22948 lsp::ServerCapabilities {
22949 definition_provider: Some(lsp::OneOf::Left(true)),
22950 references_provider: Some(lsp::OneOf::Left(true)),
22951 ..lsp::ServerCapabilities::default()
22952 },
22953 cx,
22954 )
22955 .await;
22956
22957 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22958 let go_to_definition = cx
22959 .lsp
22960 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22961 move |params, _| async move {
22962 if empty_go_to_definition {
22963 Ok(None)
22964 } else {
22965 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22966 uri: params.text_document_position_params.text_document.uri,
22967 range: lsp::Range::new(
22968 lsp::Position::new(4, 3),
22969 lsp::Position::new(4, 6),
22970 ),
22971 })))
22972 }
22973 },
22974 );
22975 let references = cx
22976 .lsp
22977 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22978 Ok(Some(vec![lsp::Location {
22979 uri: params.text_document_position.text_document.uri,
22980 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22981 }]))
22982 });
22983 (go_to_definition, references)
22984 };
22985
22986 cx.set_state(
22987 &r#"fn one() {
22988 let mut a = ˇtwo();
22989 }
22990
22991 fn two() {}"#
22992 .unindent(),
22993 );
22994 set_up_lsp_handlers(false, &mut cx);
22995 let navigated = cx
22996 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22997 .await
22998 .expect("Failed to navigate to definition");
22999 assert_eq!(
23000 navigated,
23001 Navigated::Yes,
23002 "Should have navigated to definition from the GetDefinition response"
23003 );
23004 cx.assert_editor_state(
23005 &r#"fn one() {
23006 let mut a = two();
23007 }
23008
23009 fn «twoˇ»() {}"#
23010 .unindent(),
23011 );
23012
23013 let editors = cx.update_workspace(|workspace, _, cx| {
23014 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23015 });
23016 cx.update_editor(|_, _, test_editor_cx| {
23017 assert_eq!(
23018 editors.len(),
23019 1,
23020 "Initially, only one, test, editor should be open in the workspace"
23021 );
23022 assert_eq!(
23023 test_editor_cx.entity(),
23024 editors.last().expect("Asserted len is 1").clone()
23025 );
23026 });
23027
23028 set_up_lsp_handlers(true, &mut cx);
23029 let navigated = cx
23030 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
23031 .await
23032 .expect("Failed to navigate to lookup references");
23033 assert_eq!(
23034 navigated,
23035 Navigated::Yes,
23036 "Should have navigated to references as a fallback after empty GoToDefinition response"
23037 );
23038 // We should not change the selections in the existing file,
23039 // if opening another milti buffer with the references
23040 cx.assert_editor_state(
23041 &r#"fn one() {
23042 let mut a = two();
23043 }
23044
23045 fn «twoˇ»() {}"#
23046 .unindent(),
23047 );
23048 let editors = cx.update_workspace(|workspace, _, cx| {
23049 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23050 });
23051 cx.update_editor(|_, _, test_editor_cx| {
23052 assert_eq!(
23053 editors.len(),
23054 2,
23055 "After falling back to references search, we open a new editor with the results"
23056 );
23057 let references_fallback_text = editors
23058 .into_iter()
23059 .find(|new_editor| *new_editor != test_editor_cx.entity())
23060 .expect("Should have one non-test editor now")
23061 .read(test_editor_cx)
23062 .text(test_editor_cx);
23063 assert_eq!(
23064 references_fallback_text, "fn one() {\n let mut a = two();\n}",
23065 "Should use the range from the references response and not the GoToDefinition one"
23066 );
23067 });
23068}
23069
23070#[gpui::test]
23071async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
23072 init_test(cx, |_| {});
23073 cx.update(|cx| {
23074 let mut editor_settings = EditorSettings::get_global(cx).clone();
23075 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
23076 EditorSettings::override_global(editor_settings, cx);
23077 });
23078 let mut cx = EditorLspTestContext::new_rust(
23079 lsp::ServerCapabilities {
23080 definition_provider: Some(lsp::OneOf::Left(true)),
23081 references_provider: Some(lsp::OneOf::Left(true)),
23082 ..lsp::ServerCapabilities::default()
23083 },
23084 cx,
23085 )
23086 .await;
23087 let original_state = r#"fn one() {
23088 let mut a = ˇtwo();
23089 }
23090
23091 fn two() {}"#
23092 .unindent();
23093 cx.set_state(&original_state);
23094
23095 let mut go_to_definition = cx
23096 .lsp
23097 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
23098 move |_, _| async move { Ok(None) },
23099 );
23100 let _references = cx
23101 .lsp
23102 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
23103 panic!("Should not call for references with no go to definition fallback")
23104 });
23105
23106 let navigated = cx
23107 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
23108 .await
23109 .expect("Failed to navigate to lookup references");
23110 go_to_definition
23111 .next()
23112 .await
23113 .expect("Should have called the go_to_definition handler");
23114
23115 assert_eq!(
23116 navigated,
23117 Navigated::No,
23118 "Should have navigated to references as a fallback after empty GoToDefinition response"
23119 );
23120 cx.assert_editor_state(&original_state);
23121 let editors = cx.update_workspace(|workspace, _, cx| {
23122 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23123 });
23124 cx.update_editor(|_, _, _| {
23125 assert_eq!(
23126 editors.len(),
23127 1,
23128 "After unsuccessful fallback, no other editor should have been opened"
23129 );
23130 });
23131}
23132
23133#[gpui::test]
23134async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
23135 init_test(cx, |_| {});
23136 let mut cx = EditorLspTestContext::new_rust(
23137 lsp::ServerCapabilities {
23138 references_provider: Some(lsp::OneOf::Left(true)),
23139 ..lsp::ServerCapabilities::default()
23140 },
23141 cx,
23142 )
23143 .await;
23144
23145 cx.set_state(
23146 &r#"
23147 fn one() {
23148 let mut a = two();
23149 }
23150
23151 fn ˇtwo() {}"#
23152 .unindent(),
23153 );
23154 cx.lsp
23155 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23156 Ok(Some(vec![
23157 lsp::Location {
23158 uri: params.text_document_position.text_document.uri.clone(),
23159 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
23160 },
23161 lsp::Location {
23162 uri: params.text_document_position.text_document.uri,
23163 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
23164 },
23165 ]))
23166 });
23167 let navigated = cx
23168 .update_editor(|editor, window, cx| {
23169 editor.find_all_references(&FindAllReferences::default(), window, cx)
23170 })
23171 .unwrap()
23172 .await
23173 .expect("Failed to navigate to references");
23174 assert_eq!(
23175 navigated,
23176 Navigated::Yes,
23177 "Should have navigated to references from the FindAllReferences response"
23178 );
23179 cx.assert_editor_state(
23180 &r#"fn one() {
23181 let mut a = two();
23182 }
23183
23184 fn ˇtwo() {}"#
23185 .unindent(),
23186 );
23187
23188 let editors = cx.update_workspace(|workspace, _, cx| {
23189 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23190 });
23191 cx.update_editor(|_, _, _| {
23192 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
23193 });
23194
23195 cx.set_state(
23196 &r#"fn one() {
23197 let mut a = ˇtwo();
23198 }
23199
23200 fn two() {}"#
23201 .unindent(),
23202 );
23203 let navigated = cx
23204 .update_editor(|editor, window, cx| {
23205 editor.find_all_references(&FindAllReferences::default(), window, cx)
23206 })
23207 .unwrap()
23208 .await
23209 .expect("Failed to navigate to references");
23210 assert_eq!(
23211 navigated,
23212 Navigated::Yes,
23213 "Should have navigated to references from the FindAllReferences response"
23214 );
23215 cx.assert_editor_state(
23216 &r#"fn one() {
23217 let mut a = ˇtwo();
23218 }
23219
23220 fn two() {}"#
23221 .unindent(),
23222 );
23223 let editors = cx.update_workspace(|workspace, _, cx| {
23224 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23225 });
23226 cx.update_editor(|_, _, _| {
23227 assert_eq!(
23228 editors.len(),
23229 2,
23230 "should have re-used the previous multibuffer"
23231 );
23232 });
23233
23234 cx.set_state(
23235 &r#"fn one() {
23236 let mut a = ˇtwo();
23237 }
23238 fn three() {}
23239 fn two() {}"#
23240 .unindent(),
23241 );
23242 cx.lsp
23243 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23244 Ok(Some(vec![
23245 lsp::Location {
23246 uri: params.text_document_position.text_document.uri.clone(),
23247 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
23248 },
23249 lsp::Location {
23250 uri: params.text_document_position.text_document.uri,
23251 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
23252 },
23253 ]))
23254 });
23255 let navigated = cx
23256 .update_editor(|editor, window, cx| {
23257 editor.find_all_references(&FindAllReferences::default(), window, cx)
23258 })
23259 .unwrap()
23260 .await
23261 .expect("Failed to navigate to references");
23262 assert_eq!(
23263 navigated,
23264 Navigated::Yes,
23265 "Should have navigated to references from the FindAllReferences response"
23266 );
23267 cx.assert_editor_state(
23268 &r#"fn one() {
23269 let mut a = ˇtwo();
23270 }
23271 fn three() {}
23272 fn two() {}"#
23273 .unindent(),
23274 );
23275 let editors = cx.update_workspace(|workspace, _, cx| {
23276 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23277 });
23278 cx.update_editor(|_, _, _| {
23279 assert_eq!(
23280 editors.len(),
23281 3,
23282 "should have used a new multibuffer as offsets changed"
23283 );
23284 });
23285}
23286#[gpui::test]
23287async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
23288 init_test(cx, |_| {});
23289
23290 let language = Arc::new(Language::new(
23291 LanguageConfig::default(),
23292 Some(tree_sitter_rust::LANGUAGE.into()),
23293 ));
23294
23295 let text = r#"
23296 #[cfg(test)]
23297 mod tests() {
23298 #[test]
23299 fn runnable_1() {
23300 let a = 1;
23301 }
23302
23303 #[test]
23304 fn runnable_2() {
23305 let a = 1;
23306 let b = 2;
23307 }
23308 }
23309 "#
23310 .unindent();
23311
23312 let fs = FakeFs::new(cx.executor());
23313 fs.insert_file("/file.rs", Default::default()).await;
23314
23315 let project = Project::test(fs, ["/a".as_ref()], cx).await;
23316 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23317 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23318 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
23319 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
23320
23321 let editor = cx.new_window_entity(|window, cx| {
23322 Editor::new(
23323 EditorMode::full(),
23324 multi_buffer,
23325 Some(project.clone()),
23326 window,
23327 cx,
23328 )
23329 });
23330
23331 editor.update_in(cx, |editor, window, cx| {
23332 let snapshot = editor.buffer().read(cx).snapshot(cx);
23333 editor.tasks.insert(
23334 (buffer.read(cx).remote_id(), 3),
23335 RunnableTasks {
23336 templates: vec![],
23337 offset: snapshot.anchor_before(MultiBufferOffset(43)),
23338 column: 0,
23339 extra_variables: HashMap::default(),
23340 context_range: BufferOffset(43)..BufferOffset(85),
23341 },
23342 );
23343 editor.tasks.insert(
23344 (buffer.read(cx).remote_id(), 8),
23345 RunnableTasks {
23346 templates: vec![],
23347 offset: snapshot.anchor_before(MultiBufferOffset(86)),
23348 column: 0,
23349 extra_variables: HashMap::default(),
23350 context_range: BufferOffset(86)..BufferOffset(191),
23351 },
23352 );
23353
23354 // Test finding task when cursor is inside function body
23355 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23356 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
23357 });
23358 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23359 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
23360
23361 // Test finding task when cursor is on function name
23362 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23363 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
23364 });
23365 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23366 assert_eq!(row, 8, "Should find task when cursor is on function name");
23367 });
23368}
23369
23370#[gpui::test]
23371async fn test_folding_buffers(cx: &mut TestAppContext) {
23372 init_test(cx, |_| {});
23373
23374 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23375 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
23376 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
23377
23378 let fs = FakeFs::new(cx.executor());
23379 fs.insert_tree(
23380 path!("/a"),
23381 json!({
23382 "first.rs": sample_text_1,
23383 "second.rs": sample_text_2,
23384 "third.rs": sample_text_3,
23385 }),
23386 )
23387 .await;
23388 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23389 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23390 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23391 let worktree = project.update(cx, |project, cx| {
23392 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23393 assert_eq!(worktrees.len(), 1);
23394 worktrees.pop().unwrap()
23395 });
23396 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23397
23398 let buffer_1 = project
23399 .update(cx, |project, cx| {
23400 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23401 })
23402 .await
23403 .unwrap();
23404 let buffer_2 = project
23405 .update(cx, |project, cx| {
23406 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23407 })
23408 .await
23409 .unwrap();
23410 let buffer_3 = project
23411 .update(cx, |project, cx| {
23412 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23413 })
23414 .await
23415 .unwrap();
23416
23417 let multi_buffer = cx.new(|cx| {
23418 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23419 multi_buffer.push_excerpts(
23420 buffer_1.clone(),
23421 [
23422 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23423 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23424 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23425 ],
23426 cx,
23427 );
23428 multi_buffer.push_excerpts(
23429 buffer_2.clone(),
23430 [
23431 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23432 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23433 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23434 ],
23435 cx,
23436 );
23437 multi_buffer.push_excerpts(
23438 buffer_3.clone(),
23439 [
23440 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23441 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23442 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23443 ],
23444 cx,
23445 );
23446 multi_buffer
23447 });
23448 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23449 Editor::new(
23450 EditorMode::full(),
23451 multi_buffer.clone(),
23452 Some(project.clone()),
23453 window,
23454 cx,
23455 )
23456 });
23457
23458 assert_eq!(
23459 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23460 "\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",
23461 );
23462
23463 multi_buffer_editor.update(cx, |editor, cx| {
23464 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23465 });
23466 assert_eq!(
23467 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23468 "\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",
23469 "After folding the first buffer, its text should not be displayed"
23470 );
23471
23472 multi_buffer_editor.update(cx, |editor, cx| {
23473 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23474 });
23475 assert_eq!(
23476 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23477 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
23478 "After folding the second buffer, its text should not be displayed"
23479 );
23480
23481 multi_buffer_editor.update(cx, |editor, cx| {
23482 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23483 });
23484 assert_eq!(
23485 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23486 "\n\n\n\n\n",
23487 "After folding the third buffer, its text should not be displayed"
23488 );
23489
23490 // Emulate selection inside the fold logic, that should work
23491 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23492 editor
23493 .snapshot(window, cx)
23494 .next_line_boundary(Point::new(0, 4));
23495 });
23496
23497 multi_buffer_editor.update(cx, |editor, cx| {
23498 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23499 });
23500 assert_eq!(
23501 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23502 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
23503 "After unfolding the second buffer, its text should be displayed"
23504 );
23505
23506 // Typing inside of buffer 1 causes that buffer to be unfolded.
23507 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23508 assert_eq!(
23509 multi_buffer
23510 .read(cx)
23511 .snapshot(cx)
23512 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
23513 .collect::<String>(),
23514 "bbbb"
23515 );
23516 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23517 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
23518 });
23519 editor.handle_input("B", window, cx);
23520 });
23521
23522 assert_eq!(
23523 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23524 "\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",
23525 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
23526 );
23527
23528 multi_buffer_editor.update(cx, |editor, cx| {
23529 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23530 });
23531 assert_eq!(
23532 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23533 "\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",
23534 "After unfolding the all buffers, all original text should be displayed"
23535 );
23536}
23537
23538#[gpui::test]
23539async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
23540 init_test(cx, |_| {});
23541
23542 let sample_text_1 = "1111\n2222\n3333".to_string();
23543 let sample_text_2 = "4444\n5555\n6666".to_string();
23544 let sample_text_3 = "7777\n8888\n9999".to_string();
23545
23546 let fs = FakeFs::new(cx.executor());
23547 fs.insert_tree(
23548 path!("/a"),
23549 json!({
23550 "first.rs": sample_text_1,
23551 "second.rs": sample_text_2,
23552 "third.rs": sample_text_3,
23553 }),
23554 )
23555 .await;
23556 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23557 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23558 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23559 let worktree = project.update(cx, |project, cx| {
23560 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23561 assert_eq!(worktrees.len(), 1);
23562 worktrees.pop().unwrap()
23563 });
23564 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23565
23566 let buffer_1 = project
23567 .update(cx, |project, cx| {
23568 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23569 })
23570 .await
23571 .unwrap();
23572 let buffer_2 = project
23573 .update(cx, |project, cx| {
23574 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23575 })
23576 .await
23577 .unwrap();
23578 let buffer_3 = project
23579 .update(cx, |project, cx| {
23580 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23581 })
23582 .await
23583 .unwrap();
23584
23585 let multi_buffer = cx.new(|cx| {
23586 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23587 multi_buffer.push_excerpts(
23588 buffer_1.clone(),
23589 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23590 cx,
23591 );
23592 multi_buffer.push_excerpts(
23593 buffer_2.clone(),
23594 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23595 cx,
23596 );
23597 multi_buffer.push_excerpts(
23598 buffer_3.clone(),
23599 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23600 cx,
23601 );
23602 multi_buffer
23603 });
23604
23605 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23606 Editor::new(
23607 EditorMode::full(),
23608 multi_buffer,
23609 Some(project.clone()),
23610 window,
23611 cx,
23612 )
23613 });
23614
23615 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
23616 assert_eq!(
23617 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23618 full_text,
23619 );
23620
23621 multi_buffer_editor.update(cx, |editor, cx| {
23622 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23623 });
23624 assert_eq!(
23625 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23626 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
23627 "After folding the first buffer, its text should not be displayed"
23628 );
23629
23630 multi_buffer_editor.update(cx, |editor, cx| {
23631 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23632 });
23633
23634 assert_eq!(
23635 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23636 "\n\n\n\n\n\n7777\n8888\n9999",
23637 "After folding the second buffer, its text should not be displayed"
23638 );
23639
23640 multi_buffer_editor.update(cx, |editor, cx| {
23641 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23642 });
23643 assert_eq!(
23644 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23645 "\n\n\n\n\n",
23646 "After folding the third buffer, its text should not be displayed"
23647 );
23648
23649 multi_buffer_editor.update(cx, |editor, cx| {
23650 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23651 });
23652 assert_eq!(
23653 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23654 "\n\n\n\n4444\n5555\n6666\n\n",
23655 "After unfolding the second buffer, its text should be displayed"
23656 );
23657
23658 multi_buffer_editor.update(cx, |editor, cx| {
23659 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
23660 });
23661 assert_eq!(
23662 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23663 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
23664 "After unfolding the first buffer, its text should be displayed"
23665 );
23666
23667 multi_buffer_editor.update(cx, |editor, cx| {
23668 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23669 });
23670 assert_eq!(
23671 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23672 full_text,
23673 "After unfolding all buffers, all original text should be displayed"
23674 );
23675}
23676
23677#[gpui::test]
23678async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
23679 init_test(cx, |_| {});
23680
23681 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23682
23683 let fs = FakeFs::new(cx.executor());
23684 fs.insert_tree(
23685 path!("/a"),
23686 json!({
23687 "main.rs": sample_text,
23688 }),
23689 )
23690 .await;
23691 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23692 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23693 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23694 let worktree = project.update(cx, |project, cx| {
23695 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23696 assert_eq!(worktrees.len(), 1);
23697 worktrees.pop().unwrap()
23698 });
23699 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23700
23701 let buffer_1 = project
23702 .update(cx, |project, cx| {
23703 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23704 })
23705 .await
23706 .unwrap();
23707
23708 let multi_buffer = cx.new(|cx| {
23709 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23710 multi_buffer.push_excerpts(
23711 buffer_1.clone(),
23712 [ExcerptRange::new(
23713 Point::new(0, 0)
23714 ..Point::new(
23715 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
23716 0,
23717 ),
23718 )],
23719 cx,
23720 );
23721 multi_buffer
23722 });
23723 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23724 Editor::new(
23725 EditorMode::full(),
23726 multi_buffer,
23727 Some(project.clone()),
23728 window,
23729 cx,
23730 )
23731 });
23732
23733 let selection_range = Point::new(1, 0)..Point::new(2, 0);
23734 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23735 enum TestHighlight {}
23736 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
23737 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
23738 editor.highlight_text::<TestHighlight>(
23739 vec![highlight_range.clone()],
23740 HighlightStyle::color(Hsla::green()),
23741 cx,
23742 );
23743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23744 s.select_ranges(Some(highlight_range))
23745 });
23746 });
23747
23748 let full_text = format!("\n\n{sample_text}");
23749 assert_eq!(
23750 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23751 full_text,
23752 );
23753}
23754
23755#[gpui::test]
23756async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
23757 init_test(cx, |_| {});
23758 cx.update(|cx| {
23759 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
23760 "keymaps/default-linux.json",
23761 cx,
23762 )
23763 .unwrap();
23764 cx.bind_keys(default_key_bindings);
23765 });
23766
23767 let (editor, cx) = cx.add_window_view(|window, cx| {
23768 let multi_buffer = MultiBuffer::build_multi(
23769 [
23770 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
23771 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
23772 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
23773 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
23774 ],
23775 cx,
23776 );
23777 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
23778
23779 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
23780 // fold all but the second buffer, so that we test navigating between two
23781 // adjacent folded buffers, as well as folded buffers at the start and
23782 // end the multibuffer
23783 editor.fold_buffer(buffer_ids[0], cx);
23784 editor.fold_buffer(buffer_ids[2], cx);
23785 editor.fold_buffer(buffer_ids[3], cx);
23786
23787 editor
23788 });
23789 cx.simulate_resize(size(px(1000.), px(1000.)));
23790
23791 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
23792 cx.assert_excerpts_with_selections(indoc! {"
23793 [EXCERPT]
23794 ˇ[FOLDED]
23795 [EXCERPT]
23796 a1
23797 b1
23798 [EXCERPT]
23799 [FOLDED]
23800 [EXCERPT]
23801 [FOLDED]
23802 "
23803 });
23804 cx.simulate_keystroke("down");
23805 cx.assert_excerpts_with_selections(indoc! {"
23806 [EXCERPT]
23807 [FOLDED]
23808 [EXCERPT]
23809 ˇa1
23810 b1
23811 [EXCERPT]
23812 [FOLDED]
23813 [EXCERPT]
23814 [FOLDED]
23815 "
23816 });
23817 cx.simulate_keystroke("down");
23818 cx.assert_excerpts_with_selections(indoc! {"
23819 [EXCERPT]
23820 [FOLDED]
23821 [EXCERPT]
23822 a1
23823 ˇb1
23824 [EXCERPT]
23825 [FOLDED]
23826 [EXCERPT]
23827 [FOLDED]
23828 "
23829 });
23830 cx.simulate_keystroke("down");
23831 cx.assert_excerpts_with_selections(indoc! {"
23832 [EXCERPT]
23833 [FOLDED]
23834 [EXCERPT]
23835 a1
23836 b1
23837 ˇ[EXCERPT]
23838 [FOLDED]
23839 [EXCERPT]
23840 [FOLDED]
23841 "
23842 });
23843 cx.simulate_keystroke("down");
23844 cx.assert_excerpts_with_selections(indoc! {"
23845 [EXCERPT]
23846 [FOLDED]
23847 [EXCERPT]
23848 a1
23849 b1
23850 [EXCERPT]
23851 ˇ[FOLDED]
23852 [EXCERPT]
23853 [FOLDED]
23854 "
23855 });
23856 for _ in 0..5 {
23857 cx.simulate_keystroke("down");
23858 cx.assert_excerpts_with_selections(indoc! {"
23859 [EXCERPT]
23860 [FOLDED]
23861 [EXCERPT]
23862 a1
23863 b1
23864 [EXCERPT]
23865 [FOLDED]
23866 [EXCERPT]
23867 ˇ[FOLDED]
23868 "
23869 });
23870 }
23871
23872 cx.simulate_keystroke("up");
23873 cx.assert_excerpts_with_selections(indoc! {"
23874 [EXCERPT]
23875 [FOLDED]
23876 [EXCERPT]
23877 a1
23878 b1
23879 [EXCERPT]
23880 ˇ[FOLDED]
23881 [EXCERPT]
23882 [FOLDED]
23883 "
23884 });
23885 cx.simulate_keystroke("up");
23886 cx.assert_excerpts_with_selections(indoc! {"
23887 [EXCERPT]
23888 [FOLDED]
23889 [EXCERPT]
23890 a1
23891 b1
23892 ˇ[EXCERPT]
23893 [FOLDED]
23894 [EXCERPT]
23895 [FOLDED]
23896 "
23897 });
23898 cx.simulate_keystroke("up");
23899 cx.assert_excerpts_with_selections(indoc! {"
23900 [EXCERPT]
23901 [FOLDED]
23902 [EXCERPT]
23903 a1
23904 ˇb1
23905 [EXCERPT]
23906 [FOLDED]
23907 [EXCERPT]
23908 [FOLDED]
23909 "
23910 });
23911 cx.simulate_keystroke("up");
23912 cx.assert_excerpts_with_selections(indoc! {"
23913 [EXCERPT]
23914 [FOLDED]
23915 [EXCERPT]
23916 ˇa1
23917 b1
23918 [EXCERPT]
23919 [FOLDED]
23920 [EXCERPT]
23921 [FOLDED]
23922 "
23923 });
23924 for _ in 0..5 {
23925 cx.simulate_keystroke("up");
23926 cx.assert_excerpts_with_selections(indoc! {"
23927 [EXCERPT]
23928 ˇ[FOLDED]
23929 [EXCERPT]
23930 a1
23931 b1
23932 [EXCERPT]
23933 [FOLDED]
23934 [EXCERPT]
23935 [FOLDED]
23936 "
23937 });
23938 }
23939}
23940
23941#[gpui::test]
23942async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23943 init_test(cx, |_| {});
23944
23945 // Simple insertion
23946 assert_highlighted_edits(
23947 "Hello, world!",
23948 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23949 true,
23950 cx,
23951 |highlighted_edits, cx| {
23952 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23953 assert_eq!(highlighted_edits.highlights.len(), 1);
23954 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23955 assert_eq!(
23956 highlighted_edits.highlights[0].1.background_color,
23957 Some(cx.theme().status().created_background)
23958 );
23959 },
23960 )
23961 .await;
23962
23963 // Replacement
23964 assert_highlighted_edits(
23965 "This is a test.",
23966 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23967 false,
23968 cx,
23969 |highlighted_edits, cx| {
23970 assert_eq!(highlighted_edits.text, "That is a test.");
23971 assert_eq!(highlighted_edits.highlights.len(), 1);
23972 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23973 assert_eq!(
23974 highlighted_edits.highlights[0].1.background_color,
23975 Some(cx.theme().status().created_background)
23976 );
23977 },
23978 )
23979 .await;
23980
23981 // Multiple edits
23982 assert_highlighted_edits(
23983 "Hello, world!",
23984 vec![
23985 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23986 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23987 ],
23988 false,
23989 cx,
23990 |highlighted_edits, cx| {
23991 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23992 assert_eq!(highlighted_edits.highlights.len(), 2);
23993 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23994 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23995 assert_eq!(
23996 highlighted_edits.highlights[0].1.background_color,
23997 Some(cx.theme().status().created_background)
23998 );
23999 assert_eq!(
24000 highlighted_edits.highlights[1].1.background_color,
24001 Some(cx.theme().status().created_background)
24002 );
24003 },
24004 )
24005 .await;
24006
24007 // Multiple lines with edits
24008 assert_highlighted_edits(
24009 "First line\nSecond line\nThird line\nFourth line",
24010 vec![
24011 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
24012 (
24013 Point::new(2, 0)..Point::new(2, 10),
24014 "New third line".to_string(),
24015 ),
24016 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
24017 ],
24018 false,
24019 cx,
24020 |highlighted_edits, cx| {
24021 assert_eq!(
24022 highlighted_edits.text,
24023 "Second modified\nNew third line\nFourth updated line"
24024 );
24025 assert_eq!(highlighted_edits.highlights.len(), 3);
24026 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
24027 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
24028 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
24029 for highlight in &highlighted_edits.highlights {
24030 assert_eq!(
24031 highlight.1.background_color,
24032 Some(cx.theme().status().created_background)
24033 );
24034 }
24035 },
24036 )
24037 .await;
24038}
24039
24040#[gpui::test]
24041async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
24042 init_test(cx, |_| {});
24043
24044 // Deletion
24045 assert_highlighted_edits(
24046 "Hello, world!",
24047 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
24048 true,
24049 cx,
24050 |highlighted_edits, cx| {
24051 assert_eq!(highlighted_edits.text, "Hello, world!");
24052 assert_eq!(highlighted_edits.highlights.len(), 1);
24053 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
24054 assert_eq!(
24055 highlighted_edits.highlights[0].1.background_color,
24056 Some(cx.theme().status().deleted_background)
24057 );
24058 },
24059 )
24060 .await;
24061
24062 // Insertion
24063 assert_highlighted_edits(
24064 "Hello, world!",
24065 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
24066 true,
24067 cx,
24068 |highlighted_edits, cx| {
24069 assert_eq!(highlighted_edits.highlights.len(), 1);
24070 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
24071 assert_eq!(
24072 highlighted_edits.highlights[0].1.background_color,
24073 Some(cx.theme().status().created_background)
24074 );
24075 },
24076 )
24077 .await;
24078}
24079
24080async fn assert_highlighted_edits(
24081 text: &str,
24082 edits: Vec<(Range<Point>, String)>,
24083 include_deletions: bool,
24084 cx: &mut TestAppContext,
24085 assertion_fn: impl Fn(HighlightedText, &App),
24086) {
24087 let window = cx.add_window(|window, cx| {
24088 let buffer = MultiBuffer::build_simple(text, cx);
24089 Editor::new(EditorMode::full(), buffer, None, window, cx)
24090 });
24091 let cx = &mut VisualTestContext::from_window(*window, cx);
24092
24093 let (buffer, snapshot) = window
24094 .update(cx, |editor, _window, cx| {
24095 (
24096 editor.buffer().clone(),
24097 editor.buffer().read(cx).snapshot(cx),
24098 )
24099 })
24100 .unwrap();
24101
24102 let edits = edits
24103 .into_iter()
24104 .map(|(range, edit)| {
24105 (
24106 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
24107 edit,
24108 )
24109 })
24110 .collect::<Vec<_>>();
24111
24112 let text_anchor_edits = edits
24113 .clone()
24114 .into_iter()
24115 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
24116 .collect::<Vec<_>>();
24117
24118 let edit_preview = window
24119 .update(cx, |_, _window, cx| {
24120 buffer
24121 .read(cx)
24122 .as_singleton()
24123 .unwrap()
24124 .read(cx)
24125 .preview_edits(text_anchor_edits.into(), cx)
24126 })
24127 .unwrap()
24128 .await;
24129
24130 cx.update(|_window, cx| {
24131 let highlighted_edits = edit_prediction_edit_text(
24132 snapshot.as_singleton().unwrap().2,
24133 &edits,
24134 &edit_preview,
24135 include_deletions,
24136 cx,
24137 );
24138 assertion_fn(highlighted_edits, cx)
24139 });
24140}
24141
24142#[track_caller]
24143fn assert_breakpoint(
24144 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
24145 path: &Arc<Path>,
24146 expected: Vec<(u32, Breakpoint)>,
24147) {
24148 if expected.is_empty() {
24149 assert!(!breakpoints.contains_key(path), "{}", path.display());
24150 } else {
24151 let mut breakpoint = breakpoints
24152 .get(path)
24153 .unwrap()
24154 .iter()
24155 .map(|breakpoint| {
24156 (
24157 breakpoint.row,
24158 Breakpoint {
24159 message: breakpoint.message.clone(),
24160 state: breakpoint.state,
24161 condition: breakpoint.condition.clone(),
24162 hit_condition: breakpoint.hit_condition.clone(),
24163 },
24164 )
24165 })
24166 .collect::<Vec<_>>();
24167
24168 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
24169
24170 assert_eq!(expected, breakpoint);
24171 }
24172}
24173
24174fn add_log_breakpoint_at_cursor(
24175 editor: &mut Editor,
24176 log_message: &str,
24177 window: &mut Window,
24178 cx: &mut Context<Editor>,
24179) {
24180 let (anchor, bp) = editor
24181 .breakpoints_at_cursors(window, cx)
24182 .first()
24183 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
24184 .unwrap_or_else(|| {
24185 let snapshot = editor.snapshot(window, cx);
24186 let cursor_position: Point =
24187 editor.selections.newest(&snapshot.display_snapshot).head();
24188
24189 let breakpoint_position = snapshot
24190 .buffer_snapshot()
24191 .anchor_before(Point::new(cursor_position.row, 0));
24192
24193 (breakpoint_position, Breakpoint::new_log(log_message))
24194 });
24195
24196 editor.edit_breakpoint_at_anchor(
24197 anchor,
24198 bp,
24199 BreakpointEditAction::EditLogMessage(log_message.into()),
24200 cx,
24201 );
24202}
24203
24204#[gpui::test]
24205async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
24206 init_test(cx, |_| {});
24207
24208 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24209 let fs = FakeFs::new(cx.executor());
24210 fs.insert_tree(
24211 path!("/a"),
24212 json!({
24213 "main.rs": sample_text,
24214 }),
24215 )
24216 .await;
24217 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24218 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24219 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24220
24221 let fs = FakeFs::new(cx.executor());
24222 fs.insert_tree(
24223 path!("/a"),
24224 json!({
24225 "main.rs": sample_text,
24226 }),
24227 )
24228 .await;
24229 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24230 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24231 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24232 let worktree_id = workspace
24233 .update(cx, |workspace, _window, cx| {
24234 workspace.project().update(cx, |project, cx| {
24235 project.worktrees(cx).next().unwrap().read(cx).id()
24236 })
24237 })
24238 .unwrap();
24239
24240 let buffer = project
24241 .update(cx, |project, cx| {
24242 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24243 })
24244 .await
24245 .unwrap();
24246
24247 let (editor, cx) = cx.add_window_view(|window, cx| {
24248 Editor::new(
24249 EditorMode::full(),
24250 MultiBuffer::build_from_buffer(buffer, cx),
24251 Some(project.clone()),
24252 window,
24253 cx,
24254 )
24255 });
24256
24257 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24258 let abs_path = project.read_with(cx, |project, cx| {
24259 project
24260 .absolute_path(&project_path, cx)
24261 .map(Arc::from)
24262 .unwrap()
24263 });
24264
24265 // assert we can add breakpoint on the first line
24266 editor.update_in(cx, |editor, window, cx| {
24267 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24268 editor.move_to_end(&MoveToEnd, window, cx);
24269 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24270 });
24271
24272 let breakpoints = editor.update(cx, |editor, cx| {
24273 editor
24274 .breakpoint_store()
24275 .as_ref()
24276 .unwrap()
24277 .read(cx)
24278 .all_source_breakpoints(cx)
24279 });
24280
24281 assert_eq!(1, breakpoints.len());
24282 assert_breakpoint(
24283 &breakpoints,
24284 &abs_path,
24285 vec![
24286 (0, Breakpoint::new_standard()),
24287 (3, Breakpoint::new_standard()),
24288 ],
24289 );
24290
24291 editor.update_in(cx, |editor, window, cx| {
24292 editor.move_to_beginning(&MoveToBeginning, window, cx);
24293 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24294 });
24295
24296 let breakpoints = editor.update(cx, |editor, cx| {
24297 editor
24298 .breakpoint_store()
24299 .as_ref()
24300 .unwrap()
24301 .read(cx)
24302 .all_source_breakpoints(cx)
24303 });
24304
24305 assert_eq!(1, breakpoints.len());
24306 assert_breakpoint(
24307 &breakpoints,
24308 &abs_path,
24309 vec![(3, Breakpoint::new_standard())],
24310 );
24311
24312 editor.update_in(cx, |editor, window, cx| {
24313 editor.move_to_end(&MoveToEnd, window, cx);
24314 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24315 });
24316
24317 let breakpoints = editor.update(cx, |editor, cx| {
24318 editor
24319 .breakpoint_store()
24320 .as_ref()
24321 .unwrap()
24322 .read(cx)
24323 .all_source_breakpoints(cx)
24324 });
24325
24326 assert_eq!(0, breakpoints.len());
24327 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24328}
24329
24330#[gpui::test]
24331async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
24332 init_test(cx, |_| {});
24333
24334 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24335
24336 let fs = FakeFs::new(cx.executor());
24337 fs.insert_tree(
24338 path!("/a"),
24339 json!({
24340 "main.rs": sample_text,
24341 }),
24342 )
24343 .await;
24344 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24345 let (workspace, cx) =
24346 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24347
24348 let worktree_id = workspace.update(cx, |workspace, cx| {
24349 workspace.project().update(cx, |project, cx| {
24350 project.worktrees(cx).next().unwrap().read(cx).id()
24351 })
24352 });
24353
24354 let buffer = project
24355 .update(cx, |project, cx| {
24356 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24357 })
24358 .await
24359 .unwrap();
24360
24361 let (editor, cx) = cx.add_window_view(|window, cx| {
24362 Editor::new(
24363 EditorMode::full(),
24364 MultiBuffer::build_from_buffer(buffer, cx),
24365 Some(project.clone()),
24366 window,
24367 cx,
24368 )
24369 });
24370
24371 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24372 let abs_path = project.read_with(cx, |project, cx| {
24373 project
24374 .absolute_path(&project_path, cx)
24375 .map(Arc::from)
24376 .unwrap()
24377 });
24378
24379 editor.update_in(cx, |editor, window, cx| {
24380 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24381 });
24382
24383 let breakpoints = editor.update(cx, |editor, cx| {
24384 editor
24385 .breakpoint_store()
24386 .as_ref()
24387 .unwrap()
24388 .read(cx)
24389 .all_source_breakpoints(cx)
24390 });
24391
24392 assert_breakpoint(
24393 &breakpoints,
24394 &abs_path,
24395 vec![(0, Breakpoint::new_log("hello world"))],
24396 );
24397
24398 // Removing a log message from a log breakpoint should remove it
24399 editor.update_in(cx, |editor, window, cx| {
24400 add_log_breakpoint_at_cursor(editor, "", window, cx);
24401 });
24402
24403 let breakpoints = editor.update(cx, |editor, cx| {
24404 editor
24405 .breakpoint_store()
24406 .as_ref()
24407 .unwrap()
24408 .read(cx)
24409 .all_source_breakpoints(cx)
24410 });
24411
24412 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24413
24414 editor.update_in(cx, |editor, window, cx| {
24415 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24416 editor.move_to_end(&MoveToEnd, window, cx);
24417 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24418 // Not adding a log message to a standard breakpoint shouldn't remove it
24419 add_log_breakpoint_at_cursor(editor, "", window, cx);
24420 });
24421
24422 let breakpoints = editor.update(cx, |editor, cx| {
24423 editor
24424 .breakpoint_store()
24425 .as_ref()
24426 .unwrap()
24427 .read(cx)
24428 .all_source_breakpoints(cx)
24429 });
24430
24431 assert_breakpoint(
24432 &breakpoints,
24433 &abs_path,
24434 vec![
24435 (0, Breakpoint::new_standard()),
24436 (3, Breakpoint::new_standard()),
24437 ],
24438 );
24439
24440 editor.update_in(cx, |editor, window, cx| {
24441 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24442 });
24443
24444 let breakpoints = editor.update(cx, |editor, cx| {
24445 editor
24446 .breakpoint_store()
24447 .as_ref()
24448 .unwrap()
24449 .read(cx)
24450 .all_source_breakpoints(cx)
24451 });
24452
24453 assert_breakpoint(
24454 &breakpoints,
24455 &abs_path,
24456 vec![
24457 (0, Breakpoint::new_standard()),
24458 (3, Breakpoint::new_log("hello world")),
24459 ],
24460 );
24461
24462 editor.update_in(cx, |editor, window, cx| {
24463 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
24464 });
24465
24466 let breakpoints = editor.update(cx, |editor, cx| {
24467 editor
24468 .breakpoint_store()
24469 .as_ref()
24470 .unwrap()
24471 .read(cx)
24472 .all_source_breakpoints(cx)
24473 });
24474
24475 assert_breakpoint(
24476 &breakpoints,
24477 &abs_path,
24478 vec![
24479 (0, Breakpoint::new_standard()),
24480 (3, Breakpoint::new_log("hello Earth!!")),
24481 ],
24482 );
24483}
24484
24485/// This also tests that Editor::breakpoint_at_cursor_head is working properly
24486/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
24487/// or when breakpoints were placed out of order. This tests for a regression too
24488#[gpui::test]
24489async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
24490 init_test(cx, |_| {});
24491
24492 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24493 let fs = FakeFs::new(cx.executor());
24494 fs.insert_tree(
24495 path!("/a"),
24496 json!({
24497 "main.rs": sample_text,
24498 }),
24499 )
24500 .await;
24501 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24502 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24503 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24504
24505 let fs = FakeFs::new(cx.executor());
24506 fs.insert_tree(
24507 path!("/a"),
24508 json!({
24509 "main.rs": sample_text,
24510 }),
24511 )
24512 .await;
24513 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24514 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24515 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24516 let worktree_id = workspace
24517 .update(cx, |workspace, _window, cx| {
24518 workspace.project().update(cx, |project, cx| {
24519 project.worktrees(cx).next().unwrap().read(cx).id()
24520 })
24521 })
24522 .unwrap();
24523
24524 let buffer = project
24525 .update(cx, |project, cx| {
24526 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24527 })
24528 .await
24529 .unwrap();
24530
24531 let (editor, cx) = cx.add_window_view(|window, cx| {
24532 Editor::new(
24533 EditorMode::full(),
24534 MultiBuffer::build_from_buffer(buffer, cx),
24535 Some(project.clone()),
24536 window,
24537 cx,
24538 )
24539 });
24540
24541 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24542 let abs_path = project.read_with(cx, |project, cx| {
24543 project
24544 .absolute_path(&project_path, cx)
24545 .map(Arc::from)
24546 .unwrap()
24547 });
24548
24549 // assert we can add breakpoint on the first line
24550 editor.update_in(cx, |editor, window, cx| {
24551 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24552 editor.move_to_end(&MoveToEnd, window, cx);
24553 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24554 editor.move_up(&MoveUp, window, cx);
24555 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24556 });
24557
24558 let breakpoints = editor.update(cx, |editor, cx| {
24559 editor
24560 .breakpoint_store()
24561 .as_ref()
24562 .unwrap()
24563 .read(cx)
24564 .all_source_breakpoints(cx)
24565 });
24566
24567 assert_eq!(1, breakpoints.len());
24568 assert_breakpoint(
24569 &breakpoints,
24570 &abs_path,
24571 vec![
24572 (0, Breakpoint::new_standard()),
24573 (2, Breakpoint::new_standard()),
24574 (3, Breakpoint::new_standard()),
24575 ],
24576 );
24577
24578 editor.update_in(cx, |editor, window, cx| {
24579 editor.move_to_beginning(&MoveToBeginning, window, cx);
24580 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24581 editor.move_to_end(&MoveToEnd, window, cx);
24582 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24583 // Disabling a breakpoint that doesn't exist should do nothing
24584 editor.move_up(&MoveUp, window, cx);
24585 editor.move_up(&MoveUp, window, cx);
24586 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24587 });
24588
24589 let breakpoints = editor.update(cx, |editor, cx| {
24590 editor
24591 .breakpoint_store()
24592 .as_ref()
24593 .unwrap()
24594 .read(cx)
24595 .all_source_breakpoints(cx)
24596 });
24597
24598 let disable_breakpoint = {
24599 let mut bp = Breakpoint::new_standard();
24600 bp.state = BreakpointState::Disabled;
24601 bp
24602 };
24603
24604 assert_eq!(1, breakpoints.len());
24605 assert_breakpoint(
24606 &breakpoints,
24607 &abs_path,
24608 vec![
24609 (0, disable_breakpoint.clone()),
24610 (2, Breakpoint::new_standard()),
24611 (3, disable_breakpoint.clone()),
24612 ],
24613 );
24614
24615 editor.update_in(cx, |editor, window, cx| {
24616 editor.move_to_beginning(&MoveToBeginning, window, cx);
24617 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24618 editor.move_to_end(&MoveToEnd, window, cx);
24619 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24620 editor.move_up(&MoveUp, window, cx);
24621 editor.disable_breakpoint(&actions::DisableBreakpoint, 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_eq!(1, breakpoints.len());
24634 assert_breakpoint(
24635 &breakpoints,
24636 &abs_path,
24637 vec![
24638 (0, Breakpoint::new_standard()),
24639 (2, disable_breakpoint),
24640 (3, Breakpoint::new_standard()),
24641 ],
24642 );
24643}
24644
24645#[gpui::test]
24646async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
24647 init_test(cx, |_| {});
24648 let capabilities = lsp::ServerCapabilities {
24649 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
24650 prepare_provider: Some(true),
24651 work_done_progress_options: Default::default(),
24652 })),
24653 ..Default::default()
24654 };
24655 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24656
24657 cx.set_state(indoc! {"
24658 struct Fˇoo {}
24659 "});
24660
24661 cx.update_editor(|editor, _, cx| {
24662 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24663 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24664 editor.highlight_background::<DocumentHighlightRead>(
24665 &[highlight_range],
24666 |_, theme| theme.colors().editor_document_highlight_read_background,
24667 cx,
24668 );
24669 });
24670
24671 let mut prepare_rename_handler = cx
24672 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
24673 move |_, _, _| async move {
24674 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
24675 start: lsp::Position {
24676 line: 0,
24677 character: 7,
24678 },
24679 end: lsp::Position {
24680 line: 0,
24681 character: 10,
24682 },
24683 })))
24684 },
24685 );
24686 let prepare_rename_task = cx
24687 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24688 .expect("Prepare rename was not started");
24689 prepare_rename_handler.next().await.unwrap();
24690 prepare_rename_task.await.expect("Prepare rename failed");
24691
24692 let mut rename_handler =
24693 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24694 let edit = lsp::TextEdit {
24695 range: lsp::Range {
24696 start: lsp::Position {
24697 line: 0,
24698 character: 7,
24699 },
24700 end: lsp::Position {
24701 line: 0,
24702 character: 10,
24703 },
24704 },
24705 new_text: "FooRenamed".to_string(),
24706 };
24707 Ok(Some(lsp::WorkspaceEdit::new(
24708 // Specify the same edit twice
24709 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
24710 )))
24711 });
24712 let rename_task = cx
24713 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24714 .expect("Confirm rename was not started");
24715 rename_handler.next().await.unwrap();
24716 rename_task.await.expect("Confirm rename failed");
24717 cx.run_until_parked();
24718
24719 // Despite two edits, only one is actually applied as those are identical
24720 cx.assert_editor_state(indoc! {"
24721 struct FooRenamedˇ {}
24722 "});
24723}
24724
24725#[gpui::test]
24726async fn test_rename_without_prepare(cx: &mut TestAppContext) {
24727 init_test(cx, |_| {});
24728 // These capabilities indicate that the server does not support prepare rename.
24729 let capabilities = lsp::ServerCapabilities {
24730 rename_provider: Some(lsp::OneOf::Left(true)),
24731 ..Default::default()
24732 };
24733 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24734
24735 cx.set_state(indoc! {"
24736 struct Fˇoo {}
24737 "});
24738
24739 cx.update_editor(|editor, _window, cx| {
24740 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24741 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24742 editor.highlight_background::<DocumentHighlightRead>(
24743 &[highlight_range],
24744 |_, theme| theme.colors().editor_document_highlight_read_background,
24745 cx,
24746 );
24747 });
24748
24749 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24750 .expect("Prepare rename was not started")
24751 .await
24752 .expect("Prepare rename failed");
24753
24754 let mut rename_handler =
24755 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24756 let edit = lsp::TextEdit {
24757 range: lsp::Range {
24758 start: lsp::Position {
24759 line: 0,
24760 character: 7,
24761 },
24762 end: lsp::Position {
24763 line: 0,
24764 character: 10,
24765 },
24766 },
24767 new_text: "FooRenamed".to_string(),
24768 };
24769 Ok(Some(lsp::WorkspaceEdit::new(
24770 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
24771 )))
24772 });
24773 let rename_task = cx
24774 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24775 .expect("Confirm rename was not started");
24776 rename_handler.next().await.unwrap();
24777 rename_task.await.expect("Confirm rename failed");
24778 cx.run_until_parked();
24779
24780 // Correct range is renamed, as `surrounding_word` is used to find it.
24781 cx.assert_editor_state(indoc! {"
24782 struct FooRenamedˇ {}
24783 "});
24784}
24785
24786#[gpui::test]
24787async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
24788 init_test(cx, |_| {});
24789 let mut cx = EditorTestContext::new(cx).await;
24790
24791 let language = Arc::new(
24792 Language::new(
24793 LanguageConfig::default(),
24794 Some(tree_sitter_html::LANGUAGE.into()),
24795 )
24796 .with_brackets_query(
24797 r#"
24798 ("<" @open "/>" @close)
24799 ("</" @open ">" @close)
24800 ("<" @open ">" @close)
24801 ("\"" @open "\"" @close)
24802 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
24803 "#,
24804 )
24805 .unwrap(),
24806 );
24807 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24808
24809 cx.set_state(indoc! {"
24810 <span>ˇ</span>
24811 "});
24812 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24813 cx.assert_editor_state(indoc! {"
24814 <span>
24815 ˇ
24816 </span>
24817 "});
24818
24819 cx.set_state(indoc! {"
24820 <span><span></span>ˇ</span>
24821 "});
24822 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24823 cx.assert_editor_state(indoc! {"
24824 <span><span></span>
24825 ˇ</span>
24826 "});
24827
24828 cx.set_state(indoc! {"
24829 <span>ˇ
24830 </span>
24831 "});
24832 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24833 cx.assert_editor_state(indoc! {"
24834 <span>
24835 ˇ
24836 </span>
24837 "});
24838}
24839
24840#[gpui::test(iterations = 10)]
24841async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
24842 init_test(cx, |_| {});
24843
24844 let fs = FakeFs::new(cx.executor());
24845 fs.insert_tree(
24846 path!("/dir"),
24847 json!({
24848 "a.ts": "a",
24849 }),
24850 )
24851 .await;
24852
24853 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
24854 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24855 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24856
24857 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24858 language_registry.add(Arc::new(Language::new(
24859 LanguageConfig {
24860 name: "TypeScript".into(),
24861 matcher: LanguageMatcher {
24862 path_suffixes: vec!["ts".to_string()],
24863 ..Default::default()
24864 },
24865 ..Default::default()
24866 },
24867 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24868 )));
24869 let mut fake_language_servers = language_registry.register_fake_lsp(
24870 "TypeScript",
24871 FakeLspAdapter {
24872 capabilities: lsp::ServerCapabilities {
24873 code_lens_provider: Some(lsp::CodeLensOptions {
24874 resolve_provider: Some(true),
24875 }),
24876 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24877 commands: vec!["_the/command".to_string()],
24878 ..lsp::ExecuteCommandOptions::default()
24879 }),
24880 ..lsp::ServerCapabilities::default()
24881 },
24882 ..FakeLspAdapter::default()
24883 },
24884 );
24885
24886 let editor = workspace
24887 .update(cx, |workspace, window, cx| {
24888 workspace.open_abs_path(
24889 PathBuf::from(path!("/dir/a.ts")),
24890 OpenOptions::default(),
24891 window,
24892 cx,
24893 )
24894 })
24895 .unwrap()
24896 .await
24897 .unwrap()
24898 .downcast::<Editor>()
24899 .unwrap();
24900 cx.executor().run_until_parked();
24901
24902 let fake_server = fake_language_servers.next().await.unwrap();
24903
24904 let buffer = editor.update(cx, |editor, cx| {
24905 editor
24906 .buffer()
24907 .read(cx)
24908 .as_singleton()
24909 .expect("have opened a single file by path")
24910 });
24911
24912 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24913 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24914 drop(buffer_snapshot);
24915 let actions = cx
24916 .update_window(*workspace, |_, window, cx| {
24917 project.code_actions(&buffer, anchor..anchor, window, cx)
24918 })
24919 .unwrap();
24920
24921 fake_server
24922 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24923 Ok(Some(vec![
24924 lsp::CodeLens {
24925 range: lsp::Range::default(),
24926 command: Some(lsp::Command {
24927 title: "Code lens command".to_owned(),
24928 command: "_the/command".to_owned(),
24929 arguments: None,
24930 }),
24931 data: None,
24932 },
24933 lsp::CodeLens {
24934 range: lsp::Range::default(),
24935 command: Some(lsp::Command {
24936 title: "Command not in capabilities".to_owned(),
24937 command: "not in capabilities".to_owned(),
24938 arguments: None,
24939 }),
24940 data: None,
24941 },
24942 lsp::CodeLens {
24943 range: lsp::Range {
24944 start: lsp::Position {
24945 line: 1,
24946 character: 1,
24947 },
24948 end: lsp::Position {
24949 line: 1,
24950 character: 1,
24951 },
24952 },
24953 command: Some(lsp::Command {
24954 title: "Command not in range".to_owned(),
24955 command: "_the/command".to_owned(),
24956 arguments: None,
24957 }),
24958 data: None,
24959 },
24960 ]))
24961 })
24962 .next()
24963 .await;
24964
24965 let actions = actions.await.unwrap();
24966 assert_eq!(
24967 actions.len(),
24968 1,
24969 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24970 );
24971 let action = actions[0].clone();
24972 let apply = project.update(cx, |project, cx| {
24973 project.apply_code_action(buffer.clone(), action, true, cx)
24974 });
24975
24976 // Resolving the code action does not populate its edits. In absence of
24977 // edits, we must execute the given command.
24978 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24979 |mut lens, _| async move {
24980 let lens_command = lens.command.as_mut().expect("should have a command");
24981 assert_eq!(lens_command.title, "Code lens command");
24982 lens_command.arguments = Some(vec![json!("the-argument")]);
24983 Ok(lens)
24984 },
24985 );
24986
24987 // While executing the command, the language server sends the editor
24988 // a `workspaceEdit` request.
24989 fake_server
24990 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24991 let fake = fake_server.clone();
24992 move |params, _| {
24993 assert_eq!(params.command, "_the/command");
24994 let fake = fake.clone();
24995 async move {
24996 fake.server
24997 .request::<lsp::request::ApplyWorkspaceEdit>(
24998 lsp::ApplyWorkspaceEditParams {
24999 label: None,
25000 edit: lsp::WorkspaceEdit {
25001 changes: Some(
25002 [(
25003 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
25004 vec![lsp::TextEdit {
25005 range: lsp::Range::new(
25006 lsp::Position::new(0, 0),
25007 lsp::Position::new(0, 0),
25008 ),
25009 new_text: "X".into(),
25010 }],
25011 )]
25012 .into_iter()
25013 .collect(),
25014 ),
25015 ..lsp::WorkspaceEdit::default()
25016 },
25017 },
25018 )
25019 .await
25020 .into_response()
25021 .unwrap();
25022 Ok(Some(json!(null)))
25023 }
25024 }
25025 })
25026 .next()
25027 .await;
25028
25029 // Applying the code lens command returns a project transaction containing the edits
25030 // sent by the language server in its `workspaceEdit` request.
25031 let transaction = apply.await.unwrap();
25032 assert!(transaction.0.contains_key(&buffer));
25033 buffer.update(cx, |buffer, cx| {
25034 assert_eq!(buffer.text(), "Xa");
25035 buffer.undo(cx);
25036 assert_eq!(buffer.text(), "a");
25037 });
25038
25039 let actions_after_edits = cx
25040 .update_window(*workspace, |_, window, cx| {
25041 project.code_actions(&buffer, anchor..anchor, window, cx)
25042 })
25043 .unwrap()
25044 .await
25045 .unwrap();
25046 assert_eq!(
25047 actions, actions_after_edits,
25048 "For the same selection, same code lens actions should be returned"
25049 );
25050
25051 let _responses =
25052 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
25053 panic!("No more code lens requests are expected");
25054 });
25055 editor.update_in(cx, |editor, window, cx| {
25056 editor.select_all(&SelectAll, window, cx);
25057 });
25058 cx.executor().run_until_parked();
25059 let new_actions = cx
25060 .update_window(*workspace, |_, window, cx| {
25061 project.code_actions(&buffer, anchor..anchor, window, cx)
25062 })
25063 .unwrap()
25064 .await
25065 .unwrap();
25066 assert_eq!(
25067 actions, new_actions,
25068 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
25069 );
25070}
25071
25072#[gpui::test]
25073async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
25074 init_test(cx, |_| {});
25075
25076 let fs = FakeFs::new(cx.executor());
25077 let main_text = r#"fn main() {
25078println!("1");
25079println!("2");
25080println!("3");
25081println!("4");
25082println!("5");
25083}"#;
25084 let lib_text = "mod foo {}";
25085 fs.insert_tree(
25086 path!("/a"),
25087 json!({
25088 "lib.rs": lib_text,
25089 "main.rs": main_text,
25090 }),
25091 )
25092 .await;
25093
25094 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25095 let (workspace, cx) =
25096 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25097 let worktree_id = workspace.update(cx, |workspace, cx| {
25098 workspace.project().update(cx, |project, cx| {
25099 project.worktrees(cx).next().unwrap().read(cx).id()
25100 })
25101 });
25102
25103 let expected_ranges = vec![
25104 Point::new(0, 0)..Point::new(0, 0),
25105 Point::new(1, 0)..Point::new(1, 1),
25106 Point::new(2, 0)..Point::new(2, 2),
25107 Point::new(3, 0)..Point::new(3, 3),
25108 ];
25109
25110 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25111 let editor_1 = workspace
25112 .update_in(cx, |workspace, window, cx| {
25113 workspace.open_path(
25114 (worktree_id, rel_path("main.rs")),
25115 Some(pane_1.downgrade()),
25116 true,
25117 window,
25118 cx,
25119 )
25120 })
25121 .unwrap()
25122 .await
25123 .downcast::<Editor>()
25124 .unwrap();
25125 pane_1.update(cx, |pane, cx| {
25126 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25127 open_editor.update(cx, |editor, cx| {
25128 assert_eq!(
25129 editor.display_text(cx),
25130 main_text,
25131 "Original main.rs text on initial open",
25132 );
25133 assert_eq!(
25134 editor
25135 .selections
25136 .all::<Point>(&editor.display_snapshot(cx))
25137 .into_iter()
25138 .map(|s| s.range())
25139 .collect::<Vec<_>>(),
25140 vec![Point::zero()..Point::zero()],
25141 "Default selections on initial open",
25142 );
25143 })
25144 });
25145 editor_1.update_in(cx, |editor, window, cx| {
25146 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25147 s.select_ranges(expected_ranges.clone());
25148 });
25149 });
25150
25151 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
25152 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
25153 });
25154 let editor_2 = workspace
25155 .update_in(cx, |workspace, window, cx| {
25156 workspace.open_path(
25157 (worktree_id, rel_path("main.rs")),
25158 Some(pane_2.downgrade()),
25159 true,
25160 window,
25161 cx,
25162 )
25163 })
25164 .unwrap()
25165 .await
25166 .downcast::<Editor>()
25167 .unwrap();
25168 pane_2.update(cx, |pane, cx| {
25169 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25170 open_editor.update(cx, |editor, cx| {
25171 assert_eq!(
25172 editor.display_text(cx),
25173 main_text,
25174 "Original main.rs text on initial open in another panel",
25175 );
25176 assert_eq!(
25177 editor
25178 .selections
25179 .all::<Point>(&editor.display_snapshot(cx))
25180 .into_iter()
25181 .map(|s| s.range())
25182 .collect::<Vec<_>>(),
25183 vec![Point::zero()..Point::zero()],
25184 "Default selections on initial open in another panel",
25185 );
25186 })
25187 });
25188
25189 editor_2.update_in(cx, |editor, window, cx| {
25190 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
25191 });
25192
25193 let _other_editor_1 = workspace
25194 .update_in(cx, |workspace, window, cx| {
25195 workspace.open_path(
25196 (worktree_id, rel_path("lib.rs")),
25197 Some(pane_1.downgrade()),
25198 true,
25199 window,
25200 cx,
25201 )
25202 })
25203 .unwrap()
25204 .await
25205 .downcast::<Editor>()
25206 .unwrap();
25207 pane_1
25208 .update_in(cx, |pane, window, cx| {
25209 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
25210 })
25211 .await
25212 .unwrap();
25213 drop(editor_1);
25214 pane_1.update(cx, |pane, cx| {
25215 pane.active_item()
25216 .unwrap()
25217 .downcast::<Editor>()
25218 .unwrap()
25219 .update(cx, |editor, cx| {
25220 assert_eq!(
25221 editor.display_text(cx),
25222 lib_text,
25223 "Other file should be open and active",
25224 );
25225 });
25226 assert_eq!(pane.items().count(), 1, "No other editors should be open");
25227 });
25228
25229 let _other_editor_2 = workspace
25230 .update_in(cx, |workspace, window, cx| {
25231 workspace.open_path(
25232 (worktree_id, rel_path("lib.rs")),
25233 Some(pane_2.downgrade()),
25234 true,
25235 window,
25236 cx,
25237 )
25238 })
25239 .unwrap()
25240 .await
25241 .downcast::<Editor>()
25242 .unwrap();
25243 pane_2
25244 .update_in(cx, |pane, window, cx| {
25245 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
25246 })
25247 .await
25248 .unwrap();
25249 drop(editor_2);
25250 pane_2.update(cx, |pane, cx| {
25251 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25252 open_editor.update(cx, |editor, cx| {
25253 assert_eq!(
25254 editor.display_text(cx),
25255 lib_text,
25256 "Other file should be open and active in another panel too",
25257 );
25258 });
25259 assert_eq!(
25260 pane.items().count(),
25261 1,
25262 "No other editors should be open in another pane",
25263 );
25264 });
25265
25266 let _editor_1_reopened = workspace
25267 .update_in(cx, |workspace, window, cx| {
25268 workspace.open_path(
25269 (worktree_id, rel_path("main.rs")),
25270 Some(pane_1.downgrade()),
25271 true,
25272 window,
25273 cx,
25274 )
25275 })
25276 .unwrap()
25277 .await
25278 .downcast::<Editor>()
25279 .unwrap();
25280 let _editor_2_reopened = workspace
25281 .update_in(cx, |workspace, window, cx| {
25282 workspace.open_path(
25283 (worktree_id, rel_path("main.rs")),
25284 Some(pane_2.downgrade()),
25285 true,
25286 window,
25287 cx,
25288 )
25289 })
25290 .unwrap()
25291 .await
25292 .downcast::<Editor>()
25293 .unwrap();
25294 pane_1.update(cx, |pane, cx| {
25295 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25296 open_editor.update(cx, |editor, cx| {
25297 assert_eq!(
25298 editor.display_text(cx),
25299 main_text,
25300 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
25301 );
25302 assert_eq!(
25303 editor
25304 .selections
25305 .all::<Point>(&editor.display_snapshot(cx))
25306 .into_iter()
25307 .map(|s| s.range())
25308 .collect::<Vec<_>>(),
25309 expected_ranges,
25310 "Previous editor in the 1st panel had selections and should get them restored on reopen",
25311 );
25312 })
25313 });
25314 pane_2.update(cx, |pane, cx| {
25315 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25316 open_editor.update(cx, |editor, cx| {
25317 assert_eq!(
25318 editor.display_text(cx),
25319 r#"fn main() {
25320⋯rintln!("1");
25321⋯intln!("2");
25322⋯ntln!("3");
25323println!("4");
25324println!("5");
25325}"#,
25326 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
25327 );
25328 assert_eq!(
25329 editor
25330 .selections
25331 .all::<Point>(&editor.display_snapshot(cx))
25332 .into_iter()
25333 .map(|s| s.range())
25334 .collect::<Vec<_>>(),
25335 vec![Point::zero()..Point::zero()],
25336 "Previous editor in the 2nd pane had no selections changed hence should restore none",
25337 );
25338 })
25339 });
25340}
25341
25342#[gpui::test]
25343async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
25344 init_test(cx, |_| {});
25345
25346 let fs = FakeFs::new(cx.executor());
25347 let main_text = r#"fn main() {
25348println!("1");
25349println!("2");
25350println!("3");
25351println!("4");
25352println!("5");
25353}"#;
25354 let lib_text = "mod foo {}";
25355 fs.insert_tree(
25356 path!("/a"),
25357 json!({
25358 "lib.rs": lib_text,
25359 "main.rs": main_text,
25360 }),
25361 )
25362 .await;
25363
25364 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25365 let (workspace, cx) =
25366 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25367 let worktree_id = workspace.update(cx, |workspace, cx| {
25368 workspace.project().update(cx, |project, cx| {
25369 project.worktrees(cx).next().unwrap().read(cx).id()
25370 })
25371 });
25372
25373 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25374 let editor = workspace
25375 .update_in(cx, |workspace, window, cx| {
25376 workspace.open_path(
25377 (worktree_id, rel_path("main.rs")),
25378 Some(pane.downgrade()),
25379 true,
25380 window,
25381 cx,
25382 )
25383 })
25384 .unwrap()
25385 .await
25386 .downcast::<Editor>()
25387 .unwrap();
25388 pane.update(cx, |pane, cx| {
25389 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25390 open_editor.update(cx, |editor, cx| {
25391 assert_eq!(
25392 editor.display_text(cx),
25393 main_text,
25394 "Original main.rs text on initial open",
25395 );
25396 })
25397 });
25398 editor.update_in(cx, |editor, window, cx| {
25399 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
25400 });
25401
25402 cx.update_global(|store: &mut SettingsStore, cx| {
25403 store.update_user_settings(cx, |s| {
25404 s.workspace.restore_on_file_reopen = Some(false);
25405 });
25406 });
25407 editor.update_in(cx, |editor, window, cx| {
25408 editor.fold_ranges(
25409 vec![
25410 Point::new(1, 0)..Point::new(1, 1),
25411 Point::new(2, 0)..Point::new(2, 2),
25412 Point::new(3, 0)..Point::new(3, 3),
25413 ],
25414 false,
25415 window,
25416 cx,
25417 );
25418 });
25419 pane.update_in(cx, |pane, window, cx| {
25420 pane.close_all_items(&CloseAllItems::default(), window, cx)
25421 })
25422 .await
25423 .unwrap();
25424 pane.update(cx, |pane, _| {
25425 assert!(pane.active_item().is_none());
25426 });
25427 cx.update_global(|store: &mut SettingsStore, cx| {
25428 store.update_user_settings(cx, |s| {
25429 s.workspace.restore_on_file_reopen = Some(true);
25430 });
25431 });
25432
25433 let _editor_reopened = workspace
25434 .update_in(cx, |workspace, window, cx| {
25435 workspace.open_path(
25436 (worktree_id, rel_path("main.rs")),
25437 Some(pane.downgrade()),
25438 true,
25439 window,
25440 cx,
25441 )
25442 })
25443 .unwrap()
25444 .await
25445 .downcast::<Editor>()
25446 .unwrap();
25447 pane.update(cx, |pane, cx| {
25448 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25449 open_editor.update(cx, |editor, cx| {
25450 assert_eq!(
25451 editor.display_text(cx),
25452 main_text,
25453 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
25454 );
25455 })
25456 });
25457}
25458
25459#[gpui::test]
25460async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
25461 struct EmptyModalView {
25462 focus_handle: gpui::FocusHandle,
25463 }
25464 impl EventEmitter<DismissEvent> for EmptyModalView {}
25465 impl Render for EmptyModalView {
25466 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
25467 div()
25468 }
25469 }
25470 impl Focusable for EmptyModalView {
25471 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
25472 self.focus_handle.clone()
25473 }
25474 }
25475 impl workspace::ModalView for EmptyModalView {}
25476 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
25477 EmptyModalView {
25478 focus_handle: cx.focus_handle(),
25479 }
25480 }
25481
25482 init_test(cx, |_| {});
25483
25484 let fs = FakeFs::new(cx.executor());
25485 let project = Project::test(fs, [], cx).await;
25486 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25487 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
25488 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
25489 let editor = cx.new_window_entity(|window, cx| {
25490 Editor::new(
25491 EditorMode::full(),
25492 buffer,
25493 Some(project.clone()),
25494 window,
25495 cx,
25496 )
25497 });
25498 workspace
25499 .update(cx, |workspace, window, cx| {
25500 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
25501 })
25502 .unwrap();
25503 editor.update_in(cx, |editor, window, cx| {
25504 editor.open_context_menu(&OpenContextMenu, window, cx);
25505 assert!(editor.mouse_context_menu.is_some());
25506 });
25507 workspace
25508 .update(cx, |workspace, window, cx| {
25509 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
25510 })
25511 .unwrap();
25512 cx.read(|cx| {
25513 assert!(editor.read(cx).mouse_context_menu.is_none());
25514 });
25515}
25516
25517fn set_linked_edit_ranges(
25518 opening: (Point, Point),
25519 closing: (Point, Point),
25520 editor: &mut Editor,
25521 cx: &mut Context<Editor>,
25522) {
25523 let Some((buffer, _)) = editor
25524 .buffer
25525 .read(cx)
25526 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
25527 else {
25528 panic!("Failed to get buffer for selection position");
25529 };
25530 let buffer = buffer.read(cx);
25531 let buffer_id = buffer.remote_id();
25532 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
25533 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
25534 let mut linked_ranges = HashMap::default();
25535 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
25536 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
25537}
25538
25539#[gpui::test]
25540async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
25541 init_test(cx, |_| {});
25542
25543 let fs = FakeFs::new(cx.executor());
25544 fs.insert_file(path!("/file.html"), Default::default())
25545 .await;
25546
25547 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
25548
25549 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25550 let html_language = Arc::new(Language::new(
25551 LanguageConfig {
25552 name: "HTML".into(),
25553 matcher: LanguageMatcher {
25554 path_suffixes: vec!["html".to_string()],
25555 ..LanguageMatcher::default()
25556 },
25557 brackets: BracketPairConfig {
25558 pairs: vec![BracketPair {
25559 start: "<".into(),
25560 end: ">".into(),
25561 close: true,
25562 ..Default::default()
25563 }],
25564 ..Default::default()
25565 },
25566 ..Default::default()
25567 },
25568 Some(tree_sitter_html::LANGUAGE.into()),
25569 ));
25570 language_registry.add(html_language);
25571 let mut fake_servers = language_registry.register_fake_lsp(
25572 "HTML",
25573 FakeLspAdapter {
25574 capabilities: lsp::ServerCapabilities {
25575 completion_provider: Some(lsp::CompletionOptions {
25576 resolve_provider: Some(true),
25577 ..Default::default()
25578 }),
25579 ..Default::default()
25580 },
25581 ..Default::default()
25582 },
25583 );
25584
25585 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25586 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25587
25588 let worktree_id = workspace
25589 .update(cx, |workspace, _window, cx| {
25590 workspace.project().update(cx, |project, cx| {
25591 project.worktrees(cx).next().unwrap().read(cx).id()
25592 })
25593 })
25594 .unwrap();
25595 project
25596 .update(cx, |project, cx| {
25597 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
25598 })
25599 .await
25600 .unwrap();
25601 let editor = workspace
25602 .update(cx, |workspace, window, cx| {
25603 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
25604 })
25605 .unwrap()
25606 .await
25607 .unwrap()
25608 .downcast::<Editor>()
25609 .unwrap();
25610
25611 let fake_server = fake_servers.next().await.unwrap();
25612 cx.run_until_parked();
25613 editor.update_in(cx, |editor, window, cx| {
25614 editor.set_text("<ad></ad>", window, cx);
25615 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
25616 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
25617 });
25618 set_linked_edit_ranges(
25619 (Point::new(0, 1), Point::new(0, 3)),
25620 (Point::new(0, 6), Point::new(0, 8)),
25621 editor,
25622 cx,
25623 );
25624 });
25625 let mut completion_handle =
25626 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25627 Ok(Some(lsp::CompletionResponse::Array(vec![
25628 lsp::CompletionItem {
25629 label: "head".to_string(),
25630 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25631 lsp::InsertReplaceEdit {
25632 new_text: "head".to_string(),
25633 insert: lsp::Range::new(
25634 lsp::Position::new(0, 1),
25635 lsp::Position::new(0, 3),
25636 ),
25637 replace: lsp::Range::new(
25638 lsp::Position::new(0, 1),
25639 lsp::Position::new(0, 3),
25640 ),
25641 },
25642 )),
25643 ..Default::default()
25644 },
25645 ])))
25646 });
25647 editor.update_in(cx, |editor, window, cx| {
25648 editor.show_completions(&ShowCompletions, window, cx);
25649 });
25650 cx.run_until_parked();
25651 completion_handle.next().await.unwrap();
25652 editor.update(cx, |editor, _| {
25653 assert!(
25654 editor.context_menu_visible(),
25655 "Completion menu should be visible"
25656 );
25657 });
25658 editor.update_in(cx, |editor, window, cx| {
25659 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
25660 });
25661 cx.executor().run_until_parked();
25662 editor.update(cx, |editor, cx| {
25663 assert_eq!(editor.text(cx), "<head></head>");
25664 });
25665}
25666
25667#[gpui::test]
25668async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
25669 init_test(cx, |_| {});
25670
25671 let mut cx = EditorTestContext::new(cx).await;
25672 let language = Arc::new(Language::new(
25673 LanguageConfig {
25674 name: "TSX".into(),
25675 matcher: LanguageMatcher {
25676 path_suffixes: vec!["tsx".to_string()],
25677 ..LanguageMatcher::default()
25678 },
25679 brackets: BracketPairConfig {
25680 pairs: vec![BracketPair {
25681 start: "<".into(),
25682 end: ">".into(),
25683 close: true,
25684 ..Default::default()
25685 }],
25686 ..Default::default()
25687 },
25688 linked_edit_characters: HashSet::from_iter(['.']),
25689 ..Default::default()
25690 },
25691 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
25692 ));
25693 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25694
25695 // Test typing > does not extend linked pair
25696 cx.set_state("<divˇ<div></div>");
25697 cx.update_editor(|editor, _, cx| {
25698 set_linked_edit_ranges(
25699 (Point::new(0, 1), Point::new(0, 4)),
25700 (Point::new(0, 11), Point::new(0, 14)),
25701 editor,
25702 cx,
25703 );
25704 });
25705 cx.update_editor(|editor, window, cx| {
25706 editor.handle_input(">", window, cx);
25707 });
25708 cx.assert_editor_state("<div>ˇ<div></div>");
25709
25710 // Test typing . do extend linked pair
25711 cx.set_state("<Animatedˇ></Animated>");
25712 cx.update_editor(|editor, _, cx| {
25713 set_linked_edit_ranges(
25714 (Point::new(0, 1), Point::new(0, 9)),
25715 (Point::new(0, 12), Point::new(0, 20)),
25716 editor,
25717 cx,
25718 );
25719 });
25720 cx.update_editor(|editor, window, cx| {
25721 editor.handle_input(".", window, cx);
25722 });
25723 cx.assert_editor_state("<Animated.ˇ></Animated.>");
25724 cx.update_editor(|editor, _, cx| {
25725 set_linked_edit_ranges(
25726 (Point::new(0, 1), Point::new(0, 10)),
25727 (Point::new(0, 13), Point::new(0, 21)),
25728 editor,
25729 cx,
25730 );
25731 });
25732 cx.update_editor(|editor, window, cx| {
25733 editor.handle_input("V", window, cx);
25734 });
25735 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
25736}
25737
25738#[gpui::test]
25739async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
25740 init_test(cx, |_| {});
25741
25742 let fs = FakeFs::new(cx.executor());
25743 fs.insert_tree(
25744 path!("/root"),
25745 json!({
25746 "a": {
25747 "main.rs": "fn main() {}",
25748 },
25749 "foo": {
25750 "bar": {
25751 "external_file.rs": "pub mod external {}",
25752 }
25753 }
25754 }),
25755 )
25756 .await;
25757
25758 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
25759 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25760 language_registry.add(rust_lang());
25761 let _fake_servers = language_registry.register_fake_lsp(
25762 "Rust",
25763 FakeLspAdapter {
25764 ..FakeLspAdapter::default()
25765 },
25766 );
25767 let (workspace, cx) =
25768 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25769 let worktree_id = workspace.update(cx, |workspace, cx| {
25770 workspace.project().update(cx, |project, cx| {
25771 project.worktrees(cx).next().unwrap().read(cx).id()
25772 })
25773 });
25774
25775 let assert_language_servers_count =
25776 |expected: usize, context: &str, cx: &mut VisualTestContext| {
25777 project.update(cx, |project, cx| {
25778 let current = project
25779 .lsp_store()
25780 .read(cx)
25781 .as_local()
25782 .unwrap()
25783 .language_servers
25784 .len();
25785 assert_eq!(expected, current, "{context}");
25786 });
25787 };
25788
25789 assert_language_servers_count(
25790 0,
25791 "No servers should be running before any file is open",
25792 cx,
25793 );
25794 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25795 let main_editor = workspace
25796 .update_in(cx, |workspace, window, cx| {
25797 workspace.open_path(
25798 (worktree_id, rel_path("main.rs")),
25799 Some(pane.downgrade()),
25800 true,
25801 window,
25802 cx,
25803 )
25804 })
25805 .unwrap()
25806 .await
25807 .downcast::<Editor>()
25808 .unwrap();
25809 pane.update(cx, |pane, cx| {
25810 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25811 open_editor.update(cx, |editor, cx| {
25812 assert_eq!(
25813 editor.display_text(cx),
25814 "fn main() {}",
25815 "Original main.rs text on initial open",
25816 );
25817 });
25818 assert_eq!(open_editor, main_editor);
25819 });
25820 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
25821
25822 let external_editor = workspace
25823 .update_in(cx, |workspace, window, cx| {
25824 workspace.open_abs_path(
25825 PathBuf::from("/root/foo/bar/external_file.rs"),
25826 OpenOptions::default(),
25827 window,
25828 cx,
25829 )
25830 })
25831 .await
25832 .expect("opening external file")
25833 .downcast::<Editor>()
25834 .expect("downcasted external file's open element to editor");
25835 pane.update(cx, |pane, cx| {
25836 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25837 open_editor.update(cx, |editor, cx| {
25838 assert_eq!(
25839 editor.display_text(cx),
25840 "pub mod external {}",
25841 "External file is open now",
25842 );
25843 });
25844 assert_eq!(open_editor, external_editor);
25845 });
25846 assert_language_servers_count(
25847 1,
25848 "Second, external, *.rs file should join the existing server",
25849 cx,
25850 );
25851
25852 pane.update_in(cx, |pane, window, cx| {
25853 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25854 })
25855 .await
25856 .unwrap();
25857 pane.update_in(cx, |pane, window, cx| {
25858 pane.navigate_backward(&Default::default(), window, cx);
25859 });
25860 cx.run_until_parked();
25861 pane.update(cx, |pane, cx| {
25862 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25863 open_editor.update(cx, |editor, cx| {
25864 assert_eq!(
25865 editor.display_text(cx),
25866 "pub mod external {}",
25867 "External file is open now",
25868 );
25869 });
25870 });
25871 assert_language_servers_count(
25872 1,
25873 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25874 cx,
25875 );
25876
25877 cx.update(|_, cx| {
25878 workspace::reload(cx);
25879 });
25880 assert_language_servers_count(
25881 1,
25882 "After reloading the worktree with local and external files opened, only one project should be started",
25883 cx,
25884 );
25885}
25886
25887#[gpui::test]
25888async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25889 init_test(cx, |_| {});
25890
25891 let mut cx = EditorTestContext::new(cx).await;
25892 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25893 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25894
25895 // test cursor move to start of each line on tab
25896 // for `if`, `elif`, `else`, `while`, `with` and `for`
25897 cx.set_state(indoc! {"
25898 def main():
25899 ˇ for item in items:
25900 ˇ while item.active:
25901 ˇ if item.value > 10:
25902 ˇ continue
25903 ˇ elif item.value < 0:
25904 ˇ break
25905 ˇ else:
25906 ˇ with item.context() as ctx:
25907 ˇ yield count
25908 ˇ else:
25909 ˇ log('while else')
25910 ˇ else:
25911 ˇ log('for else')
25912 "});
25913 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25914 cx.wait_for_autoindent_applied().await;
25915 cx.assert_editor_state(indoc! {"
25916 def main():
25917 ˇfor item in items:
25918 ˇwhile item.active:
25919 ˇif item.value > 10:
25920 ˇcontinue
25921 ˇelif item.value < 0:
25922 ˇbreak
25923 ˇelse:
25924 ˇwith item.context() as ctx:
25925 ˇyield count
25926 ˇelse:
25927 ˇlog('while else')
25928 ˇelse:
25929 ˇlog('for else')
25930 "});
25931 // test relative indent is preserved when tab
25932 // for `if`, `elif`, `else`, `while`, `with` and `for`
25933 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25934 cx.wait_for_autoindent_applied().await;
25935 cx.assert_editor_state(indoc! {"
25936 def main():
25937 ˇfor item in items:
25938 ˇwhile item.active:
25939 ˇif item.value > 10:
25940 ˇcontinue
25941 ˇelif item.value < 0:
25942 ˇbreak
25943 ˇelse:
25944 ˇwith item.context() as ctx:
25945 ˇyield count
25946 ˇelse:
25947 ˇlog('while else')
25948 ˇelse:
25949 ˇlog('for else')
25950 "});
25951
25952 // test cursor move to start of each line on tab
25953 // for `try`, `except`, `else`, `finally`, `match` and `def`
25954 cx.set_state(indoc! {"
25955 def main():
25956 ˇ try:
25957 ˇ fetch()
25958 ˇ except ValueError:
25959 ˇ handle_error()
25960 ˇ else:
25961 ˇ match value:
25962 ˇ case _:
25963 ˇ finally:
25964 ˇ def status():
25965 ˇ return 0
25966 "});
25967 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25968 cx.wait_for_autoindent_applied().await;
25969 cx.assert_editor_state(indoc! {"
25970 def main():
25971 ˇtry:
25972 ˇfetch()
25973 ˇexcept ValueError:
25974 ˇhandle_error()
25975 ˇelse:
25976 ˇmatch value:
25977 ˇcase _:
25978 ˇfinally:
25979 ˇdef status():
25980 ˇreturn 0
25981 "});
25982 // test relative indent is preserved when tab
25983 // for `try`, `except`, `else`, `finally`, `match` and `def`
25984 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25985 cx.wait_for_autoindent_applied().await;
25986 cx.assert_editor_state(indoc! {"
25987 def main():
25988 ˇtry:
25989 ˇfetch()
25990 ˇexcept ValueError:
25991 ˇhandle_error()
25992 ˇelse:
25993 ˇmatch value:
25994 ˇcase _:
25995 ˇfinally:
25996 ˇdef status():
25997 ˇreturn 0
25998 "});
25999}
26000
26001#[gpui::test]
26002async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
26003 init_test(cx, |_| {});
26004
26005 let mut cx = EditorTestContext::new(cx).await;
26006 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26007 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26008
26009 // test `else` auto outdents when typed inside `if` block
26010 cx.set_state(indoc! {"
26011 def main():
26012 if i == 2:
26013 return
26014 ˇ
26015 "});
26016 cx.update_editor(|editor, window, cx| {
26017 editor.handle_input("else:", window, cx);
26018 });
26019 cx.wait_for_autoindent_applied().await;
26020 cx.assert_editor_state(indoc! {"
26021 def main():
26022 if i == 2:
26023 return
26024 else:ˇ
26025 "});
26026
26027 // test `except` auto outdents when typed inside `try` block
26028 cx.set_state(indoc! {"
26029 def main():
26030 try:
26031 i = 2
26032 ˇ
26033 "});
26034 cx.update_editor(|editor, window, cx| {
26035 editor.handle_input("except:", window, cx);
26036 });
26037 cx.wait_for_autoindent_applied().await;
26038 cx.assert_editor_state(indoc! {"
26039 def main():
26040 try:
26041 i = 2
26042 except:ˇ
26043 "});
26044
26045 // test `else` auto outdents when typed inside `except` block
26046 cx.set_state(indoc! {"
26047 def main():
26048 try:
26049 i = 2
26050 except:
26051 j = 2
26052 ˇ
26053 "});
26054 cx.update_editor(|editor, window, cx| {
26055 editor.handle_input("else:", window, cx);
26056 });
26057 cx.wait_for_autoindent_applied().await;
26058 cx.assert_editor_state(indoc! {"
26059 def main():
26060 try:
26061 i = 2
26062 except:
26063 j = 2
26064 else:ˇ
26065 "});
26066
26067 // test `finally` auto outdents when typed inside `else` block
26068 cx.set_state(indoc! {"
26069 def main():
26070 try:
26071 i = 2
26072 except:
26073 j = 2
26074 else:
26075 k = 2
26076 ˇ
26077 "});
26078 cx.update_editor(|editor, window, cx| {
26079 editor.handle_input("finally:", window, cx);
26080 });
26081 cx.wait_for_autoindent_applied().await;
26082 cx.assert_editor_state(indoc! {"
26083 def main():
26084 try:
26085 i = 2
26086 except:
26087 j = 2
26088 else:
26089 k = 2
26090 finally:ˇ
26091 "});
26092
26093 // test `else` does not outdents when typed inside `except` block right after for block
26094 cx.set_state(indoc! {"
26095 def main():
26096 try:
26097 i = 2
26098 except:
26099 for i in range(n):
26100 pass
26101 ˇ
26102 "});
26103 cx.update_editor(|editor, window, cx| {
26104 editor.handle_input("else:", window, cx);
26105 });
26106 cx.wait_for_autoindent_applied().await;
26107 cx.assert_editor_state(indoc! {"
26108 def main():
26109 try:
26110 i = 2
26111 except:
26112 for i in range(n):
26113 pass
26114 else:ˇ
26115 "});
26116
26117 // test `finally` auto outdents when typed inside `else` block right after for block
26118 cx.set_state(indoc! {"
26119 def main():
26120 try:
26121 i = 2
26122 except:
26123 j = 2
26124 else:
26125 for i in range(n):
26126 pass
26127 ˇ
26128 "});
26129 cx.update_editor(|editor, window, cx| {
26130 editor.handle_input("finally:", window, cx);
26131 });
26132 cx.wait_for_autoindent_applied().await;
26133 cx.assert_editor_state(indoc! {"
26134 def main():
26135 try:
26136 i = 2
26137 except:
26138 j = 2
26139 else:
26140 for i in range(n):
26141 pass
26142 finally:ˇ
26143 "});
26144
26145 // test `except` outdents to inner "try" block
26146 cx.set_state(indoc! {"
26147 def main():
26148 try:
26149 i = 2
26150 if i == 2:
26151 try:
26152 i = 3
26153 ˇ
26154 "});
26155 cx.update_editor(|editor, window, cx| {
26156 editor.handle_input("except:", window, cx);
26157 });
26158 cx.wait_for_autoindent_applied().await;
26159 cx.assert_editor_state(indoc! {"
26160 def main():
26161 try:
26162 i = 2
26163 if i == 2:
26164 try:
26165 i = 3
26166 except:ˇ
26167 "});
26168
26169 // test `except` outdents to outer "try" block
26170 cx.set_state(indoc! {"
26171 def main():
26172 try:
26173 i = 2
26174 if i == 2:
26175 try:
26176 i = 3
26177 ˇ
26178 "});
26179 cx.update_editor(|editor, window, cx| {
26180 editor.handle_input("except:", window, cx);
26181 });
26182 cx.wait_for_autoindent_applied().await;
26183 cx.assert_editor_state(indoc! {"
26184 def main():
26185 try:
26186 i = 2
26187 if i == 2:
26188 try:
26189 i = 3
26190 except:ˇ
26191 "});
26192
26193 // test `else` stays at correct indent when typed after `for` block
26194 cx.set_state(indoc! {"
26195 def main():
26196 for i in range(10):
26197 if i == 3:
26198 break
26199 ˇ
26200 "});
26201 cx.update_editor(|editor, window, cx| {
26202 editor.handle_input("else:", window, cx);
26203 });
26204 cx.wait_for_autoindent_applied().await;
26205 cx.assert_editor_state(indoc! {"
26206 def main():
26207 for i in range(10):
26208 if i == 3:
26209 break
26210 else:ˇ
26211 "});
26212
26213 // test does not outdent on typing after line with square brackets
26214 cx.set_state(indoc! {"
26215 def f() -> list[str]:
26216 ˇ
26217 "});
26218 cx.update_editor(|editor, window, cx| {
26219 editor.handle_input("a", window, cx);
26220 });
26221 cx.wait_for_autoindent_applied().await;
26222 cx.assert_editor_state(indoc! {"
26223 def f() -> list[str]:
26224 aˇ
26225 "});
26226
26227 // test does not outdent on typing : after case keyword
26228 cx.set_state(indoc! {"
26229 match 1:
26230 caseˇ
26231 "});
26232 cx.update_editor(|editor, window, cx| {
26233 editor.handle_input(":", window, cx);
26234 });
26235 cx.wait_for_autoindent_applied().await;
26236 cx.assert_editor_state(indoc! {"
26237 match 1:
26238 case:ˇ
26239 "});
26240}
26241
26242#[gpui::test]
26243async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
26244 init_test(cx, |_| {});
26245 update_test_language_settings(cx, |settings| {
26246 settings.defaults.extend_comment_on_newline = Some(false);
26247 });
26248 let mut cx = EditorTestContext::new(cx).await;
26249 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26250 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26251
26252 // test correct indent after newline on comment
26253 cx.set_state(indoc! {"
26254 # COMMENT:ˇ
26255 "});
26256 cx.update_editor(|editor, window, cx| {
26257 editor.newline(&Newline, window, cx);
26258 });
26259 cx.wait_for_autoindent_applied().await;
26260 cx.assert_editor_state(indoc! {"
26261 # COMMENT:
26262 ˇ
26263 "});
26264
26265 // test correct indent after newline in brackets
26266 cx.set_state(indoc! {"
26267 {ˇ}
26268 "});
26269 cx.update_editor(|editor, window, cx| {
26270 editor.newline(&Newline, window, cx);
26271 });
26272 cx.wait_for_autoindent_applied().await;
26273 cx.assert_editor_state(indoc! {"
26274 {
26275 ˇ
26276 }
26277 "});
26278
26279 cx.set_state(indoc! {"
26280 (ˇ)
26281 "});
26282 cx.update_editor(|editor, window, cx| {
26283 editor.newline(&Newline, window, cx);
26284 });
26285 cx.run_until_parked();
26286 cx.assert_editor_state(indoc! {"
26287 (
26288 ˇ
26289 )
26290 "});
26291
26292 // do not indent after empty lists or dictionaries
26293 cx.set_state(indoc! {"
26294 a = []ˇ
26295 "});
26296 cx.update_editor(|editor, window, cx| {
26297 editor.newline(&Newline, window, cx);
26298 });
26299 cx.run_until_parked();
26300 cx.assert_editor_state(indoc! {"
26301 a = []
26302 ˇ
26303 "});
26304}
26305
26306#[gpui::test]
26307async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
26308 init_test(cx, |_| {});
26309
26310 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
26311 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
26312 language_registry.add(markdown_lang());
26313 language_registry.add(python_lang);
26314
26315 let mut cx = EditorTestContext::new(cx).await;
26316 cx.update_buffer(|buffer, cx| {
26317 buffer.set_language_registry(language_registry);
26318 buffer.set_language(Some(markdown_lang()), cx);
26319 });
26320
26321 // Test that `else:` correctly outdents to match `if:` inside the Python code block
26322 cx.set_state(indoc! {"
26323 # Heading
26324
26325 ```python
26326 def main():
26327 if condition:
26328 pass
26329 ˇ
26330 ```
26331 "});
26332 cx.update_editor(|editor, window, cx| {
26333 editor.handle_input("else:", window, cx);
26334 });
26335 cx.run_until_parked();
26336 cx.assert_editor_state(indoc! {"
26337 # Heading
26338
26339 ```python
26340 def main():
26341 if condition:
26342 pass
26343 else:ˇ
26344 ```
26345 "});
26346}
26347
26348#[gpui::test]
26349async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
26350 init_test(cx, |_| {});
26351
26352 let mut cx = EditorTestContext::new(cx).await;
26353 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26354 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26355
26356 // test cursor move to start of each line on tab
26357 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
26358 cx.set_state(indoc! {"
26359 function main() {
26360 ˇ for item in $items; do
26361 ˇ while [ -n \"$item\" ]; do
26362 ˇ if [ \"$value\" -gt 10 ]; then
26363 ˇ continue
26364 ˇ elif [ \"$value\" -lt 0 ]; then
26365 ˇ break
26366 ˇ else
26367 ˇ echo \"$item\"
26368 ˇ fi
26369 ˇ done
26370 ˇ done
26371 ˇ}
26372 "});
26373 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26374 cx.wait_for_autoindent_applied().await;
26375 cx.assert_editor_state(indoc! {"
26376 function main() {
26377 ˇfor item in $items; do
26378 ˇwhile [ -n \"$item\" ]; do
26379 ˇif [ \"$value\" -gt 10 ]; then
26380 ˇcontinue
26381 ˇelif [ \"$value\" -lt 0 ]; then
26382 ˇbreak
26383 ˇelse
26384 ˇecho \"$item\"
26385 ˇfi
26386 ˇdone
26387 ˇdone
26388 ˇ}
26389 "});
26390 // test relative indent is preserved when tab
26391 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26392 cx.wait_for_autoindent_applied().await;
26393 cx.assert_editor_state(indoc! {"
26394 function main() {
26395 ˇfor item in $items; do
26396 ˇwhile [ -n \"$item\" ]; do
26397 ˇif [ \"$value\" -gt 10 ]; then
26398 ˇcontinue
26399 ˇelif [ \"$value\" -lt 0 ]; then
26400 ˇbreak
26401 ˇelse
26402 ˇecho \"$item\"
26403 ˇfi
26404 ˇdone
26405 ˇdone
26406 ˇ}
26407 "});
26408
26409 // test cursor move to start of each line on tab
26410 // for `case` statement with patterns
26411 cx.set_state(indoc! {"
26412 function handle() {
26413 ˇ case \"$1\" in
26414 ˇ start)
26415 ˇ echo \"a\"
26416 ˇ ;;
26417 ˇ stop)
26418 ˇ echo \"b\"
26419 ˇ ;;
26420 ˇ *)
26421 ˇ echo \"c\"
26422 ˇ ;;
26423 ˇ esac
26424 ˇ}
26425 "});
26426 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26427 cx.wait_for_autoindent_applied().await;
26428 cx.assert_editor_state(indoc! {"
26429 function handle() {
26430 ˇcase \"$1\" in
26431 ˇstart)
26432 ˇecho \"a\"
26433 ˇ;;
26434 ˇstop)
26435 ˇecho \"b\"
26436 ˇ;;
26437 ˇ*)
26438 ˇecho \"c\"
26439 ˇ;;
26440 ˇesac
26441 ˇ}
26442 "});
26443}
26444
26445#[gpui::test]
26446async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
26447 init_test(cx, |_| {});
26448
26449 let mut cx = EditorTestContext::new(cx).await;
26450 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26451 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26452
26453 // test indents on comment insert
26454 cx.set_state(indoc! {"
26455 function main() {
26456 ˇ for item in $items; do
26457 ˇ while [ -n \"$item\" ]; do
26458 ˇ if [ \"$value\" -gt 10 ]; then
26459 ˇ continue
26460 ˇ elif [ \"$value\" -lt 0 ]; then
26461 ˇ break
26462 ˇ else
26463 ˇ echo \"$item\"
26464 ˇ fi
26465 ˇ done
26466 ˇ done
26467 ˇ}
26468 "});
26469 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
26470 cx.wait_for_autoindent_applied().await;
26471 cx.assert_editor_state(indoc! {"
26472 function main() {
26473 #ˇ for item in $items; do
26474 #ˇ while [ -n \"$item\" ]; do
26475 #ˇ if [ \"$value\" -gt 10 ]; then
26476 #ˇ continue
26477 #ˇ elif [ \"$value\" -lt 0 ]; then
26478 #ˇ break
26479 #ˇ else
26480 #ˇ echo \"$item\"
26481 #ˇ fi
26482 #ˇ done
26483 #ˇ done
26484 #ˇ}
26485 "});
26486}
26487
26488#[gpui::test]
26489async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
26490 init_test(cx, |_| {});
26491
26492 let mut cx = EditorTestContext::new(cx).await;
26493 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26494 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26495
26496 // test `else` auto outdents when typed inside `if` block
26497 cx.set_state(indoc! {"
26498 if [ \"$1\" = \"test\" ]; then
26499 echo \"foo bar\"
26500 ˇ
26501 "});
26502 cx.update_editor(|editor, window, cx| {
26503 editor.handle_input("else", window, cx);
26504 });
26505 cx.wait_for_autoindent_applied().await;
26506 cx.assert_editor_state(indoc! {"
26507 if [ \"$1\" = \"test\" ]; then
26508 echo \"foo bar\"
26509 elseˇ
26510 "});
26511
26512 // test `elif` auto outdents when typed inside `if` block
26513 cx.set_state(indoc! {"
26514 if [ \"$1\" = \"test\" ]; then
26515 echo \"foo bar\"
26516 ˇ
26517 "});
26518 cx.update_editor(|editor, window, cx| {
26519 editor.handle_input("elif", window, cx);
26520 });
26521 cx.wait_for_autoindent_applied().await;
26522 cx.assert_editor_state(indoc! {"
26523 if [ \"$1\" = \"test\" ]; then
26524 echo \"foo bar\"
26525 elifˇ
26526 "});
26527
26528 // test `fi` auto outdents when typed inside `else` block
26529 cx.set_state(indoc! {"
26530 if [ \"$1\" = \"test\" ]; then
26531 echo \"foo bar\"
26532 else
26533 echo \"bar baz\"
26534 ˇ
26535 "});
26536 cx.update_editor(|editor, window, cx| {
26537 editor.handle_input("fi", window, cx);
26538 });
26539 cx.wait_for_autoindent_applied().await;
26540 cx.assert_editor_state(indoc! {"
26541 if [ \"$1\" = \"test\" ]; then
26542 echo \"foo bar\"
26543 else
26544 echo \"bar baz\"
26545 fiˇ
26546 "});
26547
26548 // test `done` auto outdents when typed inside `while` block
26549 cx.set_state(indoc! {"
26550 while read line; do
26551 echo \"$line\"
26552 ˇ
26553 "});
26554 cx.update_editor(|editor, window, cx| {
26555 editor.handle_input("done", window, cx);
26556 });
26557 cx.wait_for_autoindent_applied().await;
26558 cx.assert_editor_state(indoc! {"
26559 while read line; do
26560 echo \"$line\"
26561 doneˇ
26562 "});
26563
26564 // test `done` auto outdents when typed inside `for` block
26565 cx.set_state(indoc! {"
26566 for file in *.txt; do
26567 cat \"$file\"
26568 ˇ
26569 "});
26570 cx.update_editor(|editor, window, cx| {
26571 editor.handle_input("done", window, cx);
26572 });
26573 cx.wait_for_autoindent_applied().await;
26574 cx.assert_editor_state(indoc! {"
26575 for file in *.txt; do
26576 cat \"$file\"
26577 doneˇ
26578 "});
26579
26580 // test `esac` auto outdents when typed inside `case` block
26581 cx.set_state(indoc! {"
26582 case \"$1\" in
26583 start)
26584 echo \"foo bar\"
26585 ;;
26586 stop)
26587 echo \"bar baz\"
26588 ;;
26589 ˇ
26590 "});
26591 cx.update_editor(|editor, window, cx| {
26592 editor.handle_input("esac", window, cx);
26593 });
26594 cx.wait_for_autoindent_applied().await;
26595 cx.assert_editor_state(indoc! {"
26596 case \"$1\" in
26597 start)
26598 echo \"foo bar\"
26599 ;;
26600 stop)
26601 echo \"bar baz\"
26602 ;;
26603 esacˇ
26604 "});
26605
26606 // test `*)` auto outdents when typed inside `case` block
26607 cx.set_state(indoc! {"
26608 case \"$1\" in
26609 start)
26610 echo \"foo bar\"
26611 ;;
26612 ˇ
26613 "});
26614 cx.update_editor(|editor, window, cx| {
26615 editor.handle_input("*)", window, cx);
26616 });
26617 cx.wait_for_autoindent_applied().await;
26618 cx.assert_editor_state(indoc! {"
26619 case \"$1\" in
26620 start)
26621 echo \"foo bar\"
26622 ;;
26623 *)ˇ
26624 "});
26625
26626 // test `fi` outdents to correct level with nested if blocks
26627 cx.set_state(indoc! {"
26628 if [ \"$1\" = \"test\" ]; then
26629 echo \"outer if\"
26630 if [ \"$2\" = \"debug\" ]; then
26631 echo \"inner if\"
26632 ˇ
26633 "});
26634 cx.update_editor(|editor, window, cx| {
26635 editor.handle_input("fi", window, cx);
26636 });
26637 cx.wait_for_autoindent_applied().await;
26638 cx.assert_editor_state(indoc! {"
26639 if [ \"$1\" = \"test\" ]; then
26640 echo \"outer if\"
26641 if [ \"$2\" = \"debug\" ]; then
26642 echo \"inner if\"
26643 fiˇ
26644 "});
26645}
26646
26647#[gpui::test]
26648async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
26649 init_test(cx, |_| {});
26650 update_test_language_settings(cx, |settings| {
26651 settings.defaults.extend_comment_on_newline = Some(false);
26652 });
26653 let mut cx = EditorTestContext::new(cx).await;
26654 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26655 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26656
26657 // test correct indent after newline on comment
26658 cx.set_state(indoc! {"
26659 # COMMENT:ˇ
26660 "});
26661 cx.update_editor(|editor, window, cx| {
26662 editor.newline(&Newline, window, cx);
26663 });
26664 cx.wait_for_autoindent_applied().await;
26665 cx.assert_editor_state(indoc! {"
26666 # COMMENT:
26667 ˇ
26668 "});
26669
26670 // test correct indent after newline after `then`
26671 cx.set_state(indoc! {"
26672
26673 if [ \"$1\" = \"test\" ]; thenˇ
26674 "});
26675 cx.update_editor(|editor, window, cx| {
26676 editor.newline(&Newline, window, cx);
26677 });
26678 cx.wait_for_autoindent_applied().await;
26679 cx.assert_editor_state(indoc! {"
26680
26681 if [ \"$1\" = \"test\" ]; then
26682 ˇ
26683 "});
26684
26685 // test correct indent after newline after `else`
26686 cx.set_state(indoc! {"
26687 if [ \"$1\" = \"test\" ]; then
26688 elseˇ
26689 "});
26690 cx.update_editor(|editor, window, cx| {
26691 editor.newline(&Newline, window, cx);
26692 });
26693 cx.wait_for_autoindent_applied().await;
26694 cx.assert_editor_state(indoc! {"
26695 if [ \"$1\" = \"test\" ]; then
26696 else
26697 ˇ
26698 "});
26699
26700 // test correct indent after newline after `elif`
26701 cx.set_state(indoc! {"
26702 if [ \"$1\" = \"test\" ]; then
26703 elifˇ
26704 "});
26705 cx.update_editor(|editor, window, cx| {
26706 editor.newline(&Newline, window, cx);
26707 });
26708 cx.wait_for_autoindent_applied().await;
26709 cx.assert_editor_state(indoc! {"
26710 if [ \"$1\" = \"test\" ]; then
26711 elif
26712 ˇ
26713 "});
26714
26715 // test correct indent after newline after `do`
26716 cx.set_state(indoc! {"
26717 for file in *.txt; doˇ
26718 "});
26719 cx.update_editor(|editor, window, cx| {
26720 editor.newline(&Newline, window, cx);
26721 });
26722 cx.wait_for_autoindent_applied().await;
26723 cx.assert_editor_state(indoc! {"
26724 for file in *.txt; do
26725 ˇ
26726 "});
26727
26728 // test correct indent after newline after case pattern
26729 cx.set_state(indoc! {"
26730 case \"$1\" in
26731 start)ˇ
26732 "});
26733 cx.update_editor(|editor, window, cx| {
26734 editor.newline(&Newline, window, cx);
26735 });
26736 cx.wait_for_autoindent_applied().await;
26737 cx.assert_editor_state(indoc! {"
26738 case \"$1\" in
26739 start)
26740 ˇ
26741 "});
26742
26743 // test correct indent after newline after case pattern
26744 cx.set_state(indoc! {"
26745 case \"$1\" in
26746 start)
26747 ;;
26748 *)ˇ
26749 "});
26750 cx.update_editor(|editor, window, cx| {
26751 editor.newline(&Newline, window, cx);
26752 });
26753 cx.wait_for_autoindent_applied().await;
26754 cx.assert_editor_state(indoc! {"
26755 case \"$1\" in
26756 start)
26757 ;;
26758 *)
26759 ˇ
26760 "});
26761
26762 // test correct indent after newline after function opening brace
26763 cx.set_state(indoc! {"
26764 function test() {ˇ}
26765 "});
26766 cx.update_editor(|editor, window, cx| {
26767 editor.newline(&Newline, window, cx);
26768 });
26769 cx.wait_for_autoindent_applied().await;
26770 cx.assert_editor_state(indoc! {"
26771 function test() {
26772 ˇ
26773 }
26774 "});
26775
26776 // test no extra indent after semicolon on same line
26777 cx.set_state(indoc! {"
26778 echo \"test\";ˇ
26779 "});
26780 cx.update_editor(|editor, window, cx| {
26781 editor.newline(&Newline, window, cx);
26782 });
26783 cx.wait_for_autoindent_applied().await;
26784 cx.assert_editor_state(indoc! {"
26785 echo \"test\";
26786 ˇ
26787 "});
26788}
26789
26790fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
26791 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
26792 point..point
26793}
26794
26795#[track_caller]
26796fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
26797 let (text, ranges) = marked_text_ranges(marked_text, true);
26798 assert_eq!(editor.text(cx), text);
26799 assert_eq!(
26800 editor.selections.ranges(&editor.display_snapshot(cx)),
26801 ranges
26802 .iter()
26803 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
26804 .collect::<Vec<_>>(),
26805 "Assert selections are {}",
26806 marked_text
26807 );
26808}
26809
26810pub fn handle_signature_help_request(
26811 cx: &mut EditorLspTestContext,
26812 mocked_response: lsp::SignatureHelp,
26813) -> impl Future<Output = ()> + use<> {
26814 let mut request =
26815 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
26816 let mocked_response = mocked_response.clone();
26817 async move { Ok(Some(mocked_response)) }
26818 });
26819
26820 async move {
26821 request.next().await;
26822 }
26823}
26824
26825#[track_caller]
26826pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
26827 cx.update_editor(|editor, _, _| {
26828 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
26829 let entries = menu.entries.borrow();
26830 let entries = entries
26831 .iter()
26832 .map(|entry| entry.string.as_str())
26833 .collect::<Vec<_>>();
26834 assert_eq!(entries, expected);
26835 } else {
26836 panic!("Expected completions menu");
26837 }
26838 });
26839}
26840
26841#[gpui::test]
26842async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
26843 init_test(cx, |_| {});
26844 let mut cx = EditorLspTestContext::new_rust(
26845 lsp::ServerCapabilities {
26846 completion_provider: Some(lsp::CompletionOptions {
26847 ..Default::default()
26848 }),
26849 ..Default::default()
26850 },
26851 cx,
26852 )
26853 .await;
26854 cx.lsp
26855 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
26856 Ok(Some(lsp::CompletionResponse::Array(vec![
26857 lsp::CompletionItem {
26858 label: "unsafe".into(),
26859 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26860 range: lsp::Range {
26861 start: lsp::Position {
26862 line: 0,
26863 character: 9,
26864 },
26865 end: lsp::Position {
26866 line: 0,
26867 character: 11,
26868 },
26869 },
26870 new_text: "unsafe".to_string(),
26871 })),
26872 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
26873 ..Default::default()
26874 },
26875 ])))
26876 });
26877
26878 cx.update_editor(|editor, _, cx| {
26879 editor.project().unwrap().update(cx, |project, cx| {
26880 project.snippets().update(cx, |snippets, _cx| {
26881 snippets.add_snippet_for_test(
26882 None,
26883 PathBuf::from("test_snippets.json"),
26884 vec![
26885 Arc::new(project::snippet_provider::Snippet {
26886 prefix: vec![
26887 "unlimited word count".to_string(),
26888 "unlimit word count".to_string(),
26889 "unlimited unknown".to_string(),
26890 ],
26891 body: "this is many words".to_string(),
26892 description: Some("description".to_string()),
26893 name: "multi-word snippet test".to_string(),
26894 }),
26895 Arc::new(project::snippet_provider::Snippet {
26896 prefix: vec!["unsnip".to_string(), "@few".to_string()],
26897 body: "fewer words".to_string(),
26898 description: Some("alt description".to_string()),
26899 name: "other name".to_string(),
26900 }),
26901 Arc::new(project::snippet_provider::Snippet {
26902 prefix: vec!["ab aa".to_string()],
26903 body: "abcd".to_string(),
26904 description: None,
26905 name: "alphabet".to_string(),
26906 }),
26907 ],
26908 );
26909 });
26910 })
26911 });
26912
26913 let get_completions = |cx: &mut EditorLspTestContext| {
26914 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
26915 Some(CodeContextMenu::Completions(context_menu)) => {
26916 let entries = context_menu.entries.borrow();
26917 entries
26918 .iter()
26919 .map(|entry| entry.string.clone())
26920 .collect_vec()
26921 }
26922 _ => vec![],
26923 })
26924 };
26925
26926 // snippets:
26927 // @foo
26928 // foo bar
26929 //
26930 // when typing:
26931 //
26932 // when typing:
26933 // - if I type a symbol "open the completions with snippets only"
26934 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
26935 //
26936 // stuff we need:
26937 // - filtering logic change?
26938 // - remember how far back the completion started.
26939
26940 let test_cases: &[(&str, &[&str])] = &[
26941 (
26942 "un",
26943 &[
26944 "unsafe",
26945 "unlimit word count",
26946 "unlimited unknown",
26947 "unlimited word count",
26948 "unsnip",
26949 ],
26950 ),
26951 (
26952 "u ",
26953 &[
26954 "unlimit word count",
26955 "unlimited unknown",
26956 "unlimited word count",
26957 ],
26958 ),
26959 ("u a", &["ab aa", "unsafe"]), // unsAfe
26960 (
26961 "u u",
26962 &[
26963 "unsafe",
26964 "unlimit word count",
26965 "unlimited unknown", // ranked highest among snippets
26966 "unlimited word count",
26967 "unsnip",
26968 ],
26969 ),
26970 ("uw c", &["unlimit word count", "unlimited word count"]),
26971 (
26972 "u w",
26973 &[
26974 "unlimit word count",
26975 "unlimited word count",
26976 "unlimited unknown",
26977 ],
26978 ),
26979 ("u w ", &["unlimit word count", "unlimited word count"]),
26980 (
26981 "u ",
26982 &[
26983 "unlimit word count",
26984 "unlimited unknown",
26985 "unlimited word count",
26986 ],
26987 ),
26988 ("wor", &[]),
26989 ("uf", &["unsafe"]),
26990 ("af", &["unsafe"]),
26991 ("afu", &[]),
26992 (
26993 "ue",
26994 &["unsafe", "unlimited unknown", "unlimited word count"],
26995 ),
26996 ("@", &["@few"]),
26997 ("@few", &["@few"]),
26998 ("@ ", &[]),
26999 ("a@", &["@few"]),
27000 ("a@f", &["@few", "unsafe"]),
27001 ("a@fw", &["@few"]),
27002 ("a", &["ab aa", "unsafe"]),
27003 ("aa", &["ab aa"]),
27004 ("aaa", &["ab aa"]),
27005 ("ab", &["ab aa"]),
27006 ("ab ", &["ab aa"]),
27007 ("ab a", &["ab aa", "unsafe"]),
27008 ("ab ab", &["ab aa"]),
27009 ("ab ab aa", &["ab aa"]),
27010 ];
27011
27012 for &(input_to_simulate, expected_completions) in test_cases {
27013 cx.set_state("fn a() { ˇ }\n");
27014 for c in input_to_simulate.split("") {
27015 cx.simulate_input(c);
27016 cx.run_until_parked();
27017 }
27018 let expected_completions = expected_completions
27019 .iter()
27020 .map(|s| s.to_string())
27021 .collect_vec();
27022 assert_eq!(
27023 get_completions(&mut cx),
27024 expected_completions,
27025 "< actual / expected >, input = {input_to_simulate:?}",
27026 );
27027 }
27028}
27029
27030/// Handle completion request passing a marked string specifying where the completion
27031/// should be triggered from using '|' character, what range should be replaced, and what completions
27032/// should be returned using '<' and '>' to delimit the range.
27033///
27034/// Also see `handle_completion_request_with_insert_and_replace`.
27035#[track_caller]
27036pub fn handle_completion_request(
27037 marked_string: &str,
27038 completions: Vec<&'static str>,
27039 is_incomplete: bool,
27040 counter: Arc<AtomicUsize>,
27041 cx: &mut EditorLspTestContext,
27042) -> impl Future<Output = ()> {
27043 let complete_from_marker: TextRangeMarker = '|'.into();
27044 let replace_range_marker: TextRangeMarker = ('<', '>').into();
27045 let (_, mut marked_ranges) = marked_text_ranges_by(
27046 marked_string,
27047 vec![complete_from_marker.clone(), replace_range_marker.clone()],
27048 );
27049
27050 let complete_from_position = cx.to_lsp(MultiBufferOffset(
27051 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
27052 ));
27053 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
27054 let replace_range =
27055 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
27056
27057 let mut request =
27058 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
27059 let completions = completions.clone();
27060 counter.fetch_add(1, atomic::Ordering::Release);
27061 async move {
27062 assert_eq!(params.text_document_position.text_document.uri, url.clone());
27063 assert_eq!(
27064 params.text_document_position.position,
27065 complete_from_position
27066 );
27067 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
27068 is_incomplete,
27069 item_defaults: None,
27070 items: completions
27071 .iter()
27072 .map(|completion_text| lsp::CompletionItem {
27073 label: completion_text.to_string(),
27074 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
27075 range: replace_range,
27076 new_text: completion_text.to_string(),
27077 })),
27078 ..Default::default()
27079 })
27080 .collect(),
27081 })))
27082 }
27083 });
27084
27085 async move {
27086 request.next().await;
27087 }
27088}
27089
27090/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
27091/// given instead, which also contains an `insert` range.
27092///
27093/// This function uses markers to define ranges:
27094/// - `|` marks the cursor position
27095/// - `<>` marks the replace range
27096/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
27097pub fn handle_completion_request_with_insert_and_replace(
27098 cx: &mut EditorLspTestContext,
27099 marked_string: &str,
27100 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
27101 counter: Arc<AtomicUsize>,
27102) -> impl Future<Output = ()> {
27103 let complete_from_marker: TextRangeMarker = '|'.into();
27104 let replace_range_marker: TextRangeMarker = ('<', '>').into();
27105 let insert_range_marker: TextRangeMarker = ('{', '}').into();
27106
27107 let (_, mut marked_ranges) = marked_text_ranges_by(
27108 marked_string,
27109 vec![
27110 complete_from_marker.clone(),
27111 replace_range_marker.clone(),
27112 insert_range_marker.clone(),
27113 ],
27114 );
27115
27116 let complete_from_position = cx.to_lsp(MultiBufferOffset(
27117 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
27118 ));
27119 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
27120 let replace_range =
27121 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
27122
27123 let insert_range = match marked_ranges.remove(&insert_range_marker) {
27124 Some(ranges) if !ranges.is_empty() => {
27125 let range1 = ranges[0].clone();
27126 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
27127 }
27128 _ => lsp::Range {
27129 start: replace_range.start,
27130 end: complete_from_position,
27131 },
27132 };
27133
27134 let mut request =
27135 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
27136 let completions = completions.clone();
27137 counter.fetch_add(1, atomic::Ordering::Release);
27138 async move {
27139 assert_eq!(params.text_document_position.text_document.uri, url.clone());
27140 assert_eq!(
27141 params.text_document_position.position, complete_from_position,
27142 "marker `|` position doesn't match",
27143 );
27144 Ok(Some(lsp::CompletionResponse::Array(
27145 completions
27146 .iter()
27147 .map(|(label, new_text)| lsp::CompletionItem {
27148 label: label.to_string(),
27149 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
27150 lsp::InsertReplaceEdit {
27151 insert: insert_range,
27152 replace: replace_range,
27153 new_text: new_text.to_string(),
27154 },
27155 )),
27156 ..Default::default()
27157 })
27158 .collect(),
27159 )))
27160 }
27161 });
27162
27163 async move {
27164 request.next().await;
27165 }
27166}
27167
27168fn handle_resolve_completion_request(
27169 cx: &mut EditorLspTestContext,
27170 edits: Option<Vec<(&'static str, &'static str)>>,
27171) -> impl Future<Output = ()> {
27172 let edits = edits.map(|edits| {
27173 edits
27174 .iter()
27175 .map(|(marked_string, new_text)| {
27176 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
27177 let replace_range = cx.to_lsp_range(
27178 MultiBufferOffset(marked_ranges[0].start)
27179 ..MultiBufferOffset(marked_ranges[0].end),
27180 );
27181 lsp::TextEdit::new(replace_range, new_text.to_string())
27182 })
27183 .collect::<Vec<_>>()
27184 });
27185
27186 let mut request =
27187 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
27188 let edits = edits.clone();
27189 async move {
27190 Ok(lsp::CompletionItem {
27191 additional_text_edits: edits,
27192 ..Default::default()
27193 })
27194 }
27195 });
27196
27197 async move {
27198 request.next().await;
27199 }
27200}
27201
27202pub(crate) fn update_test_language_settings(
27203 cx: &mut TestAppContext,
27204 f: impl Fn(&mut AllLanguageSettingsContent),
27205) {
27206 cx.update(|cx| {
27207 SettingsStore::update_global(cx, |store, cx| {
27208 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
27209 });
27210 });
27211}
27212
27213pub(crate) fn update_test_project_settings(
27214 cx: &mut TestAppContext,
27215 f: impl Fn(&mut ProjectSettingsContent),
27216) {
27217 cx.update(|cx| {
27218 SettingsStore::update_global(cx, |store, cx| {
27219 store.update_user_settings(cx, |settings| f(&mut settings.project));
27220 });
27221 });
27222}
27223
27224pub(crate) fn update_test_editor_settings(
27225 cx: &mut TestAppContext,
27226 f: impl Fn(&mut EditorSettingsContent),
27227) {
27228 cx.update(|cx| {
27229 SettingsStore::update_global(cx, |store, cx| {
27230 store.update_user_settings(cx, |settings| f(&mut settings.editor));
27231 })
27232 })
27233}
27234
27235pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
27236 cx.update(|cx| {
27237 assets::Assets.load_test_fonts(cx);
27238 let store = SettingsStore::test(cx);
27239 cx.set_global(store);
27240 theme::init(theme::LoadThemes::JustBase, cx);
27241 release_channel::init(semver::Version::new(0, 0, 0), cx);
27242 crate::init(cx);
27243 });
27244 zlog::init_test();
27245 update_test_language_settings(cx, f);
27246}
27247
27248#[track_caller]
27249fn assert_hunk_revert(
27250 not_reverted_text_with_selections: &str,
27251 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
27252 expected_reverted_text_with_selections: &str,
27253 base_text: &str,
27254 cx: &mut EditorLspTestContext,
27255) {
27256 cx.set_state(not_reverted_text_with_selections);
27257 cx.set_head_text(base_text);
27258 cx.executor().run_until_parked();
27259
27260 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
27261 let snapshot = editor.snapshot(window, cx);
27262 let reverted_hunk_statuses = snapshot
27263 .buffer_snapshot()
27264 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
27265 .map(|hunk| hunk.status().kind)
27266 .collect::<Vec<_>>();
27267
27268 editor.git_restore(&Default::default(), window, cx);
27269 reverted_hunk_statuses
27270 });
27271 cx.executor().run_until_parked();
27272 cx.assert_editor_state(expected_reverted_text_with_selections);
27273 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
27274}
27275
27276#[gpui::test(iterations = 10)]
27277async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
27278 init_test(cx, |_| {});
27279
27280 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
27281 let counter = diagnostic_requests.clone();
27282
27283 let fs = FakeFs::new(cx.executor());
27284 fs.insert_tree(
27285 path!("/a"),
27286 json!({
27287 "first.rs": "fn main() { let a = 5; }",
27288 "second.rs": "// Test file",
27289 }),
27290 )
27291 .await;
27292
27293 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27294 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27295 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27296
27297 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27298 language_registry.add(rust_lang());
27299 let mut fake_servers = language_registry.register_fake_lsp(
27300 "Rust",
27301 FakeLspAdapter {
27302 capabilities: lsp::ServerCapabilities {
27303 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
27304 lsp::DiagnosticOptions {
27305 identifier: None,
27306 inter_file_dependencies: true,
27307 workspace_diagnostics: true,
27308 work_done_progress_options: Default::default(),
27309 },
27310 )),
27311 ..Default::default()
27312 },
27313 ..Default::default()
27314 },
27315 );
27316
27317 let editor = workspace
27318 .update(cx, |workspace, window, cx| {
27319 workspace.open_abs_path(
27320 PathBuf::from(path!("/a/first.rs")),
27321 OpenOptions::default(),
27322 window,
27323 cx,
27324 )
27325 })
27326 .unwrap()
27327 .await
27328 .unwrap()
27329 .downcast::<Editor>()
27330 .unwrap();
27331 let fake_server = fake_servers.next().await.unwrap();
27332 let server_id = fake_server.server.server_id();
27333 let mut first_request = fake_server
27334 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
27335 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
27336 let result_id = Some(new_result_id.to_string());
27337 assert_eq!(
27338 params.text_document.uri,
27339 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27340 );
27341 async move {
27342 Ok(lsp::DocumentDiagnosticReportResult::Report(
27343 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
27344 related_documents: None,
27345 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
27346 items: Vec::new(),
27347 result_id,
27348 },
27349 }),
27350 ))
27351 }
27352 });
27353
27354 let ensure_result_id = |expected: Option<SharedString>, cx: &mut TestAppContext| {
27355 project.update(cx, |project, cx| {
27356 let buffer_id = editor
27357 .read(cx)
27358 .buffer()
27359 .read(cx)
27360 .as_singleton()
27361 .expect("created a singleton buffer")
27362 .read(cx)
27363 .remote_id();
27364 let buffer_result_id = project
27365 .lsp_store()
27366 .read(cx)
27367 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
27368 assert_eq!(expected, buffer_result_id);
27369 });
27370 };
27371
27372 ensure_result_id(None, cx);
27373 cx.executor().advance_clock(Duration::from_millis(60));
27374 cx.executor().run_until_parked();
27375 assert_eq!(
27376 diagnostic_requests.load(atomic::Ordering::Acquire),
27377 1,
27378 "Opening file should trigger diagnostic request"
27379 );
27380 first_request
27381 .next()
27382 .await
27383 .expect("should have sent the first diagnostics pull request");
27384 ensure_result_id(Some(SharedString::new("1")), cx);
27385
27386 // Editing should trigger diagnostics
27387 editor.update_in(cx, |editor, window, cx| {
27388 editor.handle_input("2", window, cx)
27389 });
27390 cx.executor().advance_clock(Duration::from_millis(60));
27391 cx.executor().run_until_parked();
27392 assert_eq!(
27393 diagnostic_requests.load(atomic::Ordering::Acquire),
27394 2,
27395 "Editing should trigger diagnostic request"
27396 );
27397 ensure_result_id(Some(SharedString::new("2")), cx);
27398
27399 // Moving cursor should not trigger diagnostic request
27400 editor.update_in(cx, |editor, window, cx| {
27401 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27402 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
27403 });
27404 });
27405 cx.executor().advance_clock(Duration::from_millis(60));
27406 cx.executor().run_until_parked();
27407 assert_eq!(
27408 diagnostic_requests.load(atomic::Ordering::Acquire),
27409 2,
27410 "Cursor movement should not trigger diagnostic request"
27411 );
27412 ensure_result_id(Some(SharedString::new("2")), cx);
27413 // Multiple rapid edits should be debounced
27414 for _ in 0..5 {
27415 editor.update_in(cx, |editor, window, cx| {
27416 editor.handle_input("x", window, cx)
27417 });
27418 }
27419 cx.executor().advance_clock(Duration::from_millis(60));
27420 cx.executor().run_until_parked();
27421
27422 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
27423 assert!(
27424 final_requests <= 4,
27425 "Multiple rapid edits should be debounced (got {final_requests} requests)",
27426 );
27427 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
27428}
27429
27430#[gpui::test]
27431async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
27432 // Regression test for issue #11671
27433 // Previously, adding a cursor after moving multiple cursors would reset
27434 // the cursor count instead of adding to the existing cursors.
27435 init_test(cx, |_| {});
27436 let mut cx = EditorTestContext::new(cx).await;
27437
27438 // Create a simple buffer with cursor at start
27439 cx.set_state(indoc! {"
27440 ˇaaaa
27441 bbbb
27442 cccc
27443 dddd
27444 eeee
27445 ffff
27446 gggg
27447 hhhh"});
27448
27449 // Add 2 cursors below (so we have 3 total)
27450 cx.update_editor(|editor, window, cx| {
27451 editor.add_selection_below(&Default::default(), window, cx);
27452 editor.add_selection_below(&Default::default(), window, cx);
27453 });
27454
27455 // Verify we have 3 cursors
27456 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
27457 assert_eq!(
27458 initial_count, 3,
27459 "Should have 3 cursors after adding 2 below"
27460 );
27461
27462 // Move down one line
27463 cx.update_editor(|editor, window, cx| {
27464 editor.move_down(&MoveDown, window, cx);
27465 });
27466
27467 // Add another cursor below
27468 cx.update_editor(|editor, window, cx| {
27469 editor.add_selection_below(&Default::default(), window, cx);
27470 });
27471
27472 // Should now have 4 cursors (3 original + 1 new)
27473 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
27474 assert_eq!(
27475 final_count, 4,
27476 "Should have 4 cursors after moving and adding another"
27477 );
27478}
27479
27480#[gpui::test]
27481async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
27482 init_test(cx, |_| {});
27483
27484 let mut cx = EditorTestContext::new(cx).await;
27485
27486 cx.set_state(indoc!(
27487 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
27488 Second line here"#
27489 ));
27490
27491 cx.update_editor(|editor, window, cx| {
27492 // Enable soft wrapping with a narrow width to force soft wrapping and
27493 // confirm that more than 2 rows are being displayed.
27494 editor.set_wrap_width(Some(100.0.into()), cx);
27495 assert!(editor.display_text(cx).lines().count() > 2);
27496
27497 editor.add_selection_below(
27498 &AddSelectionBelow {
27499 skip_soft_wrap: true,
27500 },
27501 window,
27502 cx,
27503 );
27504
27505 assert_eq!(
27506 display_ranges(editor, cx),
27507 &[
27508 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27509 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
27510 ]
27511 );
27512
27513 editor.add_selection_above(
27514 &AddSelectionAbove {
27515 skip_soft_wrap: true,
27516 },
27517 window,
27518 cx,
27519 );
27520
27521 assert_eq!(
27522 display_ranges(editor, cx),
27523 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27524 );
27525
27526 editor.add_selection_below(
27527 &AddSelectionBelow {
27528 skip_soft_wrap: false,
27529 },
27530 window,
27531 cx,
27532 );
27533
27534 assert_eq!(
27535 display_ranges(editor, cx),
27536 &[
27537 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27538 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
27539 ]
27540 );
27541
27542 editor.add_selection_above(
27543 &AddSelectionAbove {
27544 skip_soft_wrap: false,
27545 },
27546 window,
27547 cx,
27548 );
27549
27550 assert_eq!(
27551 display_ranges(editor, cx),
27552 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27553 );
27554 });
27555
27556 // Set up text where selections are in the middle of a soft-wrapped line.
27557 // When adding selection below with `skip_soft_wrap` set to `true`, the new
27558 // selection should be at the same buffer column, not the same pixel
27559 // position.
27560 cx.set_state(indoc!(
27561 r#"1. Very long line to show «howˇ» a wrapped line would look
27562 2. Very long line to show how a wrapped line would look"#
27563 ));
27564
27565 cx.update_editor(|editor, window, cx| {
27566 // Enable soft wrapping with a narrow width to force soft wrapping and
27567 // confirm that more than 2 rows are being displayed.
27568 editor.set_wrap_width(Some(100.0.into()), cx);
27569 assert!(editor.display_text(cx).lines().count() > 2);
27570
27571 editor.add_selection_below(
27572 &AddSelectionBelow {
27573 skip_soft_wrap: true,
27574 },
27575 window,
27576 cx,
27577 );
27578
27579 // Assert that there's now 2 selections, both selecting the same column
27580 // range in the buffer row.
27581 let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
27582 let selections = editor.selections.all::<Point>(&display_map);
27583 assert_eq!(selections.len(), 2);
27584 assert_eq!(selections[0].start.column, selections[1].start.column);
27585 assert_eq!(selections[0].end.column, selections[1].end.column);
27586 });
27587}
27588
27589#[gpui::test]
27590async fn test_insert_snippet(cx: &mut TestAppContext) {
27591 init_test(cx, |_| {});
27592 let mut cx = EditorTestContext::new(cx).await;
27593
27594 cx.update_editor(|editor, _, cx| {
27595 editor.project().unwrap().update(cx, |project, cx| {
27596 project.snippets().update(cx, |snippets, _cx| {
27597 let snippet = project::snippet_provider::Snippet {
27598 prefix: vec![], // no prefix needed!
27599 body: "an Unspecified".to_string(),
27600 description: Some("shhhh it's a secret".to_string()),
27601 name: "super secret snippet".to_string(),
27602 };
27603 snippets.add_snippet_for_test(
27604 None,
27605 PathBuf::from("test_snippets.json"),
27606 vec![Arc::new(snippet)],
27607 );
27608
27609 let snippet = project::snippet_provider::Snippet {
27610 prefix: vec![], // no prefix needed!
27611 body: " Location".to_string(),
27612 description: Some("the word 'location'".to_string()),
27613 name: "location word".to_string(),
27614 };
27615 snippets.add_snippet_for_test(
27616 Some("Markdown".to_string()),
27617 PathBuf::from("test_snippets.json"),
27618 vec![Arc::new(snippet)],
27619 );
27620 });
27621 })
27622 });
27623
27624 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
27625
27626 cx.update_editor(|editor, window, cx| {
27627 editor.insert_snippet_at_selections(
27628 &InsertSnippet {
27629 language: None,
27630 name: Some("super secret snippet".to_string()),
27631 snippet: None,
27632 },
27633 window,
27634 cx,
27635 );
27636
27637 // Language is specified in the action,
27638 // so the buffer language does not need to match
27639 editor.insert_snippet_at_selections(
27640 &InsertSnippet {
27641 language: Some("Markdown".to_string()),
27642 name: Some("location word".to_string()),
27643 snippet: None,
27644 },
27645 window,
27646 cx,
27647 );
27648
27649 editor.insert_snippet_at_selections(
27650 &InsertSnippet {
27651 language: None,
27652 name: None,
27653 snippet: Some("$0 after".to_string()),
27654 },
27655 window,
27656 cx,
27657 );
27658 });
27659
27660 cx.assert_editor_state(
27661 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
27662 );
27663}
27664
27665#[gpui::test(iterations = 10)]
27666async fn test_document_colors(cx: &mut TestAppContext) {
27667 let expected_color = Rgba {
27668 r: 0.33,
27669 g: 0.33,
27670 b: 0.33,
27671 a: 0.33,
27672 };
27673
27674 init_test(cx, |_| {});
27675
27676 let fs = FakeFs::new(cx.executor());
27677 fs.insert_tree(
27678 path!("/a"),
27679 json!({
27680 "first.rs": "fn main() { let a = 5; }",
27681 }),
27682 )
27683 .await;
27684
27685 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27686 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27687 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27688
27689 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27690 language_registry.add(rust_lang());
27691 let mut fake_servers = language_registry.register_fake_lsp(
27692 "Rust",
27693 FakeLspAdapter {
27694 capabilities: lsp::ServerCapabilities {
27695 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
27696 ..lsp::ServerCapabilities::default()
27697 },
27698 name: "rust-analyzer",
27699 ..FakeLspAdapter::default()
27700 },
27701 );
27702 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
27703 "Rust",
27704 FakeLspAdapter {
27705 capabilities: lsp::ServerCapabilities {
27706 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
27707 ..lsp::ServerCapabilities::default()
27708 },
27709 name: "not-rust-analyzer",
27710 ..FakeLspAdapter::default()
27711 },
27712 );
27713
27714 let editor = workspace
27715 .update(cx, |workspace, window, cx| {
27716 workspace.open_abs_path(
27717 PathBuf::from(path!("/a/first.rs")),
27718 OpenOptions::default(),
27719 window,
27720 cx,
27721 )
27722 })
27723 .unwrap()
27724 .await
27725 .unwrap()
27726 .downcast::<Editor>()
27727 .unwrap();
27728 let fake_language_server = fake_servers.next().await.unwrap();
27729 let fake_language_server_without_capabilities =
27730 fake_servers_without_capabilities.next().await.unwrap();
27731 let requests_made = Arc::new(AtomicUsize::new(0));
27732 let closure_requests_made = Arc::clone(&requests_made);
27733 let mut color_request_handle = fake_language_server
27734 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27735 let requests_made = Arc::clone(&closure_requests_made);
27736 async move {
27737 assert_eq!(
27738 params.text_document.uri,
27739 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27740 );
27741 requests_made.fetch_add(1, atomic::Ordering::Release);
27742 Ok(vec![
27743 lsp::ColorInformation {
27744 range: lsp::Range {
27745 start: lsp::Position {
27746 line: 0,
27747 character: 0,
27748 },
27749 end: lsp::Position {
27750 line: 0,
27751 character: 1,
27752 },
27753 },
27754 color: lsp::Color {
27755 red: 0.33,
27756 green: 0.33,
27757 blue: 0.33,
27758 alpha: 0.33,
27759 },
27760 },
27761 lsp::ColorInformation {
27762 range: lsp::Range {
27763 start: lsp::Position {
27764 line: 0,
27765 character: 0,
27766 },
27767 end: lsp::Position {
27768 line: 0,
27769 character: 1,
27770 },
27771 },
27772 color: lsp::Color {
27773 red: 0.33,
27774 green: 0.33,
27775 blue: 0.33,
27776 alpha: 0.33,
27777 },
27778 },
27779 ])
27780 }
27781 });
27782
27783 let _handle = fake_language_server_without_capabilities
27784 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
27785 panic!("Should not be called");
27786 });
27787 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27788 color_request_handle.next().await.unwrap();
27789 cx.run_until_parked();
27790 assert_eq!(
27791 1,
27792 requests_made.load(atomic::Ordering::Acquire),
27793 "Should query for colors once per editor open"
27794 );
27795 editor.update_in(cx, |editor, _, cx| {
27796 assert_eq!(
27797 vec![expected_color],
27798 extract_color_inlays(editor, cx),
27799 "Should have an initial inlay"
27800 );
27801 });
27802
27803 // opening another file in a split should not influence the LSP query counter
27804 workspace
27805 .update(cx, |workspace, window, cx| {
27806 assert_eq!(
27807 workspace.panes().len(),
27808 1,
27809 "Should have one pane with one editor"
27810 );
27811 workspace.move_item_to_pane_in_direction(
27812 &MoveItemToPaneInDirection {
27813 direction: SplitDirection::Right,
27814 focus: false,
27815 clone: true,
27816 },
27817 window,
27818 cx,
27819 );
27820 })
27821 .unwrap();
27822 cx.run_until_parked();
27823 workspace
27824 .update(cx, |workspace, _, cx| {
27825 let panes = workspace.panes();
27826 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
27827 for pane in panes {
27828 let editor = pane
27829 .read(cx)
27830 .active_item()
27831 .and_then(|item| item.downcast::<Editor>())
27832 .expect("Should have opened an editor in each split");
27833 let editor_file = editor
27834 .read(cx)
27835 .buffer()
27836 .read(cx)
27837 .as_singleton()
27838 .expect("test deals with singleton buffers")
27839 .read(cx)
27840 .file()
27841 .expect("test buffese should have a file")
27842 .path();
27843 assert_eq!(
27844 editor_file.as_ref(),
27845 rel_path("first.rs"),
27846 "Both editors should be opened for the same file"
27847 )
27848 }
27849 })
27850 .unwrap();
27851
27852 cx.executor().advance_clock(Duration::from_millis(500));
27853 let save = editor.update_in(cx, |editor, window, cx| {
27854 editor.move_to_end(&MoveToEnd, window, cx);
27855 editor.handle_input("dirty", window, cx);
27856 editor.save(
27857 SaveOptions {
27858 format: true,
27859 autosave: true,
27860 },
27861 project.clone(),
27862 window,
27863 cx,
27864 )
27865 });
27866 save.await.unwrap();
27867
27868 color_request_handle.next().await.unwrap();
27869 cx.run_until_parked();
27870 assert_eq!(
27871 2,
27872 requests_made.load(atomic::Ordering::Acquire),
27873 "Should query for colors once per save (deduplicated) and once per formatting after save"
27874 );
27875
27876 drop(editor);
27877 let close = workspace
27878 .update(cx, |workspace, window, cx| {
27879 workspace.active_pane().update(cx, |pane, cx| {
27880 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27881 })
27882 })
27883 .unwrap();
27884 close.await.unwrap();
27885 let close = workspace
27886 .update(cx, |workspace, window, cx| {
27887 workspace.active_pane().update(cx, |pane, cx| {
27888 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27889 })
27890 })
27891 .unwrap();
27892 close.await.unwrap();
27893 assert_eq!(
27894 2,
27895 requests_made.load(atomic::Ordering::Acquire),
27896 "After saving and closing all editors, no extra requests should be made"
27897 );
27898 workspace
27899 .update(cx, |workspace, _, cx| {
27900 assert!(
27901 workspace.active_item(cx).is_none(),
27902 "Should close all editors"
27903 )
27904 })
27905 .unwrap();
27906
27907 workspace
27908 .update(cx, |workspace, window, cx| {
27909 workspace.active_pane().update(cx, |pane, cx| {
27910 pane.navigate_backward(&workspace::GoBack, window, cx);
27911 })
27912 })
27913 .unwrap();
27914 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27915 cx.run_until_parked();
27916 let editor = workspace
27917 .update(cx, |workspace, _, cx| {
27918 workspace
27919 .active_item(cx)
27920 .expect("Should have reopened the editor again after navigating back")
27921 .downcast::<Editor>()
27922 .expect("Should be an editor")
27923 })
27924 .unwrap();
27925
27926 assert_eq!(
27927 2,
27928 requests_made.load(atomic::Ordering::Acquire),
27929 "Cache should be reused on buffer close and reopen"
27930 );
27931 editor.update(cx, |editor, cx| {
27932 assert_eq!(
27933 vec![expected_color],
27934 extract_color_inlays(editor, cx),
27935 "Should have an initial inlay"
27936 );
27937 });
27938
27939 drop(color_request_handle);
27940 let closure_requests_made = Arc::clone(&requests_made);
27941 let mut empty_color_request_handle = fake_language_server
27942 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27943 let requests_made = Arc::clone(&closure_requests_made);
27944 async move {
27945 assert_eq!(
27946 params.text_document.uri,
27947 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27948 );
27949 requests_made.fetch_add(1, atomic::Ordering::Release);
27950 Ok(Vec::new())
27951 }
27952 });
27953 let save = editor.update_in(cx, |editor, window, cx| {
27954 editor.move_to_end(&MoveToEnd, window, cx);
27955 editor.handle_input("dirty_again", window, cx);
27956 editor.save(
27957 SaveOptions {
27958 format: false,
27959 autosave: true,
27960 },
27961 project.clone(),
27962 window,
27963 cx,
27964 )
27965 });
27966 save.await.unwrap();
27967
27968 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27969 empty_color_request_handle.next().await.unwrap();
27970 cx.run_until_parked();
27971 assert_eq!(
27972 3,
27973 requests_made.load(atomic::Ordering::Acquire),
27974 "Should query for colors once per save only, as formatting was not requested"
27975 );
27976 editor.update(cx, |editor, cx| {
27977 assert_eq!(
27978 Vec::<Rgba>::new(),
27979 extract_color_inlays(editor, cx),
27980 "Should clear all colors when the server returns an empty response"
27981 );
27982 });
27983}
27984
27985#[gpui::test]
27986async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
27987 init_test(cx, |_| {});
27988 let (editor, cx) = cx.add_window_view(Editor::single_line);
27989 editor.update_in(cx, |editor, window, cx| {
27990 editor.set_text("oops\n\nwow\n", window, cx)
27991 });
27992 cx.run_until_parked();
27993 editor.update(cx, |editor, cx| {
27994 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
27995 });
27996 editor.update(cx, |editor, cx| {
27997 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
27998 });
27999 cx.run_until_parked();
28000 editor.update(cx, |editor, cx| {
28001 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
28002 });
28003}
28004
28005#[gpui::test]
28006async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
28007 init_test(cx, |_| {});
28008
28009 cx.update(|cx| {
28010 register_project_item::<Editor>(cx);
28011 });
28012
28013 let fs = FakeFs::new(cx.executor());
28014 fs.insert_tree("/root1", json!({})).await;
28015 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
28016 .await;
28017
28018 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
28019 let (workspace, cx) =
28020 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
28021
28022 let worktree_id = project.update(cx, |project, cx| {
28023 project.worktrees(cx).next().unwrap().read(cx).id()
28024 });
28025
28026 let handle = workspace
28027 .update_in(cx, |workspace, window, cx| {
28028 let project_path = (worktree_id, rel_path("one.pdf"));
28029 workspace.open_path(project_path, None, true, window, cx)
28030 })
28031 .await
28032 .unwrap();
28033 // The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
28034 // Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
28035 // With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
28036 assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
28037}
28038
28039#[gpui::test]
28040async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
28041 init_test(cx, |_| {});
28042
28043 let language = Arc::new(Language::new(
28044 LanguageConfig::default(),
28045 Some(tree_sitter_rust::LANGUAGE.into()),
28046 ));
28047
28048 // Test hierarchical sibling navigation
28049 let text = r#"
28050 fn outer() {
28051 if condition {
28052 let a = 1;
28053 }
28054 let b = 2;
28055 }
28056
28057 fn another() {
28058 let c = 3;
28059 }
28060 "#;
28061
28062 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
28063 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
28064 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
28065
28066 // Wait for parsing to complete
28067 editor
28068 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
28069 .await;
28070
28071 editor.update_in(cx, |editor, window, cx| {
28072 // Start by selecting "let a = 1;" inside the if block
28073 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28074 s.select_display_ranges([
28075 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
28076 ]);
28077 });
28078
28079 let initial_selection = editor
28080 .selections
28081 .display_ranges(&editor.display_snapshot(cx));
28082 assert_eq!(initial_selection.len(), 1, "Should have one selection");
28083
28084 // Test select next sibling - should move up levels to find the next sibling
28085 // Since "let a = 1;" has no siblings in the if block, it should move up
28086 // to find "let b = 2;" which is a sibling of the if block
28087 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
28088 let next_selection = editor
28089 .selections
28090 .display_ranges(&editor.display_snapshot(cx));
28091
28092 // Should have a selection and it should be different from the initial
28093 assert_eq!(
28094 next_selection.len(),
28095 1,
28096 "Should have one selection after next"
28097 );
28098 assert_ne!(
28099 next_selection[0], initial_selection[0],
28100 "Next sibling selection should be different"
28101 );
28102
28103 // Test hierarchical navigation by going to the end of the current function
28104 // and trying to navigate to the next function
28105 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28106 s.select_display_ranges([
28107 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
28108 ]);
28109 });
28110
28111 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
28112 let function_next_selection = editor
28113 .selections
28114 .display_ranges(&editor.display_snapshot(cx));
28115
28116 // Should move to the next function
28117 assert_eq!(
28118 function_next_selection.len(),
28119 1,
28120 "Should have one selection after function next"
28121 );
28122
28123 // Test select previous sibling navigation
28124 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
28125 let prev_selection = editor
28126 .selections
28127 .display_ranges(&editor.display_snapshot(cx));
28128
28129 // Should have a selection and it should be different
28130 assert_eq!(
28131 prev_selection.len(),
28132 1,
28133 "Should have one selection after prev"
28134 );
28135 assert_ne!(
28136 prev_selection[0], function_next_selection[0],
28137 "Previous sibling selection should be different from next"
28138 );
28139 });
28140}
28141
28142#[gpui::test]
28143async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
28144 init_test(cx, |_| {});
28145
28146 let mut cx = EditorTestContext::new(cx).await;
28147 cx.set_state(
28148 "let ˇvariable = 42;
28149let another = variable + 1;
28150let result = variable * 2;",
28151 );
28152
28153 // Set up document highlights manually (simulating LSP response)
28154 cx.update_editor(|editor, _window, cx| {
28155 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
28156
28157 // Create highlights for "variable" occurrences
28158 let highlight_ranges = [
28159 Point::new(0, 4)..Point::new(0, 12), // First "variable"
28160 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
28161 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
28162 ];
28163
28164 let anchor_ranges: Vec<_> = highlight_ranges
28165 .iter()
28166 .map(|range| range.clone().to_anchors(&buffer_snapshot))
28167 .collect();
28168
28169 editor.highlight_background::<DocumentHighlightRead>(
28170 &anchor_ranges,
28171 |_, theme| theme.colors().editor_document_highlight_read_background,
28172 cx,
28173 );
28174 });
28175
28176 // Go to next highlight - should move to second "variable"
28177 cx.update_editor(|editor, window, cx| {
28178 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28179 });
28180 cx.assert_editor_state(
28181 "let variable = 42;
28182let another = ˇvariable + 1;
28183let result = variable * 2;",
28184 );
28185
28186 // Go to next highlight - should move to third "variable"
28187 cx.update_editor(|editor, window, cx| {
28188 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28189 });
28190 cx.assert_editor_state(
28191 "let variable = 42;
28192let another = variable + 1;
28193let result = ˇvariable * 2;",
28194 );
28195
28196 // Go to next highlight - should stay at third "variable" (no wrap-around)
28197 cx.update_editor(|editor, window, cx| {
28198 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28199 });
28200 cx.assert_editor_state(
28201 "let variable = 42;
28202let another = variable + 1;
28203let result = ˇvariable * 2;",
28204 );
28205
28206 // Now test going backwards from third position
28207 cx.update_editor(|editor, window, cx| {
28208 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28209 });
28210 cx.assert_editor_state(
28211 "let variable = 42;
28212let another = ˇvariable + 1;
28213let result = variable * 2;",
28214 );
28215
28216 // Go to previous highlight - should move to first "variable"
28217 cx.update_editor(|editor, window, cx| {
28218 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28219 });
28220 cx.assert_editor_state(
28221 "let ˇvariable = 42;
28222let another = variable + 1;
28223let result = variable * 2;",
28224 );
28225
28226 // Go to previous highlight - should stay on first "variable"
28227 cx.update_editor(|editor, window, cx| {
28228 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28229 });
28230 cx.assert_editor_state(
28231 "let ˇvariable = 42;
28232let another = variable + 1;
28233let result = variable * 2;",
28234 );
28235}
28236
28237#[gpui::test]
28238async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
28239 cx: &mut gpui::TestAppContext,
28240) {
28241 init_test(cx, |_| {});
28242
28243 let url = "https://zed.dev";
28244
28245 let markdown_language = Arc::new(Language::new(
28246 LanguageConfig {
28247 name: "Markdown".into(),
28248 ..LanguageConfig::default()
28249 },
28250 None,
28251 ));
28252
28253 let mut cx = EditorTestContext::new(cx).await;
28254 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28255 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
28256
28257 cx.update_editor(|editor, window, cx| {
28258 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28259 editor.paste(&Paste, window, cx);
28260 });
28261
28262 cx.assert_editor_state(&format!(
28263 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
28264 ));
28265}
28266
28267#[gpui::test]
28268async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
28269 init_test(cx, |_| {});
28270
28271 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
28272 let mut cx = EditorTestContext::new(cx).await;
28273
28274 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28275
28276 // Case 1: Test if adding a character with multi cursors preserves nested list indents
28277 cx.set_state(&indoc! {"
28278 - [ ] Item 1
28279 - [ ] Item 1.a
28280 - [ˇ] Item 2
28281 - [ˇ] Item 2.a
28282 - [ˇ] Item 2.b
28283 "
28284 });
28285 cx.update_editor(|editor, window, cx| {
28286 editor.handle_input("x", window, cx);
28287 });
28288 cx.run_until_parked();
28289 cx.assert_editor_state(indoc! {"
28290 - [ ] Item 1
28291 - [ ] Item 1.a
28292 - [xˇ] Item 2
28293 - [xˇ] Item 2.a
28294 - [xˇ] Item 2.b
28295 "
28296 });
28297
28298 // Case 2: Test adding new line after nested list continues the list with unchecked task
28299 cx.set_state(&indoc! {"
28300 - [ ] Item 1
28301 - [ ] Item 1.a
28302 - [x] Item 2
28303 - [x] Item 2.a
28304 - [x] Item 2.bˇ"
28305 });
28306 cx.update_editor(|editor, window, cx| {
28307 editor.newline(&Newline, window, cx);
28308 });
28309 cx.assert_editor_state(indoc! {"
28310 - [ ] Item 1
28311 - [ ] Item 1.a
28312 - [x] Item 2
28313 - [x] Item 2.a
28314 - [x] Item 2.b
28315 - [ ] ˇ"
28316 });
28317
28318 // Case 3: Test adding content to continued list item
28319 cx.update_editor(|editor, window, cx| {
28320 editor.handle_input("Item 2.c", window, cx);
28321 });
28322 cx.run_until_parked();
28323 cx.assert_editor_state(indoc! {"
28324 - [ ] Item 1
28325 - [ ] Item 1.a
28326 - [x] Item 2
28327 - [x] Item 2.a
28328 - [x] Item 2.b
28329 - [ ] Item 2.cˇ"
28330 });
28331
28332 // Case 4: Test adding new line after nested ordered list continues with next number
28333 cx.set_state(indoc! {"
28334 1. Item 1
28335 1. Item 1.a
28336 2. Item 2
28337 1. Item 2.a
28338 2. Item 2.bˇ"
28339 });
28340 cx.update_editor(|editor, window, cx| {
28341 editor.newline(&Newline, window, cx);
28342 });
28343 cx.assert_editor_state(indoc! {"
28344 1. Item 1
28345 1. Item 1.a
28346 2. Item 2
28347 1. Item 2.a
28348 2. Item 2.b
28349 3. ˇ"
28350 });
28351
28352 // Case 5: Adding content to continued ordered list item
28353 cx.update_editor(|editor, window, cx| {
28354 editor.handle_input("Item 2.c", window, cx);
28355 });
28356 cx.run_until_parked();
28357 cx.assert_editor_state(indoc! {"
28358 1. Item 1
28359 1. Item 1.a
28360 2. Item 2
28361 1. Item 2.a
28362 2. Item 2.b
28363 3. Item 2.cˇ"
28364 });
28365
28366 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
28367 cx.set_state(indoc! {"
28368 - Item 1
28369 - Item 1.a
28370 - Item 1.a
28371 ˇ"});
28372 cx.update_editor(|editor, window, cx| {
28373 editor.handle_input("-", window, cx);
28374 });
28375 cx.run_until_parked();
28376 cx.assert_editor_state(indoc! {"
28377 - Item 1
28378 - Item 1.a
28379 - Item 1.a
28380 -ˇ"});
28381
28382 // Case 7: Test blockquote newline preserves something
28383 cx.set_state(indoc! {"
28384 > Item 1ˇ"
28385 });
28386 cx.update_editor(|editor, window, cx| {
28387 editor.newline(&Newline, window, cx);
28388 });
28389 cx.assert_editor_state(indoc! {"
28390 > Item 1
28391 ˇ"
28392 });
28393}
28394
28395#[gpui::test]
28396async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
28397 cx: &mut gpui::TestAppContext,
28398) {
28399 init_test(cx, |_| {});
28400
28401 let url = "https://zed.dev";
28402
28403 let markdown_language = Arc::new(Language::new(
28404 LanguageConfig {
28405 name: "Markdown".into(),
28406 ..LanguageConfig::default()
28407 },
28408 None,
28409 ));
28410
28411 let mut cx = EditorTestContext::new(cx).await;
28412 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28413 cx.set_state(&format!(
28414 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
28415 ));
28416
28417 cx.update_editor(|editor, window, cx| {
28418 editor.copy(&Copy, window, cx);
28419 });
28420
28421 cx.set_state(&format!(
28422 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
28423 ));
28424
28425 cx.update_editor(|editor, window, cx| {
28426 editor.paste(&Paste, window, cx);
28427 });
28428
28429 cx.assert_editor_state(&format!(
28430 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
28431 ));
28432}
28433
28434#[gpui::test]
28435async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
28436 cx: &mut gpui::TestAppContext,
28437) {
28438 init_test(cx, |_| {});
28439
28440 let url = "https://zed.dev";
28441
28442 let markdown_language = Arc::new(Language::new(
28443 LanguageConfig {
28444 name: "Markdown".into(),
28445 ..LanguageConfig::default()
28446 },
28447 None,
28448 ));
28449
28450 let mut cx = EditorTestContext::new(cx).await;
28451 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28452 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
28453
28454 cx.update_editor(|editor, window, cx| {
28455 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28456 editor.paste(&Paste, window, cx);
28457 });
28458
28459 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
28460}
28461
28462#[gpui::test]
28463async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
28464 cx: &mut gpui::TestAppContext,
28465) {
28466 init_test(cx, |_| {});
28467
28468 let text = "Awesome";
28469
28470 let markdown_language = Arc::new(Language::new(
28471 LanguageConfig {
28472 name: "Markdown".into(),
28473 ..LanguageConfig::default()
28474 },
28475 None,
28476 ));
28477
28478 let mut cx = EditorTestContext::new(cx).await;
28479 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28480 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
28481
28482 cx.update_editor(|editor, window, cx| {
28483 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
28484 editor.paste(&Paste, window, cx);
28485 });
28486
28487 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
28488}
28489
28490#[gpui::test]
28491async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
28492 cx: &mut gpui::TestAppContext,
28493) {
28494 init_test(cx, |_| {});
28495
28496 let url = "https://zed.dev";
28497
28498 let markdown_language = Arc::new(Language::new(
28499 LanguageConfig {
28500 name: "Rust".into(),
28501 ..LanguageConfig::default()
28502 },
28503 None,
28504 ));
28505
28506 let mut cx = EditorTestContext::new(cx).await;
28507 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28508 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
28509
28510 cx.update_editor(|editor, window, cx| {
28511 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28512 editor.paste(&Paste, window, cx);
28513 });
28514
28515 cx.assert_editor_state(&format!(
28516 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
28517 ));
28518}
28519
28520#[gpui::test]
28521async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
28522 cx: &mut TestAppContext,
28523) {
28524 init_test(cx, |_| {});
28525
28526 let url = "https://zed.dev";
28527
28528 let markdown_language = Arc::new(Language::new(
28529 LanguageConfig {
28530 name: "Markdown".into(),
28531 ..LanguageConfig::default()
28532 },
28533 None,
28534 ));
28535
28536 let (editor, cx) = cx.add_window_view(|window, cx| {
28537 let multi_buffer = MultiBuffer::build_multi(
28538 [
28539 ("this will embed -> link", vec![Point::row_range(0..1)]),
28540 ("this will replace -> link", vec![Point::row_range(0..1)]),
28541 ],
28542 cx,
28543 );
28544 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
28545 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28546 s.select_ranges(vec![
28547 Point::new(0, 19)..Point::new(0, 23),
28548 Point::new(1, 21)..Point::new(1, 25),
28549 ])
28550 });
28551 let first_buffer_id = multi_buffer
28552 .read(cx)
28553 .excerpt_buffer_ids()
28554 .into_iter()
28555 .next()
28556 .unwrap();
28557 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
28558 first_buffer.update(cx, |buffer, cx| {
28559 buffer.set_language(Some(markdown_language.clone()), cx);
28560 });
28561
28562 editor
28563 });
28564 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28565
28566 cx.update_editor(|editor, window, cx| {
28567 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28568 editor.paste(&Paste, window, cx);
28569 });
28570
28571 cx.assert_editor_state(&format!(
28572 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
28573 ));
28574}
28575
28576#[gpui::test]
28577async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
28578 init_test(cx, |_| {});
28579
28580 let fs = FakeFs::new(cx.executor());
28581 fs.insert_tree(
28582 path!("/project"),
28583 json!({
28584 "first.rs": "# First Document\nSome content here.",
28585 "second.rs": "Plain text content for second file.",
28586 }),
28587 )
28588 .await;
28589
28590 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
28591 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
28592 let cx = &mut VisualTestContext::from_window(*workspace, cx);
28593
28594 let language = rust_lang();
28595 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28596 language_registry.add(language.clone());
28597 let mut fake_servers = language_registry.register_fake_lsp(
28598 "Rust",
28599 FakeLspAdapter {
28600 ..FakeLspAdapter::default()
28601 },
28602 );
28603
28604 let buffer1 = project
28605 .update(cx, |project, cx| {
28606 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
28607 })
28608 .await
28609 .unwrap();
28610 let buffer2 = project
28611 .update(cx, |project, cx| {
28612 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
28613 })
28614 .await
28615 .unwrap();
28616
28617 let multi_buffer = cx.new(|cx| {
28618 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
28619 multi_buffer.set_excerpts_for_path(
28620 PathKey::for_buffer(&buffer1, cx),
28621 buffer1.clone(),
28622 [Point::zero()..buffer1.read(cx).max_point()],
28623 3,
28624 cx,
28625 );
28626 multi_buffer.set_excerpts_for_path(
28627 PathKey::for_buffer(&buffer2, cx),
28628 buffer2.clone(),
28629 [Point::zero()..buffer1.read(cx).max_point()],
28630 3,
28631 cx,
28632 );
28633 multi_buffer
28634 });
28635
28636 let (editor, cx) = cx.add_window_view(|window, cx| {
28637 Editor::new(
28638 EditorMode::full(),
28639 multi_buffer,
28640 Some(project.clone()),
28641 window,
28642 cx,
28643 )
28644 });
28645
28646 let fake_language_server = fake_servers.next().await.unwrap();
28647
28648 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
28649
28650 let save = editor.update_in(cx, |editor, window, cx| {
28651 assert!(editor.is_dirty(cx));
28652
28653 editor.save(
28654 SaveOptions {
28655 format: true,
28656 autosave: true,
28657 },
28658 project,
28659 window,
28660 cx,
28661 )
28662 });
28663 let (start_edit_tx, start_edit_rx) = oneshot::channel();
28664 let (done_edit_tx, done_edit_rx) = oneshot::channel();
28665 let mut done_edit_rx = Some(done_edit_rx);
28666 let mut start_edit_tx = Some(start_edit_tx);
28667
28668 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
28669 start_edit_tx.take().unwrap().send(()).unwrap();
28670 let done_edit_rx = done_edit_rx.take().unwrap();
28671 async move {
28672 done_edit_rx.await.unwrap();
28673 Ok(None)
28674 }
28675 });
28676
28677 start_edit_rx.await.unwrap();
28678 buffer2
28679 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
28680 .unwrap();
28681
28682 done_edit_tx.send(()).unwrap();
28683
28684 save.await.unwrap();
28685 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
28686}
28687
28688#[track_caller]
28689fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
28690 editor
28691 .all_inlays(cx)
28692 .into_iter()
28693 .filter_map(|inlay| inlay.get_color())
28694 .map(Rgba::from)
28695 .collect()
28696}
28697
28698#[gpui::test]
28699fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
28700 init_test(cx, |_| {});
28701
28702 let editor = cx.add_window(|window, cx| {
28703 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
28704 build_editor(buffer, window, cx)
28705 });
28706
28707 editor
28708 .update(cx, |editor, window, cx| {
28709 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28710 s.select_display_ranges([
28711 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
28712 ])
28713 });
28714
28715 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
28716
28717 assert_eq!(
28718 editor.display_text(cx),
28719 "line1\nline2\nline2",
28720 "Duplicating last line upward should create duplicate above, not on same line"
28721 );
28722
28723 assert_eq!(
28724 editor
28725 .selections
28726 .display_ranges(&editor.display_snapshot(cx)),
28727 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
28728 "Selection should move to the duplicated line"
28729 );
28730 })
28731 .unwrap();
28732}
28733
28734#[gpui::test]
28735async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
28736 init_test(cx, |_| {});
28737
28738 let mut cx = EditorTestContext::new(cx).await;
28739
28740 cx.set_state("line1\nline2ˇ");
28741
28742 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28743
28744 let clipboard_text = cx
28745 .read_from_clipboard()
28746 .and_then(|item| item.text().as_deref().map(str::to_string));
28747
28748 assert_eq!(
28749 clipboard_text,
28750 Some("line2\n".to_string()),
28751 "Copying a line without trailing newline should include a newline"
28752 );
28753
28754 cx.set_state("line1\nˇ");
28755
28756 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28757
28758 cx.assert_editor_state("line1\nline2\nˇ");
28759}
28760
28761#[gpui::test]
28762async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28763 init_test(cx, |_| {});
28764
28765 let mut cx = EditorTestContext::new(cx).await;
28766
28767 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28768
28769 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28770
28771 let clipboard_text = cx
28772 .read_from_clipboard()
28773 .and_then(|item| item.text().as_deref().map(str::to_string));
28774
28775 assert_eq!(
28776 clipboard_text,
28777 Some("line1\nline2\nline3\n".to_string()),
28778 "Copying multiple lines should include a single newline between lines"
28779 );
28780
28781 cx.set_state("lineA\nˇ");
28782
28783 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28784
28785 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28786}
28787
28788#[gpui::test]
28789async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28790 init_test(cx, |_| {});
28791
28792 let mut cx = EditorTestContext::new(cx).await;
28793
28794 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28795
28796 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
28797
28798 let clipboard_text = cx
28799 .read_from_clipboard()
28800 .and_then(|item| item.text().as_deref().map(str::to_string));
28801
28802 assert_eq!(
28803 clipboard_text,
28804 Some("line1\nline2\nline3\n".to_string()),
28805 "Copying multiple lines should include a single newline between lines"
28806 );
28807
28808 cx.set_state("lineA\nˇ");
28809
28810 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28811
28812 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28813}
28814
28815#[gpui::test]
28816async fn test_end_of_editor_context(cx: &mut TestAppContext) {
28817 init_test(cx, |_| {});
28818
28819 let mut cx = EditorTestContext::new(cx).await;
28820
28821 cx.set_state("line1\nline2ˇ");
28822 cx.update_editor(|e, window, cx| {
28823 e.set_mode(EditorMode::SingleLine);
28824 assert!(e.key_context(window, cx).contains("end_of_input"));
28825 });
28826 cx.set_state("ˇline1\nline2");
28827 cx.update_editor(|e, window, cx| {
28828 assert!(!e.key_context(window, cx).contains("end_of_input"));
28829 });
28830 cx.set_state("line1ˇ\nline2");
28831 cx.update_editor(|e, window, cx| {
28832 assert!(!e.key_context(window, cx).contains("end_of_input"));
28833 });
28834}
28835
28836#[gpui::test]
28837async fn test_sticky_scroll(cx: &mut TestAppContext) {
28838 init_test(cx, |_| {});
28839 let mut cx = EditorTestContext::new(cx).await;
28840
28841 let buffer = indoc! {"
28842 ˇfn foo() {
28843 let abc = 123;
28844 }
28845 struct Bar;
28846 impl Bar {
28847 fn new() -> Self {
28848 Self
28849 }
28850 }
28851 fn baz() {
28852 }
28853 "};
28854 cx.set_state(&buffer);
28855
28856 cx.update_editor(|e, _, cx| {
28857 e.buffer()
28858 .read(cx)
28859 .as_singleton()
28860 .unwrap()
28861 .update(cx, |buffer, cx| {
28862 buffer.set_language(Some(rust_lang()), cx);
28863 })
28864 });
28865
28866 let mut sticky_headers = |offset: ScrollOffset| {
28867 cx.update_editor(|e, window, cx| {
28868 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
28869 let style = e.style(cx).clone();
28870 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), &style, cx)
28871 .into_iter()
28872 .map(
28873 |StickyHeader {
28874 start_point,
28875 offset,
28876 ..
28877 }| { (start_point, offset) },
28878 )
28879 .collect::<Vec<_>>()
28880 })
28881 };
28882
28883 let fn_foo = Point { row: 0, column: 0 };
28884 let impl_bar = Point { row: 4, column: 0 };
28885 let fn_new = Point { row: 5, column: 4 };
28886
28887 assert_eq!(sticky_headers(0.0), vec![]);
28888 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
28889 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
28890 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
28891 assert_eq!(sticky_headers(2.0), vec![]);
28892 assert_eq!(sticky_headers(2.5), vec![]);
28893 assert_eq!(sticky_headers(3.0), vec![]);
28894 assert_eq!(sticky_headers(3.5), vec![]);
28895 assert_eq!(sticky_headers(4.0), vec![]);
28896 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28897 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28898 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
28899 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
28900 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
28901 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
28902 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
28903 assert_eq!(sticky_headers(8.0), vec![]);
28904 assert_eq!(sticky_headers(8.5), vec![]);
28905 assert_eq!(sticky_headers(9.0), vec![]);
28906 assert_eq!(sticky_headers(9.5), vec![]);
28907 assert_eq!(sticky_headers(10.0), vec![]);
28908}
28909
28910#[gpui::test]
28911fn test_relative_line_numbers(cx: &mut TestAppContext) {
28912 init_test(cx, |_| {});
28913
28914 let buffer_1 = cx.new(|cx| Buffer::local("aaaaaaaaaa\nbbb\n", cx));
28915 let buffer_2 = cx.new(|cx| Buffer::local("cccccccccc\nddd\n", cx));
28916 let buffer_3 = cx.new(|cx| Buffer::local("eee\nffffffffff\n", cx));
28917
28918 let multibuffer = cx.new(|cx| {
28919 let mut multibuffer = MultiBuffer::new(ReadWrite);
28920 multibuffer.push_excerpts(
28921 buffer_1.clone(),
28922 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
28923 cx,
28924 );
28925 multibuffer.push_excerpts(
28926 buffer_2.clone(),
28927 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
28928 cx,
28929 );
28930 multibuffer.push_excerpts(
28931 buffer_3.clone(),
28932 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
28933 cx,
28934 );
28935 multibuffer
28936 });
28937
28938 // wrapped contents of multibuffer:
28939 // aaa
28940 // aaa
28941 // aaa
28942 // a
28943 // bbb
28944 //
28945 // ccc
28946 // ccc
28947 // ccc
28948 // c
28949 // ddd
28950 //
28951 // eee
28952 // fff
28953 // fff
28954 // fff
28955 // f
28956
28957 let editor = cx.add_window(|window, cx| build_editor(multibuffer, window, cx));
28958 _ = editor.update(cx, |editor, window, cx| {
28959 editor.set_wrap_width(Some(30.0.into()), cx); // every 3 characters
28960
28961 // includes trailing newlines.
28962 let expected_line_numbers = [2, 6, 7, 10, 14, 15, 18, 19, 23];
28963 let expected_wrapped_line_numbers = [
28964 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23,
28965 ];
28966
28967 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28968 s.select_ranges([
28969 Point::new(7, 0)..Point::new(7, 1), // second row of `ccc`
28970 ]);
28971 });
28972
28973 let snapshot = editor.snapshot(window, cx);
28974
28975 // these are all 0-indexed
28976 let base_display_row = DisplayRow(11);
28977 let base_row = 3;
28978 let wrapped_base_row = 7;
28979
28980 // test not counting wrapped lines
28981 let expected_relative_numbers = expected_line_numbers
28982 .into_iter()
28983 .enumerate()
28984 .map(|(i, row)| (DisplayRow(row), i.abs_diff(base_row) as u32))
28985 .collect_vec();
28986 let actual_relative_numbers = snapshot
28987 .calculate_relative_line_numbers(
28988 &(DisplayRow(0)..DisplayRow(24)),
28989 base_display_row,
28990 false,
28991 )
28992 .into_iter()
28993 .sorted()
28994 .collect_vec();
28995 assert_eq!(expected_relative_numbers, actual_relative_numbers);
28996 // check `calculate_relative_line_numbers()` against `relative_line_delta()` for each line
28997 for (display_row, relative_number) in expected_relative_numbers {
28998 assert_eq!(
28999 relative_number,
29000 snapshot
29001 .relative_line_delta(display_row, base_display_row, false)
29002 .unsigned_abs() as u32,
29003 );
29004 }
29005
29006 // test counting wrapped lines
29007 let expected_wrapped_relative_numbers = expected_wrapped_line_numbers
29008 .into_iter()
29009 .enumerate()
29010 .map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32))
29011 .filter(|(row, _)| *row != base_display_row)
29012 .collect_vec();
29013 let actual_relative_numbers = snapshot
29014 .calculate_relative_line_numbers(
29015 &(DisplayRow(0)..DisplayRow(24)),
29016 base_display_row,
29017 true,
29018 )
29019 .into_iter()
29020 .sorted()
29021 .collect_vec();
29022 assert_eq!(expected_wrapped_relative_numbers, actual_relative_numbers);
29023 // check `calculate_relative_line_numbers()` against `relative_wrapped_line_delta()` for each line
29024 for (display_row, relative_number) in expected_wrapped_relative_numbers {
29025 assert_eq!(
29026 relative_number,
29027 snapshot
29028 .relative_line_delta(display_row, base_display_row, true)
29029 .unsigned_abs() as u32,
29030 );
29031 }
29032 });
29033}
29034
29035#[gpui::test]
29036async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
29037 init_test(cx, |_| {});
29038 cx.update(|cx| {
29039 SettingsStore::update_global(cx, |store, cx| {
29040 store.update_user_settings(cx, |settings| {
29041 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
29042 enabled: Some(true),
29043 })
29044 });
29045 });
29046 });
29047 let mut cx = EditorTestContext::new(cx).await;
29048
29049 let line_height = cx.update_editor(|editor, window, cx| {
29050 editor
29051 .style(cx)
29052 .text
29053 .line_height_in_pixels(window.rem_size())
29054 });
29055
29056 let buffer = indoc! {"
29057 ˇfn foo() {
29058 let abc = 123;
29059 }
29060 struct Bar;
29061 impl Bar {
29062 fn new() -> Self {
29063 Self
29064 }
29065 }
29066 fn baz() {
29067 }
29068 "};
29069 cx.set_state(&buffer);
29070
29071 cx.update_editor(|e, _, cx| {
29072 e.buffer()
29073 .read(cx)
29074 .as_singleton()
29075 .unwrap()
29076 .update(cx, |buffer, cx| {
29077 buffer.set_language(Some(rust_lang()), cx);
29078 })
29079 });
29080
29081 let fn_foo = || empty_range(0, 0);
29082 let impl_bar = || empty_range(4, 0);
29083 let fn_new = || empty_range(5, 4);
29084
29085 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
29086 cx.update_editor(|e, window, cx| {
29087 e.scroll(
29088 gpui::Point {
29089 x: 0.,
29090 y: scroll_offset,
29091 },
29092 None,
29093 window,
29094 cx,
29095 );
29096 });
29097 cx.simulate_click(
29098 gpui::Point {
29099 x: px(0.),
29100 y: click_offset as f32 * line_height,
29101 },
29102 Modifiers::none(),
29103 );
29104 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
29105 };
29106
29107 assert_eq!(
29108 scroll_and_click(
29109 4.5, // impl Bar is halfway off the screen
29110 0.0 // click top of screen
29111 ),
29112 // scrolled to impl Bar
29113 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29114 );
29115
29116 assert_eq!(
29117 scroll_and_click(
29118 4.5, // impl Bar is halfway off the screen
29119 0.25 // click middle of impl Bar
29120 ),
29121 // scrolled to impl Bar
29122 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29123 );
29124
29125 assert_eq!(
29126 scroll_and_click(
29127 4.5, // impl Bar is halfway off the screen
29128 1.5 // click below impl Bar (e.g. fn new())
29129 ),
29130 // scrolled to fn new() - this is below the impl Bar header which has persisted
29131 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
29132 );
29133
29134 assert_eq!(
29135 scroll_and_click(
29136 5.5, // fn new is halfway underneath impl Bar
29137 0.75 // click on the overlap of impl Bar and fn new()
29138 ),
29139 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29140 );
29141
29142 assert_eq!(
29143 scroll_and_click(
29144 5.5, // fn new is halfway underneath impl Bar
29145 1.25 // click on the visible part of fn new()
29146 ),
29147 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
29148 );
29149
29150 assert_eq!(
29151 scroll_and_click(
29152 1.5, // fn foo is halfway off the screen
29153 0.0 // click top of screen
29154 ),
29155 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
29156 );
29157
29158 assert_eq!(
29159 scroll_and_click(
29160 1.5, // fn foo is halfway off the screen
29161 0.75 // click visible part of let abc...
29162 )
29163 .0,
29164 // no change in scroll
29165 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
29166 (gpui::Point { x: 0., y: 1.5 })
29167 );
29168}
29169
29170#[gpui::test]
29171async fn test_next_prev_reference(cx: &mut TestAppContext) {
29172 const CYCLE_POSITIONS: &[&'static str] = &[
29173 indoc! {"
29174 fn foo() {
29175 let ˇabc = 123;
29176 let x = abc + 1;
29177 let y = abc + 2;
29178 let z = abc + 2;
29179 }
29180 "},
29181 indoc! {"
29182 fn foo() {
29183 let abc = 123;
29184 let x = ˇabc + 1;
29185 let y = abc + 2;
29186 let z = abc + 2;
29187 }
29188 "},
29189 indoc! {"
29190 fn foo() {
29191 let abc = 123;
29192 let x = abc + 1;
29193 let y = ˇabc + 2;
29194 let z = abc + 2;
29195 }
29196 "},
29197 indoc! {"
29198 fn foo() {
29199 let abc = 123;
29200 let x = abc + 1;
29201 let y = abc + 2;
29202 let z = ˇabc + 2;
29203 }
29204 "},
29205 ];
29206
29207 init_test(cx, |_| {});
29208
29209 let mut cx = EditorLspTestContext::new_rust(
29210 lsp::ServerCapabilities {
29211 references_provider: Some(lsp::OneOf::Left(true)),
29212 ..Default::default()
29213 },
29214 cx,
29215 )
29216 .await;
29217
29218 // importantly, the cursor is in the middle
29219 cx.set_state(indoc! {"
29220 fn foo() {
29221 let aˇbc = 123;
29222 let x = abc + 1;
29223 let y = abc + 2;
29224 let z = abc + 2;
29225 }
29226 "});
29227
29228 let reference_ranges = [
29229 lsp::Position::new(1, 8),
29230 lsp::Position::new(2, 12),
29231 lsp::Position::new(3, 12),
29232 lsp::Position::new(4, 12),
29233 ]
29234 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
29235
29236 cx.lsp
29237 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
29238 Ok(Some(
29239 reference_ranges
29240 .map(|range| lsp::Location {
29241 uri: params.text_document_position.text_document.uri.clone(),
29242 range,
29243 })
29244 .to_vec(),
29245 ))
29246 });
29247
29248 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
29249 cx.update_editor(|editor, window, cx| {
29250 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
29251 })
29252 .unwrap()
29253 .await
29254 .unwrap()
29255 };
29256
29257 _move(Direction::Next, 1, &mut cx).await;
29258 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29259
29260 _move(Direction::Next, 1, &mut cx).await;
29261 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29262
29263 _move(Direction::Next, 1, &mut cx).await;
29264 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29265
29266 // loops back to the start
29267 _move(Direction::Next, 1, &mut cx).await;
29268 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29269
29270 // loops back to the end
29271 _move(Direction::Prev, 1, &mut cx).await;
29272 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29273
29274 _move(Direction::Prev, 1, &mut cx).await;
29275 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29276
29277 _move(Direction::Prev, 1, &mut cx).await;
29278 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29279
29280 _move(Direction::Prev, 1, &mut cx).await;
29281 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29282
29283 _move(Direction::Next, 3, &mut cx).await;
29284 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29285
29286 _move(Direction::Prev, 2, &mut cx).await;
29287 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29288}
29289
29290#[gpui::test]
29291async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
29292 init_test(cx, |_| {});
29293
29294 let (editor, cx) = cx.add_window_view(|window, cx| {
29295 let multi_buffer = MultiBuffer::build_multi(
29296 [
29297 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29298 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29299 ],
29300 cx,
29301 );
29302 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29303 });
29304
29305 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29306 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
29307
29308 cx.assert_excerpts_with_selections(indoc! {"
29309 [EXCERPT]
29310 ˇ1
29311 2
29312 3
29313 [EXCERPT]
29314 1
29315 2
29316 3
29317 "});
29318
29319 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
29320 cx.update_editor(|editor, window, cx| {
29321 editor.change_selections(None.into(), window, cx, |s| {
29322 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29323 });
29324 });
29325 cx.assert_excerpts_with_selections(indoc! {"
29326 [EXCERPT]
29327 1
29328 2ˇ
29329 3
29330 [EXCERPT]
29331 1
29332 2
29333 3
29334 "});
29335
29336 cx.update_editor(|editor, window, cx| {
29337 editor
29338 .select_all_matches(&SelectAllMatches, window, cx)
29339 .unwrap();
29340 });
29341 cx.assert_excerpts_with_selections(indoc! {"
29342 [EXCERPT]
29343 1
29344 2ˇ
29345 3
29346 [EXCERPT]
29347 1
29348 2ˇ
29349 3
29350 "});
29351
29352 cx.update_editor(|editor, window, cx| {
29353 editor.handle_input("X", window, cx);
29354 });
29355 cx.assert_excerpts_with_selections(indoc! {"
29356 [EXCERPT]
29357 1
29358 Xˇ
29359 3
29360 [EXCERPT]
29361 1
29362 Xˇ
29363 3
29364 "});
29365
29366 // Scenario 2: Select "2", then fold second buffer before insertion
29367 cx.update_multibuffer(|mb, cx| {
29368 for buffer_id in buffer_ids.iter() {
29369 let buffer = mb.buffer(*buffer_id).unwrap();
29370 buffer.update(cx, |buffer, cx| {
29371 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29372 });
29373 }
29374 });
29375
29376 // Select "2" and select all matches
29377 cx.update_editor(|editor, window, cx| {
29378 editor.change_selections(None.into(), window, cx, |s| {
29379 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29380 });
29381 editor
29382 .select_all_matches(&SelectAllMatches, window, cx)
29383 .unwrap();
29384 });
29385
29386 // Fold second buffer - should remove selections from folded buffer
29387 cx.update_editor(|editor, _, cx| {
29388 editor.fold_buffer(buffer_ids[1], cx);
29389 });
29390 cx.assert_excerpts_with_selections(indoc! {"
29391 [EXCERPT]
29392 1
29393 2ˇ
29394 3
29395 [EXCERPT]
29396 [FOLDED]
29397 "});
29398
29399 // Insert text - should only affect first buffer
29400 cx.update_editor(|editor, window, cx| {
29401 editor.handle_input("Y", window, cx);
29402 });
29403 cx.update_editor(|editor, _, cx| {
29404 editor.unfold_buffer(buffer_ids[1], cx);
29405 });
29406 cx.assert_excerpts_with_selections(indoc! {"
29407 [EXCERPT]
29408 1
29409 Yˇ
29410 3
29411 [EXCERPT]
29412 1
29413 2
29414 3
29415 "});
29416
29417 // Scenario 3: Select "2", then fold first buffer before insertion
29418 cx.update_multibuffer(|mb, cx| {
29419 for buffer_id in buffer_ids.iter() {
29420 let buffer = mb.buffer(*buffer_id).unwrap();
29421 buffer.update(cx, |buffer, cx| {
29422 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29423 });
29424 }
29425 });
29426
29427 // Select "2" and select all matches
29428 cx.update_editor(|editor, window, cx| {
29429 editor.change_selections(None.into(), window, cx, |s| {
29430 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29431 });
29432 editor
29433 .select_all_matches(&SelectAllMatches, window, cx)
29434 .unwrap();
29435 });
29436
29437 // Fold first buffer - should remove selections from folded buffer
29438 cx.update_editor(|editor, _, cx| {
29439 editor.fold_buffer(buffer_ids[0], cx);
29440 });
29441 cx.assert_excerpts_with_selections(indoc! {"
29442 [EXCERPT]
29443 [FOLDED]
29444 [EXCERPT]
29445 1
29446 2ˇ
29447 3
29448 "});
29449
29450 // Insert text - should only affect second buffer
29451 cx.update_editor(|editor, window, cx| {
29452 editor.handle_input("Z", window, cx);
29453 });
29454 cx.update_editor(|editor, _, cx| {
29455 editor.unfold_buffer(buffer_ids[0], cx);
29456 });
29457 cx.assert_excerpts_with_selections(indoc! {"
29458 [EXCERPT]
29459 1
29460 2
29461 3
29462 [EXCERPT]
29463 1
29464 Zˇ
29465 3
29466 "});
29467
29468 // Test correct folded header is selected upon fold
29469 cx.update_editor(|editor, _, cx| {
29470 editor.fold_buffer(buffer_ids[0], cx);
29471 editor.fold_buffer(buffer_ids[1], cx);
29472 });
29473 cx.assert_excerpts_with_selections(indoc! {"
29474 [EXCERPT]
29475 [FOLDED]
29476 [EXCERPT]
29477 ˇ[FOLDED]
29478 "});
29479
29480 // Test selection inside folded buffer unfolds it on type
29481 cx.update_editor(|editor, window, cx| {
29482 editor.handle_input("W", window, cx);
29483 });
29484 cx.update_editor(|editor, _, cx| {
29485 editor.unfold_buffer(buffer_ids[0], cx);
29486 });
29487 cx.assert_excerpts_with_selections(indoc! {"
29488 [EXCERPT]
29489 1
29490 2
29491 3
29492 [EXCERPT]
29493 Wˇ1
29494 Z
29495 3
29496 "});
29497}
29498
29499#[gpui::test]
29500async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
29501 init_test(cx, |_| {});
29502
29503 let (editor, cx) = cx.add_window_view(|window, cx| {
29504 let multi_buffer = MultiBuffer::build_multi(
29505 [
29506 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29507 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
29508 ],
29509 cx,
29510 );
29511 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29512 });
29513
29514 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29515
29516 cx.assert_excerpts_with_selections(indoc! {"
29517 [EXCERPT]
29518 ˇ1
29519 2
29520 3
29521 [EXCERPT]
29522 1
29523 2
29524 3
29525 4
29526 5
29527 6
29528 7
29529 8
29530 9
29531 "});
29532
29533 cx.update_editor(|editor, window, cx| {
29534 editor.change_selections(None.into(), window, cx, |s| {
29535 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
29536 });
29537 });
29538
29539 cx.assert_excerpts_with_selections(indoc! {"
29540 [EXCERPT]
29541 1
29542 2
29543 3
29544 [EXCERPT]
29545 1
29546 2
29547 3
29548 4
29549 5
29550 6
29551 ˇ7
29552 8
29553 9
29554 "});
29555
29556 cx.update_editor(|editor, _window, cx| {
29557 editor.set_vertical_scroll_margin(0, cx);
29558 });
29559
29560 cx.update_editor(|editor, window, cx| {
29561 assert_eq!(editor.vertical_scroll_margin(), 0);
29562 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29563 assert_eq!(
29564 editor.snapshot(window, cx).scroll_position(),
29565 gpui::Point::new(0., 12.0)
29566 );
29567 });
29568
29569 cx.update_editor(|editor, _window, cx| {
29570 editor.set_vertical_scroll_margin(3, cx);
29571 });
29572
29573 cx.update_editor(|editor, window, cx| {
29574 assert_eq!(editor.vertical_scroll_margin(), 3);
29575 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29576 assert_eq!(
29577 editor.snapshot(window, cx).scroll_position(),
29578 gpui::Point::new(0., 9.0)
29579 );
29580 });
29581}
29582
29583#[gpui::test]
29584async fn test_find_references_single_case(cx: &mut TestAppContext) {
29585 init_test(cx, |_| {});
29586 let mut cx = EditorLspTestContext::new_rust(
29587 lsp::ServerCapabilities {
29588 references_provider: Some(lsp::OneOf::Left(true)),
29589 ..lsp::ServerCapabilities::default()
29590 },
29591 cx,
29592 )
29593 .await;
29594
29595 let before = indoc!(
29596 r#"
29597 fn main() {
29598 let aˇbc = 123;
29599 let xyz = abc;
29600 }
29601 "#
29602 );
29603 let after = indoc!(
29604 r#"
29605 fn main() {
29606 let abc = 123;
29607 let xyz = ˇabc;
29608 }
29609 "#
29610 );
29611
29612 cx.lsp
29613 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
29614 Ok(Some(vec![
29615 lsp::Location {
29616 uri: params.text_document_position.text_document.uri.clone(),
29617 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
29618 },
29619 lsp::Location {
29620 uri: params.text_document_position.text_document.uri,
29621 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
29622 },
29623 ]))
29624 });
29625
29626 cx.set_state(before);
29627
29628 let action = FindAllReferences {
29629 always_open_multibuffer: false,
29630 };
29631
29632 let navigated = cx
29633 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
29634 .expect("should have spawned a task")
29635 .await
29636 .unwrap();
29637
29638 assert_eq!(navigated, Navigated::No);
29639
29640 cx.run_until_parked();
29641
29642 cx.assert_editor_state(after);
29643}
29644
29645#[gpui::test]
29646async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
29647 init_test(cx, |settings| {
29648 settings.defaults.tab_size = Some(2.try_into().unwrap());
29649 });
29650
29651 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29652 let mut cx = EditorTestContext::new(cx).await;
29653 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29654
29655 // Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
29656 cx.set_state(indoc! {"
29657 - [ ] taskˇ
29658 "});
29659 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29660 cx.wait_for_autoindent_applied().await;
29661 cx.assert_editor_state(indoc! {"
29662 - [ ] task
29663 - [ ] ˇ
29664 "});
29665
29666 // Case 2: Works with checked task items too
29667 cx.set_state(indoc! {"
29668 - [x] completed taskˇ
29669 "});
29670 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29671 cx.wait_for_autoindent_applied().await;
29672 cx.assert_editor_state(indoc! {"
29673 - [x] completed task
29674 - [ ] ˇ
29675 "});
29676
29677 // Case 2.1: Works with uppercase checked marker too
29678 cx.set_state(indoc! {"
29679 - [X] completed taskˇ
29680 "});
29681 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29682 cx.wait_for_autoindent_applied().await;
29683 cx.assert_editor_state(indoc! {"
29684 - [X] completed task
29685 - [ ] ˇ
29686 "});
29687
29688 // Case 3: Cursor position doesn't matter - content after marker is what counts
29689 cx.set_state(indoc! {"
29690 - [ ] taˇsk
29691 "});
29692 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29693 cx.wait_for_autoindent_applied().await;
29694 cx.assert_editor_state(indoc! {"
29695 - [ ] ta
29696 - [ ] ˇsk
29697 "});
29698
29699 // Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
29700 cx.set_state(indoc! {"
29701 - [ ] ˇ
29702 "});
29703 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29704 cx.wait_for_autoindent_applied().await;
29705 cx.assert_editor_state(
29706 indoc! {"
29707 - [ ]$$
29708 ˇ
29709 "}
29710 .replace("$", " ")
29711 .as_str(),
29712 );
29713
29714 // Case 5: Adding newline with content adds marker preserving indentation
29715 cx.set_state(indoc! {"
29716 - [ ] task
29717 - [ ] indentedˇ
29718 "});
29719 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29720 cx.wait_for_autoindent_applied().await;
29721 cx.assert_editor_state(indoc! {"
29722 - [ ] task
29723 - [ ] indented
29724 - [ ] ˇ
29725 "});
29726
29727 // Case 6: Adding newline with cursor right after prefix, unindents
29728 cx.set_state(indoc! {"
29729 - [ ] task
29730 - [ ] sub task
29731 - [ ] ˇ
29732 "});
29733 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29734 cx.wait_for_autoindent_applied().await;
29735 cx.assert_editor_state(indoc! {"
29736 - [ ] task
29737 - [ ] sub task
29738 - [ ] ˇ
29739 "});
29740 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29741 cx.wait_for_autoindent_applied().await;
29742
29743 // Case 7: Adding newline with cursor right after prefix, removes marker
29744 cx.assert_editor_state(indoc! {"
29745 - [ ] task
29746 - [ ] sub task
29747 - [ ] ˇ
29748 "});
29749 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29750 cx.wait_for_autoindent_applied().await;
29751 cx.assert_editor_state(indoc! {"
29752 - [ ] task
29753 - [ ] sub task
29754 ˇ
29755 "});
29756
29757 // Case 8: Cursor before or inside prefix does not add marker
29758 cx.set_state(indoc! {"
29759 ˇ- [ ] task
29760 "});
29761 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29762 cx.wait_for_autoindent_applied().await;
29763 cx.assert_editor_state(indoc! {"
29764
29765 ˇ- [ ] task
29766 "});
29767
29768 cx.set_state(indoc! {"
29769 - [ˇ ] task
29770 "});
29771 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29772 cx.wait_for_autoindent_applied().await;
29773 cx.assert_editor_state(indoc! {"
29774 - [
29775 ˇ
29776 ] task
29777 "});
29778}
29779
29780#[gpui::test]
29781async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
29782 init_test(cx, |settings| {
29783 settings.defaults.tab_size = Some(2.try_into().unwrap());
29784 });
29785
29786 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29787 let mut cx = EditorTestContext::new(cx).await;
29788 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29789
29790 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
29791 cx.set_state(indoc! {"
29792 - itemˇ
29793 "});
29794 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29795 cx.wait_for_autoindent_applied().await;
29796 cx.assert_editor_state(indoc! {"
29797 - item
29798 - ˇ
29799 "});
29800
29801 // Case 2: Works with different markers
29802 cx.set_state(indoc! {"
29803 * starred itemˇ
29804 "});
29805 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29806 cx.wait_for_autoindent_applied().await;
29807 cx.assert_editor_state(indoc! {"
29808 * starred item
29809 * ˇ
29810 "});
29811
29812 cx.set_state(indoc! {"
29813 + plus itemˇ
29814 "});
29815 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29816 cx.wait_for_autoindent_applied().await;
29817 cx.assert_editor_state(indoc! {"
29818 + plus item
29819 + ˇ
29820 "});
29821
29822 // Case 3: Cursor position doesn't matter - content after marker is what counts
29823 cx.set_state(indoc! {"
29824 - itˇem
29825 "});
29826 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29827 cx.wait_for_autoindent_applied().await;
29828 cx.assert_editor_state(indoc! {"
29829 - it
29830 - ˇem
29831 "});
29832
29833 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
29834 cx.set_state(indoc! {"
29835 - ˇ
29836 "});
29837 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29838 cx.wait_for_autoindent_applied().await;
29839 cx.assert_editor_state(
29840 indoc! {"
29841 - $
29842 ˇ
29843 "}
29844 .replace("$", " ")
29845 .as_str(),
29846 );
29847
29848 // Case 5: Adding newline with content adds marker preserving indentation
29849 cx.set_state(indoc! {"
29850 - item
29851 - indentedˇ
29852 "});
29853 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29854 cx.wait_for_autoindent_applied().await;
29855 cx.assert_editor_state(indoc! {"
29856 - item
29857 - indented
29858 - ˇ
29859 "});
29860
29861 // Case 6: Adding newline with cursor right after marker, unindents
29862 cx.set_state(indoc! {"
29863 - item
29864 - sub item
29865 - ˇ
29866 "});
29867 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29868 cx.wait_for_autoindent_applied().await;
29869 cx.assert_editor_state(indoc! {"
29870 - item
29871 - sub item
29872 - ˇ
29873 "});
29874 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29875 cx.wait_for_autoindent_applied().await;
29876
29877 // Case 7: Adding newline with cursor right after marker, removes marker
29878 cx.assert_editor_state(indoc! {"
29879 - item
29880 - sub item
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(indoc! {"
29886 - item
29887 - sub item
29888 ˇ
29889 "});
29890
29891 // Case 8: Cursor before or inside prefix does not add marker
29892 cx.set_state(indoc! {"
29893 ˇ- item
29894 "});
29895 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29896 cx.wait_for_autoindent_applied().await;
29897 cx.assert_editor_state(indoc! {"
29898
29899 ˇ- item
29900 "});
29901
29902 cx.set_state(indoc! {"
29903 -ˇ item
29904 "});
29905 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29906 cx.wait_for_autoindent_applied().await;
29907 cx.assert_editor_state(indoc! {"
29908 -
29909 ˇitem
29910 "});
29911}
29912
29913#[gpui::test]
29914async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
29915 init_test(cx, |settings| {
29916 settings.defaults.tab_size = Some(2.try_into().unwrap());
29917 });
29918
29919 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29920 let mut cx = EditorTestContext::new(cx).await;
29921 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29922
29923 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
29924 cx.set_state(indoc! {"
29925 1. first itemˇ
29926 "});
29927 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29928 cx.wait_for_autoindent_applied().await;
29929 cx.assert_editor_state(indoc! {"
29930 1. first item
29931 2. ˇ
29932 "});
29933
29934 // Case 2: Works with larger numbers
29935 cx.set_state(indoc! {"
29936 10. tenth itemˇ
29937 "});
29938 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29939 cx.wait_for_autoindent_applied().await;
29940 cx.assert_editor_state(indoc! {"
29941 10. tenth item
29942 11. ˇ
29943 "});
29944
29945 // Case 3: Cursor position doesn't matter - content after marker is what counts
29946 cx.set_state(indoc! {"
29947 1. itˇem
29948 "});
29949 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29950 cx.wait_for_autoindent_applied().await;
29951 cx.assert_editor_state(indoc! {"
29952 1. it
29953 2. ˇem
29954 "});
29955
29956 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
29957 cx.set_state(indoc! {"
29958 1. ˇ
29959 "});
29960 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29961 cx.wait_for_autoindent_applied().await;
29962 cx.assert_editor_state(
29963 indoc! {"
29964 1. $
29965 ˇ
29966 "}
29967 .replace("$", " ")
29968 .as_str(),
29969 );
29970
29971 // Case 5: Adding newline with content adds marker preserving indentation
29972 cx.set_state(indoc! {"
29973 1. item
29974 2. indentedˇ
29975 "});
29976 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29977 cx.wait_for_autoindent_applied().await;
29978 cx.assert_editor_state(indoc! {"
29979 1. item
29980 2. indented
29981 3. ˇ
29982 "});
29983
29984 // Case 6: Adding newline with cursor right after marker, unindents
29985 cx.set_state(indoc! {"
29986 1. item
29987 2. sub item
29988 3. ˇ
29989 "});
29990 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29991 cx.wait_for_autoindent_applied().await;
29992 cx.assert_editor_state(indoc! {"
29993 1. item
29994 2. sub item
29995 1. ˇ
29996 "});
29997 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29998 cx.wait_for_autoindent_applied().await;
29999
30000 // Case 7: Adding newline with cursor right after marker, removes marker
30001 cx.assert_editor_state(indoc! {"
30002 1. item
30003 2. sub item
30004 1. ˇ
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 1. item
30010 2. sub item
30011 ˇ
30012 "});
30013
30014 // Case 8: Cursor before or inside prefix does not add marker
30015 cx.set_state(indoc! {"
30016 ˇ1. item
30017 "});
30018 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30019 cx.wait_for_autoindent_applied().await;
30020 cx.assert_editor_state(indoc! {"
30021
30022 ˇ1. item
30023 "});
30024
30025 cx.set_state(indoc! {"
30026 1ˇ. item
30027 "});
30028 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30029 cx.wait_for_autoindent_applied().await;
30030 cx.assert_editor_state(indoc! {"
30031 1
30032 ˇ. item
30033 "});
30034}
30035
30036#[gpui::test]
30037async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
30038 init_test(cx, |settings| {
30039 settings.defaults.tab_size = Some(2.try_into().unwrap());
30040 });
30041
30042 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30043 let mut cx = EditorTestContext::new(cx).await;
30044 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30045
30046 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
30047 cx.set_state(indoc! {"
30048 1. first item
30049 1. sub first item
30050 2. sub second item
30051 3. ˇ
30052 "});
30053 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30054 cx.wait_for_autoindent_applied().await;
30055 cx.assert_editor_state(indoc! {"
30056 1. first item
30057 1. sub first item
30058 2. sub second item
30059 1. ˇ
30060 "});
30061}
30062
30063#[gpui::test]
30064async fn test_tab_list_indent(cx: &mut TestAppContext) {
30065 init_test(cx, |settings| {
30066 settings.defaults.tab_size = Some(2.try_into().unwrap());
30067 });
30068
30069 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30070 let mut cx = EditorTestContext::new(cx).await;
30071 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30072
30073 // Case 1: Unordered list - cursor after prefix, adds indent before prefix
30074 cx.set_state(indoc! {"
30075 - ˇitem
30076 "});
30077 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30078 cx.wait_for_autoindent_applied().await;
30079 let expected = indoc! {"
30080 $$- ˇitem
30081 "};
30082 cx.assert_editor_state(expected.replace("$", " ").as_str());
30083
30084 // Case 2: Task list - cursor after prefix
30085 cx.set_state(indoc! {"
30086 - [ ] ˇtask
30087 "});
30088 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30089 cx.wait_for_autoindent_applied().await;
30090 let expected = indoc! {"
30091 $$- [ ] ˇtask
30092 "};
30093 cx.assert_editor_state(expected.replace("$", " ").as_str());
30094
30095 // Case 3: Ordered list - cursor after prefix
30096 cx.set_state(indoc! {"
30097 1. ˇfirst
30098 "});
30099 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30100 cx.wait_for_autoindent_applied().await;
30101 let expected = indoc! {"
30102 $$1. ˇfirst
30103 "};
30104 cx.assert_editor_state(expected.replace("$", " ").as_str());
30105
30106 // Case 4: With existing indentation - adds more indent
30107 let initial = indoc! {"
30108 $$- ˇitem
30109 "};
30110 cx.set_state(initial.replace("$", " ").as_str());
30111 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30112 cx.wait_for_autoindent_applied().await;
30113 let expected = indoc! {"
30114 $$$$- ˇitem
30115 "};
30116 cx.assert_editor_state(expected.replace("$", " ").as_str());
30117
30118 // Case 5: Empty list item
30119 cx.set_state(indoc! {"
30120 - ˇ
30121 "});
30122 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30123 cx.wait_for_autoindent_applied().await;
30124 let expected = indoc! {"
30125 $$- ˇ
30126 "};
30127 cx.assert_editor_state(expected.replace("$", " ").as_str());
30128
30129 // Case 6: Cursor at end of line with content
30130 cx.set_state(indoc! {"
30131 - itemˇ
30132 "});
30133 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30134 cx.wait_for_autoindent_applied().await;
30135 let expected = indoc! {"
30136 $$- itemˇ
30137 "};
30138 cx.assert_editor_state(expected.replace("$", " ").as_str());
30139
30140 // Case 7: Cursor at start of list item, indents it
30141 cx.set_state(indoc! {"
30142 - item
30143 ˇ - sub item
30144 "});
30145 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30146 cx.wait_for_autoindent_applied().await;
30147 let expected = indoc! {"
30148 - item
30149 ˇ - sub item
30150 "};
30151 cx.assert_editor_state(expected);
30152
30153 // Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
30154 cx.update_editor(|_, _, cx| {
30155 SettingsStore::update_global(cx, |store, cx| {
30156 store.update_user_settings(cx, |settings| {
30157 settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
30158 });
30159 });
30160 });
30161 cx.set_state(indoc! {"
30162 - item
30163 ˇ - sub item
30164 "});
30165 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30166 cx.wait_for_autoindent_applied().await;
30167 let expected = indoc! {"
30168 - item
30169 ˇ- sub item
30170 "};
30171 cx.assert_editor_state(expected);
30172}
30173
30174#[gpui::test]
30175async fn test_local_worktree_trust(cx: &mut TestAppContext) {
30176 init_test(cx, |_| {});
30177 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), cx));
30178
30179 cx.update(|cx| {
30180 SettingsStore::update_global(cx, |store, cx| {
30181 store.update_user_settings(cx, |settings| {
30182 settings.project.all_languages.defaults.inlay_hints =
30183 Some(InlayHintSettingsContent {
30184 enabled: Some(true),
30185 ..InlayHintSettingsContent::default()
30186 });
30187 });
30188 });
30189 });
30190
30191 let fs = FakeFs::new(cx.executor());
30192 fs.insert_tree(
30193 path!("/project"),
30194 json!({
30195 ".zed": {
30196 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
30197 },
30198 "main.rs": "fn main() {}"
30199 }),
30200 )
30201 .await;
30202
30203 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
30204 let server_name = "override-rust-analyzer";
30205 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
30206
30207 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
30208 language_registry.add(rust_lang());
30209
30210 let capabilities = lsp::ServerCapabilities {
30211 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
30212 ..lsp::ServerCapabilities::default()
30213 };
30214 let mut fake_language_servers = language_registry.register_fake_lsp(
30215 "Rust",
30216 FakeLspAdapter {
30217 name: server_name,
30218 capabilities,
30219 initializer: Some(Box::new({
30220 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30221 move |fake_server| {
30222 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30223 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
30224 move |_params, _| {
30225 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
30226 async move {
30227 Ok(Some(vec![lsp::InlayHint {
30228 position: lsp::Position::new(0, 0),
30229 label: lsp::InlayHintLabel::String("hint".to_string()),
30230 kind: None,
30231 text_edits: None,
30232 tooltip: None,
30233 padding_left: None,
30234 padding_right: None,
30235 data: None,
30236 }]))
30237 }
30238 },
30239 );
30240 }
30241 })),
30242 ..FakeLspAdapter::default()
30243 },
30244 );
30245
30246 cx.run_until_parked();
30247
30248 let worktree_id = project.read_with(cx, |project, cx| {
30249 project
30250 .worktrees(cx)
30251 .next()
30252 .map(|wt| wt.read(cx).id())
30253 .expect("should have a worktree")
30254 });
30255 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
30256
30257 let trusted_worktrees =
30258 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
30259
30260 let can_trust = trusted_worktrees.update(cx, |store, cx| {
30261 store.can_trust(&worktree_store, worktree_id, cx)
30262 });
30263 assert!(!can_trust, "worktree should be restricted initially");
30264
30265 let buffer_before_approval = project
30266 .update(cx, |project, cx| {
30267 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
30268 })
30269 .await
30270 .unwrap();
30271
30272 let (editor, cx) = cx.add_window_view(|window, cx| {
30273 Editor::new(
30274 EditorMode::full(),
30275 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
30276 Some(project.clone()),
30277 window,
30278 cx,
30279 )
30280 });
30281 cx.run_until_parked();
30282 let fake_language_server = fake_language_servers.next();
30283
30284 cx.read(|cx| {
30285 let file = buffer_before_approval.read(cx).file();
30286 assert_eq!(
30287 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30288 .language_servers,
30289 ["...".to_string()],
30290 "local .zed/settings.json must not apply before trust approval"
30291 )
30292 });
30293
30294 editor.update_in(cx, |editor, window, cx| {
30295 editor.handle_input("1", window, cx);
30296 });
30297 cx.run_until_parked();
30298 cx.executor()
30299 .advance_clock(std::time::Duration::from_secs(1));
30300 assert_eq!(
30301 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
30302 0,
30303 "inlay hints must not be queried before trust approval"
30304 );
30305
30306 trusted_worktrees.update(cx, |store, cx| {
30307 store.trust(
30308 &worktree_store,
30309 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
30310 cx,
30311 );
30312 });
30313 cx.run_until_parked();
30314
30315 cx.read(|cx| {
30316 let file = buffer_before_approval.read(cx).file();
30317 assert_eq!(
30318 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30319 .language_servers,
30320 ["override-rust-analyzer".to_string()],
30321 "local .zed/settings.json should apply after trust approval"
30322 )
30323 });
30324 let _fake_language_server = fake_language_server.await.unwrap();
30325 editor.update_in(cx, |editor, window, cx| {
30326 editor.handle_input("1", window, cx);
30327 });
30328 cx.run_until_parked();
30329 cx.executor()
30330 .advance_clock(std::time::Duration::from_secs(1));
30331 assert!(
30332 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
30333 "inlay hints should be queried after trust approval"
30334 );
30335
30336 let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
30337 store.can_trust(&worktree_store, worktree_id, cx)
30338 });
30339 assert!(can_trust_after, "worktree should be trusted after trust()");
30340}
30341
30342#[gpui::test]
30343fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
30344 // This test reproduces a bug where drawing an editor at a position above the viewport
30345 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
30346 // causes an infinite loop in blocks_in_range.
30347 //
30348 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
30349 // the content mask intersection produces visible_bounds with origin at the viewport top.
30350 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
30351 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
30352 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
30353 init_test(cx, |_| {});
30354
30355 let window = cx.add_window(|_, _| gpui::Empty);
30356 let mut cx = VisualTestContext::from_window(*window, cx);
30357
30358 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
30359 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
30360
30361 // Simulate a small viewport (500x500 pixels at origin 0,0)
30362 cx.simulate_resize(gpui::size(px(500.), px(500.)));
30363
30364 // Draw the editor at a very negative Y position, simulating an editor that's been
30365 // scrolled way above the visible viewport (like in a List that has scrolled past it).
30366 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
30367 // This should NOT hang - it should just render nothing.
30368 cx.draw(
30369 gpui::point(px(0.), px(-10000.)),
30370 gpui::size(px(500.), px(3000.)),
30371 |_, _| editor.clone(),
30372 );
30373
30374 // If we get here without hanging, the test passes
30375}
30376
30377#[gpui::test]
30378async fn test_diff_review_indicator_created_on_gutter_hover(cx: &mut TestAppContext) {
30379 init_test(cx, |_| {});
30380
30381 let fs = FakeFs::new(cx.executor());
30382 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30383 .await;
30384
30385 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30386 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30387 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30388
30389 let editor = workspace
30390 .update(cx, |workspace, window, cx| {
30391 workspace.open_abs_path(
30392 PathBuf::from(path!("/root/file.txt")),
30393 OpenOptions::default(),
30394 window,
30395 cx,
30396 )
30397 })
30398 .unwrap()
30399 .await
30400 .unwrap()
30401 .downcast::<Editor>()
30402 .unwrap();
30403
30404 // Enable diff review button mode
30405 editor.update(cx, |editor, cx| {
30406 editor.set_show_diff_review_button(true, cx);
30407 });
30408
30409 // Initially, no indicator should be present
30410 editor.update(cx, |editor, _cx| {
30411 assert!(
30412 editor.gutter_diff_review_indicator.0.is_none(),
30413 "Indicator should be None initially"
30414 );
30415 });
30416}
30417
30418#[gpui::test]
30419async fn test_diff_review_button_hidden_when_ai_disabled(cx: &mut TestAppContext) {
30420 init_test(cx, |_| {});
30421
30422 // Register DisableAiSettings and set disable_ai to true
30423 cx.update(|cx| {
30424 project::DisableAiSettings::register(cx);
30425 project::DisableAiSettings::override_global(
30426 project::DisableAiSettings { disable_ai: true },
30427 cx,
30428 );
30429 });
30430
30431 let fs = FakeFs::new(cx.executor());
30432 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30433 .await;
30434
30435 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30436 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30437 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30438
30439 let editor = workspace
30440 .update(cx, |workspace, window, cx| {
30441 workspace.open_abs_path(
30442 PathBuf::from(path!("/root/file.txt")),
30443 OpenOptions::default(),
30444 window,
30445 cx,
30446 )
30447 })
30448 .unwrap()
30449 .await
30450 .unwrap()
30451 .downcast::<Editor>()
30452 .unwrap();
30453
30454 // Enable diff review button mode
30455 editor.update(cx, |editor, cx| {
30456 editor.set_show_diff_review_button(true, cx);
30457 });
30458
30459 // Verify AI is disabled
30460 cx.read(|cx| {
30461 assert!(
30462 project::DisableAiSettings::get_global(cx).disable_ai,
30463 "AI should be disabled"
30464 );
30465 });
30466
30467 // The indicator should not be created when AI is disabled
30468 // (The mouse_moved handler checks DisableAiSettings before creating the indicator)
30469 editor.update(cx, |editor, _cx| {
30470 assert!(
30471 editor.gutter_diff_review_indicator.0.is_none(),
30472 "Indicator should be None when AI is disabled"
30473 );
30474 });
30475}
30476
30477#[gpui::test]
30478async fn test_diff_review_button_shown_when_ai_enabled(cx: &mut TestAppContext) {
30479 init_test(cx, |_| {});
30480
30481 // Register DisableAiSettings and set disable_ai to false
30482 cx.update(|cx| {
30483 project::DisableAiSettings::register(cx);
30484 project::DisableAiSettings::override_global(
30485 project::DisableAiSettings { disable_ai: false },
30486 cx,
30487 );
30488 });
30489
30490 let fs = FakeFs::new(cx.executor());
30491 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30492 .await;
30493
30494 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30495 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30496 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30497
30498 let editor = workspace
30499 .update(cx, |workspace, window, cx| {
30500 workspace.open_abs_path(
30501 PathBuf::from(path!("/root/file.txt")),
30502 OpenOptions::default(),
30503 window,
30504 cx,
30505 )
30506 })
30507 .unwrap()
30508 .await
30509 .unwrap()
30510 .downcast::<Editor>()
30511 .unwrap();
30512
30513 // Enable diff review button mode
30514 editor.update(cx, |editor, cx| {
30515 editor.set_show_diff_review_button(true, cx);
30516 });
30517
30518 // Verify AI is enabled
30519 cx.read(|cx| {
30520 assert!(
30521 !project::DisableAiSettings::get_global(cx).disable_ai,
30522 "AI should be enabled"
30523 );
30524 });
30525
30526 // The show_diff_review_button flag should be true
30527 editor.update(cx, |editor, _cx| {
30528 assert!(
30529 editor.show_diff_review_button(),
30530 "show_diff_review_button should be true"
30531 );
30532 });
30533}
30534
30535/// Helper function to create a DiffHunkKey for testing.
30536/// Uses Anchor::min() as a placeholder anchor since these tests don't need
30537/// real buffer positioning.
30538fn test_hunk_key(file_path: &str) -> DiffHunkKey {
30539 DiffHunkKey {
30540 file_path: if file_path.is_empty() {
30541 Arc::from(util::rel_path::RelPath::empty())
30542 } else {
30543 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
30544 },
30545 hunk_start_anchor: Anchor::min(),
30546 }
30547}
30548
30549/// Helper function to create a DiffHunkKey with a specific anchor for testing.
30550fn test_hunk_key_with_anchor(file_path: &str, anchor: Anchor) -> DiffHunkKey {
30551 DiffHunkKey {
30552 file_path: if file_path.is_empty() {
30553 Arc::from(util::rel_path::RelPath::empty())
30554 } else {
30555 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
30556 },
30557 hunk_start_anchor: anchor,
30558 }
30559}
30560
30561/// Helper function to add a review comment with default anchors for testing.
30562fn add_test_comment(
30563 editor: &mut Editor,
30564 key: DiffHunkKey,
30565 comment: &str,
30566 display_row: u32,
30567 cx: &mut Context<Editor>,
30568) -> usize {
30569 editor.add_review_comment(
30570 key,
30571 comment.to_string(),
30572 DisplayRow(display_row),
30573 Anchor::min()..Anchor::max(),
30574 cx,
30575 )
30576}
30577
30578#[gpui::test]
30579fn test_review_comment_add_to_hunk(cx: &mut TestAppContext) {
30580 init_test(cx, |_| {});
30581
30582 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30583
30584 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30585 let key = test_hunk_key("");
30586
30587 let id = add_test_comment(editor, key.clone(), "Test comment", 0, cx);
30588
30589 let snapshot = editor.buffer().read(cx).snapshot(cx);
30590 assert_eq!(editor.total_review_comment_count(), 1);
30591 assert_eq!(editor.hunk_comment_count(&key, &snapshot), 1);
30592
30593 let comments = editor.comments_for_hunk(&key, &snapshot);
30594 assert_eq!(comments.len(), 1);
30595 assert_eq!(comments[0].comment, "Test comment");
30596 assert_eq!(comments[0].id, id);
30597 });
30598}
30599
30600#[gpui::test]
30601fn test_review_comments_are_per_hunk(cx: &mut TestAppContext) {
30602 init_test(cx, |_| {});
30603
30604 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30605
30606 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30607 let snapshot = editor.buffer().read(cx).snapshot(cx);
30608 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
30609 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
30610 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
30611 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
30612
30613 add_test_comment(editor, key1.clone(), "Comment for file1", 0, cx);
30614 add_test_comment(editor, key2.clone(), "Comment for file2", 10, cx);
30615
30616 let snapshot = editor.buffer().read(cx).snapshot(cx);
30617 assert_eq!(editor.total_review_comment_count(), 2);
30618 assert_eq!(editor.hunk_comment_count(&key1, &snapshot), 1);
30619 assert_eq!(editor.hunk_comment_count(&key2, &snapshot), 1);
30620
30621 assert_eq!(
30622 editor.comments_for_hunk(&key1, &snapshot)[0].comment,
30623 "Comment for file1"
30624 );
30625 assert_eq!(
30626 editor.comments_for_hunk(&key2, &snapshot)[0].comment,
30627 "Comment for file2"
30628 );
30629 });
30630}
30631
30632#[gpui::test]
30633fn test_review_comment_remove(cx: &mut TestAppContext) {
30634 init_test(cx, |_| {});
30635
30636 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30637
30638 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30639 let key = test_hunk_key("");
30640
30641 let id = add_test_comment(editor, key, "To be removed", 0, cx);
30642
30643 assert_eq!(editor.total_review_comment_count(), 1);
30644
30645 let removed = editor.remove_review_comment(id, cx);
30646 assert!(removed);
30647 assert_eq!(editor.total_review_comment_count(), 0);
30648
30649 // Try to remove again
30650 let removed_again = editor.remove_review_comment(id, cx);
30651 assert!(!removed_again);
30652 });
30653}
30654
30655#[gpui::test]
30656fn test_review_comment_update(cx: &mut TestAppContext) {
30657 init_test(cx, |_| {});
30658
30659 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30660
30661 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30662 let key = test_hunk_key("");
30663
30664 let id = add_test_comment(editor, key.clone(), "Original text", 0, cx);
30665
30666 let updated = editor.update_review_comment(id, "Updated text".to_string(), cx);
30667 assert!(updated);
30668
30669 let snapshot = editor.buffer().read(cx).snapshot(cx);
30670 let comments = editor.comments_for_hunk(&key, &snapshot);
30671 assert_eq!(comments[0].comment, "Updated text");
30672 assert!(!comments[0].is_editing); // Should clear editing flag
30673 });
30674}
30675
30676#[gpui::test]
30677fn test_review_comment_take_all(cx: &mut TestAppContext) {
30678 init_test(cx, |_| {});
30679
30680 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30681
30682 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30683 let snapshot = editor.buffer().read(cx).snapshot(cx);
30684 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
30685 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
30686 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
30687 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
30688
30689 let id1 = add_test_comment(editor, key1.clone(), "Comment 1", 0, cx);
30690 let id2 = add_test_comment(editor, key1.clone(), "Comment 2", 1, cx);
30691 let id3 = add_test_comment(editor, key2.clone(), "Comment 3", 10, cx);
30692
30693 // IDs should be sequential starting from 0
30694 assert_eq!(id1, 0);
30695 assert_eq!(id2, 1);
30696 assert_eq!(id3, 2);
30697
30698 assert_eq!(editor.total_review_comment_count(), 3);
30699
30700 let taken = editor.take_all_review_comments(cx);
30701
30702 // Should have 2 entries (one per hunk)
30703 assert_eq!(taken.len(), 2);
30704
30705 // Total comments should be 3
30706 let total: usize = taken
30707 .iter()
30708 .map(|(_, comments): &(DiffHunkKey, Vec<StoredReviewComment>)| comments.len())
30709 .sum();
30710 assert_eq!(total, 3);
30711
30712 // Storage should be empty
30713 assert_eq!(editor.total_review_comment_count(), 0);
30714
30715 // After taking all comments, ID counter should reset
30716 // New comments should get IDs starting from 0 again
30717 let new_id1 = add_test_comment(editor, key1, "New Comment 1", 0, cx);
30718 let new_id2 = add_test_comment(editor, key2, "New Comment 2", 0, cx);
30719
30720 assert_eq!(new_id1, 0, "ID counter should reset after take_all");
30721 assert_eq!(new_id2, 1, "IDs should be sequential after reset");
30722 });
30723}
30724
30725#[gpui::test]
30726fn test_diff_review_overlay_show_and_dismiss(cx: &mut TestAppContext) {
30727 init_test(cx, |_| {});
30728
30729 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30730
30731 // Show overlay
30732 editor
30733 .update(cx, |editor, window, cx| {
30734 editor.show_diff_review_overlay(DisplayRow(0), window, cx);
30735 })
30736 .unwrap();
30737
30738 // Verify overlay is shown
30739 editor
30740 .update(cx, |editor, _window, _cx| {
30741 assert!(!editor.diff_review_overlays.is_empty());
30742 assert_eq!(editor.diff_review_display_row(), Some(DisplayRow(0)));
30743 assert!(editor.diff_review_prompt_editor().is_some());
30744 })
30745 .unwrap();
30746
30747 // Dismiss overlay
30748 editor
30749 .update(cx, |editor, _window, cx| {
30750 editor.dismiss_all_diff_review_overlays(cx);
30751 })
30752 .unwrap();
30753
30754 // Verify overlay is dismissed
30755 editor
30756 .update(cx, |editor, _window, _cx| {
30757 assert!(editor.diff_review_overlays.is_empty());
30758 assert_eq!(editor.diff_review_display_row(), None);
30759 assert!(editor.diff_review_prompt_editor().is_none());
30760 })
30761 .unwrap();
30762}
30763
30764#[gpui::test]
30765fn test_diff_review_overlay_dismiss_via_cancel(cx: &mut TestAppContext) {
30766 init_test(cx, |_| {});
30767
30768 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30769
30770 // Show overlay
30771 editor
30772 .update(cx, |editor, window, cx| {
30773 editor.show_diff_review_overlay(DisplayRow(0), window, cx);
30774 })
30775 .unwrap();
30776
30777 // Verify overlay is shown
30778 editor
30779 .update(cx, |editor, _window, _cx| {
30780 assert!(!editor.diff_review_overlays.is_empty());
30781 })
30782 .unwrap();
30783
30784 // Dismiss via dismiss_menus_and_popups (which is called by cancel action)
30785 editor
30786 .update(cx, |editor, window, cx| {
30787 editor.dismiss_menus_and_popups(true, window, cx);
30788 })
30789 .unwrap();
30790
30791 // Verify overlay is dismissed
30792 editor
30793 .update(cx, |editor, _window, _cx| {
30794 assert!(editor.diff_review_overlays.is_empty());
30795 })
30796 .unwrap();
30797}
30798
30799#[gpui::test]
30800fn test_diff_review_empty_comment_not_submitted(cx: &mut TestAppContext) {
30801 init_test(cx, |_| {});
30802
30803 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30804
30805 // Show overlay
30806 editor
30807 .update(cx, |editor, window, cx| {
30808 editor.show_diff_review_overlay(DisplayRow(0), window, cx);
30809 })
30810 .unwrap();
30811
30812 // Try to submit without typing anything (empty comment)
30813 editor
30814 .update(cx, |editor, window, cx| {
30815 editor.submit_diff_review_comment(window, cx);
30816 })
30817 .unwrap();
30818
30819 // Verify no comment was added
30820 editor
30821 .update(cx, |editor, _window, _cx| {
30822 assert_eq!(editor.total_review_comment_count(), 0);
30823 })
30824 .unwrap();
30825
30826 // Try to submit with whitespace-only comment
30827 editor
30828 .update(cx, |editor, window, cx| {
30829 if let Some(prompt_editor) = editor.diff_review_prompt_editor().cloned() {
30830 prompt_editor.update(cx, |pe, cx| {
30831 pe.insert(" \n\t ", window, cx);
30832 });
30833 }
30834 editor.submit_diff_review_comment(window, cx);
30835 })
30836 .unwrap();
30837
30838 // Verify still no comment was added
30839 editor
30840 .update(cx, |editor, _window, _cx| {
30841 assert_eq!(editor.total_review_comment_count(), 0);
30842 })
30843 .unwrap();
30844}
30845
30846#[gpui::test]
30847fn test_diff_review_inline_edit_flow(cx: &mut TestAppContext) {
30848 init_test(cx, |_| {});
30849
30850 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30851
30852 // Add a comment directly
30853 let comment_id = editor
30854 .update(cx, |editor, _window, cx| {
30855 let key = test_hunk_key("");
30856 add_test_comment(editor, key, "Original comment", 0, cx)
30857 })
30858 .unwrap();
30859
30860 // Set comment to editing mode
30861 editor
30862 .update(cx, |editor, _window, cx| {
30863 editor.set_comment_editing(comment_id, true, cx);
30864 })
30865 .unwrap();
30866
30867 // Verify editing flag is set
30868 editor
30869 .update(cx, |editor, _window, cx| {
30870 let key = test_hunk_key("");
30871 let snapshot = editor.buffer().read(cx).snapshot(cx);
30872 let comments = editor.comments_for_hunk(&key, &snapshot);
30873 assert_eq!(comments.len(), 1);
30874 assert!(comments[0].is_editing);
30875 })
30876 .unwrap();
30877
30878 // Update the comment
30879 editor
30880 .update(cx, |editor, _window, cx| {
30881 let updated =
30882 editor.update_review_comment(comment_id, "Updated comment".to_string(), cx);
30883 assert!(updated);
30884 })
30885 .unwrap();
30886
30887 // Verify comment was updated and editing flag is cleared
30888 editor
30889 .update(cx, |editor, _window, cx| {
30890 let key = test_hunk_key("");
30891 let snapshot = editor.buffer().read(cx).snapshot(cx);
30892 let comments = editor.comments_for_hunk(&key, &snapshot);
30893 assert_eq!(comments[0].comment, "Updated comment");
30894 assert!(!comments[0].is_editing);
30895 })
30896 .unwrap();
30897}
30898
30899#[gpui::test]
30900fn test_orphaned_comments_are_cleaned_up(cx: &mut TestAppContext) {
30901 init_test(cx, |_| {});
30902
30903 // Create an editor with some text
30904 let editor = cx.add_window(|window, cx| {
30905 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
30906 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
30907 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
30908 });
30909
30910 // Add a comment with an anchor on line 2
30911 editor
30912 .update(cx, |editor, _window, cx| {
30913 let snapshot = editor.buffer().read(cx).snapshot(cx);
30914 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
30915 let key = DiffHunkKey {
30916 file_path: Arc::from(util::rel_path::RelPath::empty()),
30917 hunk_start_anchor: anchor,
30918 };
30919 editor.add_review_comment(
30920 key,
30921 "Comment on line 2".to_string(),
30922 DisplayRow(1),
30923 anchor..anchor,
30924 cx,
30925 );
30926 assert_eq!(editor.total_review_comment_count(), 1);
30927 })
30928 .unwrap();
30929
30930 // Delete all content (this should orphan the comment's anchor)
30931 editor
30932 .update(cx, |editor, window, cx| {
30933 editor.select_all(&SelectAll, window, cx);
30934 editor.insert("completely new content", window, cx);
30935 })
30936 .unwrap();
30937
30938 // Trigger cleanup
30939 editor
30940 .update(cx, |editor, _window, cx| {
30941 editor.cleanup_orphaned_review_comments(cx);
30942 // Comment should be removed because its anchor is invalid
30943 assert_eq!(editor.total_review_comment_count(), 0);
30944 })
30945 .unwrap();
30946}
30947
30948#[gpui::test]
30949fn test_orphaned_comments_cleanup_called_on_buffer_edit(cx: &mut TestAppContext) {
30950 init_test(cx, |_| {});
30951
30952 // Create an editor with some text
30953 let editor = cx.add_window(|window, cx| {
30954 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
30955 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
30956 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
30957 });
30958
30959 // Add a comment with an anchor on line 2
30960 editor
30961 .update(cx, |editor, _window, cx| {
30962 let snapshot = editor.buffer().read(cx).snapshot(cx);
30963 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
30964 let key = DiffHunkKey {
30965 file_path: Arc::from(util::rel_path::RelPath::empty()),
30966 hunk_start_anchor: anchor,
30967 };
30968 editor.add_review_comment(
30969 key,
30970 "Comment on line 2".to_string(),
30971 DisplayRow(1),
30972 anchor..anchor,
30973 cx,
30974 );
30975 assert_eq!(editor.total_review_comment_count(), 1);
30976 })
30977 .unwrap();
30978
30979 // Edit the buffer - this should trigger cleanup via on_buffer_event
30980 // Delete all content which orphans the anchor
30981 editor
30982 .update(cx, |editor, window, cx| {
30983 editor.select_all(&SelectAll, window, cx);
30984 editor.insert("completely new content", window, cx);
30985 // The cleanup is called automatically in on_buffer_event when Edited fires
30986 })
30987 .unwrap();
30988
30989 // Verify cleanup happened automatically (not manually triggered)
30990 editor
30991 .update(cx, |editor, _window, _cx| {
30992 // Comment should be removed because its anchor became invalid
30993 // and cleanup was called automatically on buffer edit
30994 assert_eq!(editor.total_review_comment_count(), 0);
30995 })
30996 .unwrap();
30997}
30998
30999#[gpui::test]
31000fn test_comments_stored_for_multiple_hunks(cx: &mut TestAppContext) {
31001 init_test(cx, |_| {});
31002
31003 // This test verifies that comments can be stored for multiple different hunks
31004 // and that hunk_comment_count correctly identifies comments per hunk.
31005 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31006
31007 _ = editor.update(cx, |editor, _window, cx| {
31008 let snapshot = editor.buffer().read(cx).snapshot(cx);
31009
31010 // Create two different hunk keys (simulating two different files)
31011 let anchor = snapshot.anchor_before(Point::new(0, 0));
31012 let key1 = DiffHunkKey {
31013 file_path: Arc::from(util::rel_path::RelPath::unix("file1.rs").unwrap()),
31014 hunk_start_anchor: anchor,
31015 };
31016 let key2 = DiffHunkKey {
31017 file_path: Arc::from(util::rel_path::RelPath::unix("file2.rs").unwrap()),
31018 hunk_start_anchor: anchor,
31019 };
31020
31021 // Add comments to first hunk
31022 editor.add_review_comment(
31023 key1.clone(),
31024 "Comment 1 for file1".to_string(),
31025 DisplayRow(0),
31026 anchor..anchor,
31027 cx,
31028 );
31029 editor.add_review_comment(
31030 key1.clone(),
31031 "Comment 2 for file1".to_string(),
31032 DisplayRow(1),
31033 anchor..anchor,
31034 cx,
31035 );
31036
31037 // Add comment to second hunk
31038 editor.add_review_comment(
31039 key2.clone(),
31040 "Comment for file2".to_string(),
31041 DisplayRow(0),
31042 anchor..anchor,
31043 cx,
31044 );
31045
31046 // Verify total count
31047 assert_eq!(editor.total_review_comment_count(), 3);
31048
31049 // Verify per-hunk counts
31050 let snapshot = editor.buffer().read(cx).snapshot(cx);
31051 assert_eq!(
31052 editor.hunk_comment_count(&key1, &snapshot),
31053 2,
31054 "file1 should have 2 comments"
31055 );
31056 assert_eq!(
31057 editor.hunk_comment_count(&key2, &snapshot),
31058 1,
31059 "file2 should have 1 comment"
31060 );
31061
31062 // Verify comments_for_hunk returns correct comments
31063 let file1_comments = editor.comments_for_hunk(&key1, &snapshot);
31064 assert_eq!(file1_comments.len(), 2);
31065 assert_eq!(file1_comments[0].comment, "Comment 1 for file1");
31066 assert_eq!(file1_comments[1].comment, "Comment 2 for file1");
31067
31068 let file2_comments = editor.comments_for_hunk(&key2, &snapshot);
31069 assert_eq!(file2_comments.len(), 1);
31070 assert_eq!(file2_comments[0].comment, "Comment for file2");
31071 });
31072}
31073
31074#[gpui::test]
31075fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
31076 init_test(cx, |_| {});
31077
31078 // This test verifies that hunk_keys_match correctly identifies when two
31079 // DiffHunkKeys refer to the same hunk (same file path and anchor point).
31080 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31081
31082 _ = editor.update(cx, |editor, _window, cx| {
31083 let snapshot = editor.buffer().read(cx).snapshot(cx);
31084 let anchor = snapshot.anchor_before(Point::new(0, 0));
31085
31086 // Create two keys with the same file path and anchor
31087 let key1 = DiffHunkKey {
31088 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
31089 hunk_start_anchor: anchor,
31090 };
31091 let key2 = DiffHunkKey {
31092 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
31093 hunk_start_anchor: anchor,
31094 };
31095
31096 // Add comment to first key
31097 editor.add_review_comment(
31098 key1,
31099 "Test comment".to_string(),
31100 DisplayRow(0),
31101 anchor..anchor,
31102 cx,
31103 );
31104
31105 // Verify second key (same hunk) finds the comment
31106 let snapshot = editor.buffer().read(cx).snapshot(cx);
31107 assert_eq!(
31108 editor.hunk_comment_count(&key2, &snapshot),
31109 1,
31110 "Same hunk should find the comment"
31111 );
31112
31113 // Create a key with different file path
31114 let different_file_key = DiffHunkKey {
31115 file_path: Arc::from(util::rel_path::RelPath::unix("other.rs").unwrap()),
31116 hunk_start_anchor: anchor,
31117 };
31118
31119 // Different file should not find the comment
31120 assert_eq!(
31121 editor.hunk_comment_count(&different_file_key, &snapshot),
31122 0,
31123 "Different file should not find the comment"
31124 );
31125 });
31126}
31127
31128#[gpui::test]
31129fn test_overlay_comments_expanded_state(cx: &mut TestAppContext) {
31130 init_test(cx, |_| {});
31131
31132 // This test verifies that set_diff_review_comments_expanded correctly
31133 // updates the expanded state of overlays.
31134 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31135
31136 // Show overlay
31137 editor
31138 .update(cx, |editor, window, cx| {
31139 editor.show_diff_review_overlay(DisplayRow(0), window, cx);
31140 })
31141 .unwrap();
31142
31143 // Verify initially expanded (default)
31144 editor
31145 .update(cx, |editor, _window, _cx| {
31146 assert!(
31147 editor.diff_review_overlays[0].comments_expanded,
31148 "Should be expanded by default"
31149 );
31150 })
31151 .unwrap();
31152
31153 // Set to collapsed using the public method
31154 editor
31155 .update(cx, |editor, _window, cx| {
31156 editor.set_diff_review_comments_expanded(false, cx);
31157 })
31158 .unwrap();
31159
31160 // Verify collapsed
31161 editor
31162 .update(cx, |editor, _window, _cx| {
31163 assert!(
31164 !editor.diff_review_overlays[0].comments_expanded,
31165 "Should be collapsed after setting to false"
31166 );
31167 })
31168 .unwrap();
31169
31170 // Set back to expanded
31171 editor
31172 .update(cx, |editor, _window, cx| {
31173 editor.set_diff_review_comments_expanded(true, cx);
31174 })
31175 .unwrap();
31176
31177 // Verify expanded again
31178 editor
31179 .update(cx, |editor, _window, _cx| {
31180 assert!(
31181 editor.diff_review_overlays[0].comments_expanded,
31182 "Should be expanded after setting to true"
31183 );
31184 })
31185 .unwrap();
31186}
31187
31188#[gpui::test]
31189fn test_calculate_overlay_height(cx: &mut TestAppContext) {
31190 init_test(cx, |_| {});
31191
31192 // This test verifies that calculate_overlay_height returns correct heights
31193 // based on comment count and expanded state.
31194 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31195
31196 _ = editor.update(cx, |editor, _window, cx| {
31197 let snapshot = editor.buffer().read(cx).snapshot(cx);
31198 let anchor = snapshot.anchor_before(Point::new(0, 0));
31199 let key = DiffHunkKey {
31200 file_path: Arc::from(util::rel_path::RelPath::empty()),
31201 hunk_start_anchor: anchor,
31202 };
31203
31204 // No comments: base height of 2
31205 let height_no_comments = editor.calculate_overlay_height(&key, true, &snapshot);
31206 assert_eq!(
31207 height_no_comments, 2,
31208 "Base height should be 2 with no comments"
31209 );
31210
31211 // Add one comment
31212 editor.add_review_comment(
31213 key.clone(),
31214 "Comment 1".to_string(),
31215 DisplayRow(0),
31216 anchor..anchor,
31217 cx,
31218 );
31219
31220 let snapshot = editor.buffer().read(cx).snapshot(cx);
31221
31222 // With comments expanded: base (2) + header (1) + 2 per comment
31223 let height_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
31224 assert_eq!(
31225 height_expanded,
31226 2 + 1 + 2, // base + header + 1 comment * 2
31227 "Height with 1 comment expanded"
31228 );
31229
31230 // With comments collapsed: base (2) + header (1)
31231 let height_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
31232 assert_eq!(
31233 height_collapsed,
31234 2 + 1, // base + header only
31235 "Height with comments collapsed"
31236 );
31237
31238 // Add more comments
31239 editor.add_review_comment(
31240 key.clone(),
31241 "Comment 2".to_string(),
31242 DisplayRow(0),
31243 anchor..anchor,
31244 cx,
31245 );
31246 editor.add_review_comment(
31247 key.clone(),
31248 "Comment 3".to_string(),
31249 DisplayRow(0),
31250 anchor..anchor,
31251 cx,
31252 );
31253
31254 let snapshot = editor.buffer().read(cx).snapshot(cx);
31255
31256 // With 3 comments expanded
31257 let height_3_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
31258 assert_eq!(
31259 height_3_expanded,
31260 2 + 1 + (3 * 2), // base + header + 3 comments * 2
31261 "Height with 3 comments expanded"
31262 );
31263
31264 // Collapsed height stays the same regardless of comment count
31265 let height_3_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
31266 assert_eq!(
31267 height_3_collapsed,
31268 2 + 1, // base + header only
31269 "Height with 3 comments collapsed should be same as 1 comment collapsed"
31270 );
31271 });
31272}
31273
31274#[gpui::test]
31275async fn test_move_to_start_end_of_larger_syntax_node_single_cursor(cx: &mut TestAppContext) {
31276 init_test(cx, |_| {});
31277
31278 let language = Arc::new(Language::new(
31279 LanguageConfig::default(),
31280 Some(tree_sitter_rust::LANGUAGE.into()),
31281 ));
31282
31283 let text = r#"
31284 fn main() {
31285 let x = foo(1, 2);
31286 }
31287 "#
31288 .unindent();
31289
31290 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
31291 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31292 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
31293
31294 editor
31295 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
31296 .await;
31297
31298 // Test case 1: Move to end of syntax nodes
31299 editor.update_in(cx, |editor, window, cx| {
31300 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31301 s.select_display_ranges([
31302 DisplayPoint::new(DisplayRow(1), 16)..DisplayPoint::new(DisplayRow(1), 16)
31303 ]);
31304 });
31305 });
31306 editor.update(cx, |editor, cx| {
31307 assert_text_with_selections(
31308 editor,
31309 indoc! {r#"
31310 fn main() {
31311 let x = foo(ˇ1, 2);
31312 }
31313 "#},
31314 cx,
31315 );
31316 });
31317 editor.update_in(cx, |editor, window, cx| {
31318 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31319 });
31320 editor.update(cx, |editor, cx| {
31321 assert_text_with_selections(
31322 editor,
31323 indoc! {r#"
31324 fn main() {
31325 let x = foo(1ˇ, 2);
31326 }
31327 "#},
31328 cx,
31329 );
31330 });
31331 editor.update_in(cx, |editor, window, cx| {
31332 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31333 });
31334 editor.update(cx, |editor, cx| {
31335 assert_text_with_selections(
31336 editor,
31337 indoc! {r#"
31338 fn main() {
31339 let x = foo(1, 2)ˇ;
31340 }
31341 "#},
31342 cx,
31343 );
31344 });
31345 editor.update_in(cx, |editor, window, cx| {
31346 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31347 });
31348 editor.update(cx, |editor, cx| {
31349 assert_text_with_selections(
31350 editor,
31351 indoc! {r#"
31352 fn main() {
31353 let x = foo(1, 2);ˇ
31354 }
31355 "#},
31356 cx,
31357 );
31358 });
31359 editor.update_in(cx, |editor, window, cx| {
31360 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31361 });
31362 editor.update(cx, |editor, cx| {
31363 assert_text_with_selections(
31364 editor,
31365 indoc! {r#"
31366 fn main() {
31367 let x = foo(1, 2);
31368 }ˇ
31369 "#},
31370 cx,
31371 );
31372 });
31373
31374 // Test case 2: Move to start of syntax nodes
31375 editor.update_in(cx, |editor, window, cx| {
31376 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31377 s.select_display_ranges([
31378 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20)
31379 ]);
31380 });
31381 });
31382 editor.update(cx, |editor, cx| {
31383 assert_text_with_selections(
31384 editor,
31385 indoc! {r#"
31386 fn main() {
31387 let x = foo(1, 2ˇ);
31388 }
31389 "#},
31390 cx,
31391 );
31392 });
31393 editor.update_in(cx, |editor, window, cx| {
31394 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31395 });
31396 editor.update(cx, |editor, cx| {
31397 assert_text_with_selections(
31398 editor,
31399 indoc! {r#"
31400 fn main() {
31401 let x = fooˇ(1, 2);
31402 }
31403 "#},
31404 cx,
31405 );
31406 });
31407 editor.update_in(cx, |editor, window, cx| {
31408 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31409 });
31410 editor.update(cx, |editor, cx| {
31411 assert_text_with_selections(
31412 editor,
31413 indoc! {r#"
31414 fn main() {
31415 let x = ˇfoo(1, 2);
31416 }
31417 "#},
31418 cx,
31419 );
31420 });
31421 editor.update_in(cx, |editor, window, cx| {
31422 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31423 });
31424 editor.update(cx, |editor, cx| {
31425 assert_text_with_selections(
31426 editor,
31427 indoc! {r#"
31428 fn main() {
31429 ˇlet x = foo(1, 2);
31430 }
31431 "#},
31432 cx,
31433 );
31434 });
31435 editor.update_in(cx, |editor, window, cx| {
31436 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31437 });
31438 editor.update(cx, |editor, cx| {
31439 assert_text_with_selections(
31440 editor,
31441 indoc! {r#"
31442 fn main() ˇ{
31443 let x = foo(1, 2);
31444 }
31445 "#},
31446 cx,
31447 );
31448 });
31449 editor.update_in(cx, |editor, window, cx| {
31450 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31451 });
31452 editor.update(cx, |editor, cx| {
31453 assert_text_with_selections(
31454 editor,
31455 indoc! {r#"
31456 ˇfn main() {
31457 let x = foo(1, 2);
31458 }
31459 "#},
31460 cx,
31461 );
31462 });
31463}
31464
31465#[gpui::test]
31466async fn test_move_to_start_end_of_larger_syntax_node_two_cursors(cx: &mut TestAppContext) {
31467 init_test(cx, |_| {});
31468
31469 let language = Arc::new(Language::new(
31470 LanguageConfig::default(),
31471 Some(tree_sitter_rust::LANGUAGE.into()),
31472 ));
31473
31474 let text = r#"
31475 fn main() {
31476 let x = foo(1, 2);
31477 let y = bar(3, 4);
31478 }
31479 "#
31480 .unindent();
31481
31482 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
31483 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31484 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
31485
31486 editor
31487 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
31488 .await;
31489
31490 // Test case 1: Move to end of syntax nodes with two cursors
31491 editor.update_in(cx, |editor, window, cx| {
31492 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31493 s.select_display_ranges([
31494 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20),
31495 DisplayPoint::new(DisplayRow(2), 20)..DisplayPoint::new(DisplayRow(2), 20),
31496 ]);
31497 });
31498 });
31499 editor.update(cx, |editor, cx| {
31500 assert_text_with_selections(
31501 editor,
31502 indoc! {r#"
31503 fn main() {
31504 let x = foo(1, 2ˇ);
31505 let y = bar(3, 4ˇ);
31506 }
31507 "#},
31508 cx,
31509 );
31510 });
31511 editor.update_in(cx, |editor, window, cx| {
31512 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31513 });
31514 editor.update(cx, |editor, cx| {
31515 assert_text_with_selections(
31516 editor,
31517 indoc! {r#"
31518 fn main() {
31519 let x = foo(1, 2)ˇ;
31520 let y = bar(3, 4)ˇ;
31521 }
31522 "#},
31523 cx,
31524 );
31525 });
31526 editor.update_in(cx, |editor, window, cx| {
31527 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31528 });
31529 editor.update(cx, |editor, cx| {
31530 assert_text_with_selections(
31531 editor,
31532 indoc! {r#"
31533 fn main() {
31534 let x = foo(1, 2);ˇ
31535 let y = bar(3, 4);ˇ
31536 }
31537 "#},
31538 cx,
31539 );
31540 });
31541
31542 // Test case 2: Move to start of syntax nodes with two cursors
31543 editor.update_in(cx, |editor, window, cx| {
31544 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31545 s.select_display_ranges([
31546 DisplayPoint::new(DisplayRow(1), 19)..DisplayPoint::new(DisplayRow(1), 19),
31547 DisplayPoint::new(DisplayRow(2), 19)..DisplayPoint::new(DisplayRow(2), 19),
31548 ]);
31549 });
31550 });
31551 editor.update(cx, |editor, cx| {
31552 assert_text_with_selections(
31553 editor,
31554 indoc! {r#"
31555 fn main() {
31556 let x = foo(1, ˇ2);
31557 let y = bar(3, ˇ4);
31558 }
31559 "#},
31560 cx,
31561 );
31562 });
31563 editor.update_in(cx, |editor, window, cx| {
31564 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31565 });
31566 editor.update(cx, |editor, cx| {
31567 assert_text_with_selections(
31568 editor,
31569 indoc! {r#"
31570 fn main() {
31571 let x = fooˇ(1, 2);
31572 let y = barˇ(3, 4);
31573 }
31574 "#},
31575 cx,
31576 );
31577 });
31578 editor.update_in(cx, |editor, window, cx| {
31579 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31580 });
31581 editor.update(cx, |editor, cx| {
31582 assert_text_with_selections(
31583 editor,
31584 indoc! {r#"
31585 fn main() {
31586 let x = ˇfoo(1, 2);
31587 let y = ˇbar(3, 4);
31588 }
31589 "#},
31590 cx,
31591 );
31592 });
31593 editor.update_in(cx, |editor, window, cx| {
31594 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31595 });
31596 editor.update(cx, |editor, cx| {
31597 assert_text_with_selections(
31598 editor,
31599 indoc! {r#"
31600 fn main() {
31601 ˇlet x = foo(1, 2);
31602 ˇlet y = bar(3, 4);
31603 }
31604 "#},
31605 cx,
31606 );
31607 });
31608}
31609
31610#[gpui::test]
31611async fn test_move_to_start_end_of_larger_syntax_node_with_selections_and_strings(
31612 cx: &mut TestAppContext,
31613) {
31614 init_test(cx, |_| {});
31615
31616 let language = Arc::new(Language::new(
31617 LanguageConfig::default(),
31618 Some(tree_sitter_rust::LANGUAGE.into()),
31619 ));
31620
31621 let text = r#"
31622 fn main() {
31623 let x = foo(1, 2);
31624 let msg = "hello world";
31625 }
31626 "#
31627 .unindent();
31628
31629 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
31630 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31631 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
31632
31633 editor
31634 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
31635 .await;
31636
31637 // Test case 1: With existing selection, move_to_end keeps selection
31638 editor.update_in(cx, |editor, window, cx| {
31639 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31640 s.select_display_ranges([
31641 DisplayPoint::new(DisplayRow(1), 12)..DisplayPoint::new(DisplayRow(1), 21)
31642 ]);
31643 });
31644 });
31645 editor.update(cx, |editor, cx| {
31646 assert_text_with_selections(
31647 editor,
31648 indoc! {r#"
31649 fn main() {
31650 let x = «foo(1, 2)ˇ»;
31651 let msg = "hello world";
31652 }
31653 "#},
31654 cx,
31655 );
31656 });
31657 editor.update_in(cx, |editor, window, cx| {
31658 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31659 });
31660 editor.update(cx, |editor, cx| {
31661 assert_text_with_selections(
31662 editor,
31663 indoc! {r#"
31664 fn main() {
31665 let x = «foo(1, 2)ˇ»;
31666 let msg = "hello world";
31667 }
31668 "#},
31669 cx,
31670 );
31671 });
31672
31673 // Test case 2: Move to end within a string
31674 editor.update_in(cx, |editor, window, cx| {
31675 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31676 s.select_display_ranges([
31677 DisplayPoint::new(DisplayRow(2), 15)..DisplayPoint::new(DisplayRow(2), 15)
31678 ]);
31679 });
31680 });
31681 editor.update(cx, |editor, cx| {
31682 assert_text_with_selections(
31683 editor,
31684 indoc! {r#"
31685 fn main() {
31686 let x = foo(1, 2);
31687 let msg = "ˇhello world";
31688 }
31689 "#},
31690 cx,
31691 );
31692 });
31693 editor.update_in(cx, |editor, window, cx| {
31694 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31695 });
31696 editor.update(cx, |editor, cx| {
31697 assert_text_with_selections(
31698 editor,
31699 indoc! {r#"
31700 fn main() {
31701 let x = foo(1, 2);
31702 let msg = "hello worldˇ";
31703 }
31704 "#},
31705 cx,
31706 );
31707 });
31708
31709 // Test case 3: Move to start within a string
31710 editor.update_in(cx, |editor, window, cx| {
31711 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31712 s.select_display_ranges([
31713 DisplayPoint::new(DisplayRow(2), 21)..DisplayPoint::new(DisplayRow(2), 21)
31714 ]);
31715 });
31716 });
31717 editor.update(cx, |editor, cx| {
31718 assert_text_with_selections(
31719 editor,
31720 indoc! {r#"
31721 fn main() {
31722 let x = foo(1, 2);
31723 let msg = "hello ˇworld";
31724 }
31725 "#},
31726 cx,
31727 );
31728 });
31729 editor.update_in(cx, |editor, window, cx| {
31730 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31731 });
31732 editor.update(cx, |editor, cx| {
31733 assert_text_with_selections(
31734 editor,
31735 indoc! {r#"
31736 fn main() {
31737 let x = foo(1, 2);
31738 let msg = "ˇhello world";
31739 }
31740 "#},
31741 cx,
31742 );
31743 });
31744}