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 Box::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
8369#[gpui::test]
8370async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8371 init_test(cx, |_| {});
8372 let mut cx = EditorTestContext::new(cx).await;
8373
8374 cx.set_state(indoc!(
8375 r#"line onˇe
8376 liˇne two
8377 line three
8378 line four"#
8379 ));
8380
8381 cx.update_editor(|editor, window, cx| {
8382 editor.add_selection_below(&Default::default(), window, cx);
8383 });
8384
8385 // test multiple cursors expand in the same direction
8386 cx.assert_editor_state(indoc!(
8387 r#"line onˇe
8388 liˇne twˇo
8389 liˇne three
8390 line four"#
8391 ));
8392
8393 cx.update_editor(|editor, window, cx| {
8394 editor.add_selection_below(&Default::default(), window, cx);
8395 });
8396
8397 cx.update_editor(|editor, window, cx| {
8398 editor.add_selection_below(&Default::default(), window, cx);
8399 });
8400
8401 // test multiple cursors expand below overflow
8402 cx.assert_editor_state(indoc!(
8403 r#"line onˇe
8404 liˇne twˇo
8405 liˇne thˇree
8406 liˇne foˇur"#
8407 ));
8408
8409 cx.update_editor(|editor, window, cx| {
8410 editor.add_selection_above(&Default::default(), window, cx);
8411 });
8412
8413 // test multiple cursors retrieves back correctly
8414 cx.assert_editor_state(indoc!(
8415 r#"line onˇe
8416 liˇne twˇo
8417 liˇne thˇree
8418 line four"#
8419 ));
8420
8421 cx.update_editor(|editor, window, cx| {
8422 editor.add_selection_above(&Default::default(), window, cx);
8423 });
8424
8425 cx.update_editor(|editor, window, cx| {
8426 editor.add_selection_above(&Default::default(), window, cx);
8427 });
8428
8429 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8430 cx.assert_editor_state(indoc!(
8431 r#"liˇne onˇe
8432 liˇne two
8433 line three
8434 line four"#
8435 ));
8436
8437 cx.update_editor(|editor, window, cx| {
8438 editor.undo_selection(&Default::default(), window, cx);
8439 });
8440
8441 // test undo
8442 cx.assert_editor_state(indoc!(
8443 r#"line onˇe
8444 liˇne twˇo
8445 line three
8446 line four"#
8447 ));
8448
8449 cx.update_editor(|editor, window, cx| {
8450 editor.redo_selection(&Default::default(), window, cx);
8451 });
8452
8453 // test redo
8454 cx.assert_editor_state(indoc!(
8455 r#"liˇne onˇe
8456 liˇne two
8457 line three
8458 line four"#
8459 ));
8460
8461 cx.set_state(indoc!(
8462 r#"abcd
8463 ef«ghˇ»
8464 ijkl
8465 «mˇ»nop"#
8466 ));
8467
8468 cx.update_editor(|editor, window, cx| {
8469 editor.add_selection_above(&Default::default(), window, cx);
8470 });
8471
8472 // test multiple selections expand in the same direction
8473 cx.assert_editor_state(indoc!(
8474 r#"ab«cdˇ»
8475 ef«ghˇ»
8476 «iˇ»jkl
8477 «mˇ»nop"#
8478 ));
8479
8480 cx.update_editor(|editor, window, cx| {
8481 editor.add_selection_above(&Default::default(), window, cx);
8482 });
8483
8484 // test multiple selection upward overflow
8485 cx.assert_editor_state(indoc!(
8486 r#"ab«cdˇ»
8487 «eˇ»f«ghˇ»
8488 «iˇ»jkl
8489 «mˇ»nop"#
8490 ));
8491
8492 cx.update_editor(|editor, window, cx| {
8493 editor.add_selection_below(&Default::default(), window, cx);
8494 });
8495
8496 // test multiple selection retrieves back correctly
8497 cx.assert_editor_state(indoc!(
8498 r#"abcd
8499 ef«ghˇ»
8500 «iˇ»jkl
8501 «mˇ»nop"#
8502 ));
8503
8504 cx.update_editor(|editor, window, cx| {
8505 editor.add_selection_below(&Default::default(), window, cx);
8506 });
8507
8508 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8509 cx.assert_editor_state(indoc!(
8510 r#"abcd
8511 ef«ghˇ»
8512 ij«klˇ»
8513 «mˇ»nop"#
8514 ));
8515
8516 cx.update_editor(|editor, window, cx| {
8517 editor.undo_selection(&Default::default(), window, cx);
8518 });
8519
8520 // test undo
8521 cx.assert_editor_state(indoc!(
8522 r#"abcd
8523 ef«ghˇ»
8524 «iˇ»jkl
8525 «mˇ»nop"#
8526 ));
8527
8528 cx.update_editor(|editor, window, cx| {
8529 editor.redo_selection(&Default::default(), window, cx);
8530 });
8531
8532 // test redo
8533 cx.assert_editor_state(indoc!(
8534 r#"abcd
8535 ef«ghˇ»
8536 ij«klˇ»
8537 «mˇ»nop"#
8538 ));
8539}
8540
8541#[gpui::test]
8542async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8543 init_test(cx, |_| {});
8544 let mut cx = EditorTestContext::new(cx).await;
8545
8546 cx.set_state(indoc!(
8547 r#"line onˇe
8548 liˇne two
8549 line three
8550 line four"#
8551 ));
8552
8553 cx.update_editor(|editor, window, cx| {
8554 editor.add_selection_below(&Default::default(), window, cx);
8555 editor.add_selection_below(&Default::default(), window, cx);
8556 editor.add_selection_below(&Default::default(), window, cx);
8557 });
8558
8559 // initial state with two multi cursor groups
8560 cx.assert_editor_state(indoc!(
8561 r#"line onˇe
8562 liˇne twˇo
8563 liˇne thˇree
8564 liˇne foˇur"#
8565 ));
8566
8567 // add single cursor in middle - simulate opt click
8568 cx.update_editor(|editor, window, cx| {
8569 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8570 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8571 editor.end_selection(window, cx);
8572 });
8573
8574 cx.assert_editor_state(indoc!(
8575 r#"line onˇe
8576 liˇne twˇo
8577 liˇneˇ thˇree
8578 liˇne foˇur"#
8579 ));
8580
8581 cx.update_editor(|editor, window, cx| {
8582 editor.add_selection_above(&Default::default(), window, cx);
8583 });
8584
8585 // test new added selection expands above and existing selection shrinks
8586 cx.assert_editor_state(indoc!(
8587 r#"line onˇe
8588 liˇneˇ twˇo
8589 liˇneˇ thˇree
8590 line four"#
8591 ));
8592
8593 cx.update_editor(|editor, window, cx| {
8594 editor.add_selection_above(&Default::default(), window, cx);
8595 });
8596
8597 // test new added selection expands above and existing selection shrinks
8598 cx.assert_editor_state(indoc!(
8599 r#"lineˇ onˇe
8600 liˇneˇ twˇo
8601 lineˇ three
8602 line four"#
8603 ));
8604
8605 // intial state with two selection groups
8606 cx.set_state(indoc!(
8607 r#"abcd
8608 ef«ghˇ»
8609 ijkl
8610 «mˇ»nop"#
8611 ));
8612
8613 cx.update_editor(|editor, window, cx| {
8614 editor.add_selection_above(&Default::default(), window, cx);
8615 editor.add_selection_above(&Default::default(), window, cx);
8616 });
8617
8618 cx.assert_editor_state(indoc!(
8619 r#"ab«cdˇ»
8620 «eˇ»f«ghˇ»
8621 «iˇ»jkl
8622 «mˇ»nop"#
8623 ));
8624
8625 // add single selection in middle - simulate opt drag
8626 cx.update_editor(|editor, window, cx| {
8627 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8628 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8629 editor.update_selection(
8630 DisplayPoint::new(DisplayRow(2), 4),
8631 0,
8632 gpui::Point::<f32>::default(),
8633 window,
8634 cx,
8635 );
8636 editor.end_selection(window, cx);
8637 });
8638
8639 cx.assert_editor_state(indoc!(
8640 r#"ab«cdˇ»
8641 «eˇ»f«ghˇ»
8642 «iˇ»jk«lˇ»
8643 «mˇ»nop"#
8644 ));
8645
8646 cx.update_editor(|editor, window, cx| {
8647 editor.add_selection_below(&Default::default(), window, cx);
8648 });
8649
8650 // test new added selection expands below, others shrinks from above
8651 cx.assert_editor_state(indoc!(
8652 r#"abcd
8653 ef«ghˇ»
8654 «iˇ»jk«lˇ»
8655 «mˇ»no«pˇ»"#
8656 ));
8657}
8658
8659#[gpui::test]
8660async fn test_select_next(cx: &mut TestAppContext) {
8661 init_test(cx, |_| {});
8662 let mut cx = EditorTestContext::new(cx).await;
8663
8664 // Enable case sensitive search.
8665 update_test_editor_settings(&mut cx, |settings| {
8666 let mut search_settings = SearchSettingsContent::default();
8667 search_settings.case_sensitive = Some(true);
8668 settings.search = Some(search_settings);
8669 });
8670
8671 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8672
8673 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8674 .unwrap();
8675 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8676
8677 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8678 .unwrap();
8679 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8680
8681 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8682 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8683
8684 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8685 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8686
8687 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8688 .unwrap();
8689 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8690
8691 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8692 .unwrap();
8693 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8694
8695 // Test selection direction should be preserved
8696 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8697
8698 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8699 .unwrap();
8700 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8701
8702 // Test case sensitivity
8703 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8704 cx.update_editor(|e, window, cx| {
8705 e.select_next(&SelectNext::default(), window, cx).unwrap();
8706 });
8707 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8708
8709 // Disable case sensitive search.
8710 update_test_editor_settings(&mut cx, |settings| {
8711 let mut search_settings = SearchSettingsContent::default();
8712 search_settings.case_sensitive = Some(false);
8713 settings.search = Some(search_settings);
8714 });
8715
8716 cx.set_state("«ˇfoo»\nFOO\nFoo");
8717 cx.update_editor(|e, window, cx| {
8718 e.select_next(&SelectNext::default(), window, cx).unwrap();
8719 e.select_next(&SelectNext::default(), window, cx).unwrap();
8720 });
8721 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8722}
8723
8724#[gpui::test]
8725async fn test_select_all_matches(cx: &mut TestAppContext) {
8726 init_test(cx, |_| {});
8727 let mut cx = EditorTestContext::new(cx).await;
8728
8729 // Enable case sensitive search.
8730 update_test_editor_settings(&mut cx, |settings| {
8731 let mut search_settings = SearchSettingsContent::default();
8732 search_settings.case_sensitive = Some(true);
8733 settings.search = Some(search_settings);
8734 });
8735
8736 // Test caret-only selections
8737 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8738 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8739 .unwrap();
8740 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8741
8742 // Test left-to-right selections
8743 cx.set_state("abc\n«abcˇ»\nabc");
8744 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8745 .unwrap();
8746 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8747
8748 // Test right-to-left selections
8749 cx.set_state("abc\n«ˇabc»\nabc");
8750 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8751 .unwrap();
8752 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8753
8754 // Test selecting whitespace with caret selection
8755 cx.set_state("abc\nˇ abc\nabc");
8756 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8757 .unwrap();
8758 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8759
8760 // Test selecting whitespace with left-to-right selection
8761 cx.set_state("abc\n«ˇ »abc\nabc");
8762 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8763 .unwrap();
8764 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8765
8766 // Test no matches with right-to-left selection
8767 cx.set_state("abc\n« ˇ»abc\nabc");
8768 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8769 .unwrap();
8770 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8771
8772 // Test with a single word and clip_at_line_ends=true (#29823)
8773 cx.set_state("aˇbc");
8774 cx.update_editor(|e, window, cx| {
8775 e.set_clip_at_line_ends(true, cx);
8776 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8777 e.set_clip_at_line_ends(false, cx);
8778 });
8779 cx.assert_editor_state("«abcˇ»");
8780
8781 // Test case sensitivity
8782 cx.set_state("fˇoo\nFOO\nFoo");
8783 cx.update_editor(|e, window, cx| {
8784 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8785 });
8786 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8787
8788 // Disable case sensitive search.
8789 update_test_editor_settings(&mut cx, |settings| {
8790 let mut search_settings = SearchSettingsContent::default();
8791 search_settings.case_sensitive = Some(false);
8792 settings.search = Some(search_settings);
8793 });
8794
8795 cx.set_state("fˇoo\nFOO\nFoo");
8796 cx.update_editor(|e, window, cx| {
8797 e.select_all_matches(&SelectAllMatches, 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_does_not_scroll(cx: &mut TestAppContext) {
8804 init_test(cx, |_| {});
8805
8806 let mut cx = EditorTestContext::new(cx).await;
8807
8808 let large_body_1 = "\nd".repeat(200);
8809 let large_body_2 = "\ne".repeat(200);
8810
8811 cx.set_state(&format!(
8812 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8813 ));
8814 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8815 let scroll_position = editor.scroll_position(cx);
8816 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8817 scroll_position
8818 });
8819
8820 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8821 .unwrap();
8822 cx.assert_editor_state(&format!(
8823 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8824 ));
8825 let scroll_position_after_selection =
8826 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8827 assert_eq!(
8828 initial_scroll_position, scroll_position_after_selection,
8829 "Scroll position should not change after selecting all matches"
8830 );
8831}
8832
8833#[gpui::test]
8834async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8835 init_test(cx, |_| {});
8836
8837 let mut cx = EditorLspTestContext::new_rust(
8838 lsp::ServerCapabilities {
8839 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8840 ..Default::default()
8841 },
8842 cx,
8843 )
8844 .await;
8845
8846 cx.set_state(indoc! {"
8847 line 1
8848 line 2
8849 linˇe 3
8850 line 4
8851 line 5
8852 "});
8853
8854 // Make an edit
8855 cx.update_editor(|editor, window, cx| {
8856 editor.handle_input("X", window, cx);
8857 });
8858
8859 // Move cursor to a different position
8860 cx.update_editor(|editor, window, cx| {
8861 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8862 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8863 });
8864 });
8865
8866 cx.assert_editor_state(indoc! {"
8867 line 1
8868 line 2
8869 linXe 3
8870 line 4
8871 liˇne 5
8872 "});
8873
8874 cx.lsp
8875 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8876 Ok(Some(vec![lsp::TextEdit::new(
8877 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8878 "PREFIX ".to_string(),
8879 )]))
8880 });
8881
8882 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8883 .unwrap()
8884 .await
8885 .unwrap();
8886
8887 cx.assert_editor_state(indoc! {"
8888 PREFIX line 1
8889 line 2
8890 linXe 3
8891 line 4
8892 liˇne 5
8893 "});
8894
8895 // Undo formatting
8896 cx.update_editor(|editor, window, cx| {
8897 editor.undo(&Default::default(), window, cx);
8898 });
8899
8900 // Verify cursor moved back to position after edit
8901 cx.assert_editor_state(indoc! {"
8902 line 1
8903 line 2
8904 linXˇe 3
8905 line 4
8906 line 5
8907 "});
8908}
8909
8910#[gpui::test]
8911async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8912 init_test(cx, |_| {});
8913
8914 let mut cx = EditorTestContext::new(cx).await;
8915
8916 let provider = cx.new(|_| FakeEditPredictionDelegate::default());
8917 cx.update_editor(|editor, window, cx| {
8918 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8919 });
8920
8921 cx.set_state(indoc! {"
8922 line 1
8923 line 2
8924 linˇe 3
8925 line 4
8926 line 5
8927 line 6
8928 line 7
8929 line 8
8930 line 9
8931 line 10
8932 "});
8933
8934 let snapshot = cx.buffer_snapshot();
8935 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8936
8937 cx.update(|_, cx| {
8938 provider.update(cx, |provider, _| {
8939 provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
8940 id: None,
8941 edits: vec![(edit_position..edit_position, "X".into())],
8942 edit_preview: None,
8943 }))
8944 })
8945 });
8946
8947 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8948 cx.update_editor(|editor, window, cx| {
8949 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8950 });
8951
8952 cx.assert_editor_state(indoc! {"
8953 line 1
8954 line 2
8955 lineXˇ 3
8956 line 4
8957 line 5
8958 line 6
8959 line 7
8960 line 8
8961 line 9
8962 line 10
8963 "});
8964
8965 cx.update_editor(|editor, window, cx| {
8966 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8967 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8968 });
8969 });
8970
8971 cx.assert_editor_state(indoc! {"
8972 line 1
8973 line 2
8974 lineX 3
8975 line 4
8976 line 5
8977 line 6
8978 line 7
8979 line 8
8980 line 9
8981 liˇne 10
8982 "});
8983
8984 cx.update_editor(|editor, window, cx| {
8985 editor.undo(&Default::default(), window, cx);
8986 });
8987
8988 cx.assert_editor_state(indoc! {"
8989 line 1
8990 line 2
8991 lineˇ 3
8992 line 4
8993 line 5
8994 line 6
8995 line 7
8996 line 8
8997 line 9
8998 line 10
8999 "});
9000}
9001
9002#[gpui::test]
9003async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
9004 init_test(cx, |_| {});
9005
9006 let mut cx = EditorTestContext::new(cx).await;
9007 cx.set_state(
9008 r#"let foo = 2;
9009lˇet foo = 2;
9010let fooˇ = 2;
9011let foo = 2;
9012let foo = ˇ2;"#,
9013 );
9014
9015 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9016 .unwrap();
9017 cx.assert_editor_state(
9018 r#"let foo = 2;
9019«letˇ» foo = 2;
9020let «fooˇ» = 2;
9021let foo = 2;
9022let foo = «2ˇ»;"#,
9023 );
9024
9025 // noop for multiple selections with different contents
9026 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9027 .unwrap();
9028 cx.assert_editor_state(
9029 r#"let foo = 2;
9030«letˇ» foo = 2;
9031let «fooˇ» = 2;
9032let foo = 2;
9033let foo = «2ˇ»;"#,
9034 );
9035
9036 // Test last selection direction should be preserved
9037 cx.set_state(
9038 r#"let foo = 2;
9039let foo = 2;
9040let «fooˇ» = 2;
9041let «ˇfoo» = 2;
9042let foo = 2;"#,
9043 );
9044
9045 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9046 .unwrap();
9047 cx.assert_editor_state(
9048 r#"let foo = 2;
9049let foo = 2;
9050let «fooˇ» = 2;
9051let «ˇfoo» = 2;
9052let «ˇfoo» = 2;"#,
9053 );
9054}
9055
9056#[gpui::test]
9057async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
9058 init_test(cx, |_| {});
9059
9060 let mut cx =
9061 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
9062
9063 cx.assert_editor_state(indoc! {"
9064 ˇbbb
9065 ccc
9066
9067 bbb
9068 ccc
9069 "});
9070 cx.dispatch_action(SelectPrevious::default());
9071 cx.assert_editor_state(indoc! {"
9072 «bbbˇ»
9073 ccc
9074
9075 bbb
9076 ccc
9077 "});
9078 cx.dispatch_action(SelectPrevious::default());
9079 cx.assert_editor_state(indoc! {"
9080 «bbbˇ»
9081 ccc
9082
9083 «bbbˇ»
9084 ccc
9085 "});
9086}
9087
9088#[gpui::test]
9089async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
9090 init_test(cx, |_| {});
9091
9092 let mut cx = EditorTestContext::new(cx).await;
9093 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
9094
9095 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9096 .unwrap();
9097 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9098
9099 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9100 .unwrap();
9101 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
9102
9103 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9104 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9105
9106 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9107 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
9108
9109 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9110 .unwrap();
9111 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
9112
9113 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9114 .unwrap();
9115 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
9116}
9117
9118#[gpui::test]
9119async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
9120 init_test(cx, |_| {});
9121
9122 let mut cx = EditorTestContext::new(cx).await;
9123 cx.set_state("aˇ");
9124
9125 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9126 .unwrap();
9127 cx.assert_editor_state("«aˇ»");
9128 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9129 .unwrap();
9130 cx.assert_editor_state("«aˇ»");
9131}
9132
9133#[gpui::test]
9134async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
9135 init_test(cx, |_| {});
9136
9137 let mut cx = EditorTestContext::new(cx).await;
9138 cx.set_state(
9139 r#"let foo = 2;
9140lˇet foo = 2;
9141let fooˇ = 2;
9142let foo = 2;
9143let foo = ˇ2;"#,
9144 );
9145
9146 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9147 .unwrap();
9148 cx.assert_editor_state(
9149 r#"let foo = 2;
9150«letˇ» foo = 2;
9151let «fooˇ» = 2;
9152let foo = 2;
9153let foo = «2ˇ»;"#,
9154 );
9155
9156 // noop for multiple selections with different contents
9157 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9158 .unwrap();
9159 cx.assert_editor_state(
9160 r#"let foo = 2;
9161«letˇ» foo = 2;
9162let «fooˇ» = 2;
9163let foo = 2;
9164let foo = «2ˇ»;"#,
9165 );
9166}
9167
9168#[gpui::test]
9169async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
9170 init_test(cx, |_| {});
9171 let mut cx = EditorTestContext::new(cx).await;
9172
9173 // Enable case sensitive search.
9174 update_test_editor_settings(&mut cx, |settings| {
9175 let mut search_settings = SearchSettingsContent::default();
9176 search_settings.case_sensitive = Some(true);
9177 settings.search = Some(search_settings);
9178 });
9179
9180 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
9181
9182 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9183 .unwrap();
9184 // selection direction is preserved
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(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9192 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9193
9194 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9195 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9196
9197 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9198 .unwrap();
9199 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
9200
9201 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9202 .unwrap();
9203 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
9204
9205 // Test case sensitivity
9206 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
9207 cx.update_editor(|e, window, cx| {
9208 e.select_previous(&SelectPrevious::default(), window, cx)
9209 .unwrap();
9210 e.select_previous(&SelectPrevious::default(), window, cx)
9211 .unwrap();
9212 });
9213 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
9214
9215 // Disable case sensitive search.
9216 update_test_editor_settings(&mut cx, |settings| {
9217 let mut search_settings = SearchSettingsContent::default();
9218 search_settings.case_sensitive = Some(false);
9219 settings.search = Some(search_settings);
9220 });
9221
9222 cx.set_state("foo\nFOO\n«ˇFoo»");
9223 cx.update_editor(|e, window, cx| {
9224 e.select_previous(&SelectPrevious::default(), window, cx)
9225 .unwrap();
9226 e.select_previous(&SelectPrevious::default(), window, cx)
9227 .unwrap();
9228 });
9229 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
9230}
9231
9232#[gpui::test]
9233async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
9234 init_test(cx, |_| {});
9235
9236 let language = Arc::new(Language::new(
9237 LanguageConfig::default(),
9238 Some(tree_sitter_rust::LANGUAGE.into()),
9239 ));
9240
9241 let text = r#"
9242 use mod1::mod2::{mod3, mod4};
9243
9244 fn fn_1(param1: bool, param2: &str) {
9245 let var1 = "text";
9246 }
9247 "#
9248 .unindent();
9249
9250 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9251 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9252 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9253
9254 editor
9255 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9256 .await;
9257
9258 editor.update_in(cx, |editor, window, cx| {
9259 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9260 s.select_display_ranges([
9261 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
9262 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
9263 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
9264 ]);
9265 });
9266 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9267 });
9268 editor.update(cx, |editor, cx| {
9269 assert_text_with_selections(
9270 editor,
9271 indoc! {r#"
9272 use mod1::mod2::{mod3, «mod4ˇ»};
9273
9274 fn fn_1«ˇ(param1: bool, param2: &str)» {
9275 let var1 = "«ˇtext»";
9276 }
9277 "#},
9278 cx,
9279 );
9280 });
9281
9282 editor.update_in(cx, |editor, window, cx| {
9283 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9284 });
9285 editor.update(cx, |editor, cx| {
9286 assert_text_with_selections(
9287 editor,
9288 indoc! {r#"
9289 use mod1::mod2::«{mod3, mod4}ˇ»;
9290
9291 «ˇfn fn_1(param1: bool, param2: &str) {
9292 let var1 = "text";
9293 }»
9294 "#},
9295 cx,
9296 );
9297 });
9298
9299 editor.update_in(cx, |editor, window, cx| {
9300 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9301 });
9302 assert_eq!(
9303 editor.update(cx, |editor, cx| editor
9304 .selections
9305 .display_ranges(&editor.display_snapshot(cx))),
9306 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9307 );
9308
9309 // Trying to expand the selected syntax node one more time has no effect.
9310 editor.update_in(cx, |editor, window, cx| {
9311 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9312 });
9313 assert_eq!(
9314 editor.update(cx, |editor, cx| editor
9315 .selections
9316 .display_ranges(&editor.display_snapshot(cx))),
9317 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9318 );
9319
9320 editor.update_in(cx, |editor, window, cx| {
9321 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9322 });
9323 editor.update(cx, |editor, cx| {
9324 assert_text_with_selections(
9325 editor,
9326 indoc! {r#"
9327 use mod1::mod2::«{mod3, mod4}ˇ»;
9328
9329 «ˇfn fn_1(param1: bool, param2: &str) {
9330 let var1 = "text";
9331 }»
9332 "#},
9333 cx,
9334 );
9335 });
9336
9337 editor.update_in(cx, |editor, window, cx| {
9338 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9339 });
9340 editor.update(cx, |editor, cx| {
9341 assert_text_with_selections(
9342 editor,
9343 indoc! {r#"
9344 use mod1::mod2::{mod3, «mod4ˇ»};
9345
9346 fn fn_1«ˇ(param1: bool, param2: &str)» {
9347 let var1 = "«ˇtext»";
9348 }
9349 "#},
9350 cx,
9351 );
9352 });
9353
9354 editor.update_in(cx, |editor, window, cx| {
9355 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9356 });
9357 editor.update(cx, |editor, cx| {
9358 assert_text_with_selections(
9359 editor,
9360 indoc! {r#"
9361 use mod1::mod2::{mod3, moˇd4};
9362
9363 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9364 let var1 = "teˇxt";
9365 }
9366 "#},
9367 cx,
9368 );
9369 });
9370
9371 // Trying to shrink the selected syntax node one more time has no effect.
9372 editor.update_in(cx, |editor, window, cx| {
9373 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9374 });
9375 editor.update_in(cx, |editor, _, cx| {
9376 assert_text_with_selections(
9377 editor,
9378 indoc! {r#"
9379 use mod1::mod2::{mod3, moˇd4};
9380
9381 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9382 let var1 = "teˇxt";
9383 }
9384 "#},
9385 cx,
9386 );
9387 });
9388
9389 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9390 // a fold.
9391 editor.update_in(cx, |editor, window, cx| {
9392 editor.fold_creases(
9393 vec![
9394 Crease::simple(
9395 Point::new(0, 21)..Point::new(0, 24),
9396 FoldPlaceholder::test(),
9397 ),
9398 Crease::simple(
9399 Point::new(3, 20)..Point::new(3, 22),
9400 FoldPlaceholder::test(),
9401 ),
9402 ],
9403 true,
9404 window,
9405 cx,
9406 );
9407 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9408 });
9409 editor.update(cx, |editor, cx| {
9410 assert_text_with_selections(
9411 editor,
9412 indoc! {r#"
9413 use mod1::mod2::«{mod3, mod4}ˇ»;
9414
9415 fn fn_1«ˇ(param1: bool, param2: &str)» {
9416 let var1 = "«ˇtext»";
9417 }
9418 "#},
9419 cx,
9420 );
9421 });
9422}
9423
9424#[gpui::test]
9425async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9426 init_test(cx, |_| {});
9427
9428 let language = Arc::new(Language::new(
9429 LanguageConfig::default(),
9430 Some(tree_sitter_rust::LANGUAGE.into()),
9431 ));
9432
9433 let text = "let a = 2;";
9434
9435 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9436 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9437 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9438
9439 editor
9440 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9441 .await;
9442
9443 // Test case 1: Cursor at end of word
9444 editor.update_in(cx, |editor, window, cx| {
9445 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9446 s.select_display_ranges([
9447 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9448 ]);
9449 });
9450 });
9451 editor.update(cx, |editor, cx| {
9452 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9453 });
9454 editor.update_in(cx, |editor, window, cx| {
9455 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9456 });
9457 editor.update(cx, |editor, cx| {
9458 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9459 });
9460 editor.update_in(cx, |editor, window, cx| {
9461 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9462 });
9463 editor.update(cx, |editor, cx| {
9464 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9465 });
9466
9467 // Test case 2: Cursor at end of statement
9468 editor.update_in(cx, |editor, window, cx| {
9469 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9470 s.select_display_ranges([
9471 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9472 ]);
9473 });
9474 });
9475 editor.update(cx, |editor, cx| {
9476 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9477 });
9478 editor.update_in(cx, |editor, window, cx| {
9479 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9480 });
9481 editor.update(cx, |editor, cx| {
9482 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9483 });
9484}
9485
9486#[gpui::test]
9487async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9488 init_test(cx, |_| {});
9489
9490 let language = Arc::new(Language::new(
9491 LanguageConfig {
9492 name: "JavaScript".into(),
9493 ..Default::default()
9494 },
9495 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9496 ));
9497
9498 let text = r#"
9499 let a = {
9500 key: "value",
9501 };
9502 "#
9503 .unindent();
9504
9505 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9506 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9507 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9508
9509 editor
9510 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9511 .await;
9512
9513 // Test case 1: Cursor after '{'
9514 editor.update_in(cx, |editor, window, cx| {
9515 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9516 s.select_display_ranges([
9517 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9518 ]);
9519 });
9520 });
9521 editor.update(cx, |editor, cx| {
9522 assert_text_with_selections(
9523 editor,
9524 indoc! {r#"
9525 let a = {ˇ
9526 key: "value",
9527 };
9528 "#},
9529 cx,
9530 );
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(
9537 editor,
9538 indoc! {r#"
9539 let a = «ˇ{
9540 key: "value",
9541 }»;
9542 "#},
9543 cx,
9544 );
9545 });
9546
9547 // Test case 2: Cursor after ':'
9548 editor.update_in(cx, |editor, window, cx| {
9549 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9550 s.select_display_ranges([
9551 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9552 ]);
9553 });
9554 });
9555 editor.update(cx, |editor, cx| {
9556 assert_text_with_selections(
9557 editor,
9558 indoc! {r#"
9559 let a = {
9560 key:ˇ "value",
9561 };
9562 "#},
9563 cx,
9564 );
9565 });
9566 editor.update_in(cx, |editor, window, cx| {
9567 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9568 });
9569 editor.update(cx, |editor, cx| {
9570 assert_text_with_selections(
9571 editor,
9572 indoc! {r#"
9573 let a = {
9574 «ˇkey: "value"»,
9575 };
9576 "#},
9577 cx,
9578 );
9579 });
9580 editor.update_in(cx, |editor, window, cx| {
9581 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9582 });
9583 editor.update(cx, |editor, cx| {
9584 assert_text_with_selections(
9585 editor,
9586 indoc! {r#"
9587 let a = «ˇ{
9588 key: "value",
9589 }»;
9590 "#},
9591 cx,
9592 );
9593 });
9594
9595 // Test case 3: Cursor after ','
9596 editor.update_in(cx, |editor, window, cx| {
9597 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9598 s.select_display_ranges([
9599 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9600 ]);
9601 });
9602 });
9603 editor.update(cx, |editor, cx| {
9604 assert_text_with_selections(
9605 editor,
9606 indoc! {r#"
9607 let a = {
9608 key: "value",ˇ
9609 };
9610 "#},
9611 cx,
9612 );
9613 });
9614 editor.update_in(cx, |editor, window, cx| {
9615 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9616 });
9617 editor.update(cx, |editor, cx| {
9618 assert_text_with_selections(
9619 editor,
9620 indoc! {r#"
9621 let a = «ˇ{
9622 key: "value",
9623 }»;
9624 "#},
9625 cx,
9626 );
9627 });
9628
9629 // Test case 4: Cursor after ';'
9630 editor.update_in(cx, |editor, window, cx| {
9631 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9632 s.select_display_ranges([
9633 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9634 ]);
9635 });
9636 });
9637 editor.update(cx, |editor, cx| {
9638 assert_text_with_selections(
9639 editor,
9640 indoc! {r#"
9641 let a = {
9642 key: "value",
9643 };ˇ
9644 "#},
9645 cx,
9646 );
9647 });
9648 editor.update_in(cx, |editor, window, cx| {
9649 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9650 });
9651 editor.update(cx, |editor, cx| {
9652 assert_text_with_selections(
9653 editor,
9654 indoc! {r#"
9655 «ˇlet a = {
9656 key: "value",
9657 };
9658 »"#},
9659 cx,
9660 );
9661 });
9662}
9663
9664#[gpui::test]
9665async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9666 init_test(cx, |_| {});
9667
9668 let language = Arc::new(Language::new(
9669 LanguageConfig::default(),
9670 Some(tree_sitter_rust::LANGUAGE.into()),
9671 ));
9672
9673 let text = r#"
9674 use mod1::mod2::{mod3, mod4};
9675
9676 fn fn_1(param1: bool, param2: &str) {
9677 let var1 = "hello world";
9678 }
9679 "#
9680 .unindent();
9681
9682 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9683 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9684 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9685
9686 editor
9687 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9688 .await;
9689
9690 // Test 1: Cursor on a letter of a string word
9691 editor.update_in(cx, |editor, window, cx| {
9692 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9693 s.select_display_ranges([
9694 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9695 ]);
9696 });
9697 });
9698 editor.update_in(cx, |editor, window, cx| {
9699 assert_text_with_selections(
9700 editor,
9701 indoc! {r#"
9702 use mod1::mod2::{mod3, mod4};
9703
9704 fn fn_1(param1: bool, param2: &str) {
9705 let var1 = "hˇello world";
9706 }
9707 "#},
9708 cx,
9709 );
9710 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9711 assert_text_with_selections(
9712 editor,
9713 indoc! {r#"
9714 use mod1::mod2::{mod3, mod4};
9715
9716 fn fn_1(param1: bool, param2: &str) {
9717 let var1 = "«ˇhello» world";
9718 }
9719 "#},
9720 cx,
9721 );
9722 });
9723
9724 // Test 2: Partial selection within a word
9725 editor.update_in(cx, |editor, window, cx| {
9726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9727 s.select_display_ranges([
9728 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9729 ]);
9730 });
9731 });
9732 editor.update_in(cx, |editor, window, cx| {
9733 assert_text_with_selections(
9734 editor,
9735 indoc! {r#"
9736 use mod1::mod2::{mod3, mod4};
9737
9738 fn fn_1(param1: bool, param2: &str) {
9739 let var1 = "h«elˇ»lo world";
9740 }
9741 "#},
9742 cx,
9743 );
9744 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9745 assert_text_with_selections(
9746 editor,
9747 indoc! {r#"
9748 use mod1::mod2::{mod3, mod4};
9749
9750 fn fn_1(param1: bool, param2: &str) {
9751 let var1 = "«ˇhello» world";
9752 }
9753 "#},
9754 cx,
9755 );
9756 });
9757
9758 // Test 3: Complete word already selected
9759 editor.update_in(cx, |editor, window, cx| {
9760 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9761 s.select_display_ranges([
9762 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9763 ]);
9764 });
9765 });
9766 editor.update_in(cx, |editor, window, cx| {
9767 assert_text_with_selections(
9768 editor,
9769 indoc! {r#"
9770 use mod1::mod2::{mod3, mod4};
9771
9772 fn fn_1(param1: bool, param2: &str) {
9773 let var1 = "«helloˇ» world";
9774 }
9775 "#},
9776 cx,
9777 );
9778 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9779 assert_text_with_selections(
9780 editor,
9781 indoc! {r#"
9782 use mod1::mod2::{mod3, mod4};
9783
9784 fn fn_1(param1: bool, param2: &str) {
9785 let var1 = "«hello worldˇ»";
9786 }
9787 "#},
9788 cx,
9789 );
9790 });
9791
9792 // Test 4: Selection spanning across words
9793 editor.update_in(cx, |editor, window, cx| {
9794 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9795 s.select_display_ranges([
9796 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9797 ]);
9798 });
9799 });
9800 editor.update_in(cx, |editor, window, cx| {
9801 assert_text_with_selections(
9802 editor,
9803 indoc! {r#"
9804 use mod1::mod2::{mod3, mod4};
9805
9806 fn fn_1(param1: bool, param2: &str) {
9807 let var1 = "hel«lo woˇ»rld";
9808 }
9809 "#},
9810 cx,
9811 );
9812 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9813 assert_text_with_selections(
9814 editor,
9815 indoc! {r#"
9816 use mod1::mod2::{mod3, mod4};
9817
9818 fn fn_1(param1: bool, param2: &str) {
9819 let var1 = "«ˇhello world»";
9820 }
9821 "#},
9822 cx,
9823 );
9824 });
9825
9826 // Test 5: Expansion beyond string
9827 editor.update_in(cx, |editor, window, cx| {
9828 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9829 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9830 assert_text_with_selections(
9831 editor,
9832 indoc! {r#"
9833 use mod1::mod2::{mod3, mod4};
9834
9835 fn fn_1(param1: bool, param2: &str) {
9836 «ˇlet var1 = "hello world";»
9837 }
9838 "#},
9839 cx,
9840 );
9841 });
9842}
9843
9844#[gpui::test]
9845async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9846 init_test(cx, |_| {});
9847
9848 let mut cx = EditorTestContext::new(cx).await;
9849
9850 let language = Arc::new(Language::new(
9851 LanguageConfig::default(),
9852 Some(tree_sitter_rust::LANGUAGE.into()),
9853 ));
9854
9855 cx.update_buffer(|buffer, cx| {
9856 buffer.set_language(Some(language), cx);
9857 });
9858
9859 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9860 cx.update_editor(|editor, window, cx| {
9861 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9862 });
9863
9864 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9865
9866 cx.set_state(indoc! { r#"fn a() {
9867 // what
9868 // a
9869 // ˇlong
9870 // method
9871 // I
9872 // sure
9873 // hope
9874 // it
9875 // works
9876 }"# });
9877
9878 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9879 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9880 cx.update(|_, cx| {
9881 multi_buffer.update(cx, |multi_buffer, cx| {
9882 multi_buffer.set_excerpts_for_path(
9883 PathKey::for_buffer(&buffer, cx),
9884 buffer,
9885 [Point::new(1, 0)..Point::new(1, 0)],
9886 3,
9887 cx,
9888 );
9889 });
9890 });
9891
9892 let editor2 = cx.new_window_entity(|window, cx| {
9893 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9894 });
9895
9896 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9897 cx.update_editor(|editor, window, cx| {
9898 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9899 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9900 })
9901 });
9902
9903 cx.assert_editor_state(indoc! { "
9904 fn a() {
9905 // what
9906 // a
9907 ˇ // long
9908 // method"});
9909
9910 cx.update_editor(|editor, window, cx| {
9911 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9912 });
9913
9914 // Although we could potentially make the action work when the syntax node
9915 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9916 // did. Maybe we could also expand the excerpt to contain the range?
9917 cx.assert_editor_state(indoc! { "
9918 fn a() {
9919 // what
9920 // a
9921 ˇ // long
9922 // method"});
9923}
9924
9925#[gpui::test]
9926async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9927 init_test(cx, |_| {});
9928
9929 let base_text = r#"
9930 impl A {
9931 // this is an uncommitted comment
9932
9933 fn b() {
9934 c();
9935 }
9936
9937 // this is another uncommitted comment
9938
9939 fn d() {
9940 // e
9941 // f
9942 }
9943 }
9944
9945 fn g() {
9946 // h
9947 }
9948 "#
9949 .unindent();
9950
9951 let text = r#"
9952 ˇimpl A {
9953
9954 fn b() {
9955 c();
9956 }
9957
9958 fn d() {
9959 // e
9960 // f
9961 }
9962 }
9963
9964 fn g() {
9965 // h
9966 }
9967 "#
9968 .unindent();
9969
9970 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9971 cx.set_state(&text);
9972 cx.set_head_text(&base_text);
9973 cx.update_editor(|editor, window, cx| {
9974 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9975 });
9976
9977 cx.assert_state_with_diff(
9978 "
9979 ˇimpl A {
9980 - // this is an uncommitted comment
9981
9982 fn b() {
9983 c();
9984 }
9985
9986 - // this is another uncommitted comment
9987 -
9988 fn d() {
9989 // e
9990 // f
9991 }
9992 }
9993
9994 fn g() {
9995 // h
9996 }
9997 "
9998 .unindent(),
9999 );
10000
10001 let expected_display_text = "
10002 impl A {
10003 // this is an uncommitted comment
10004
10005 fn b() {
10006 ⋯
10007 }
10008
10009 // this is another uncommitted comment
10010
10011 fn d() {
10012 ⋯
10013 }
10014 }
10015
10016 fn g() {
10017 ⋯
10018 }
10019 "
10020 .unindent();
10021
10022 cx.update_editor(|editor, window, cx| {
10023 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
10024 assert_eq!(editor.display_text(cx), expected_display_text);
10025 });
10026}
10027
10028#[gpui::test]
10029async fn test_autoindent(cx: &mut TestAppContext) {
10030 init_test(cx, |_| {});
10031
10032 let language = Arc::new(
10033 Language::new(
10034 LanguageConfig {
10035 brackets: BracketPairConfig {
10036 pairs: vec![
10037 BracketPair {
10038 start: "{".to_string(),
10039 end: "}".to_string(),
10040 close: false,
10041 surround: false,
10042 newline: true,
10043 },
10044 BracketPair {
10045 start: "(".to_string(),
10046 end: ")".to_string(),
10047 close: false,
10048 surround: false,
10049 newline: true,
10050 },
10051 ],
10052 ..Default::default()
10053 },
10054 ..Default::default()
10055 },
10056 Some(tree_sitter_rust::LANGUAGE.into()),
10057 )
10058 .with_indents_query(
10059 r#"
10060 (_ "(" ")" @end) @indent
10061 (_ "{" "}" @end) @indent
10062 "#,
10063 )
10064 .unwrap(),
10065 );
10066
10067 let text = "fn a() {}";
10068
10069 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10070 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10071 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10072 editor
10073 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10074 .await;
10075
10076 editor.update_in(cx, |editor, window, cx| {
10077 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10078 s.select_ranges([
10079 MultiBufferOffset(5)..MultiBufferOffset(5),
10080 MultiBufferOffset(8)..MultiBufferOffset(8),
10081 MultiBufferOffset(9)..MultiBufferOffset(9),
10082 ])
10083 });
10084 editor.newline(&Newline, window, cx);
10085 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
10086 assert_eq!(
10087 editor.selections.ranges(&editor.display_snapshot(cx)),
10088 &[
10089 Point::new(1, 4)..Point::new(1, 4),
10090 Point::new(3, 4)..Point::new(3, 4),
10091 Point::new(5, 0)..Point::new(5, 0)
10092 ]
10093 );
10094 });
10095}
10096
10097#[gpui::test]
10098async fn test_autoindent_disabled(cx: &mut TestAppContext) {
10099 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
10100
10101 let language = Arc::new(
10102 Language::new(
10103 LanguageConfig {
10104 brackets: BracketPairConfig {
10105 pairs: vec![
10106 BracketPair {
10107 start: "{".to_string(),
10108 end: "}".to_string(),
10109 close: false,
10110 surround: false,
10111 newline: true,
10112 },
10113 BracketPair {
10114 start: "(".to_string(),
10115 end: ")".to_string(),
10116 close: false,
10117 surround: false,
10118 newline: true,
10119 },
10120 ],
10121 ..Default::default()
10122 },
10123 ..Default::default()
10124 },
10125 Some(tree_sitter_rust::LANGUAGE.into()),
10126 )
10127 .with_indents_query(
10128 r#"
10129 (_ "(" ")" @end) @indent
10130 (_ "{" "}" @end) @indent
10131 "#,
10132 )
10133 .unwrap(),
10134 );
10135
10136 let text = "fn a() {}";
10137
10138 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10139 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10140 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10141 editor
10142 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10143 .await;
10144
10145 editor.update_in(cx, |editor, window, cx| {
10146 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10147 s.select_ranges([
10148 MultiBufferOffset(5)..MultiBufferOffset(5),
10149 MultiBufferOffset(8)..MultiBufferOffset(8),
10150 MultiBufferOffset(9)..MultiBufferOffset(9),
10151 ])
10152 });
10153 editor.newline(&Newline, window, cx);
10154 assert_eq!(
10155 editor.text(cx),
10156 indoc!(
10157 "
10158 fn a(
10159
10160 ) {
10161
10162 }
10163 "
10164 )
10165 );
10166 assert_eq!(
10167 editor.selections.ranges(&editor.display_snapshot(cx)),
10168 &[
10169 Point::new(1, 0)..Point::new(1, 0),
10170 Point::new(3, 0)..Point::new(3, 0),
10171 Point::new(5, 0)..Point::new(5, 0)
10172 ]
10173 );
10174 });
10175}
10176
10177#[gpui::test]
10178async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
10179 init_test(cx, |settings| {
10180 settings.defaults.auto_indent = Some(true);
10181 settings.languages.0.insert(
10182 "python".into(),
10183 LanguageSettingsContent {
10184 auto_indent: Some(false),
10185 ..Default::default()
10186 },
10187 );
10188 });
10189
10190 let mut cx = EditorTestContext::new(cx).await;
10191
10192 let injected_language = Arc::new(
10193 Language::new(
10194 LanguageConfig {
10195 brackets: BracketPairConfig {
10196 pairs: vec![
10197 BracketPair {
10198 start: "{".to_string(),
10199 end: "}".to_string(),
10200 close: false,
10201 surround: false,
10202 newline: true,
10203 },
10204 BracketPair {
10205 start: "(".to_string(),
10206 end: ")".to_string(),
10207 close: true,
10208 surround: false,
10209 newline: true,
10210 },
10211 ],
10212 ..Default::default()
10213 },
10214 name: "python".into(),
10215 ..Default::default()
10216 },
10217 Some(tree_sitter_python::LANGUAGE.into()),
10218 )
10219 .with_indents_query(
10220 r#"
10221 (_ "(" ")" @end) @indent
10222 (_ "{" "}" @end) @indent
10223 "#,
10224 )
10225 .unwrap(),
10226 );
10227
10228 let language = Arc::new(
10229 Language::new(
10230 LanguageConfig {
10231 brackets: BracketPairConfig {
10232 pairs: vec![
10233 BracketPair {
10234 start: "{".to_string(),
10235 end: "}".to_string(),
10236 close: false,
10237 surround: false,
10238 newline: true,
10239 },
10240 BracketPair {
10241 start: "(".to_string(),
10242 end: ")".to_string(),
10243 close: true,
10244 surround: false,
10245 newline: true,
10246 },
10247 ],
10248 ..Default::default()
10249 },
10250 name: LanguageName::new_static("rust"),
10251 ..Default::default()
10252 },
10253 Some(tree_sitter_rust::LANGUAGE.into()),
10254 )
10255 .with_indents_query(
10256 r#"
10257 (_ "(" ")" @end) @indent
10258 (_ "{" "}" @end) @indent
10259 "#,
10260 )
10261 .unwrap()
10262 .with_injection_query(
10263 r#"
10264 (macro_invocation
10265 macro: (identifier) @_macro_name
10266 (token_tree) @injection.content
10267 (#set! injection.language "python"))
10268 "#,
10269 )
10270 .unwrap(),
10271 );
10272
10273 cx.language_registry().add(injected_language);
10274 cx.language_registry().add(language.clone());
10275
10276 cx.update_buffer(|buffer, cx| {
10277 buffer.set_language(Some(language), cx);
10278 });
10279
10280 cx.set_state(r#"struct A {ˇ}"#);
10281
10282 cx.update_editor(|editor, window, cx| {
10283 editor.newline(&Default::default(), window, cx);
10284 });
10285
10286 cx.assert_editor_state(indoc!(
10287 "struct A {
10288 ˇ
10289 }"
10290 ));
10291
10292 cx.set_state(r#"select_biased!(ˇ)"#);
10293
10294 cx.update_editor(|editor, window, cx| {
10295 editor.newline(&Default::default(), window, cx);
10296 editor.handle_input("def ", window, cx);
10297 editor.handle_input("(", window, cx);
10298 editor.newline(&Default::default(), window, cx);
10299 editor.handle_input("a", window, cx);
10300 });
10301
10302 cx.assert_editor_state(indoc!(
10303 "select_biased!(
10304 def (
10305 aˇ
10306 )
10307 )"
10308 ));
10309}
10310
10311#[gpui::test]
10312async fn test_autoindent_selections(cx: &mut TestAppContext) {
10313 init_test(cx, |_| {});
10314
10315 {
10316 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10317 cx.set_state(indoc! {"
10318 impl A {
10319
10320 fn b() {}
10321
10322 «fn c() {
10323
10324 }ˇ»
10325 }
10326 "});
10327
10328 cx.update_editor(|editor, window, cx| {
10329 editor.autoindent(&Default::default(), window, cx);
10330 });
10331 cx.wait_for_autoindent_applied().await;
10332
10333 cx.assert_editor_state(indoc! {"
10334 impl A {
10335
10336 fn b() {}
10337
10338 «fn c() {
10339
10340 }ˇ»
10341 }
10342 "});
10343 }
10344
10345 {
10346 let mut cx = EditorTestContext::new_multibuffer(
10347 cx,
10348 [indoc! { "
10349 impl A {
10350 «
10351 // a
10352 fn b(){}
10353 »
10354 «
10355 }
10356 fn c(){}
10357 »
10358 "}],
10359 );
10360
10361 let buffer = cx.update_editor(|editor, _, cx| {
10362 let buffer = editor.buffer().update(cx, |buffer, _| {
10363 buffer.all_buffers().iter().next().unwrap().clone()
10364 });
10365 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10366 buffer
10367 });
10368
10369 cx.run_until_parked();
10370 cx.update_editor(|editor, window, cx| {
10371 editor.select_all(&Default::default(), window, cx);
10372 editor.autoindent(&Default::default(), window, cx)
10373 });
10374 cx.run_until_parked();
10375
10376 cx.update(|_, cx| {
10377 assert_eq!(
10378 buffer.read(cx).text(),
10379 indoc! { "
10380 impl A {
10381
10382 // a
10383 fn b(){}
10384
10385
10386 }
10387 fn c(){}
10388
10389 " }
10390 )
10391 });
10392 }
10393}
10394
10395#[gpui::test]
10396async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10397 init_test(cx, |_| {});
10398
10399 let mut cx = EditorTestContext::new(cx).await;
10400
10401 let language = Arc::new(Language::new(
10402 LanguageConfig {
10403 brackets: BracketPairConfig {
10404 pairs: vec![
10405 BracketPair {
10406 start: "{".to_string(),
10407 end: "}".to_string(),
10408 close: true,
10409 surround: true,
10410 newline: true,
10411 },
10412 BracketPair {
10413 start: "(".to_string(),
10414 end: ")".to_string(),
10415 close: true,
10416 surround: true,
10417 newline: true,
10418 },
10419 BracketPair {
10420 start: "/*".to_string(),
10421 end: " */".to_string(),
10422 close: true,
10423 surround: true,
10424 newline: true,
10425 },
10426 BracketPair {
10427 start: "[".to_string(),
10428 end: "]".to_string(),
10429 close: false,
10430 surround: false,
10431 newline: true,
10432 },
10433 BracketPair {
10434 start: "\"".to_string(),
10435 end: "\"".to_string(),
10436 close: true,
10437 surround: true,
10438 newline: false,
10439 },
10440 BracketPair {
10441 start: "<".to_string(),
10442 end: ">".to_string(),
10443 close: false,
10444 surround: true,
10445 newline: true,
10446 },
10447 ],
10448 ..Default::default()
10449 },
10450 autoclose_before: "})]".to_string(),
10451 ..Default::default()
10452 },
10453 Some(tree_sitter_rust::LANGUAGE.into()),
10454 ));
10455
10456 cx.language_registry().add(language.clone());
10457 cx.update_buffer(|buffer, cx| {
10458 buffer.set_language(Some(language), cx);
10459 });
10460
10461 cx.set_state(
10462 &r#"
10463 🏀ˇ
10464 εˇ
10465 ❤️ˇ
10466 "#
10467 .unindent(),
10468 );
10469
10470 // autoclose multiple nested brackets at multiple cursors
10471 cx.update_editor(|editor, window, cx| {
10472 editor.handle_input("{", window, cx);
10473 editor.handle_input("{", window, cx);
10474 editor.handle_input("{", window, cx);
10475 });
10476 cx.assert_editor_state(
10477 &"
10478 🏀{{{ˇ}}}
10479 ε{{{ˇ}}}
10480 ❤️{{{ˇ}}}
10481 "
10482 .unindent(),
10483 );
10484
10485 // insert a different closing bracket
10486 cx.update_editor(|editor, window, cx| {
10487 editor.handle_input(")", window, cx);
10488 });
10489 cx.assert_editor_state(
10490 &"
10491 🏀{{{)ˇ}}}
10492 ε{{{)ˇ}}}
10493 ❤️{{{)ˇ}}}
10494 "
10495 .unindent(),
10496 );
10497
10498 // skip over the auto-closed brackets when typing a closing bracket
10499 cx.update_editor(|editor, window, cx| {
10500 editor.move_right(&MoveRight, window, cx);
10501 editor.handle_input("}", window, cx);
10502 editor.handle_input("}", window, cx);
10503 editor.handle_input("}", window, cx);
10504 });
10505 cx.assert_editor_state(
10506 &"
10507 🏀{{{)}}}}ˇ
10508 ε{{{)}}}}ˇ
10509 ❤️{{{)}}}}ˇ
10510 "
10511 .unindent(),
10512 );
10513
10514 // autoclose multi-character pairs
10515 cx.set_state(
10516 &"
10517 ˇ
10518 ˇ
10519 "
10520 .unindent(),
10521 );
10522 cx.update_editor(|editor, window, cx| {
10523 editor.handle_input("/", window, cx);
10524 editor.handle_input("*", window, cx);
10525 });
10526 cx.assert_editor_state(
10527 &"
10528 /*ˇ */
10529 /*ˇ */
10530 "
10531 .unindent(),
10532 );
10533
10534 // one cursor autocloses a multi-character pair, one cursor
10535 // does not autoclose.
10536 cx.set_state(
10537 &"
10538 /ˇ
10539 ˇ
10540 "
10541 .unindent(),
10542 );
10543 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10544 cx.assert_editor_state(
10545 &"
10546 /*ˇ */
10547 *ˇ
10548 "
10549 .unindent(),
10550 );
10551
10552 // Don't autoclose if the next character isn't whitespace and isn't
10553 // listed in the language's "autoclose_before" section.
10554 cx.set_state("ˇa b");
10555 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10556 cx.assert_editor_state("{ˇa b");
10557
10558 // Don't autoclose if `close` is false for the bracket pair
10559 cx.set_state("ˇ");
10560 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10561 cx.assert_editor_state("[ˇ");
10562
10563 // Surround with brackets if text is selected
10564 cx.set_state("«aˇ» b");
10565 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10566 cx.assert_editor_state("{«aˇ»} b");
10567
10568 // Autoclose when not immediately after a word character
10569 cx.set_state("a ˇ");
10570 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10571 cx.assert_editor_state("a \"ˇ\"");
10572
10573 // Autoclose pair where the start and end characters are the same
10574 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10575 cx.assert_editor_state("a \"\"ˇ");
10576
10577 // Don't autoclose when immediately after a word character
10578 cx.set_state("aˇ");
10579 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10580 cx.assert_editor_state("a\"ˇ");
10581
10582 // Do autoclose when after a non-word character
10583 cx.set_state("{ˇ");
10584 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10585 cx.assert_editor_state("{\"ˇ\"");
10586
10587 // Non identical pairs autoclose regardless of preceding character
10588 cx.set_state("aˇ");
10589 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10590 cx.assert_editor_state("a{ˇ}");
10591
10592 // Don't autoclose pair if autoclose is disabled
10593 cx.set_state("ˇ");
10594 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10595 cx.assert_editor_state("<ˇ");
10596
10597 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10598 cx.set_state("«aˇ» b");
10599 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10600 cx.assert_editor_state("<«aˇ»> b");
10601}
10602
10603#[gpui::test]
10604async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10605 init_test(cx, |settings| {
10606 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10607 });
10608
10609 let mut cx = EditorTestContext::new(cx).await;
10610
10611 let language = Arc::new(Language::new(
10612 LanguageConfig {
10613 brackets: BracketPairConfig {
10614 pairs: vec![
10615 BracketPair {
10616 start: "{".to_string(),
10617 end: "}".to_string(),
10618 close: true,
10619 surround: true,
10620 newline: true,
10621 },
10622 BracketPair {
10623 start: "(".to_string(),
10624 end: ")".to_string(),
10625 close: true,
10626 surround: true,
10627 newline: true,
10628 },
10629 BracketPair {
10630 start: "[".to_string(),
10631 end: "]".to_string(),
10632 close: false,
10633 surround: false,
10634 newline: true,
10635 },
10636 ],
10637 ..Default::default()
10638 },
10639 autoclose_before: "})]".to_string(),
10640 ..Default::default()
10641 },
10642 Some(tree_sitter_rust::LANGUAGE.into()),
10643 ));
10644
10645 cx.language_registry().add(language.clone());
10646 cx.update_buffer(|buffer, cx| {
10647 buffer.set_language(Some(language), cx);
10648 });
10649
10650 cx.set_state(
10651 &"
10652 ˇ
10653 ˇ
10654 ˇ
10655 "
10656 .unindent(),
10657 );
10658
10659 // ensure only matching closing brackets are skipped over
10660 cx.update_editor(|editor, window, cx| {
10661 editor.handle_input("}", window, cx);
10662 editor.move_left(&MoveLeft, window, cx);
10663 editor.handle_input(")", window, cx);
10664 editor.move_left(&MoveLeft, window, cx);
10665 });
10666 cx.assert_editor_state(
10667 &"
10668 ˇ)}
10669 ˇ)}
10670 ˇ)}
10671 "
10672 .unindent(),
10673 );
10674
10675 // skip-over closing brackets at multiple cursors
10676 cx.update_editor(|editor, window, cx| {
10677 editor.handle_input(")", window, cx);
10678 editor.handle_input("}", window, cx);
10679 });
10680 cx.assert_editor_state(
10681 &"
10682 )}ˇ
10683 )}ˇ
10684 )}ˇ
10685 "
10686 .unindent(),
10687 );
10688
10689 // ignore non-close brackets
10690 cx.update_editor(|editor, window, cx| {
10691 editor.handle_input("]", window, cx);
10692 editor.move_left(&MoveLeft, window, cx);
10693 editor.handle_input("]", window, cx);
10694 });
10695 cx.assert_editor_state(
10696 &"
10697 )}]ˇ]
10698 )}]ˇ]
10699 )}]ˇ]
10700 "
10701 .unindent(),
10702 );
10703}
10704
10705#[gpui::test]
10706async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10707 init_test(cx, |_| {});
10708
10709 let mut cx = EditorTestContext::new(cx).await;
10710
10711 let html_language = Arc::new(
10712 Language::new(
10713 LanguageConfig {
10714 name: "HTML".into(),
10715 brackets: BracketPairConfig {
10716 pairs: vec![
10717 BracketPair {
10718 start: "<".into(),
10719 end: ">".into(),
10720 close: true,
10721 ..Default::default()
10722 },
10723 BracketPair {
10724 start: "{".into(),
10725 end: "}".into(),
10726 close: true,
10727 ..Default::default()
10728 },
10729 BracketPair {
10730 start: "(".into(),
10731 end: ")".into(),
10732 close: true,
10733 ..Default::default()
10734 },
10735 ],
10736 ..Default::default()
10737 },
10738 autoclose_before: "})]>".into(),
10739 ..Default::default()
10740 },
10741 Some(tree_sitter_html::LANGUAGE.into()),
10742 )
10743 .with_injection_query(
10744 r#"
10745 (script_element
10746 (raw_text) @injection.content
10747 (#set! injection.language "javascript"))
10748 "#,
10749 )
10750 .unwrap(),
10751 );
10752
10753 let javascript_language = Arc::new(Language::new(
10754 LanguageConfig {
10755 name: "JavaScript".into(),
10756 brackets: BracketPairConfig {
10757 pairs: vec![
10758 BracketPair {
10759 start: "/*".into(),
10760 end: " */".into(),
10761 close: true,
10762 ..Default::default()
10763 },
10764 BracketPair {
10765 start: "{".into(),
10766 end: "}".into(),
10767 close: true,
10768 ..Default::default()
10769 },
10770 BracketPair {
10771 start: "(".into(),
10772 end: ")".into(),
10773 close: true,
10774 ..Default::default()
10775 },
10776 ],
10777 ..Default::default()
10778 },
10779 autoclose_before: "})]>".into(),
10780 ..Default::default()
10781 },
10782 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10783 ));
10784
10785 cx.language_registry().add(html_language.clone());
10786 cx.language_registry().add(javascript_language);
10787 cx.executor().run_until_parked();
10788
10789 cx.update_buffer(|buffer, cx| {
10790 buffer.set_language(Some(html_language), cx);
10791 });
10792
10793 cx.set_state(
10794 &r#"
10795 <body>ˇ
10796 <script>
10797 var x = 1;ˇ
10798 </script>
10799 </body>ˇ
10800 "#
10801 .unindent(),
10802 );
10803
10804 // Precondition: different languages are active at different locations.
10805 cx.update_editor(|editor, window, cx| {
10806 let snapshot = editor.snapshot(window, cx);
10807 let cursors = editor
10808 .selections
10809 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
10810 let languages = cursors
10811 .iter()
10812 .map(|c| snapshot.language_at(c.start).unwrap().name())
10813 .collect::<Vec<_>>();
10814 assert_eq!(
10815 languages,
10816 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10817 );
10818 });
10819
10820 // Angle brackets autoclose in HTML, but not JavaScript.
10821 cx.update_editor(|editor, window, cx| {
10822 editor.handle_input("<", window, cx);
10823 editor.handle_input("a", window, cx);
10824 });
10825 cx.assert_editor_state(
10826 &r#"
10827 <body><aˇ>
10828 <script>
10829 var x = 1;<aˇ
10830 </script>
10831 </body><aˇ>
10832 "#
10833 .unindent(),
10834 );
10835
10836 // Curly braces and parens autoclose in both HTML and JavaScript.
10837 cx.update_editor(|editor, window, cx| {
10838 editor.handle_input(" b=", window, cx);
10839 editor.handle_input("{", window, cx);
10840 editor.handle_input("c", window, cx);
10841 editor.handle_input("(", window, cx);
10842 });
10843 cx.assert_editor_state(
10844 &r#"
10845 <body><a b={c(ˇ)}>
10846 <script>
10847 var x = 1;<a b={c(ˇ)}
10848 </script>
10849 </body><a b={c(ˇ)}>
10850 "#
10851 .unindent(),
10852 );
10853
10854 // Brackets that were already autoclosed are skipped.
10855 cx.update_editor(|editor, window, cx| {
10856 editor.handle_input(")", window, cx);
10857 editor.handle_input("d", window, cx);
10858 editor.handle_input("}", window, cx);
10859 });
10860 cx.assert_editor_state(
10861 &r#"
10862 <body><a b={c()d}ˇ>
10863 <script>
10864 var x = 1;<a b={c()d}ˇ
10865 </script>
10866 </body><a b={c()d}ˇ>
10867 "#
10868 .unindent(),
10869 );
10870 cx.update_editor(|editor, window, cx| {
10871 editor.handle_input(">", window, cx);
10872 });
10873 cx.assert_editor_state(
10874 &r#"
10875 <body><a b={c()d}>ˇ
10876 <script>
10877 var x = 1;<a b={c()d}>ˇ
10878 </script>
10879 </body><a b={c()d}>ˇ
10880 "#
10881 .unindent(),
10882 );
10883
10884 // Reset
10885 cx.set_state(
10886 &r#"
10887 <body>ˇ
10888 <script>
10889 var x = 1;ˇ
10890 </script>
10891 </body>ˇ
10892 "#
10893 .unindent(),
10894 );
10895
10896 cx.update_editor(|editor, window, cx| {
10897 editor.handle_input("<", window, cx);
10898 });
10899 cx.assert_editor_state(
10900 &r#"
10901 <body><ˇ>
10902 <script>
10903 var x = 1;<ˇ
10904 </script>
10905 </body><ˇ>
10906 "#
10907 .unindent(),
10908 );
10909
10910 // When backspacing, the closing angle brackets are removed.
10911 cx.update_editor(|editor, window, cx| {
10912 editor.backspace(&Backspace, window, cx);
10913 });
10914 cx.assert_editor_state(
10915 &r#"
10916 <body>ˇ
10917 <script>
10918 var x = 1;ˇ
10919 </script>
10920 </body>ˇ
10921 "#
10922 .unindent(),
10923 );
10924
10925 // Block comments autoclose in JavaScript, but not HTML.
10926 cx.update_editor(|editor, window, cx| {
10927 editor.handle_input("/", window, cx);
10928 editor.handle_input("*", window, cx);
10929 });
10930 cx.assert_editor_state(
10931 &r#"
10932 <body>/*ˇ
10933 <script>
10934 var x = 1;/*ˇ */
10935 </script>
10936 </body>/*ˇ
10937 "#
10938 .unindent(),
10939 );
10940}
10941
10942#[gpui::test]
10943async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10944 init_test(cx, |_| {});
10945
10946 let mut cx = EditorTestContext::new(cx).await;
10947
10948 let rust_language = Arc::new(
10949 Language::new(
10950 LanguageConfig {
10951 name: "Rust".into(),
10952 brackets: serde_json::from_value(json!([
10953 { "start": "{", "end": "}", "close": true, "newline": true },
10954 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10955 ]))
10956 .unwrap(),
10957 autoclose_before: "})]>".into(),
10958 ..Default::default()
10959 },
10960 Some(tree_sitter_rust::LANGUAGE.into()),
10961 )
10962 .with_override_query("(string_literal) @string")
10963 .unwrap(),
10964 );
10965
10966 cx.language_registry().add(rust_language.clone());
10967 cx.update_buffer(|buffer, cx| {
10968 buffer.set_language(Some(rust_language), cx);
10969 });
10970
10971 cx.set_state(
10972 &r#"
10973 let x = ˇ
10974 "#
10975 .unindent(),
10976 );
10977
10978 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10979 cx.update_editor(|editor, window, cx| {
10980 editor.handle_input("\"", window, cx);
10981 });
10982 cx.assert_editor_state(
10983 &r#"
10984 let x = "ˇ"
10985 "#
10986 .unindent(),
10987 );
10988
10989 // Inserting another quotation mark. The cursor moves across the existing
10990 // automatically-inserted quotation mark.
10991 cx.update_editor(|editor, window, cx| {
10992 editor.handle_input("\"", window, cx);
10993 });
10994 cx.assert_editor_state(
10995 &r#"
10996 let x = ""ˇ
10997 "#
10998 .unindent(),
10999 );
11000
11001 // Reset
11002 cx.set_state(
11003 &r#"
11004 let x = ˇ
11005 "#
11006 .unindent(),
11007 );
11008
11009 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
11010 cx.update_editor(|editor, window, cx| {
11011 editor.handle_input("\"", window, cx);
11012 editor.handle_input(" ", window, cx);
11013 editor.move_left(&Default::default(), window, cx);
11014 editor.handle_input("\\", window, cx);
11015 editor.handle_input("\"", window, cx);
11016 });
11017 cx.assert_editor_state(
11018 &r#"
11019 let x = "\"ˇ "
11020 "#
11021 .unindent(),
11022 );
11023
11024 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
11025 // mark. Nothing is inserted.
11026 cx.update_editor(|editor, window, cx| {
11027 editor.move_right(&Default::default(), window, cx);
11028 editor.handle_input("\"", window, cx);
11029 });
11030 cx.assert_editor_state(
11031 &r#"
11032 let x = "\" "ˇ
11033 "#
11034 .unindent(),
11035 );
11036}
11037
11038#[gpui::test]
11039async fn test_autoclose_quotes_with_scope_awareness(cx: &mut TestAppContext) {
11040 init_test(cx, |_| {});
11041
11042 let mut cx = EditorTestContext::new(cx).await;
11043 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
11044
11045 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11046
11047 // Double quote inside single-quoted string
11048 cx.set_state(indoc! {r#"
11049 def main():
11050 items = ['"', ˇ]
11051 "#});
11052 cx.update_editor(|editor, window, cx| {
11053 editor.handle_input("\"", window, cx);
11054 });
11055 cx.assert_editor_state(indoc! {r#"
11056 def main():
11057 items = ['"', "ˇ"]
11058 "#});
11059
11060 // Two double quotes inside single-quoted string
11061 cx.set_state(indoc! {r#"
11062 def main():
11063 items = ['""', ˇ]
11064 "#});
11065 cx.update_editor(|editor, window, cx| {
11066 editor.handle_input("\"", window, cx);
11067 });
11068 cx.assert_editor_state(indoc! {r#"
11069 def main():
11070 items = ['""', "ˇ"]
11071 "#});
11072
11073 // Single quote inside double-quoted string
11074 cx.set_state(indoc! {r#"
11075 def main():
11076 items = ["'", ˇ]
11077 "#});
11078 cx.update_editor(|editor, window, cx| {
11079 editor.handle_input("'", window, cx);
11080 });
11081 cx.assert_editor_state(indoc! {r#"
11082 def main():
11083 items = ["'", 'ˇ']
11084 "#});
11085
11086 // Two single quotes inside double-quoted string
11087 cx.set_state(indoc! {r#"
11088 def main():
11089 items = ["''", ˇ]
11090 "#});
11091 cx.update_editor(|editor, window, cx| {
11092 editor.handle_input("'", window, cx);
11093 });
11094 cx.assert_editor_state(indoc! {r#"
11095 def main():
11096 items = ["''", 'ˇ']
11097 "#});
11098
11099 // Mixed quotes on same line
11100 cx.set_state(indoc! {r#"
11101 def main():
11102 items = ['"""', "'''''", ˇ]
11103 "#});
11104 cx.update_editor(|editor, window, cx| {
11105 editor.handle_input("\"", window, cx);
11106 });
11107 cx.assert_editor_state(indoc! {r#"
11108 def main():
11109 items = ['"""', "'''''", "ˇ"]
11110 "#});
11111 cx.update_editor(|editor, window, cx| {
11112 editor.move_right(&MoveRight, window, cx);
11113 });
11114 cx.update_editor(|editor, window, cx| {
11115 editor.handle_input(", ", window, cx);
11116 });
11117 cx.update_editor(|editor, window, cx| {
11118 editor.handle_input("'", window, cx);
11119 });
11120 cx.assert_editor_state(indoc! {r#"
11121 def main():
11122 items = ['"""', "'''''", "", 'ˇ']
11123 "#});
11124}
11125
11126#[gpui::test]
11127async fn test_autoclose_quotes_with_multibyte_characters(cx: &mut TestAppContext) {
11128 init_test(cx, |_| {});
11129
11130 let mut cx = EditorTestContext::new(cx).await;
11131 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
11132 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11133
11134 cx.set_state(indoc! {r#"
11135 def main():
11136 items = ["🎉", ˇ]
11137 "#});
11138 cx.update_editor(|editor, window, cx| {
11139 editor.handle_input("\"", window, cx);
11140 });
11141 cx.assert_editor_state(indoc! {r#"
11142 def main():
11143 items = ["🎉", "ˇ"]
11144 "#});
11145}
11146
11147#[gpui::test]
11148async fn test_surround_with_pair(cx: &mut TestAppContext) {
11149 init_test(cx, |_| {});
11150
11151 let language = Arc::new(Language::new(
11152 LanguageConfig {
11153 brackets: BracketPairConfig {
11154 pairs: vec![
11155 BracketPair {
11156 start: "{".to_string(),
11157 end: "}".to_string(),
11158 close: true,
11159 surround: true,
11160 newline: true,
11161 },
11162 BracketPair {
11163 start: "/* ".to_string(),
11164 end: "*/".to_string(),
11165 close: true,
11166 surround: true,
11167 ..Default::default()
11168 },
11169 ],
11170 ..Default::default()
11171 },
11172 ..Default::default()
11173 },
11174 Some(tree_sitter_rust::LANGUAGE.into()),
11175 ));
11176
11177 let text = r#"
11178 a
11179 b
11180 c
11181 "#
11182 .unindent();
11183
11184 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11185 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11186 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11187 editor
11188 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11189 .await;
11190
11191 editor.update_in(cx, |editor, window, cx| {
11192 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11193 s.select_display_ranges([
11194 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11195 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11196 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
11197 ])
11198 });
11199
11200 editor.handle_input("{", window, cx);
11201 editor.handle_input("{", window, cx);
11202 editor.handle_input("{", window, cx);
11203 assert_eq!(
11204 editor.text(cx),
11205 "
11206 {{{a}}}
11207 {{{b}}}
11208 {{{c}}}
11209 "
11210 .unindent()
11211 );
11212 assert_eq!(
11213 display_ranges(editor, cx),
11214 [
11215 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
11216 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
11217 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
11218 ]
11219 );
11220
11221 editor.undo(&Undo, window, cx);
11222 editor.undo(&Undo, window, cx);
11223 editor.undo(&Undo, window, cx);
11224 assert_eq!(
11225 editor.text(cx),
11226 "
11227 a
11228 b
11229 c
11230 "
11231 .unindent()
11232 );
11233 assert_eq!(
11234 display_ranges(editor, cx),
11235 [
11236 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11237 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11238 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11239 ]
11240 );
11241
11242 // Ensure inserting the first character of a multi-byte bracket pair
11243 // doesn't surround the selections with the bracket.
11244 editor.handle_input("/", window, cx);
11245 assert_eq!(
11246 editor.text(cx),
11247 "
11248 /
11249 /
11250 /
11251 "
11252 .unindent()
11253 );
11254 assert_eq!(
11255 display_ranges(editor, cx),
11256 [
11257 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11258 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11259 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11260 ]
11261 );
11262
11263 editor.undo(&Undo, window, cx);
11264 assert_eq!(
11265 editor.text(cx),
11266 "
11267 a
11268 b
11269 c
11270 "
11271 .unindent()
11272 );
11273 assert_eq!(
11274 display_ranges(editor, cx),
11275 [
11276 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11277 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11278 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11279 ]
11280 );
11281
11282 // Ensure inserting the last character of a multi-byte bracket pair
11283 // doesn't surround the selections with the bracket.
11284 editor.handle_input("*", window, cx);
11285 assert_eq!(
11286 editor.text(cx),
11287 "
11288 *
11289 *
11290 *
11291 "
11292 .unindent()
11293 );
11294 assert_eq!(
11295 display_ranges(editor, cx),
11296 [
11297 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11298 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11299 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11300 ]
11301 );
11302 });
11303}
11304
11305#[gpui::test]
11306async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
11307 init_test(cx, |_| {});
11308
11309 let language = Arc::new(Language::new(
11310 LanguageConfig {
11311 brackets: BracketPairConfig {
11312 pairs: vec![BracketPair {
11313 start: "{".to_string(),
11314 end: "}".to_string(),
11315 close: true,
11316 surround: true,
11317 newline: true,
11318 }],
11319 ..Default::default()
11320 },
11321 autoclose_before: "}".to_string(),
11322 ..Default::default()
11323 },
11324 Some(tree_sitter_rust::LANGUAGE.into()),
11325 ));
11326
11327 let text = r#"
11328 a
11329 b
11330 c
11331 "#
11332 .unindent();
11333
11334 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11335 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11336 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11337 editor
11338 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11339 .await;
11340
11341 editor.update_in(cx, |editor, window, cx| {
11342 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11343 s.select_ranges([
11344 Point::new(0, 1)..Point::new(0, 1),
11345 Point::new(1, 1)..Point::new(1, 1),
11346 Point::new(2, 1)..Point::new(2, 1),
11347 ])
11348 });
11349
11350 editor.handle_input("{", window, cx);
11351 editor.handle_input("{", window, cx);
11352 editor.handle_input("_", window, cx);
11353 assert_eq!(
11354 editor.text(cx),
11355 "
11356 a{{_}}
11357 b{{_}}
11358 c{{_}}
11359 "
11360 .unindent()
11361 );
11362 assert_eq!(
11363 editor
11364 .selections
11365 .ranges::<Point>(&editor.display_snapshot(cx)),
11366 [
11367 Point::new(0, 4)..Point::new(0, 4),
11368 Point::new(1, 4)..Point::new(1, 4),
11369 Point::new(2, 4)..Point::new(2, 4)
11370 ]
11371 );
11372
11373 editor.backspace(&Default::default(), window, cx);
11374 editor.backspace(&Default::default(), window, cx);
11375 assert_eq!(
11376 editor.text(cx),
11377 "
11378 a{}
11379 b{}
11380 c{}
11381 "
11382 .unindent()
11383 );
11384 assert_eq!(
11385 editor
11386 .selections
11387 .ranges::<Point>(&editor.display_snapshot(cx)),
11388 [
11389 Point::new(0, 2)..Point::new(0, 2),
11390 Point::new(1, 2)..Point::new(1, 2),
11391 Point::new(2, 2)..Point::new(2, 2)
11392 ]
11393 );
11394
11395 editor.delete_to_previous_word_start(&Default::default(), window, cx);
11396 assert_eq!(
11397 editor.text(cx),
11398 "
11399 a
11400 b
11401 c
11402 "
11403 .unindent()
11404 );
11405 assert_eq!(
11406 editor
11407 .selections
11408 .ranges::<Point>(&editor.display_snapshot(cx)),
11409 [
11410 Point::new(0, 1)..Point::new(0, 1),
11411 Point::new(1, 1)..Point::new(1, 1),
11412 Point::new(2, 1)..Point::new(2, 1)
11413 ]
11414 );
11415 });
11416}
11417
11418#[gpui::test]
11419async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
11420 init_test(cx, |settings| {
11421 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11422 });
11423
11424 let mut cx = EditorTestContext::new(cx).await;
11425
11426 let language = Arc::new(Language::new(
11427 LanguageConfig {
11428 brackets: BracketPairConfig {
11429 pairs: vec![
11430 BracketPair {
11431 start: "{".to_string(),
11432 end: "}".to_string(),
11433 close: true,
11434 surround: true,
11435 newline: true,
11436 },
11437 BracketPair {
11438 start: "(".to_string(),
11439 end: ")".to_string(),
11440 close: true,
11441 surround: true,
11442 newline: true,
11443 },
11444 BracketPair {
11445 start: "[".to_string(),
11446 end: "]".to_string(),
11447 close: false,
11448 surround: true,
11449 newline: true,
11450 },
11451 ],
11452 ..Default::default()
11453 },
11454 autoclose_before: "})]".to_string(),
11455 ..Default::default()
11456 },
11457 Some(tree_sitter_rust::LANGUAGE.into()),
11458 ));
11459
11460 cx.language_registry().add(language.clone());
11461 cx.update_buffer(|buffer, cx| {
11462 buffer.set_language(Some(language), cx);
11463 });
11464
11465 cx.set_state(
11466 &"
11467 {(ˇ)}
11468 [[ˇ]]
11469 {(ˇ)}
11470 "
11471 .unindent(),
11472 );
11473
11474 cx.update_editor(|editor, window, cx| {
11475 editor.backspace(&Default::default(), window, cx);
11476 editor.backspace(&Default::default(), window, cx);
11477 });
11478
11479 cx.assert_editor_state(
11480 &"
11481 ˇ
11482 ˇ]]
11483 ˇ
11484 "
11485 .unindent(),
11486 );
11487
11488 cx.update_editor(|editor, window, cx| {
11489 editor.handle_input("{", window, cx);
11490 editor.handle_input("{", window, cx);
11491 editor.move_right(&MoveRight, window, cx);
11492 editor.move_right(&MoveRight, window, cx);
11493 editor.move_left(&MoveLeft, window, cx);
11494 editor.move_left(&MoveLeft, window, cx);
11495 editor.backspace(&Default::default(), window, cx);
11496 });
11497
11498 cx.assert_editor_state(
11499 &"
11500 {ˇ}
11501 {ˇ}]]
11502 {ˇ}
11503 "
11504 .unindent(),
11505 );
11506
11507 cx.update_editor(|editor, window, cx| {
11508 editor.backspace(&Default::default(), window, cx);
11509 });
11510
11511 cx.assert_editor_state(
11512 &"
11513 ˇ
11514 ˇ]]
11515 ˇ
11516 "
11517 .unindent(),
11518 );
11519}
11520
11521#[gpui::test]
11522async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11523 init_test(cx, |_| {});
11524
11525 let language = Arc::new(Language::new(
11526 LanguageConfig::default(),
11527 Some(tree_sitter_rust::LANGUAGE.into()),
11528 ));
11529
11530 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11531 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11532 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11533 editor
11534 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11535 .await;
11536
11537 editor.update_in(cx, |editor, window, cx| {
11538 editor.set_auto_replace_emoji_shortcode(true);
11539
11540 editor.handle_input("Hello ", window, cx);
11541 editor.handle_input(":wave", window, cx);
11542 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11543
11544 editor.handle_input(":", window, cx);
11545 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11546
11547 editor.handle_input(" :smile", window, cx);
11548 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11549
11550 editor.handle_input(":", window, cx);
11551 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11552
11553 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11554 editor.handle_input(":wave", window, cx);
11555 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11556
11557 editor.handle_input(":", window, cx);
11558 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11559
11560 editor.handle_input(":1", window, cx);
11561 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11562
11563 editor.handle_input(":", window, cx);
11564 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11565
11566 // Ensure shortcode does not get replaced when it is part of a word
11567 editor.handle_input(" Test:wave", window, cx);
11568 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11569
11570 editor.handle_input(":", window, cx);
11571 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11572
11573 editor.set_auto_replace_emoji_shortcode(false);
11574
11575 // Ensure shortcode does not get replaced when auto replace is off
11576 editor.handle_input(" :wave", window, cx);
11577 assert_eq!(
11578 editor.text(cx),
11579 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11580 );
11581
11582 editor.handle_input(":", window, cx);
11583 assert_eq!(
11584 editor.text(cx),
11585 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11586 );
11587 });
11588}
11589
11590#[gpui::test]
11591async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11592 init_test(cx, |_| {});
11593
11594 let (text, insertion_ranges) = marked_text_ranges(
11595 indoc! {"
11596 ˇ
11597 "},
11598 false,
11599 );
11600
11601 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11602 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11603
11604 _ = editor.update_in(cx, |editor, window, cx| {
11605 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11606
11607 editor
11608 .insert_snippet(
11609 &insertion_ranges
11610 .iter()
11611 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11612 .collect::<Vec<_>>(),
11613 snippet,
11614 window,
11615 cx,
11616 )
11617 .unwrap();
11618
11619 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11620 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11621 assert_eq!(editor.text(cx), expected_text);
11622 assert_eq!(
11623 editor.selections.ranges(&editor.display_snapshot(cx)),
11624 selection_ranges
11625 .iter()
11626 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11627 .collect::<Vec<_>>()
11628 );
11629 }
11630
11631 assert(
11632 editor,
11633 cx,
11634 indoc! {"
11635 type «» =•
11636 "},
11637 );
11638
11639 assert!(editor.context_menu_visible(), "There should be a matches");
11640 });
11641}
11642
11643#[gpui::test]
11644async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11645 init_test(cx, |_| {});
11646
11647 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11648 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11649 assert_eq!(editor.text(cx), expected_text);
11650 assert_eq!(
11651 editor.selections.ranges(&editor.display_snapshot(cx)),
11652 selection_ranges
11653 .iter()
11654 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11655 .collect::<Vec<_>>()
11656 );
11657 }
11658
11659 let (text, insertion_ranges) = marked_text_ranges(
11660 indoc! {"
11661 ˇ
11662 "},
11663 false,
11664 );
11665
11666 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11667 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11668
11669 _ = editor.update_in(cx, |editor, window, cx| {
11670 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11671
11672 editor
11673 .insert_snippet(
11674 &insertion_ranges
11675 .iter()
11676 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11677 .collect::<Vec<_>>(),
11678 snippet,
11679 window,
11680 cx,
11681 )
11682 .unwrap();
11683
11684 assert_state(
11685 editor,
11686 cx,
11687 indoc! {"
11688 type «» = ;•
11689 "},
11690 );
11691
11692 assert!(
11693 editor.context_menu_visible(),
11694 "Context menu should be visible for placeholder choices"
11695 );
11696
11697 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11698
11699 assert_state(
11700 editor,
11701 cx,
11702 indoc! {"
11703 type = «»;•
11704 "},
11705 );
11706
11707 assert!(
11708 !editor.context_menu_visible(),
11709 "Context menu should be hidden after moving to next tabstop"
11710 );
11711
11712 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11713
11714 assert_state(
11715 editor,
11716 cx,
11717 indoc! {"
11718 type = ; ˇ
11719 "},
11720 );
11721
11722 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11723
11724 assert_state(
11725 editor,
11726 cx,
11727 indoc! {"
11728 type = ; ˇ
11729 "},
11730 );
11731 });
11732
11733 _ = editor.update_in(cx, |editor, window, cx| {
11734 editor.select_all(&SelectAll, window, cx);
11735 editor.backspace(&Backspace, window, cx);
11736
11737 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11738 let insertion_ranges = editor
11739 .selections
11740 .all(&editor.display_snapshot(cx))
11741 .iter()
11742 .map(|s| s.range())
11743 .collect::<Vec<_>>();
11744
11745 editor
11746 .insert_snippet(&insertion_ranges, snippet, window, cx)
11747 .unwrap();
11748
11749 assert_state(editor, cx, "fn «» = value;•");
11750
11751 assert!(
11752 editor.context_menu_visible(),
11753 "Context menu should be visible for placeholder choices"
11754 );
11755
11756 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11757
11758 assert_state(editor, cx, "fn = «valueˇ»;•");
11759
11760 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11761
11762 assert_state(editor, cx, "fn «» = value;•");
11763
11764 assert!(
11765 editor.context_menu_visible(),
11766 "Context menu should be visible again after returning to first tabstop"
11767 );
11768
11769 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11770
11771 assert_state(editor, cx, "fn «» = value;•");
11772 });
11773}
11774
11775#[gpui::test]
11776async fn test_snippets(cx: &mut TestAppContext) {
11777 init_test(cx, |_| {});
11778
11779 let mut cx = EditorTestContext::new(cx).await;
11780
11781 cx.set_state(indoc! {"
11782 a.ˇ b
11783 a.ˇ b
11784 a.ˇ b
11785 "});
11786
11787 cx.update_editor(|editor, window, cx| {
11788 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11789 let insertion_ranges = editor
11790 .selections
11791 .all(&editor.display_snapshot(cx))
11792 .iter()
11793 .map(|s| s.range())
11794 .collect::<Vec<_>>();
11795 editor
11796 .insert_snippet(&insertion_ranges, snippet, window, cx)
11797 .unwrap();
11798 });
11799
11800 cx.assert_editor_state(indoc! {"
11801 a.f(«oneˇ», two, «threeˇ») b
11802 a.f(«oneˇ», two, «threeˇ») b
11803 a.f(«oneˇ», two, «threeˇ») b
11804 "});
11805
11806 // Can't move earlier than the first tab stop
11807 cx.update_editor(|editor, window, cx| {
11808 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11809 });
11810 cx.assert_editor_state(indoc! {"
11811 a.f(«oneˇ», two, «threeˇ») b
11812 a.f(«oneˇ», two, «threeˇ») b
11813 a.f(«oneˇ», two, «threeˇ») b
11814 "});
11815
11816 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11817 cx.assert_editor_state(indoc! {"
11818 a.f(one, «twoˇ», three) b
11819 a.f(one, «twoˇ», three) b
11820 a.f(one, «twoˇ», three) b
11821 "});
11822
11823 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11824 cx.assert_editor_state(indoc! {"
11825 a.f(«oneˇ», two, «threeˇ») b
11826 a.f(«oneˇ», two, «threeˇ») b
11827 a.f(«oneˇ», two, «threeˇ») b
11828 "});
11829
11830 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11831 cx.assert_editor_state(indoc! {"
11832 a.f(one, «twoˇ», three) b
11833 a.f(one, «twoˇ», three) b
11834 a.f(one, «twoˇ», three) b
11835 "});
11836 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11837 cx.assert_editor_state(indoc! {"
11838 a.f(one, two, three)ˇ b
11839 a.f(one, two, three)ˇ b
11840 a.f(one, two, three)ˇ b
11841 "});
11842
11843 // As soon as the last tab stop is reached, snippet state is gone
11844 cx.update_editor(|editor, window, cx| {
11845 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11846 });
11847 cx.assert_editor_state(indoc! {"
11848 a.f(one, two, three)ˇ b
11849 a.f(one, two, three)ˇ b
11850 a.f(one, two, three)ˇ b
11851 "});
11852}
11853
11854#[gpui::test]
11855async fn test_snippet_indentation(cx: &mut TestAppContext) {
11856 init_test(cx, |_| {});
11857
11858 let mut cx = EditorTestContext::new(cx).await;
11859
11860 cx.update_editor(|editor, window, cx| {
11861 let snippet = Snippet::parse(indoc! {"
11862 /*
11863 * Multiline comment with leading indentation
11864 *
11865 * $1
11866 */
11867 $0"})
11868 .unwrap();
11869 let insertion_ranges = editor
11870 .selections
11871 .all(&editor.display_snapshot(cx))
11872 .iter()
11873 .map(|s| s.range())
11874 .collect::<Vec<_>>();
11875 editor
11876 .insert_snippet(&insertion_ranges, snippet, window, cx)
11877 .unwrap();
11878 });
11879
11880 cx.assert_editor_state(indoc! {"
11881 /*
11882 * Multiline comment with leading indentation
11883 *
11884 * ˇ
11885 */
11886 "});
11887
11888 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11889 cx.assert_editor_state(indoc! {"
11890 /*
11891 * Multiline comment with leading indentation
11892 *
11893 *•
11894 */
11895 ˇ"});
11896}
11897
11898#[gpui::test]
11899async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11900 init_test(cx, |_| {});
11901
11902 let mut cx = EditorTestContext::new(cx).await;
11903 cx.update_editor(|editor, _, cx| {
11904 editor.project().unwrap().update(cx, |project, cx| {
11905 project.snippets().update(cx, |snippets, _cx| {
11906 let snippet = project::snippet_provider::Snippet {
11907 prefix: vec!["multi word".to_string()],
11908 body: "this is many words".to_string(),
11909 description: Some("description".to_string()),
11910 name: "multi-word snippet test".to_string(),
11911 };
11912 snippets.add_snippet_for_test(
11913 None,
11914 PathBuf::from("test_snippets.json"),
11915 vec![Arc::new(snippet)],
11916 );
11917 });
11918 })
11919 });
11920
11921 for (input_to_simulate, should_match_snippet) in [
11922 ("m", true),
11923 ("m ", true),
11924 ("m w", true),
11925 ("aa m w", true),
11926 ("aa m g", false),
11927 ] {
11928 cx.set_state("ˇ");
11929 cx.simulate_input(input_to_simulate); // fails correctly
11930
11931 cx.update_editor(|editor, _, _| {
11932 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
11933 else {
11934 assert!(!should_match_snippet); // no completions! don't even show the menu
11935 return;
11936 };
11937 assert!(context_menu.visible());
11938 let completions = context_menu.completions.borrow();
11939
11940 assert_eq!(!completions.is_empty(), should_match_snippet);
11941 });
11942 }
11943}
11944
11945#[gpui::test]
11946async fn test_document_format_during_save(cx: &mut TestAppContext) {
11947 init_test(cx, |_| {});
11948
11949 let fs = FakeFs::new(cx.executor());
11950 fs.insert_file(path!("/file.rs"), Default::default()).await;
11951
11952 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11953
11954 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11955 language_registry.add(rust_lang());
11956 let mut fake_servers = language_registry.register_fake_lsp(
11957 "Rust",
11958 FakeLspAdapter {
11959 capabilities: lsp::ServerCapabilities {
11960 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11961 ..Default::default()
11962 },
11963 ..Default::default()
11964 },
11965 );
11966
11967 let buffer = project
11968 .update(cx, |project, cx| {
11969 project.open_local_buffer(path!("/file.rs"), cx)
11970 })
11971 .await
11972 .unwrap();
11973
11974 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11975 let (editor, cx) = cx.add_window_view(|window, cx| {
11976 build_editor_with_project(project.clone(), buffer, window, cx)
11977 });
11978 editor.update_in(cx, |editor, window, cx| {
11979 editor.set_text("one\ntwo\nthree\n", window, cx)
11980 });
11981 assert!(cx.read(|cx| editor.is_dirty(cx)));
11982
11983 let fake_server = fake_servers.next().await.unwrap();
11984
11985 {
11986 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11987 move |params, _| async move {
11988 assert_eq!(
11989 params.text_document.uri,
11990 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11991 );
11992 assert_eq!(params.options.tab_size, 4);
11993 Ok(Some(vec![lsp::TextEdit::new(
11994 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11995 ", ".to_string(),
11996 )]))
11997 },
11998 );
11999 let save = editor
12000 .update_in(cx, |editor, window, cx| {
12001 editor.save(
12002 SaveOptions {
12003 format: true,
12004 autosave: false,
12005 },
12006 project.clone(),
12007 window,
12008 cx,
12009 )
12010 })
12011 .unwrap();
12012 save.await;
12013
12014 assert_eq!(
12015 editor.update(cx, |editor, cx| editor.text(cx)),
12016 "one, two\nthree\n"
12017 );
12018 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12019 }
12020
12021 {
12022 editor.update_in(cx, |editor, window, cx| {
12023 editor.set_text("one\ntwo\nthree\n", window, cx)
12024 });
12025 assert!(cx.read(|cx| editor.is_dirty(cx)));
12026
12027 // Ensure we can still save even if formatting hangs.
12028 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12029 move |params, _| async move {
12030 assert_eq!(
12031 params.text_document.uri,
12032 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12033 );
12034 futures::future::pending::<()>().await;
12035 unreachable!()
12036 },
12037 );
12038 let save = editor
12039 .update_in(cx, |editor, window, cx| {
12040 editor.save(
12041 SaveOptions {
12042 format: true,
12043 autosave: false,
12044 },
12045 project.clone(),
12046 window,
12047 cx,
12048 )
12049 })
12050 .unwrap();
12051 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12052 save.await;
12053 assert_eq!(
12054 editor.update(cx, |editor, cx| editor.text(cx)),
12055 "one\ntwo\nthree\n"
12056 );
12057 }
12058
12059 // Set rust language override and assert overridden tabsize is sent to language server
12060 update_test_language_settings(cx, |settings| {
12061 settings.languages.0.insert(
12062 "Rust".into(),
12063 LanguageSettingsContent {
12064 tab_size: NonZeroU32::new(8),
12065 ..Default::default()
12066 },
12067 );
12068 });
12069
12070 {
12071 editor.update_in(cx, |editor, window, cx| {
12072 editor.set_text("somehting_new\n", window, cx)
12073 });
12074 assert!(cx.read(|cx| editor.is_dirty(cx)));
12075 let _formatting_request_signal = fake_server
12076 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12077 assert_eq!(
12078 params.text_document.uri,
12079 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12080 );
12081 assert_eq!(params.options.tab_size, 8);
12082 Ok(Some(vec![]))
12083 });
12084 let save = editor
12085 .update_in(cx, |editor, window, cx| {
12086 editor.save(
12087 SaveOptions {
12088 format: true,
12089 autosave: false,
12090 },
12091 project.clone(),
12092 window,
12093 cx,
12094 )
12095 })
12096 .unwrap();
12097 save.await;
12098 }
12099}
12100
12101#[gpui::test]
12102async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
12103 init_test(cx, |settings| {
12104 settings.defaults.ensure_final_newline_on_save = Some(false);
12105 });
12106
12107 let fs = FakeFs::new(cx.executor());
12108 fs.insert_file(path!("/file.txt"), "foo".into()).await;
12109
12110 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
12111
12112 let buffer = project
12113 .update(cx, |project, cx| {
12114 project.open_local_buffer(path!("/file.txt"), cx)
12115 })
12116 .await
12117 .unwrap();
12118
12119 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12120 let (editor, cx) = cx.add_window_view(|window, cx| {
12121 build_editor_with_project(project.clone(), buffer, window, cx)
12122 });
12123 editor.update_in(cx, |editor, window, cx| {
12124 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
12125 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
12126 });
12127 });
12128 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12129
12130 editor.update_in(cx, |editor, window, cx| {
12131 editor.handle_input("\n", window, cx)
12132 });
12133 cx.run_until_parked();
12134 save(&editor, &project, cx).await;
12135 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
12136
12137 editor.update_in(cx, |editor, window, cx| {
12138 editor.undo(&Default::default(), window, cx);
12139 });
12140 save(&editor, &project, cx).await;
12141 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
12142
12143 editor.update_in(cx, |editor, window, cx| {
12144 editor.redo(&Default::default(), window, cx);
12145 });
12146 cx.run_until_parked();
12147 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
12148
12149 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
12150 let save = editor
12151 .update_in(cx, |editor, window, cx| {
12152 editor.save(
12153 SaveOptions {
12154 format: true,
12155 autosave: false,
12156 },
12157 project.clone(),
12158 window,
12159 cx,
12160 )
12161 })
12162 .unwrap();
12163 save.await;
12164 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12165 }
12166}
12167
12168#[gpui::test]
12169async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
12170 init_test(cx, |_| {});
12171
12172 let cols = 4;
12173 let rows = 10;
12174 let sample_text_1 = sample_text(rows, cols, 'a');
12175 assert_eq!(
12176 sample_text_1,
12177 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12178 );
12179 let sample_text_2 = sample_text(rows, cols, 'l');
12180 assert_eq!(
12181 sample_text_2,
12182 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12183 );
12184 let sample_text_3 = sample_text(rows, cols, 'v');
12185 assert_eq!(
12186 sample_text_3,
12187 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12188 );
12189
12190 let fs = FakeFs::new(cx.executor());
12191 fs.insert_tree(
12192 path!("/a"),
12193 json!({
12194 "main.rs": sample_text_1,
12195 "other.rs": sample_text_2,
12196 "lib.rs": sample_text_3,
12197 }),
12198 )
12199 .await;
12200
12201 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12202 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12203 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12204
12205 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12206 language_registry.add(rust_lang());
12207 let mut fake_servers = language_registry.register_fake_lsp(
12208 "Rust",
12209 FakeLspAdapter {
12210 capabilities: lsp::ServerCapabilities {
12211 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12212 ..Default::default()
12213 },
12214 ..Default::default()
12215 },
12216 );
12217
12218 let worktree = project.update(cx, |project, cx| {
12219 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
12220 assert_eq!(worktrees.len(), 1);
12221 worktrees.pop().unwrap()
12222 });
12223 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12224
12225 let buffer_1 = project
12226 .update(cx, |project, cx| {
12227 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
12228 })
12229 .await
12230 .unwrap();
12231 let buffer_2 = project
12232 .update(cx, |project, cx| {
12233 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
12234 })
12235 .await
12236 .unwrap();
12237 let buffer_3 = project
12238 .update(cx, |project, cx| {
12239 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
12240 })
12241 .await
12242 .unwrap();
12243
12244 let multi_buffer = cx.new(|cx| {
12245 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12246 multi_buffer.push_excerpts(
12247 buffer_1.clone(),
12248 [
12249 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12250 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12251 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12252 ],
12253 cx,
12254 );
12255 multi_buffer.push_excerpts(
12256 buffer_2.clone(),
12257 [
12258 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12259 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12260 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12261 ],
12262 cx,
12263 );
12264 multi_buffer.push_excerpts(
12265 buffer_3.clone(),
12266 [
12267 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12268 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12269 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12270 ],
12271 cx,
12272 );
12273 multi_buffer
12274 });
12275 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12276 Editor::new(
12277 EditorMode::full(),
12278 multi_buffer,
12279 Some(project.clone()),
12280 window,
12281 cx,
12282 )
12283 });
12284
12285 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12286 editor.change_selections(
12287 SelectionEffects::scroll(Autoscroll::Next),
12288 window,
12289 cx,
12290 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
12291 );
12292 editor.insert("|one|two|three|", window, cx);
12293 });
12294 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12295 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12296 editor.change_selections(
12297 SelectionEffects::scroll(Autoscroll::Next),
12298 window,
12299 cx,
12300 |s| s.select_ranges(Some(MultiBufferOffset(60)..MultiBufferOffset(70))),
12301 );
12302 editor.insert("|four|five|six|", window, cx);
12303 });
12304 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12305
12306 // First two buffers should be edited, but not the third one.
12307 assert_eq!(
12308 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12309 "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}",
12310 );
12311 buffer_1.update(cx, |buffer, _| {
12312 assert!(buffer.is_dirty());
12313 assert_eq!(
12314 buffer.text(),
12315 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
12316 )
12317 });
12318 buffer_2.update(cx, |buffer, _| {
12319 assert!(buffer.is_dirty());
12320 assert_eq!(
12321 buffer.text(),
12322 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
12323 )
12324 });
12325 buffer_3.update(cx, |buffer, _| {
12326 assert!(!buffer.is_dirty());
12327 assert_eq!(buffer.text(), sample_text_3,)
12328 });
12329 cx.executor().run_until_parked();
12330
12331 let save = multi_buffer_editor
12332 .update_in(cx, |editor, window, cx| {
12333 editor.save(
12334 SaveOptions {
12335 format: true,
12336 autosave: false,
12337 },
12338 project.clone(),
12339 window,
12340 cx,
12341 )
12342 })
12343 .unwrap();
12344
12345 let fake_server = fake_servers.next().await.unwrap();
12346 fake_server
12347 .server
12348 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
12349 Ok(Some(vec![lsp::TextEdit::new(
12350 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12351 format!("[{} formatted]", params.text_document.uri),
12352 )]))
12353 })
12354 .detach();
12355 save.await;
12356
12357 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
12358 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
12359 assert_eq!(
12360 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12361 uri!(
12362 "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}"
12363 ),
12364 );
12365 buffer_1.update(cx, |buffer, _| {
12366 assert!(!buffer.is_dirty());
12367 assert_eq!(
12368 buffer.text(),
12369 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
12370 )
12371 });
12372 buffer_2.update(cx, |buffer, _| {
12373 assert!(!buffer.is_dirty());
12374 assert_eq!(
12375 buffer.text(),
12376 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
12377 )
12378 });
12379 buffer_3.update(cx, |buffer, _| {
12380 assert!(!buffer.is_dirty());
12381 assert_eq!(buffer.text(), sample_text_3,)
12382 });
12383}
12384
12385#[gpui::test]
12386async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
12387 init_test(cx, |_| {});
12388
12389 let fs = FakeFs::new(cx.executor());
12390 fs.insert_tree(
12391 path!("/dir"),
12392 json!({
12393 "file1.rs": "fn main() { println!(\"hello\"); }",
12394 "file2.rs": "fn test() { println!(\"test\"); }",
12395 "file3.rs": "fn other() { println!(\"other\"); }\n",
12396 }),
12397 )
12398 .await;
12399
12400 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
12401 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12402 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12403
12404 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12405 language_registry.add(rust_lang());
12406
12407 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
12408 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12409
12410 // Open three buffers
12411 let buffer_1 = project
12412 .update(cx, |project, cx| {
12413 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
12414 })
12415 .await
12416 .unwrap();
12417 let buffer_2 = project
12418 .update(cx, |project, cx| {
12419 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
12420 })
12421 .await
12422 .unwrap();
12423 let buffer_3 = project
12424 .update(cx, |project, cx| {
12425 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
12426 })
12427 .await
12428 .unwrap();
12429
12430 // Create a multi-buffer with all three buffers
12431 let multi_buffer = cx.new(|cx| {
12432 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12433 multi_buffer.push_excerpts(
12434 buffer_1.clone(),
12435 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12436 cx,
12437 );
12438 multi_buffer.push_excerpts(
12439 buffer_2.clone(),
12440 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12441 cx,
12442 );
12443 multi_buffer.push_excerpts(
12444 buffer_3.clone(),
12445 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12446 cx,
12447 );
12448 multi_buffer
12449 });
12450
12451 let editor = cx.new_window_entity(|window, cx| {
12452 Editor::new(
12453 EditorMode::full(),
12454 multi_buffer,
12455 Some(project.clone()),
12456 window,
12457 cx,
12458 )
12459 });
12460
12461 // Edit only the first buffer
12462 editor.update_in(cx, |editor, window, cx| {
12463 editor.change_selections(
12464 SelectionEffects::scroll(Autoscroll::Next),
12465 window,
12466 cx,
12467 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
12468 );
12469 editor.insert("// edited", window, cx);
12470 });
12471
12472 // Verify that only buffer 1 is dirty
12473 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
12474 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12475 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12476
12477 // Get write counts after file creation (files were created with initial content)
12478 // We expect each file to have been written once during creation
12479 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12480 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12481 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12482
12483 // Perform autosave
12484 let save_task = editor.update_in(cx, |editor, window, cx| {
12485 editor.save(
12486 SaveOptions {
12487 format: true,
12488 autosave: true,
12489 },
12490 project.clone(),
12491 window,
12492 cx,
12493 )
12494 });
12495 save_task.await.unwrap();
12496
12497 // Only the dirty buffer should have been saved
12498 assert_eq!(
12499 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12500 1,
12501 "Buffer 1 was dirty, so it should have been written once during autosave"
12502 );
12503 assert_eq!(
12504 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12505 0,
12506 "Buffer 2 was clean, so it should not have been written during autosave"
12507 );
12508 assert_eq!(
12509 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12510 0,
12511 "Buffer 3 was clean, so it should not have been written during autosave"
12512 );
12513
12514 // Verify buffer states after autosave
12515 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12516 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12517 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12518
12519 // Now perform a manual save (format = true)
12520 let save_task = editor.update_in(cx, |editor, window, cx| {
12521 editor.save(
12522 SaveOptions {
12523 format: true,
12524 autosave: false,
12525 },
12526 project.clone(),
12527 window,
12528 cx,
12529 )
12530 });
12531 save_task.await.unwrap();
12532
12533 // During manual save, clean buffers don't get written to disk
12534 // They just get did_save called for language server notifications
12535 assert_eq!(
12536 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12537 1,
12538 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12539 );
12540 assert_eq!(
12541 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12542 0,
12543 "Buffer 2 should not have been written at all"
12544 );
12545 assert_eq!(
12546 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12547 0,
12548 "Buffer 3 should not have been written at all"
12549 );
12550}
12551
12552async fn setup_range_format_test(
12553 cx: &mut TestAppContext,
12554) -> (
12555 Entity<Project>,
12556 Entity<Editor>,
12557 &mut gpui::VisualTestContext,
12558 lsp::FakeLanguageServer,
12559) {
12560 init_test(cx, |_| {});
12561
12562 let fs = FakeFs::new(cx.executor());
12563 fs.insert_file(path!("/file.rs"), Default::default()).await;
12564
12565 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12566
12567 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12568 language_registry.add(rust_lang());
12569 let mut fake_servers = language_registry.register_fake_lsp(
12570 "Rust",
12571 FakeLspAdapter {
12572 capabilities: lsp::ServerCapabilities {
12573 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12574 ..lsp::ServerCapabilities::default()
12575 },
12576 ..FakeLspAdapter::default()
12577 },
12578 );
12579
12580 let buffer = project
12581 .update(cx, |project, cx| {
12582 project.open_local_buffer(path!("/file.rs"), cx)
12583 })
12584 .await
12585 .unwrap();
12586
12587 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12588 let (editor, cx) = cx.add_window_view(|window, cx| {
12589 build_editor_with_project(project.clone(), buffer, window, cx)
12590 });
12591
12592 let fake_server = fake_servers.next().await.unwrap();
12593
12594 (project, editor, cx, fake_server)
12595}
12596
12597#[gpui::test]
12598async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12599 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12600
12601 editor.update_in(cx, |editor, window, cx| {
12602 editor.set_text("one\ntwo\nthree\n", window, cx)
12603 });
12604 assert!(cx.read(|cx| editor.is_dirty(cx)));
12605
12606 let save = editor
12607 .update_in(cx, |editor, window, cx| {
12608 editor.save(
12609 SaveOptions {
12610 format: true,
12611 autosave: false,
12612 },
12613 project.clone(),
12614 window,
12615 cx,
12616 )
12617 })
12618 .unwrap();
12619 fake_server
12620 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12621 assert_eq!(
12622 params.text_document.uri,
12623 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12624 );
12625 assert_eq!(params.options.tab_size, 4);
12626 Ok(Some(vec![lsp::TextEdit::new(
12627 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12628 ", ".to_string(),
12629 )]))
12630 })
12631 .next()
12632 .await;
12633 save.await;
12634 assert_eq!(
12635 editor.update(cx, |editor, cx| editor.text(cx)),
12636 "one, two\nthree\n"
12637 );
12638 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12639}
12640
12641#[gpui::test]
12642async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12643 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12644
12645 editor.update_in(cx, |editor, window, cx| {
12646 editor.set_text("one\ntwo\nthree\n", window, cx)
12647 });
12648 assert!(cx.read(|cx| editor.is_dirty(cx)));
12649
12650 // Test that save still works when formatting hangs
12651 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12652 move |params, _| async move {
12653 assert_eq!(
12654 params.text_document.uri,
12655 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12656 );
12657 futures::future::pending::<()>().await;
12658 unreachable!()
12659 },
12660 );
12661 let save = editor
12662 .update_in(cx, |editor, window, cx| {
12663 editor.save(
12664 SaveOptions {
12665 format: true,
12666 autosave: false,
12667 },
12668 project.clone(),
12669 window,
12670 cx,
12671 )
12672 })
12673 .unwrap();
12674 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12675 save.await;
12676 assert_eq!(
12677 editor.update(cx, |editor, cx| editor.text(cx)),
12678 "one\ntwo\nthree\n"
12679 );
12680 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12681}
12682
12683#[gpui::test]
12684async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12685 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12686
12687 // Buffer starts clean, no formatting should be requested
12688 let save = editor
12689 .update_in(cx, |editor, window, cx| {
12690 editor.save(
12691 SaveOptions {
12692 format: false,
12693 autosave: false,
12694 },
12695 project.clone(),
12696 window,
12697 cx,
12698 )
12699 })
12700 .unwrap();
12701 let _pending_format_request = fake_server
12702 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12703 panic!("Should not be invoked");
12704 })
12705 .next();
12706 save.await;
12707 cx.run_until_parked();
12708}
12709
12710#[gpui::test]
12711async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12712 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12713
12714 // Set Rust language override and assert overridden tabsize is sent to language server
12715 update_test_language_settings(cx, |settings| {
12716 settings.languages.0.insert(
12717 "Rust".into(),
12718 LanguageSettingsContent {
12719 tab_size: NonZeroU32::new(8),
12720 ..Default::default()
12721 },
12722 );
12723 });
12724
12725 editor.update_in(cx, |editor, window, cx| {
12726 editor.set_text("something_new\n", window, cx)
12727 });
12728 assert!(cx.read(|cx| editor.is_dirty(cx)));
12729 let save = editor
12730 .update_in(cx, |editor, window, cx| {
12731 editor.save(
12732 SaveOptions {
12733 format: true,
12734 autosave: false,
12735 },
12736 project.clone(),
12737 window,
12738 cx,
12739 )
12740 })
12741 .unwrap();
12742 fake_server
12743 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12744 assert_eq!(
12745 params.text_document.uri,
12746 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12747 );
12748 assert_eq!(params.options.tab_size, 8);
12749 Ok(Some(Vec::new()))
12750 })
12751 .next()
12752 .await;
12753 save.await;
12754}
12755
12756#[gpui::test]
12757async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12758 init_test(cx, |settings| {
12759 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12760 settings::LanguageServerFormatterSpecifier::Current,
12761 )))
12762 });
12763
12764 let fs = FakeFs::new(cx.executor());
12765 fs.insert_file(path!("/file.rs"), Default::default()).await;
12766
12767 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12768
12769 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12770 language_registry.add(Arc::new(Language::new(
12771 LanguageConfig {
12772 name: "Rust".into(),
12773 matcher: LanguageMatcher {
12774 path_suffixes: vec!["rs".to_string()],
12775 ..Default::default()
12776 },
12777 ..LanguageConfig::default()
12778 },
12779 Some(tree_sitter_rust::LANGUAGE.into()),
12780 )));
12781 update_test_language_settings(cx, |settings| {
12782 // Enable Prettier formatting for the same buffer, and ensure
12783 // LSP is called instead of Prettier.
12784 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12785 });
12786 let mut fake_servers = language_registry.register_fake_lsp(
12787 "Rust",
12788 FakeLspAdapter {
12789 capabilities: lsp::ServerCapabilities {
12790 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12791 ..Default::default()
12792 },
12793 ..Default::default()
12794 },
12795 );
12796
12797 let buffer = project
12798 .update(cx, |project, cx| {
12799 project.open_local_buffer(path!("/file.rs"), cx)
12800 })
12801 .await
12802 .unwrap();
12803
12804 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12805 let (editor, cx) = cx.add_window_view(|window, cx| {
12806 build_editor_with_project(project.clone(), buffer, window, cx)
12807 });
12808 editor.update_in(cx, |editor, window, cx| {
12809 editor.set_text("one\ntwo\nthree\n", window, cx)
12810 });
12811
12812 let fake_server = fake_servers.next().await.unwrap();
12813
12814 let format = editor
12815 .update_in(cx, |editor, window, cx| {
12816 editor.perform_format(
12817 project.clone(),
12818 FormatTrigger::Manual,
12819 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12820 window,
12821 cx,
12822 )
12823 })
12824 .unwrap();
12825 fake_server
12826 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12827 assert_eq!(
12828 params.text_document.uri,
12829 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12830 );
12831 assert_eq!(params.options.tab_size, 4);
12832 Ok(Some(vec![lsp::TextEdit::new(
12833 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12834 ", ".to_string(),
12835 )]))
12836 })
12837 .next()
12838 .await;
12839 format.await;
12840 assert_eq!(
12841 editor.update(cx, |editor, cx| editor.text(cx)),
12842 "one, two\nthree\n"
12843 );
12844
12845 editor.update_in(cx, |editor, window, cx| {
12846 editor.set_text("one\ntwo\nthree\n", window, cx)
12847 });
12848 // Ensure we don't lock if formatting hangs.
12849 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12850 move |params, _| async move {
12851 assert_eq!(
12852 params.text_document.uri,
12853 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12854 );
12855 futures::future::pending::<()>().await;
12856 unreachable!()
12857 },
12858 );
12859 let format = editor
12860 .update_in(cx, |editor, window, cx| {
12861 editor.perform_format(
12862 project,
12863 FormatTrigger::Manual,
12864 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12865 window,
12866 cx,
12867 )
12868 })
12869 .unwrap();
12870 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12871 format.await;
12872 assert_eq!(
12873 editor.update(cx, |editor, cx| editor.text(cx)),
12874 "one\ntwo\nthree\n"
12875 );
12876}
12877
12878#[gpui::test]
12879async fn test_multiple_formatters(cx: &mut TestAppContext) {
12880 init_test(cx, |settings| {
12881 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12882 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12883 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12884 Formatter::CodeAction("code-action-1".into()),
12885 Formatter::CodeAction("code-action-2".into()),
12886 ]))
12887 });
12888
12889 let fs = FakeFs::new(cx.executor());
12890 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12891 .await;
12892
12893 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12894 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12895 language_registry.add(rust_lang());
12896
12897 let mut fake_servers = language_registry.register_fake_lsp(
12898 "Rust",
12899 FakeLspAdapter {
12900 capabilities: lsp::ServerCapabilities {
12901 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12902 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12903 commands: vec!["the-command-for-code-action-1".into()],
12904 ..Default::default()
12905 }),
12906 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12907 ..Default::default()
12908 },
12909 ..Default::default()
12910 },
12911 );
12912
12913 let buffer = project
12914 .update(cx, |project, cx| {
12915 project.open_local_buffer(path!("/file.rs"), cx)
12916 })
12917 .await
12918 .unwrap();
12919
12920 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12921 let (editor, cx) = cx.add_window_view(|window, cx| {
12922 build_editor_with_project(project.clone(), buffer, window, cx)
12923 });
12924
12925 let fake_server = fake_servers.next().await.unwrap();
12926 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12927 move |_params, _| async move {
12928 Ok(Some(vec![lsp::TextEdit::new(
12929 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12930 "applied-formatting\n".to_string(),
12931 )]))
12932 },
12933 );
12934 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12935 move |params, _| async move {
12936 let requested_code_actions = params.context.only.expect("Expected code action request");
12937 assert_eq!(requested_code_actions.len(), 1);
12938
12939 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12940 let code_action = match requested_code_actions[0].as_str() {
12941 "code-action-1" => lsp::CodeAction {
12942 kind: Some("code-action-1".into()),
12943 edit: Some(lsp::WorkspaceEdit::new(
12944 [(
12945 uri,
12946 vec![lsp::TextEdit::new(
12947 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12948 "applied-code-action-1-edit\n".to_string(),
12949 )],
12950 )]
12951 .into_iter()
12952 .collect(),
12953 )),
12954 command: Some(lsp::Command {
12955 command: "the-command-for-code-action-1".into(),
12956 ..Default::default()
12957 }),
12958 ..Default::default()
12959 },
12960 "code-action-2" => lsp::CodeAction {
12961 kind: Some("code-action-2".into()),
12962 edit: Some(lsp::WorkspaceEdit::new(
12963 [(
12964 uri,
12965 vec![lsp::TextEdit::new(
12966 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12967 "applied-code-action-2-edit\n".to_string(),
12968 )],
12969 )]
12970 .into_iter()
12971 .collect(),
12972 )),
12973 ..Default::default()
12974 },
12975 req => panic!("Unexpected code action request: {:?}", req),
12976 };
12977 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12978 code_action,
12979 )]))
12980 },
12981 );
12982
12983 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12984 move |params, _| async move { Ok(params) }
12985 });
12986
12987 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12988 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12989 let fake = fake_server.clone();
12990 let lock = command_lock.clone();
12991 move |params, _| {
12992 assert_eq!(params.command, "the-command-for-code-action-1");
12993 let fake = fake.clone();
12994 let lock = lock.clone();
12995 async move {
12996 lock.lock().await;
12997 fake.server
12998 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12999 label: None,
13000 edit: lsp::WorkspaceEdit {
13001 changes: Some(
13002 [(
13003 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
13004 vec![lsp::TextEdit {
13005 range: lsp::Range::new(
13006 lsp::Position::new(0, 0),
13007 lsp::Position::new(0, 0),
13008 ),
13009 new_text: "applied-code-action-1-command\n".into(),
13010 }],
13011 )]
13012 .into_iter()
13013 .collect(),
13014 ),
13015 ..Default::default()
13016 },
13017 })
13018 .await
13019 .into_response()
13020 .unwrap();
13021 Ok(Some(json!(null)))
13022 }
13023 }
13024 });
13025
13026 editor
13027 .update_in(cx, |editor, window, cx| {
13028 editor.perform_format(
13029 project.clone(),
13030 FormatTrigger::Manual,
13031 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
13032 window,
13033 cx,
13034 )
13035 })
13036 .unwrap()
13037 .await;
13038 editor.update(cx, |editor, cx| {
13039 assert_eq!(
13040 editor.text(cx),
13041 r#"
13042 applied-code-action-2-edit
13043 applied-code-action-1-command
13044 applied-code-action-1-edit
13045 applied-formatting
13046 one
13047 two
13048 three
13049 "#
13050 .unindent()
13051 );
13052 });
13053
13054 editor.update_in(cx, |editor, window, cx| {
13055 editor.undo(&Default::default(), window, cx);
13056 assert_eq!(editor.text(cx), "one \ntwo \nthree");
13057 });
13058
13059 // Perform a manual edit while waiting for an LSP command
13060 // that's being run as part of a formatting code action.
13061 let lock_guard = command_lock.lock().await;
13062 let format = editor
13063 .update_in(cx, |editor, window, cx| {
13064 editor.perform_format(
13065 project.clone(),
13066 FormatTrigger::Manual,
13067 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
13068 window,
13069 cx,
13070 )
13071 })
13072 .unwrap();
13073 cx.run_until_parked();
13074 editor.update(cx, |editor, cx| {
13075 assert_eq!(
13076 editor.text(cx),
13077 r#"
13078 applied-code-action-1-edit
13079 applied-formatting
13080 one
13081 two
13082 three
13083 "#
13084 .unindent()
13085 );
13086
13087 editor.buffer.update(cx, |buffer, cx| {
13088 let ix = buffer.len(cx);
13089 buffer.edit([(ix..ix, "edited\n")], None, cx);
13090 });
13091 });
13092
13093 // Allow the LSP command to proceed. Because the buffer was edited,
13094 // the second code action will not be run.
13095 drop(lock_guard);
13096 format.await;
13097 editor.update_in(cx, |editor, window, cx| {
13098 assert_eq!(
13099 editor.text(cx),
13100 r#"
13101 applied-code-action-1-command
13102 applied-code-action-1-edit
13103 applied-formatting
13104 one
13105 two
13106 three
13107 edited
13108 "#
13109 .unindent()
13110 );
13111
13112 // The manual edit is undone first, because it is the last thing the user did
13113 // (even though the command completed afterwards).
13114 editor.undo(&Default::default(), window, cx);
13115 assert_eq!(
13116 editor.text(cx),
13117 r#"
13118 applied-code-action-1-command
13119 applied-code-action-1-edit
13120 applied-formatting
13121 one
13122 two
13123 three
13124 "#
13125 .unindent()
13126 );
13127
13128 // All the formatting (including the command, which completed after the manual edit)
13129 // is undone together.
13130 editor.undo(&Default::default(), window, cx);
13131 assert_eq!(editor.text(cx), "one \ntwo \nthree");
13132 });
13133}
13134
13135#[gpui::test]
13136async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
13137 init_test(cx, |settings| {
13138 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
13139 settings::LanguageServerFormatterSpecifier::Current,
13140 )]))
13141 });
13142
13143 let fs = FakeFs::new(cx.executor());
13144 fs.insert_file(path!("/file.ts"), Default::default()).await;
13145
13146 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13147
13148 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13149 language_registry.add(Arc::new(Language::new(
13150 LanguageConfig {
13151 name: "TypeScript".into(),
13152 matcher: LanguageMatcher {
13153 path_suffixes: vec!["ts".to_string()],
13154 ..Default::default()
13155 },
13156 ..LanguageConfig::default()
13157 },
13158 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13159 )));
13160 update_test_language_settings(cx, |settings| {
13161 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
13162 });
13163 let mut fake_servers = language_registry.register_fake_lsp(
13164 "TypeScript",
13165 FakeLspAdapter {
13166 capabilities: lsp::ServerCapabilities {
13167 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
13168 ..Default::default()
13169 },
13170 ..Default::default()
13171 },
13172 );
13173
13174 let buffer = project
13175 .update(cx, |project, cx| {
13176 project.open_local_buffer(path!("/file.ts"), cx)
13177 })
13178 .await
13179 .unwrap();
13180
13181 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13182 let (editor, cx) = cx.add_window_view(|window, cx| {
13183 build_editor_with_project(project.clone(), buffer, window, cx)
13184 });
13185 editor.update_in(cx, |editor, window, cx| {
13186 editor.set_text(
13187 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13188 window,
13189 cx,
13190 )
13191 });
13192
13193 let fake_server = fake_servers.next().await.unwrap();
13194
13195 let format = editor
13196 .update_in(cx, |editor, window, cx| {
13197 editor.perform_code_action_kind(
13198 project.clone(),
13199 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13200 window,
13201 cx,
13202 )
13203 })
13204 .unwrap();
13205 fake_server
13206 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
13207 assert_eq!(
13208 params.text_document.uri,
13209 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13210 );
13211 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
13212 lsp::CodeAction {
13213 title: "Organize Imports".to_string(),
13214 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
13215 edit: Some(lsp::WorkspaceEdit {
13216 changes: Some(
13217 [(
13218 params.text_document.uri.clone(),
13219 vec![lsp::TextEdit::new(
13220 lsp::Range::new(
13221 lsp::Position::new(1, 0),
13222 lsp::Position::new(2, 0),
13223 ),
13224 "".to_string(),
13225 )],
13226 )]
13227 .into_iter()
13228 .collect(),
13229 ),
13230 ..Default::default()
13231 }),
13232 ..Default::default()
13233 },
13234 )]))
13235 })
13236 .next()
13237 .await;
13238 format.await;
13239 assert_eq!(
13240 editor.update(cx, |editor, cx| editor.text(cx)),
13241 "import { a } from 'module';\n\nconst x = a;\n"
13242 );
13243
13244 editor.update_in(cx, |editor, window, cx| {
13245 editor.set_text(
13246 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13247 window,
13248 cx,
13249 )
13250 });
13251 // Ensure we don't lock if code action hangs.
13252 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
13253 move |params, _| async move {
13254 assert_eq!(
13255 params.text_document.uri,
13256 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13257 );
13258 futures::future::pending::<()>().await;
13259 unreachable!()
13260 },
13261 );
13262 let format = editor
13263 .update_in(cx, |editor, window, cx| {
13264 editor.perform_code_action_kind(
13265 project,
13266 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13267 window,
13268 cx,
13269 )
13270 })
13271 .unwrap();
13272 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
13273 format.await;
13274 assert_eq!(
13275 editor.update(cx, |editor, cx| editor.text(cx)),
13276 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
13277 );
13278}
13279
13280#[gpui::test]
13281async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
13282 init_test(cx, |_| {});
13283
13284 let mut cx = EditorLspTestContext::new_rust(
13285 lsp::ServerCapabilities {
13286 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13287 ..Default::default()
13288 },
13289 cx,
13290 )
13291 .await;
13292
13293 cx.set_state(indoc! {"
13294 one.twoˇ
13295 "});
13296
13297 // The format request takes a long time. When it completes, it inserts
13298 // a newline and an indent before the `.`
13299 cx.lsp
13300 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
13301 let executor = cx.background_executor().clone();
13302 async move {
13303 executor.timer(Duration::from_millis(100)).await;
13304 Ok(Some(vec![lsp::TextEdit {
13305 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
13306 new_text: "\n ".into(),
13307 }]))
13308 }
13309 });
13310
13311 // Submit a format request.
13312 let format_1 = cx
13313 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13314 .unwrap();
13315 cx.executor().run_until_parked();
13316
13317 // Submit a second format request.
13318 let format_2 = cx
13319 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13320 .unwrap();
13321 cx.executor().run_until_parked();
13322
13323 // Wait for both format requests to complete
13324 cx.executor().advance_clock(Duration::from_millis(200));
13325 format_1.await.unwrap();
13326 format_2.await.unwrap();
13327
13328 // The formatting edits only happens once.
13329 cx.assert_editor_state(indoc! {"
13330 one
13331 .twoˇ
13332 "});
13333}
13334
13335#[gpui::test]
13336async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
13337 init_test(cx, |settings| {
13338 settings.defaults.formatter = Some(FormatterList::default())
13339 });
13340
13341 let mut cx = EditorLspTestContext::new_rust(
13342 lsp::ServerCapabilities {
13343 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13344 ..Default::default()
13345 },
13346 cx,
13347 )
13348 .await;
13349
13350 // Record which buffer changes have been sent to the language server
13351 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
13352 cx.lsp
13353 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
13354 let buffer_changes = buffer_changes.clone();
13355 move |params, _| {
13356 buffer_changes.lock().extend(
13357 params
13358 .content_changes
13359 .into_iter()
13360 .map(|e| (e.range.unwrap(), e.text)),
13361 );
13362 }
13363 });
13364 // Handle formatting requests to the language server.
13365 cx.lsp
13366 .set_request_handler::<lsp::request::Formatting, _, _>({
13367 move |_, _| {
13368 // Insert blank lines between each line of the buffer.
13369 async move {
13370 // TODO: this assertion is not reliably true. Currently nothing guarantees that we deliver
13371 // DidChangedTextDocument to the LSP before sending the formatting request.
13372 // assert_eq!(
13373 // &buffer_changes.lock()[1..],
13374 // &[
13375 // (
13376 // lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13377 // "".into()
13378 // ),
13379 // (
13380 // lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13381 // "".into()
13382 // ),
13383 // (
13384 // lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13385 // "\n".into()
13386 // ),
13387 // ]
13388 // );
13389
13390 Ok(Some(vec![
13391 lsp::TextEdit {
13392 range: lsp::Range::new(
13393 lsp::Position::new(1, 0),
13394 lsp::Position::new(1, 0),
13395 ),
13396 new_text: "\n".into(),
13397 },
13398 lsp::TextEdit {
13399 range: lsp::Range::new(
13400 lsp::Position::new(2, 0),
13401 lsp::Position::new(2, 0),
13402 ),
13403 new_text: "\n".into(),
13404 },
13405 ]))
13406 }
13407 }
13408 });
13409
13410 // Set up a buffer white some trailing whitespace and no trailing newline.
13411 cx.set_state(
13412 &[
13413 "one ", //
13414 "twoˇ", //
13415 "three ", //
13416 "four", //
13417 ]
13418 .join("\n"),
13419 );
13420
13421 // Submit a format request.
13422 let format = cx
13423 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13424 .unwrap();
13425
13426 cx.run_until_parked();
13427 // After formatting the buffer, the trailing whitespace is stripped,
13428 // a newline is appended, and the edits provided by the language server
13429 // have been applied.
13430 format.await.unwrap();
13431
13432 cx.assert_editor_state(
13433 &[
13434 "one", //
13435 "", //
13436 "twoˇ", //
13437 "", //
13438 "three", //
13439 "four", //
13440 "", //
13441 ]
13442 .join("\n"),
13443 );
13444
13445 // Undoing the formatting undoes the trailing whitespace removal, the
13446 // trailing newline, and the LSP edits.
13447 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13448 cx.assert_editor_state(
13449 &[
13450 "one ", //
13451 "twoˇ", //
13452 "three ", //
13453 "four", //
13454 ]
13455 .join("\n"),
13456 );
13457}
13458
13459#[gpui::test]
13460async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13461 cx: &mut TestAppContext,
13462) {
13463 init_test(cx, |_| {});
13464
13465 cx.update(|cx| {
13466 cx.update_global::<SettingsStore, _>(|settings, cx| {
13467 settings.update_user_settings(cx, |settings| {
13468 settings.editor.auto_signature_help = Some(true);
13469 });
13470 });
13471 });
13472
13473 let mut cx = EditorLspTestContext::new_rust(
13474 lsp::ServerCapabilities {
13475 signature_help_provider: Some(lsp::SignatureHelpOptions {
13476 ..Default::default()
13477 }),
13478 ..Default::default()
13479 },
13480 cx,
13481 )
13482 .await;
13483
13484 let language = Language::new(
13485 LanguageConfig {
13486 name: "Rust".into(),
13487 brackets: BracketPairConfig {
13488 pairs: vec![
13489 BracketPair {
13490 start: "{".to_string(),
13491 end: "}".to_string(),
13492 close: true,
13493 surround: true,
13494 newline: true,
13495 },
13496 BracketPair {
13497 start: "(".to_string(),
13498 end: ")".to_string(),
13499 close: true,
13500 surround: true,
13501 newline: true,
13502 },
13503 BracketPair {
13504 start: "/*".to_string(),
13505 end: " */".to_string(),
13506 close: true,
13507 surround: true,
13508 newline: true,
13509 },
13510 BracketPair {
13511 start: "[".to_string(),
13512 end: "]".to_string(),
13513 close: false,
13514 surround: false,
13515 newline: true,
13516 },
13517 BracketPair {
13518 start: "\"".to_string(),
13519 end: "\"".to_string(),
13520 close: true,
13521 surround: true,
13522 newline: false,
13523 },
13524 BracketPair {
13525 start: "<".to_string(),
13526 end: ">".to_string(),
13527 close: false,
13528 surround: true,
13529 newline: true,
13530 },
13531 ],
13532 ..Default::default()
13533 },
13534 autoclose_before: "})]".to_string(),
13535 ..Default::default()
13536 },
13537 Some(tree_sitter_rust::LANGUAGE.into()),
13538 );
13539 let language = Arc::new(language);
13540
13541 cx.language_registry().add(language.clone());
13542 cx.update_buffer(|buffer, cx| {
13543 buffer.set_language(Some(language), cx);
13544 });
13545
13546 cx.set_state(
13547 &r#"
13548 fn main() {
13549 sampleˇ
13550 }
13551 "#
13552 .unindent(),
13553 );
13554
13555 cx.update_editor(|editor, window, cx| {
13556 editor.handle_input("(", window, cx);
13557 });
13558 cx.assert_editor_state(
13559 &"
13560 fn main() {
13561 sample(ˇ)
13562 }
13563 "
13564 .unindent(),
13565 );
13566
13567 let mocked_response = lsp::SignatureHelp {
13568 signatures: vec![lsp::SignatureInformation {
13569 label: "fn sample(param1: u8, param2: u8)".to_string(),
13570 documentation: None,
13571 parameters: Some(vec![
13572 lsp::ParameterInformation {
13573 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13574 documentation: None,
13575 },
13576 lsp::ParameterInformation {
13577 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13578 documentation: None,
13579 },
13580 ]),
13581 active_parameter: None,
13582 }],
13583 active_signature: Some(0),
13584 active_parameter: Some(0),
13585 };
13586 handle_signature_help_request(&mut cx, mocked_response).await;
13587
13588 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13589 .await;
13590
13591 cx.editor(|editor, _, _| {
13592 let signature_help_state = editor.signature_help_state.popover().cloned();
13593 let signature = signature_help_state.unwrap();
13594 assert_eq!(
13595 signature.signatures[signature.current_signature].label,
13596 "fn sample(param1: u8, param2: u8)"
13597 );
13598 });
13599}
13600
13601#[gpui::test]
13602async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13603 init_test(cx, |_| {});
13604
13605 cx.update(|cx| {
13606 cx.update_global::<SettingsStore, _>(|settings, cx| {
13607 settings.update_user_settings(cx, |settings| {
13608 settings.editor.auto_signature_help = Some(false);
13609 settings.editor.show_signature_help_after_edits = Some(false);
13610 });
13611 });
13612 });
13613
13614 let mut cx = EditorLspTestContext::new_rust(
13615 lsp::ServerCapabilities {
13616 signature_help_provider: Some(lsp::SignatureHelpOptions {
13617 ..Default::default()
13618 }),
13619 ..Default::default()
13620 },
13621 cx,
13622 )
13623 .await;
13624
13625 let language = Language::new(
13626 LanguageConfig {
13627 name: "Rust".into(),
13628 brackets: BracketPairConfig {
13629 pairs: vec![
13630 BracketPair {
13631 start: "{".to_string(),
13632 end: "}".to_string(),
13633 close: true,
13634 surround: true,
13635 newline: true,
13636 },
13637 BracketPair {
13638 start: "(".to_string(),
13639 end: ")".to_string(),
13640 close: true,
13641 surround: true,
13642 newline: true,
13643 },
13644 BracketPair {
13645 start: "/*".to_string(),
13646 end: " */".to_string(),
13647 close: true,
13648 surround: true,
13649 newline: true,
13650 },
13651 BracketPair {
13652 start: "[".to_string(),
13653 end: "]".to_string(),
13654 close: false,
13655 surround: false,
13656 newline: true,
13657 },
13658 BracketPair {
13659 start: "\"".to_string(),
13660 end: "\"".to_string(),
13661 close: true,
13662 surround: true,
13663 newline: false,
13664 },
13665 BracketPair {
13666 start: "<".to_string(),
13667 end: ">".to_string(),
13668 close: false,
13669 surround: true,
13670 newline: true,
13671 },
13672 ],
13673 ..Default::default()
13674 },
13675 autoclose_before: "})]".to_string(),
13676 ..Default::default()
13677 },
13678 Some(tree_sitter_rust::LANGUAGE.into()),
13679 );
13680 let language = Arc::new(language);
13681
13682 cx.language_registry().add(language.clone());
13683 cx.update_buffer(|buffer, cx| {
13684 buffer.set_language(Some(language), cx);
13685 });
13686
13687 // Ensure that signature_help is not called when no signature help is enabled.
13688 cx.set_state(
13689 &r#"
13690 fn main() {
13691 sampleˇ
13692 }
13693 "#
13694 .unindent(),
13695 );
13696 cx.update_editor(|editor, window, cx| {
13697 editor.handle_input("(", window, cx);
13698 });
13699 cx.assert_editor_state(
13700 &"
13701 fn main() {
13702 sample(ˇ)
13703 }
13704 "
13705 .unindent(),
13706 );
13707 cx.editor(|editor, _, _| {
13708 assert!(editor.signature_help_state.task().is_none());
13709 });
13710
13711 let mocked_response = lsp::SignatureHelp {
13712 signatures: vec![lsp::SignatureInformation {
13713 label: "fn sample(param1: u8, param2: u8)".to_string(),
13714 documentation: None,
13715 parameters: Some(vec![
13716 lsp::ParameterInformation {
13717 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13718 documentation: None,
13719 },
13720 lsp::ParameterInformation {
13721 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13722 documentation: None,
13723 },
13724 ]),
13725 active_parameter: None,
13726 }],
13727 active_signature: Some(0),
13728 active_parameter: Some(0),
13729 };
13730
13731 // Ensure that signature_help is called when enabled afte edits
13732 cx.update(|_, cx| {
13733 cx.update_global::<SettingsStore, _>(|settings, cx| {
13734 settings.update_user_settings(cx, |settings| {
13735 settings.editor.auto_signature_help = Some(false);
13736 settings.editor.show_signature_help_after_edits = Some(true);
13737 });
13738 });
13739 });
13740 cx.set_state(
13741 &r#"
13742 fn main() {
13743 sampleˇ
13744 }
13745 "#
13746 .unindent(),
13747 );
13748 cx.update_editor(|editor, window, cx| {
13749 editor.handle_input("(", window, cx);
13750 });
13751 cx.assert_editor_state(
13752 &"
13753 fn main() {
13754 sample(ˇ)
13755 }
13756 "
13757 .unindent(),
13758 );
13759 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13760 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13761 .await;
13762 cx.update_editor(|editor, _, _| {
13763 let signature_help_state = editor.signature_help_state.popover().cloned();
13764 assert!(signature_help_state.is_some());
13765 let signature = signature_help_state.unwrap();
13766 assert_eq!(
13767 signature.signatures[signature.current_signature].label,
13768 "fn sample(param1: u8, param2: u8)"
13769 );
13770 editor.signature_help_state = SignatureHelpState::default();
13771 });
13772
13773 // Ensure that signature_help is called when auto signature help override is enabled
13774 cx.update(|_, cx| {
13775 cx.update_global::<SettingsStore, _>(|settings, cx| {
13776 settings.update_user_settings(cx, |settings| {
13777 settings.editor.auto_signature_help = Some(true);
13778 settings.editor.show_signature_help_after_edits = Some(false);
13779 });
13780 });
13781 });
13782 cx.set_state(
13783 &r#"
13784 fn main() {
13785 sampleˇ
13786 }
13787 "#
13788 .unindent(),
13789 );
13790 cx.update_editor(|editor, window, cx| {
13791 editor.handle_input("(", window, cx);
13792 });
13793 cx.assert_editor_state(
13794 &"
13795 fn main() {
13796 sample(ˇ)
13797 }
13798 "
13799 .unindent(),
13800 );
13801 handle_signature_help_request(&mut cx, mocked_response).await;
13802 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13803 .await;
13804 cx.editor(|editor, _, _| {
13805 let signature_help_state = editor.signature_help_state.popover().cloned();
13806 assert!(signature_help_state.is_some());
13807 let signature = signature_help_state.unwrap();
13808 assert_eq!(
13809 signature.signatures[signature.current_signature].label,
13810 "fn sample(param1: u8, param2: u8)"
13811 );
13812 });
13813}
13814
13815#[gpui::test]
13816async fn test_signature_help(cx: &mut TestAppContext) {
13817 init_test(cx, |_| {});
13818 cx.update(|cx| {
13819 cx.update_global::<SettingsStore, _>(|settings, cx| {
13820 settings.update_user_settings(cx, |settings| {
13821 settings.editor.auto_signature_help = Some(true);
13822 });
13823 });
13824 });
13825
13826 let mut cx = EditorLspTestContext::new_rust(
13827 lsp::ServerCapabilities {
13828 signature_help_provider: Some(lsp::SignatureHelpOptions {
13829 ..Default::default()
13830 }),
13831 ..Default::default()
13832 },
13833 cx,
13834 )
13835 .await;
13836
13837 // A test that directly calls `show_signature_help`
13838 cx.update_editor(|editor, window, cx| {
13839 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13840 });
13841
13842 let mocked_response = lsp::SignatureHelp {
13843 signatures: vec![lsp::SignatureInformation {
13844 label: "fn sample(param1: u8, param2: u8)".to_string(),
13845 documentation: None,
13846 parameters: Some(vec![
13847 lsp::ParameterInformation {
13848 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13849 documentation: None,
13850 },
13851 lsp::ParameterInformation {
13852 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13853 documentation: None,
13854 },
13855 ]),
13856 active_parameter: None,
13857 }],
13858 active_signature: Some(0),
13859 active_parameter: Some(0),
13860 };
13861 handle_signature_help_request(&mut cx, mocked_response).await;
13862
13863 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13864 .await;
13865
13866 cx.editor(|editor, _, _| {
13867 let signature_help_state = editor.signature_help_state.popover().cloned();
13868 assert!(signature_help_state.is_some());
13869 let signature = signature_help_state.unwrap();
13870 assert_eq!(
13871 signature.signatures[signature.current_signature].label,
13872 "fn sample(param1: u8, param2: u8)"
13873 );
13874 });
13875
13876 // When exiting outside from inside the brackets, `signature_help` is closed.
13877 cx.set_state(indoc! {"
13878 fn main() {
13879 sample(ˇ);
13880 }
13881
13882 fn sample(param1: u8, param2: u8) {}
13883 "});
13884
13885 cx.update_editor(|editor, window, cx| {
13886 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13887 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13888 });
13889 });
13890
13891 let mocked_response = lsp::SignatureHelp {
13892 signatures: Vec::new(),
13893 active_signature: None,
13894 active_parameter: None,
13895 };
13896 handle_signature_help_request(&mut cx, mocked_response).await;
13897
13898 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13899 .await;
13900
13901 cx.editor(|editor, _, _| {
13902 assert!(!editor.signature_help_state.is_shown());
13903 });
13904
13905 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13906 cx.set_state(indoc! {"
13907 fn main() {
13908 sample(ˇ);
13909 }
13910
13911 fn sample(param1: u8, param2: u8) {}
13912 "});
13913
13914 let mocked_response = lsp::SignatureHelp {
13915 signatures: vec![lsp::SignatureInformation {
13916 label: "fn sample(param1: u8, param2: u8)".to_string(),
13917 documentation: None,
13918 parameters: Some(vec![
13919 lsp::ParameterInformation {
13920 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13921 documentation: None,
13922 },
13923 lsp::ParameterInformation {
13924 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13925 documentation: None,
13926 },
13927 ]),
13928 active_parameter: None,
13929 }],
13930 active_signature: Some(0),
13931 active_parameter: Some(0),
13932 };
13933 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13934 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13935 .await;
13936 cx.editor(|editor, _, _| {
13937 assert!(editor.signature_help_state.is_shown());
13938 });
13939
13940 // Restore the popover with more parameter input
13941 cx.set_state(indoc! {"
13942 fn main() {
13943 sample(param1, param2ˇ);
13944 }
13945
13946 fn sample(param1: u8, param2: u8) {}
13947 "});
13948
13949 let mocked_response = lsp::SignatureHelp {
13950 signatures: vec![lsp::SignatureInformation {
13951 label: "fn sample(param1: u8, param2: u8)".to_string(),
13952 documentation: None,
13953 parameters: Some(vec![
13954 lsp::ParameterInformation {
13955 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13956 documentation: None,
13957 },
13958 lsp::ParameterInformation {
13959 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13960 documentation: None,
13961 },
13962 ]),
13963 active_parameter: None,
13964 }],
13965 active_signature: Some(0),
13966 active_parameter: Some(1),
13967 };
13968 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13969 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13970 .await;
13971
13972 // When selecting a range, the popover is gone.
13973 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13974 cx.update_editor(|editor, window, cx| {
13975 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13976 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13977 })
13978 });
13979 cx.assert_editor_state(indoc! {"
13980 fn main() {
13981 sample(param1, «ˇparam2»);
13982 }
13983
13984 fn sample(param1: u8, param2: u8) {}
13985 "});
13986 cx.editor(|editor, _, _| {
13987 assert!(!editor.signature_help_state.is_shown());
13988 });
13989
13990 // When unselecting again, the popover is back if within the brackets.
13991 cx.update_editor(|editor, window, cx| {
13992 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13993 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13994 })
13995 });
13996 cx.assert_editor_state(indoc! {"
13997 fn main() {
13998 sample(param1, ˇparam2);
13999 }
14000
14001 fn sample(param1: u8, param2: u8) {}
14002 "});
14003 handle_signature_help_request(&mut cx, mocked_response).await;
14004 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14005 .await;
14006 cx.editor(|editor, _, _| {
14007 assert!(editor.signature_help_state.is_shown());
14008 });
14009
14010 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
14011 cx.update_editor(|editor, window, cx| {
14012 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14013 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
14014 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14015 })
14016 });
14017 cx.assert_editor_state(indoc! {"
14018 fn main() {
14019 sample(param1, ˇparam2);
14020 }
14021
14022 fn sample(param1: u8, param2: u8) {}
14023 "});
14024
14025 let mocked_response = lsp::SignatureHelp {
14026 signatures: vec![lsp::SignatureInformation {
14027 label: "fn sample(param1: u8, param2: u8)".to_string(),
14028 documentation: None,
14029 parameters: Some(vec![
14030 lsp::ParameterInformation {
14031 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14032 documentation: None,
14033 },
14034 lsp::ParameterInformation {
14035 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14036 documentation: None,
14037 },
14038 ]),
14039 active_parameter: None,
14040 }],
14041 active_signature: Some(0),
14042 active_parameter: Some(1),
14043 };
14044 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14045 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14046 .await;
14047 cx.update_editor(|editor, _, cx| {
14048 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
14049 });
14050 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
14051 .await;
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.update_editor(|editor, window, cx| {
14065 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14066 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14067 })
14068 });
14069 cx.assert_editor_state(indoc! {"
14070 fn main() {
14071 sample(param1, ˇparam2);
14072 }
14073
14074 fn sample(param1: u8, param2: u8) {}
14075 "});
14076 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
14077 .await;
14078}
14079
14080#[gpui::test]
14081async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
14082 init_test(cx, |_| {});
14083
14084 let mut cx = EditorLspTestContext::new_rust(
14085 lsp::ServerCapabilities {
14086 signature_help_provider: Some(lsp::SignatureHelpOptions {
14087 ..Default::default()
14088 }),
14089 ..Default::default()
14090 },
14091 cx,
14092 )
14093 .await;
14094
14095 cx.set_state(indoc! {"
14096 fn main() {
14097 overloadedˇ
14098 }
14099 "});
14100
14101 cx.update_editor(|editor, window, cx| {
14102 editor.handle_input("(", window, cx);
14103 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14104 });
14105
14106 // Mock response with 3 signatures
14107 let mocked_response = lsp::SignatureHelp {
14108 signatures: vec![
14109 lsp::SignatureInformation {
14110 label: "fn overloaded(x: i32)".to_string(),
14111 documentation: None,
14112 parameters: Some(vec![lsp::ParameterInformation {
14113 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14114 documentation: None,
14115 }]),
14116 active_parameter: None,
14117 },
14118 lsp::SignatureInformation {
14119 label: "fn overloaded(x: i32, y: i32)".to_string(),
14120 documentation: None,
14121 parameters: Some(vec![
14122 lsp::ParameterInformation {
14123 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14124 documentation: None,
14125 },
14126 lsp::ParameterInformation {
14127 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14128 documentation: None,
14129 },
14130 ]),
14131 active_parameter: None,
14132 },
14133 lsp::SignatureInformation {
14134 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
14135 documentation: None,
14136 parameters: Some(vec![
14137 lsp::ParameterInformation {
14138 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14139 documentation: None,
14140 },
14141 lsp::ParameterInformation {
14142 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14143 documentation: None,
14144 },
14145 lsp::ParameterInformation {
14146 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
14147 documentation: None,
14148 },
14149 ]),
14150 active_parameter: None,
14151 },
14152 ],
14153 active_signature: Some(1),
14154 active_parameter: Some(0),
14155 };
14156 handle_signature_help_request(&mut cx, mocked_response).await;
14157
14158 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14159 .await;
14160
14161 // Verify we have multiple signatures and the right one is selected
14162 cx.editor(|editor, _, _| {
14163 let popover = editor.signature_help_state.popover().cloned().unwrap();
14164 assert_eq!(popover.signatures.len(), 3);
14165 // active_signature was 1, so that should be the current
14166 assert_eq!(popover.current_signature, 1);
14167 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
14168 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
14169 assert_eq!(
14170 popover.signatures[2].label,
14171 "fn overloaded(x: i32, y: i32, z: i32)"
14172 );
14173 });
14174
14175 // Test navigation functionality
14176 cx.update_editor(|editor, window, cx| {
14177 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14178 });
14179
14180 cx.editor(|editor, _, _| {
14181 let popover = editor.signature_help_state.popover().cloned().unwrap();
14182 assert_eq!(popover.current_signature, 2);
14183 });
14184
14185 // Test wrap around
14186 cx.update_editor(|editor, window, cx| {
14187 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14188 });
14189
14190 cx.editor(|editor, _, _| {
14191 let popover = editor.signature_help_state.popover().cloned().unwrap();
14192 assert_eq!(popover.current_signature, 0);
14193 });
14194
14195 // Test previous navigation
14196 cx.update_editor(|editor, window, cx| {
14197 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
14198 });
14199
14200 cx.editor(|editor, _, _| {
14201 let popover = editor.signature_help_state.popover().cloned().unwrap();
14202 assert_eq!(popover.current_signature, 2);
14203 });
14204}
14205
14206#[gpui::test]
14207async fn test_completion_mode(cx: &mut TestAppContext) {
14208 init_test(cx, |_| {});
14209 let mut cx = EditorLspTestContext::new_rust(
14210 lsp::ServerCapabilities {
14211 completion_provider: Some(lsp::CompletionOptions {
14212 resolve_provider: Some(true),
14213 ..Default::default()
14214 }),
14215 ..Default::default()
14216 },
14217 cx,
14218 )
14219 .await;
14220
14221 struct Run {
14222 run_description: &'static str,
14223 initial_state: String,
14224 buffer_marked_text: String,
14225 completion_label: &'static str,
14226 completion_text: &'static str,
14227 expected_with_insert_mode: String,
14228 expected_with_replace_mode: String,
14229 expected_with_replace_subsequence_mode: String,
14230 expected_with_replace_suffix_mode: String,
14231 }
14232
14233 let runs = [
14234 Run {
14235 run_description: "Start of word matches completion text",
14236 initial_state: "before ediˇ after".into(),
14237 buffer_marked_text: "before <edi|> after".into(),
14238 completion_label: "editor",
14239 completion_text: "editor",
14240 expected_with_insert_mode: "before editorˇ after".into(),
14241 expected_with_replace_mode: "before editorˇ after".into(),
14242 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14243 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14244 },
14245 Run {
14246 run_description: "Accept same text at the middle of the word",
14247 initial_state: "before ediˇtor after".into(),
14248 buffer_marked_text: "before <edi|tor> after".into(),
14249 completion_label: "editor",
14250 completion_text: "editor",
14251 expected_with_insert_mode: "before editorˇtor after".into(),
14252 expected_with_replace_mode: "before editorˇ after".into(),
14253 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14254 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14255 },
14256 Run {
14257 run_description: "End of word matches completion text -- cursor at end",
14258 initial_state: "before torˇ after".into(),
14259 buffer_marked_text: "before <tor|> after".into(),
14260 completion_label: "editor",
14261 completion_text: "editor",
14262 expected_with_insert_mode: "before editorˇ after".into(),
14263 expected_with_replace_mode: "before editorˇ after".into(),
14264 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14265 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14266 },
14267 Run {
14268 run_description: "End of word matches completion text -- cursor at start",
14269 initial_state: "before ˇtor after".into(),
14270 buffer_marked_text: "before <|tor> after".into(),
14271 completion_label: "editor",
14272 completion_text: "editor",
14273 expected_with_insert_mode: "before editorˇtor after".into(),
14274 expected_with_replace_mode: "before editorˇ after".into(),
14275 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14276 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14277 },
14278 Run {
14279 run_description: "Prepend text containing whitespace",
14280 initial_state: "pˇfield: bool".into(),
14281 buffer_marked_text: "<p|field>: bool".into(),
14282 completion_label: "pub ",
14283 completion_text: "pub ",
14284 expected_with_insert_mode: "pub ˇfield: bool".into(),
14285 expected_with_replace_mode: "pub ˇ: bool".into(),
14286 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
14287 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
14288 },
14289 Run {
14290 run_description: "Add element to start of list",
14291 initial_state: "[element_ˇelement_2]".into(),
14292 buffer_marked_text: "[<element_|element_2>]".into(),
14293 completion_label: "element_1",
14294 completion_text: "element_1",
14295 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
14296 expected_with_replace_mode: "[element_1ˇ]".into(),
14297 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
14298 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
14299 },
14300 Run {
14301 run_description: "Add element to start of list -- first and second elements are equal",
14302 initial_state: "[elˇelement]".into(),
14303 buffer_marked_text: "[<el|element>]".into(),
14304 completion_label: "element",
14305 completion_text: "element",
14306 expected_with_insert_mode: "[elementˇelement]".into(),
14307 expected_with_replace_mode: "[elementˇ]".into(),
14308 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
14309 expected_with_replace_suffix_mode: "[elementˇ]".into(),
14310 },
14311 Run {
14312 run_description: "Ends with matching suffix",
14313 initial_state: "SubˇError".into(),
14314 buffer_marked_text: "<Sub|Error>".into(),
14315 completion_label: "SubscriptionError",
14316 completion_text: "SubscriptionError",
14317 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
14318 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14319 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14320 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
14321 },
14322 Run {
14323 run_description: "Suffix is a subsequence -- contiguous",
14324 initial_state: "SubˇErr".into(),
14325 buffer_marked_text: "<Sub|Err>".into(),
14326 completion_label: "SubscriptionError",
14327 completion_text: "SubscriptionError",
14328 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
14329 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14330 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14331 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
14332 },
14333 Run {
14334 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
14335 initial_state: "Suˇscrirr".into(),
14336 buffer_marked_text: "<Su|scrirr>".into(),
14337 completion_label: "SubscriptionError",
14338 completion_text: "SubscriptionError",
14339 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
14340 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14341 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14342 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
14343 },
14344 Run {
14345 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
14346 initial_state: "foo(indˇix)".into(),
14347 buffer_marked_text: "foo(<ind|ix>)".into(),
14348 completion_label: "node_index",
14349 completion_text: "node_index",
14350 expected_with_insert_mode: "foo(node_indexˇix)".into(),
14351 expected_with_replace_mode: "foo(node_indexˇ)".into(),
14352 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
14353 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
14354 },
14355 Run {
14356 run_description: "Replace range ends before cursor - should extend to cursor",
14357 initial_state: "before editˇo after".into(),
14358 buffer_marked_text: "before <{ed}>it|o after".into(),
14359 completion_label: "editor",
14360 completion_text: "editor",
14361 expected_with_insert_mode: "before editorˇo after".into(),
14362 expected_with_replace_mode: "before editorˇo after".into(),
14363 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
14364 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14365 },
14366 Run {
14367 run_description: "Uses label for suffix matching",
14368 initial_state: "before ediˇtor after".into(),
14369 buffer_marked_text: "before <edi|tor> after".into(),
14370 completion_label: "editor",
14371 completion_text: "editor()",
14372 expected_with_insert_mode: "before editor()ˇtor after".into(),
14373 expected_with_replace_mode: "before editor()ˇ after".into(),
14374 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14375 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14376 },
14377 Run {
14378 run_description: "Case insensitive subsequence and suffix matching",
14379 initial_state: "before EDiˇtoR after".into(),
14380 buffer_marked_text: "before <EDi|toR> after".into(),
14381 completion_label: "editor",
14382 completion_text: "editor",
14383 expected_with_insert_mode: "before editorˇtoR after".into(),
14384 expected_with_replace_mode: "before editorˇ after".into(),
14385 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14386 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14387 },
14388 ];
14389
14390 for run in runs {
14391 let run_variations = [
14392 (LspInsertMode::Insert, run.expected_with_insert_mode),
14393 (LspInsertMode::Replace, run.expected_with_replace_mode),
14394 (
14395 LspInsertMode::ReplaceSubsequence,
14396 run.expected_with_replace_subsequence_mode,
14397 ),
14398 (
14399 LspInsertMode::ReplaceSuffix,
14400 run.expected_with_replace_suffix_mode,
14401 ),
14402 ];
14403
14404 for (lsp_insert_mode, expected_text) in run_variations {
14405 eprintln!(
14406 "run = {:?}, mode = {lsp_insert_mode:.?}",
14407 run.run_description,
14408 );
14409
14410 update_test_language_settings(&mut cx, |settings| {
14411 settings.defaults.completions = Some(CompletionSettingsContent {
14412 lsp_insert_mode: Some(lsp_insert_mode),
14413 words: Some(WordsCompletionMode::Disabled),
14414 words_min_length: Some(0),
14415 ..Default::default()
14416 });
14417 });
14418
14419 cx.set_state(&run.initial_state);
14420
14421 // Set up resolve handler before showing completions, since resolve may be
14422 // triggered when menu becomes visible (for documentation), not just on confirm.
14423 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(
14424 move |_, _, _| async move {
14425 Ok(lsp::CompletionItem {
14426 additional_text_edits: None,
14427 ..Default::default()
14428 })
14429 },
14430 );
14431
14432 cx.update_editor(|editor, window, cx| {
14433 editor.show_completions(&ShowCompletions, window, cx);
14434 });
14435
14436 let counter = Arc::new(AtomicUsize::new(0));
14437 handle_completion_request_with_insert_and_replace(
14438 &mut cx,
14439 &run.buffer_marked_text,
14440 vec![(run.completion_label, run.completion_text)],
14441 counter.clone(),
14442 )
14443 .await;
14444 cx.condition(|editor, _| editor.context_menu_visible())
14445 .await;
14446 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14447
14448 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14449 editor
14450 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14451 .unwrap()
14452 });
14453 cx.assert_editor_state(&expected_text);
14454 apply_additional_edits.await.unwrap();
14455 }
14456 }
14457}
14458
14459#[gpui::test]
14460async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14461 init_test(cx, |_| {});
14462 let mut cx = EditorLspTestContext::new_rust(
14463 lsp::ServerCapabilities {
14464 completion_provider: Some(lsp::CompletionOptions {
14465 resolve_provider: Some(true),
14466 ..Default::default()
14467 }),
14468 ..Default::default()
14469 },
14470 cx,
14471 )
14472 .await;
14473
14474 let initial_state = "SubˇError";
14475 let buffer_marked_text = "<Sub|Error>";
14476 let completion_text = "SubscriptionError";
14477 let expected_with_insert_mode = "SubscriptionErrorˇError";
14478 let expected_with_replace_mode = "SubscriptionErrorˇ";
14479
14480 update_test_language_settings(&mut cx, |settings| {
14481 settings.defaults.completions = Some(CompletionSettingsContent {
14482 words: Some(WordsCompletionMode::Disabled),
14483 words_min_length: Some(0),
14484 // set the opposite here to ensure that the action is overriding the default behavior
14485 lsp_insert_mode: Some(LspInsertMode::Insert),
14486 ..Default::default()
14487 });
14488 });
14489
14490 cx.set_state(initial_state);
14491 cx.update_editor(|editor, window, cx| {
14492 editor.show_completions(&ShowCompletions, window, cx);
14493 });
14494
14495 let counter = Arc::new(AtomicUsize::new(0));
14496 handle_completion_request_with_insert_and_replace(
14497 &mut cx,
14498 buffer_marked_text,
14499 vec![(completion_text, completion_text)],
14500 counter.clone(),
14501 )
14502 .await;
14503 cx.condition(|editor, _| editor.context_menu_visible())
14504 .await;
14505 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14506
14507 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14508 editor
14509 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14510 .unwrap()
14511 });
14512 cx.assert_editor_state(expected_with_replace_mode);
14513 handle_resolve_completion_request(&mut cx, None).await;
14514 apply_additional_edits.await.unwrap();
14515
14516 update_test_language_settings(&mut cx, |settings| {
14517 settings.defaults.completions = Some(CompletionSettingsContent {
14518 words: Some(WordsCompletionMode::Disabled),
14519 words_min_length: Some(0),
14520 // set the opposite here to ensure that the action is overriding the default behavior
14521 lsp_insert_mode: Some(LspInsertMode::Replace),
14522 ..Default::default()
14523 });
14524 });
14525
14526 cx.set_state(initial_state);
14527 cx.update_editor(|editor, window, cx| {
14528 editor.show_completions(&ShowCompletions, window, cx);
14529 });
14530 handle_completion_request_with_insert_and_replace(
14531 &mut cx,
14532 buffer_marked_text,
14533 vec![(completion_text, completion_text)],
14534 counter.clone(),
14535 )
14536 .await;
14537 cx.condition(|editor, _| editor.context_menu_visible())
14538 .await;
14539 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14540
14541 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14542 editor
14543 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14544 .unwrap()
14545 });
14546 cx.assert_editor_state(expected_with_insert_mode);
14547 handle_resolve_completion_request(&mut cx, None).await;
14548 apply_additional_edits.await.unwrap();
14549}
14550
14551#[gpui::test]
14552async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14553 init_test(cx, |_| {});
14554 let mut cx = EditorLspTestContext::new_rust(
14555 lsp::ServerCapabilities {
14556 completion_provider: Some(lsp::CompletionOptions {
14557 resolve_provider: Some(true),
14558 ..Default::default()
14559 }),
14560 ..Default::default()
14561 },
14562 cx,
14563 )
14564 .await;
14565
14566 // scenario: surrounding text matches completion text
14567 let completion_text = "to_offset";
14568 let initial_state = indoc! {"
14569 1. buf.to_offˇsuffix
14570 2. buf.to_offˇsuf
14571 3. buf.to_offˇfix
14572 4. buf.to_offˇ
14573 5. into_offˇensive
14574 6. ˇsuffix
14575 7. let ˇ //
14576 8. aaˇzz
14577 9. buf.to_off«zzzzzˇ»suffix
14578 10. buf.«ˇzzzzz»suffix
14579 11. to_off«ˇzzzzz»
14580
14581 buf.to_offˇsuffix // newest cursor
14582 "};
14583 let completion_marked_buffer = indoc! {"
14584 1. buf.to_offsuffix
14585 2. buf.to_offsuf
14586 3. buf.to_offfix
14587 4. buf.to_off
14588 5. into_offensive
14589 6. suffix
14590 7. let //
14591 8. aazz
14592 9. buf.to_offzzzzzsuffix
14593 10. buf.zzzzzsuffix
14594 11. to_offzzzzz
14595
14596 buf.<to_off|suffix> // newest cursor
14597 "};
14598 let expected = indoc! {"
14599 1. buf.to_offsetˇ
14600 2. buf.to_offsetˇsuf
14601 3. buf.to_offsetˇfix
14602 4. buf.to_offsetˇ
14603 5. into_offsetˇensive
14604 6. to_offsetˇsuffix
14605 7. let to_offsetˇ //
14606 8. aato_offsetˇzz
14607 9. buf.to_offsetˇ
14608 10. buf.to_offsetˇsuffix
14609 11. to_offsetˇ
14610
14611 buf.to_offsetˇ // newest cursor
14612 "};
14613 cx.set_state(initial_state);
14614 cx.update_editor(|editor, window, cx| {
14615 editor.show_completions(&ShowCompletions, window, cx);
14616 });
14617 handle_completion_request_with_insert_and_replace(
14618 &mut cx,
14619 completion_marked_buffer,
14620 vec![(completion_text, completion_text)],
14621 Arc::new(AtomicUsize::new(0)),
14622 )
14623 .await;
14624 cx.condition(|editor, _| editor.context_menu_visible())
14625 .await;
14626 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14627 editor
14628 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14629 .unwrap()
14630 });
14631 cx.assert_editor_state(expected);
14632 handle_resolve_completion_request(&mut cx, None).await;
14633 apply_additional_edits.await.unwrap();
14634
14635 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14636 let completion_text = "foo_and_bar";
14637 let initial_state = indoc! {"
14638 1. ooanbˇ
14639 2. zooanbˇ
14640 3. ooanbˇz
14641 4. zooanbˇz
14642 5. ooanˇ
14643 6. oanbˇ
14644
14645 ooanbˇ
14646 "};
14647 let completion_marked_buffer = indoc! {"
14648 1. ooanb
14649 2. zooanb
14650 3. ooanbz
14651 4. zooanbz
14652 5. ooan
14653 6. oanb
14654
14655 <ooanb|>
14656 "};
14657 let expected = indoc! {"
14658 1. foo_and_barˇ
14659 2. zfoo_and_barˇ
14660 3. foo_and_barˇz
14661 4. zfoo_and_barˇz
14662 5. ooanfoo_and_barˇ
14663 6. oanbfoo_and_barˇ
14664
14665 foo_and_barˇ
14666 "};
14667 cx.set_state(initial_state);
14668 cx.update_editor(|editor, window, cx| {
14669 editor.show_completions(&ShowCompletions, window, cx);
14670 });
14671 handle_completion_request_with_insert_and_replace(
14672 &mut cx,
14673 completion_marked_buffer,
14674 vec![(completion_text, completion_text)],
14675 Arc::new(AtomicUsize::new(0)),
14676 )
14677 .await;
14678 cx.condition(|editor, _| editor.context_menu_visible())
14679 .await;
14680 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14681 editor
14682 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14683 .unwrap()
14684 });
14685 cx.assert_editor_state(expected);
14686 handle_resolve_completion_request(&mut cx, None).await;
14687 apply_additional_edits.await.unwrap();
14688
14689 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14690 // (expects the same as if it was inserted at the end)
14691 let completion_text = "foo_and_bar";
14692 let initial_state = indoc! {"
14693 1. ooˇanb
14694 2. zooˇanb
14695 3. ooˇanbz
14696 4. zooˇanbz
14697
14698 ooˇanb
14699 "};
14700 let completion_marked_buffer = indoc! {"
14701 1. ooanb
14702 2. zooanb
14703 3. ooanbz
14704 4. zooanbz
14705
14706 <oo|anb>
14707 "};
14708 let expected = indoc! {"
14709 1. foo_and_barˇ
14710 2. zfoo_and_barˇ
14711 3. foo_and_barˇz
14712 4. zfoo_and_barˇz
14713
14714 foo_and_barˇ
14715 "};
14716 cx.set_state(initial_state);
14717 cx.update_editor(|editor, window, cx| {
14718 editor.show_completions(&ShowCompletions, window, cx);
14719 });
14720 handle_completion_request_with_insert_and_replace(
14721 &mut cx,
14722 completion_marked_buffer,
14723 vec![(completion_text, completion_text)],
14724 Arc::new(AtomicUsize::new(0)),
14725 )
14726 .await;
14727 cx.condition(|editor, _| editor.context_menu_visible())
14728 .await;
14729 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14730 editor
14731 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14732 .unwrap()
14733 });
14734 cx.assert_editor_state(expected);
14735 handle_resolve_completion_request(&mut cx, None).await;
14736 apply_additional_edits.await.unwrap();
14737}
14738
14739// This used to crash
14740#[gpui::test]
14741async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14742 init_test(cx, |_| {});
14743
14744 let buffer_text = indoc! {"
14745 fn main() {
14746 10.satu;
14747
14748 //
14749 // separate cursors so they open in different excerpts (manually reproducible)
14750 //
14751
14752 10.satu20;
14753 }
14754 "};
14755 let multibuffer_text_with_selections = indoc! {"
14756 fn main() {
14757 10.satuˇ;
14758
14759 //
14760
14761 //
14762
14763 10.satuˇ20;
14764 }
14765 "};
14766 let expected_multibuffer = indoc! {"
14767 fn main() {
14768 10.saturating_sub()ˇ;
14769
14770 //
14771
14772 //
14773
14774 10.saturating_sub()ˇ;
14775 }
14776 "};
14777
14778 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14779 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14780
14781 let fs = FakeFs::new(cx.executor());
14782 fs.insert_tree(
14783 path!("/a"),
14784 json!({
14785 "main.rs": buffer_text,
14786 }),
14787 )
14788 .await;
14789
14790 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14791 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14792 language_registry.add(rust_lang());
14793 let mut fake_servers = language_registry.register_fake_lsp(
14794 "Rust",
14795 FakeLspAdapter {
14796 capabilities: lsp::ServerCapabilities {
14797 completion_provider: Some(lsp::CompletionOptions {
14798 resolve_provider: None,
14799 ..lsp::CompletionOptions::default()
14800 }),
14801 ..lsp::ServerCapabilities::default()
14802 },
14803 ..FakeLspAdapter::default()
14804 },
14805 );
14806 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14807 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14808 let buffer = project
14809 .update(cx, |project, cx| {
14810 project.open_local_buffer(path!("/a/main.rs"), cx)
14811 })
14812 .await
14813 .unwrap();
14814
14815 let multi_buffer = cx.new(|cx| {
14816 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14817 multi_buffer.push_excerpts(
14818 buffer.clone(),
14819 [ExcerptRange::new(0..first_excerpt_end)],
14820 cx,
14821 );
14822 multi_buffer.push_excerpts(
14823 buffer.clone(),
14824 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14825 cx,
14826 );
14827 multi_buffer
14828 });
14829
14830 let editor = workspace
14831 .update(cx, |_, window, cx| {
14832 cx.new(|cx| {
14833 Editor::new(
14834 EditorMode::Full {
14835 scale_ui_elements_with_buffer_font_size: false,
14836 show_active_line_background: false,
14837 sizing_behavior: SizingBehavior::Default,
14838 },
14839 multi_buffer.clone(),
14840 Some(project.clone()),
14841 window,
14842 cx,
14843 )
14844 })
14845 })
14846 .unwrap();
14847
14848 let pane = workspace
14849 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14850 .unwrap();
14851 pane.update_in(cx, |pane, window, cx| {
14852 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14853 });
14854
14855 let fake_server = fake_servers.next().await.unwrap();
14856 cx.run_until_parked();
14857
14858 editor.update_in(cx, |editor, window, cx| {
14859 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14860 s.select_ranges([
14861 Point::new(1, 11)..Point::new(1, 11),
14862 Point::new(7, 11)..Point::new(7, 11),
14863 ])
14864 });
14865
14866 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14867 });
14868
14869 editor.update_in(cx, |editor, window, cx| {
14870 editor.show_completions(&ShowCompletions, window, cx);
14871 });
14872
14873 fake_server
14874 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14875 let completion_item = lsp::CompletionItem {
14876 label: "saturating_sub()".into(),
14877 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14878 lsp::InsertReplaceEdit {
14879 new_text: "saturating_sub()".to_owned(),
14880 insert: lsp::Range::new(
14881 lsp::Position::new(7, 7),
14882 lsp::Position::new(7, 11),
14883 ),
14884 replace: lsp::Range::new(
14885 lsp::Position::new(7, 7),
14886 lsp::Position::new(7, 13),
14887 ),
14888 },
14889 )),
14890 ..lsp::CompletionItem::default()
14891 };
14892
14893 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14894 })
14895 .next()
14896 .await
14897 .unwrap();
14898
14899 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14900 .await;
14901
14902 editor
14903 .update_in(cx, |editor, window, cx| {
14904 editor
14905 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14906 .unwrap()
14907 })
14908 .await
14909 .unwrap();
14910
14911 editor.update(cx, |editor, cx| {
14912 assert_text_with_selections(editor, expected_multibuffer, cx);
14913 })
14914}
14915
14916#[gpui::test]
14917async fn test_completion(cx: &mut TestAppContext) {
14918 init_test(cx, |_| {});
14919
14920 let mut cx = EditorLspTestContext::new_rust(
14921 lsp::ServerCapabilities {
14922 completion_provider: Some(lsp::CompletionOptions {
14923 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14924 resolve_provider: Some(true),
14925 ..Default::default()
14926 }),
14927 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14928 ..Default::default()
14929 },
14930 cx,
14931 )
14932 .await;
14933 let counter = Arc::new(AtomicUsize::new(0));
14934
14935 cx.set_state(indoc! {"
14936 oneˇ
14937 two
14938 three
14939 "});
14940 cx.simulate_keystroke(".");
14941 handle_completion_request(
14942 indoc! {"
14943 one.|<>
14944 two
14945 three
14946 "},
14947 vec!["first_completion", "second_completion"],
14948 true,
14949 counter.clone(),
14950 &mut cx,
14951 )
14952 .await;
14953 cx.condition(|editor, _| editor.context_menu_visible())
14954 .await;
14955 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14956
14957 let _handler = handle_signature_help_request(
14958 &mut cx,
14959 lsp::SignatureHelp {
14960 signatures: vec![lsp::SignatureInformation {
14961 label: "test signature".to_string(),
14962 documentation: None,
14963 parameters: Some(vec![lsp::ParameterInformation {
14964 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14965 documentation: None,
14966 }]),
14967 active_parameter: None,
14968 }],
14969 active_signature: None,
14970 active_parameter: None,
14971 },
14972 );
14973 cx.update_editor(|editor, window, cx| {
14974 assert!(
14975 !editor.signature_help_state.is_shown(),
14976 "No signature help was called for"
14977 );
14978 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14979 });
14980 cx.run_until_parked();
14981 cx.update_editor(|editor, _, _| {
14982 assert!(
14983 !editor.signature_help_state.is_shown(),
14984 "No signature help should be shown when completions menu is open"
14985 );
14986 });
14987
14988 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14989 editor.context_menu_next(&Default::default(), window, cx);
14990 editor
14991 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14992 .unwrap()
14993 });
14994 cx.assert_editor_state(indoc! {"
14995 one.second_completionˇ
14996 two
14997 three
14998 "});
14999
15000 handle_resolve_completion_request(
15001 &mut cx,
15002 Some(vec![
15003 (
15004 //This overlaps with the primary completion edit which is
15005 //misbehavior from the LSP spec, test that we filter it out
15006 indoc! {"
15007 one.second_ˇcompletion
15008 two
15009 threeˇ
15010 "},
15011 "overlapping additional edit",
15012 ),
15013 (
15014 indoc! {"
15015 one.second_completion
15016 two
15017 threeˇ
15018 "},
15019 "\nadditional edit",
15020 ),
15021 ]),
15022 )
15023 .await;
15024 apply_additional_edits.await.unwrap();
15025 cx.assert_editor_state(indoc! {"
15026 one.second_completionˇ
15027 two
15028 three
15029 additional edit
15030 "});
15031
15032 cx.set_state(indoc! {"
15033 one.second_completion
15034 twoˇ
15035 threeˇ
15036 additional edit
15037 "});
15038 cx.simulate_keystroke(" ");
15039 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15040 cx.simulate_keystroke("s");
15041 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15042
15043 cx.assert_editor_state(indoc! {"
15044 one.second_completion
15045 two sˇ
15046 three sˇ
15047 additional edit
15048 "});
15049 handle_completion_request(
15050 indoc! {"
15051 one.second_completion
15052 two s
15053 three <s|>
15054 additional edit
15055 "},
15056 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
15057 true,
15058 counter.clone(),
15059 &mut cx,
15060 )
15061 .await;
15062 cx.condition(|editor, _| editor.context_menu_visible())
15063 .await;
15064 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15065
15066 cx.simulate_keystroke("i");
15067
15068 handle_completion_request(
15069 indoc! {"
15070 one.second_completion
15071 two si
15072 three <si|>
15073 additional edit
15074 "},
15075 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
15076 true,
15077 counter.clone(),
15078 &mut cx,
15079 )
15080 .await;
15081 cx.condition(|editor, _| editor.context_menu_visible())
15082 .await;
15083 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15084
15085 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15086 editor
15087 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15088 .unwrap()
15089 });
15090 cx.assert_editor_state(indoc! {"
15091 one.second_completion
15092 two sixth_completionˇ
15093 three sixth_completionˇ
15094 additional edit
15095 "});
15096
15097 apply_additional_edits.await.unwrap();
15098
15099 update_test_language_settings(&mut cx, |settings| {
15100 settings.defaults.show_completions_on_input = Some(false);
15101 });
15102 cx.set_state("editorˇ");
15103 cx.simulate_keystroke(".");
15104 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15105 cx.simulate_keystrokes("c l o");
15106 cx.assert_editor_state("editor.cloˇ");
15107 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15108 cx.update_editor(|editor, window, cx| {
15109 editor.show_completions(&ShowCompletions, window, cx);
15110 });
15111 handle_completion_request(
15112 "editor.<clo|>",
15113 vec!["close", "clobber"],
15114 true,
15115 counter.clone(),
15116 &mut cx,
15117 )
15118 .await;
15119 cx.condition(|editor, _| editor.context_menu_visible())
15120 .await;
15121 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
15122
15123 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15124 editor
15125 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15126 .unwrap()
15127 });
15128 cx.assert_editor_state("editor.clobberˇ");
15129 handle_resolve_completion_request(&mut cx, None).await;
15130 apply_additional_edits.await.unwrap();
15131}
15132
15133#[gpui::test]
15134async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
15135 init_test(cx, |_| {});
15136
15137 let fs = FakeFs::new(cx.executor());
15138 fs.insert_tree(
15139 path!("/a"),
15140 json!({
15141 "main.rs": "",
15142 }),
15143 )
15144 .await;
15145
15146 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15147 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15148 language_registry.add(rust_lang());
15149 let command_calls = Arc::new(AtomicUsize::new(0));
15150 let registered_command = "_the/command";
15151
15152 let closure_command_calls = command_calls.clone();
15153 let mut fake_servers = language_registry.register_fake_lsp(
15154 "Rust",
15155 FakeLspAdapter {
15156 capabilities: lsp::ServerCapabilities {
15157 completion_provider: Some(lsp::CompletionOptions {
15158 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15159 ..lsp::CompletionOptions::default()
15160 }),
15161 execute_command_provider: Some(lsp::ExecuteCommandOptions {
15162 commands: vec![registered_command.to_owned()],
15163 ..lsp::ExecuteCommandOptions::default()
15164 }),
15165 ..lsp::ServerCapabilities::default()
15166 },
15167 initializer: Some(Box::new(move |fake_server| {
15168 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15169 move |params, _| async move {
15170 Ok(Some(lsp::CompletionResponse::Array(vec![
15171 lsp::CompletionItem {
15172 label: "registered_command".to_owned(),
15173 text_edit: gen_text_edit(¶ms, ""),
15174 command: Some(lsp::Command {
15175 title: registered_command.to_owned(),
15176 command: "_the/command".to_owned(),
15177 arguments: Some(vec![serde_json::Value::Bool(true)]),
15178 }),
15179 ..lsp::CompletionItem::default()
15180 },
15181 lsp::CompletionItem {
15182 label: "unregistered_command".to_owned(),
15183 text_edit: gen_text_edit(¶ms, ""),
15184 command: Some(lsp::Command {
15185 title: "????????????".to_owned(),
15186 command: "????????????".to_owned(),
15187 arguments: Some(vec![serde_json::Value::Null]),
15188 }),
15189 ..lsp::CompletionItem::default()
15190 },
15191 ])))
15192 },
15193 );
15194 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
15195 let command_calls = closure_command_calls.clone();
15196 move |params, _| {
15197 assert_eq!(params.command, registered_command);
15198 let command_calls = command_calls.clone();
15199 async move {
15200 command_calls.fetch_add(1, atomic::Ordering::Release);
15201 Ok(Some(json!(null)))
15202 }
15203 }
15204 });
15205 })),
15206 ..FakeLspAdapter::default()
15207 },
15208 );
15209 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15210 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15211 let editor = workspace
15212 .update(cx, |workspace, window, cx| {
15213 workspace.open_abs_path(
15214 PathBuf::from(path!("/a/main.rs")),
15215 OpenOptions::default(),
15216 window,
15217 cx,
15218 )
15219 })
15220 .unwrap()
15221 .await
15222 .unwrap()
15223 .downcast::<Editor>()
15224 .unwrap();
15225 let _fake_server = fake_servers.next().await.unwrap();
15226 cx.run_until_parked();
15227
15228 editor.update_in(cx, |editor, window, cx| {
15229 cx.focus_self(window);
15230 editor.move_to_end(&MoveToEnd, window, cx);
15231 editor.handle_input(".", window, cx);
15232 });
15233 cx.run_until_parked();
15234 editor.update(cx, |editor, _| {
15235 assert!(editor.context_menu_visible());
15236 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15237 {
15238 let completion_labels = menu
15239 .completions
15240 .borrow()
15241 .iter()
15242 .map(|c| c.label.text.clone())
15243 .collect::<Vec<_>>();
15244 assert_eq!(
15245 completion_labels,
15246 &["registered_command", "unregistered_command",],
15247 );
15248 } else {
15249 panic!("expected completion menu to be open");
15250 }
15251 });
15252
15253 editor
15254 .update_in(cx, |editor, window, cx| {
15255 editor
15256 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15257 .unwrap()
15258 })
15259 .await
15260 .unwrap();
15261 cx.run_until_parked();
15262 assert_eq!(
15263 command_calls.load(atomic::Ordering::Acquire),
15264 1,
15265 "For completion with a registered command, Zed should send a command execution request",
15266 );
15267
15268 editor.update_in(cx, |editor, window, cx| {
15269 cx.focus_self(window);
15270 editor.handle_input(".", window, cx);
15271 });
15272 cx.run_until_parked();
15273 editor.update(cx, |editor, _| {
15274 assert!(editor.context_menu_visible());
15275 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15276 {
15277 let completion_labels = menu
15278 .completions
15279 .borrow()
15280 .iter()
15281 .map(|c| c.label.text.clone())
15282 .collect::<Vec<_>>();
15283 assert_eq!(
15284 completion_labels,
15285 &["registered_command", "unregistered_command",],
15286 );
15287 } else {
15288 panic!("expected completion menu to be open");
15289 }
15290 });
15291 editor
15292 .update_in(cx, |editor, window, cx| {
15293 editor.context_menu_next(&Default::default(), window, cx);
15294 editor
15295 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15296 .unwrap()
15297 })
15298 .await
15299 .unwrap();
15300 cx.run_until_parked();
15301 assert_eq!(
15302 command_calls.load(atomic::Ordering::Acquire),
15303 1,
15304 "For completion with an unregistered command, Zed should not send a command execution request",
15305 );
15306}
15307
15308#[gpui::test]
15309async fn test_completion_reuse(cx: &mut TestAppContext) {
15310 init_test(cx, |_| {});
15311
15312 let mut cx = EditorLspTestContext::new_rust(
15313 lsp::ServerCapabilities {
15314 completion_provider: Some(lsp::CompletionOptions {
15315 trigger_characters: Some(vec![".".to_string()]),
15316 ..Default::default()
15317 }),
15318 ..Default::default()
15319 },
15320 cx,
15321 )
15322 .await;
15323
15324 let counter = Arc::new(AtomicUsize::new(0));
15325 cx.set_state("objˇ");
15326 cx.simulate_keystroke(".");
15327
15328 // Initial completion request returns complete results
15329 let is_incomplete = false;
15330 handle_completion_request(
15331 "obj.|<>",
15332 vec!["a", "ab", "abc"],
15333 is_incomplete,
15334 counter.clone(),
15335 &mut cx,
15336 )
15337 .await;
15338 cx.run_until_parked();
15339 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15340 cx.assert_editor_state("obj.ˇ");
15341 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15342
15343 // Type "a" - filters existing completions
15344 cx.simulate_keystroke("a");
15345 cx.run_until_parked();
15346 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15347 cx.assert_editor_state("obj.aˇ");
15348 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15349
15350 // Type "b" - filters existing completions
15351 cx.simulate_keystroke("b");
15352 cx.run_until_parked();
15353 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15354 cx.assert_editor_state("obj.abˇ");
15355 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15356
15357 // Type "c" - filters existing completions
15358 cx.simulate_keystroke("c");
15359 cx.run_until_parked();
15360 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15361 cx.assert_editor_state("obj.abcˇ");
15362 check_displayed_completions(vec!["abc"], &mut cx);
15363
15364 // Backspace to delete "c" - filters existing completions
15365 cx.update_editor(|editor, window, cx| {
15366 editor.backspace(&Backspace, window, cx);
15367 });
15368 cx.run_until_parked();
15369 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15370 cx.assert_editor_state("obj.abˇ");
15371 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15372
15373 // Moving cursor to the left dismisses menu.
15374 cx.update_editor(|editor, window, cx| {
15375 editor.move_left(&MoveLeft, window, cx);
15376 });
15377 cx.run_until_parked();
15378 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15379 cx.assert_editor_state("obj.aˇb");
15380 cx.update_editor(|editor, _, _| {
15381 assert_eq!(editor.context_menu_visible(), false);
15382 });
15383
15384 // Type "b" - new request
15385 cx.simulate_keystroke("b");
15386 let is_incomplete = false;
15387 handle_completion_request(
15388 "obj.<ab|>a",
15389 vec!["ab", "abc"],
15390 is_incomplete,
15391 counter.clone(),
15392 &mut cx,
15393 )
15394 .await;
15395 cx.run_until_parked();
15396 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15397 cx.assert_editor_state("obj.abˇb");
15398 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15399
15400 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
15401 cx.update_editor(|editor, window, cx| {
15402 editor.backspace(&Backspace, window, cx);
15403 });
15404 let is_incomplete = false;
15405 handle_completion_request(
15406 "obj.<a|>b",
15407 vec!["a", "ab", "abc"],
15408 is_incomplete,
15409 counter.clone(),
15410 &mut cx,
15411 )
15412 .await;
15413 cx.run_until_parked();
15414 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15415 cx.assert_editor_state("obj.aˇb");
15416 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15417
15418 // Backspace to delete "a" - dismisses menu.
15419 cx.update_editor(|editor, window, cx| {
15420 editor.backspace(&Backspace, window, cx);
15421 });
15422 cx.run_until_parked();
15423 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15424 cx.assert_editor_state("obj.ˇb");
15425 cx.update_editor(|editor, _, _| {
15426 assert_eq!(editor.context_menu_visible(), false);
15427 });
15428}
15429
15430#[gpui::test]
15431async fn test_word_completion(cx: &mut TestAppContext) {
15432 let lsp_fetch_timeout_ms = 10;
15433 init_test(cx, |language_settings| {
15434 language_settings.defaults.completions = Some(CompletionSettingsContent {
15435 words_min_length: Some(0),
15436 lsp_fetch_timeout_ms: Some(10),
15437 lsp_insert_mode: Some(LspInsertMode::Insert),
15438 ..Default::default()
15439 });
15440 });
15441
15442 let mut cx = EditorLspTestContext::new_rust(
15443 lsp::ServerCapabilities {
15444 completion_provider: Some(lsp::CompletionOptions {
15445 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15446 ..lsp::CompletionOptions::default()
15447 }),
15448 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15449 ..lsp::ServerCapabilities::default()
15450 },
15451 cx,
15452 )
15453 .await;
15454
15455 let throttle_completions = Arc::new(AtomicBool::new(false));
15456
15457 let lsp_throttle_completions = throttle_completions.clone();
15458 let _completion_requests_handler =
15459 cx.lsp
15460 .server
15461 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
15462 let lsp_throttle_completions = lsp_throttle_completions.clone();
15463 let cx = cx.clone();
15464 async move {
15465 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
15466 cx.background_executor()
15467 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
15468 .await;
15469 }
15470 Ok(Some(lsp::CompletionResponse::Array(vec![
15471 lsp::CompletionItem {
15472 label: "first".into(),
15473 ..lsp::CompletionItem::default()
15474 },
15475 lsp::CompletionItem {
15476 label: "last".into(),
15477 ..lsp::CompletionItem::default()
15478 },
15479 ])))
15480 }
15481 });
15482
15483 cx.set_state(indoc! {"
15484 oneˇ
15485 two
15486 three
15487 "});
15488 cx.simulate_keystroke(".");
15489 cx.executor().run_until_parked();
15490 cx.condition(|editor, _| editor.context_menu_visible())
15491 .await;
15492 cx.update_editor(|editor, window, cx| {
15493 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15494 {
15495 assert_eq!(
15496 completion_menu_entries(menu),
15497 &["first", "last"],
15498 "When LSP server is fast to reply, no fallback word completions are used"
15499 );
15500 } else {
15501 panic!("expected completion menu to be open");
15502 }
15503 editor.cancel(&Cancel, window, cx);
15504 });
15505 cx.executor().run_until_parked();
15506 cx.condition(|editor, _| !editor.context_menu_visible())
15507 .await;
15508
15509 throttle_completions.store(true, atomic::Ordering::Release);
15510 cx.simulate_keystroke(".");
15511 cx.executor()
15512 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
15513 cx.executor().run_until_parked();
15514 cx.condition(|editor, _| editor.context_menu_visible())
15515 .await;
15516 cx.update_editor(|editor, _, _| {
15517 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15518 {
15519 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
15520 "When LSP server is slow, document words can be shown instead, if configured accordingly");
15521 } else {
15522 panic!("expected completion menu to be open");
15523 }
15524 });
15525}
15526
15527#[gpui::test]
15528async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
15529 init_test(cx, |language_settings| {
15530 language_settings.defaults.completions = Some(CompletionSettingsContent {
15531 words: Some(WordsCompletionMode::Enabled),
15532 words_min_length: Some(0),
15533 lsp_insert_mode: Some(LspInsertMode::Insert),
15534 ..Default::default()
15535 });
15536 });
15537
15538 let mut cx = EditorLspTestContext::new_rust(
15539 lsp::ServerCapabilities {
15540 completion_provider: Some(lsp::CompletionOptions {
15541 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15542 ..lsp::CompletionOptions::default()
15543 }),
15544 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15545 ..lsp::ServerCapabilities::default()
15546 },
15547 cx,
15548 )
15549 .await;
15550
15551 let _completion_requests_handler =
15552 cx.lsp
15553 .server
15554 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15555 Ok(Some(lsp::CompletionResponse::Array(vec![
15556 lsp::CompletionItem {
15557 label: "first".into(),
15558 ..lsp::CompletionItem::default()
15559 },
15560 lsp::CompletionItem {
15561 label: "last".into(),
15562 ..lsp::CompletionItem::default()
15563 },
15564 ])))
15565 });
15566
15567 cx.set_state(indoc! {"ˇ
15568 first
15569 last
15570 second
15571 "});
15572 cx.simulate_keystroke(".");
15573 cx.executor().run_until_parked();
15574 cx.condition(|editor, _| editor.context_menu_visible())
15575 .await;
15576 cx.update_editor(|editor, _, _| {
15577 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15578 {
15579 assert_eq!(
15580 completion_menu_entries(menu),
15581 &["first", "last", "second"],
15582 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15583 );
15584 } else {
15585 panic!("expected completion menu to be open");
15586 }
15587 });
15588}
15589
15590#[gpui::test]
15591async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15592 init_test(cx, |language_settings| {
15593 language_settings.defaults.completions = Some(CompletionSettingsContent {
15594 words: Some(WordsCompletionMode::Disabled),
15595 words_min_length: Some(0),
15596 lsp_insert_mode: Some(LspInsertMode::Insert),
15597 ..Default::default()
15598 });
15599 });
15600
15601 let mut cx = EditorLspTestContext::new_rust(
15602 lsp::ServerCapabilities {
15603 completion_provider: Some(lsp::CompletionOptions {
15604 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15605 ..lsp::CompletionOptions::default()
15606 }),
15607 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15608 ..lsp::ServerCapabilities::default()
15609 },
15610 cx,
15611 )
15612 .await;
15613
15614 let _completion_requests_handler =
15615 cx.lsp
15616 .server
15617 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15618 panic!("LSP completions should not be queried when dealing with word completions")
15619 });
15620
15621 cx.set_state(indoc! {"ˇ
15622 first
15623 last
15624 second
15625 "});
15626 cx.update_editor(|editor, window, cx| {
15627 editor.show_word_completions(&ShowWordCompletions, window, cx);
15628 });
15629 cx.executor().run_until_parked();
15630 cx.condition(|editor, _| editor.context_menu_visible())
15631 .await;
15632 cx.update_editor(|editor, _, _| {
15633 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15634 {
15635 assert_eq!(
15636 completion_menu_entries(menu),
15637 &["first", "last", "second"],
15638 "`ShowWordCompletions` action should show word completions"
15639 );
15640 } else {
15641 panic!("expected completion menu to be open");
15642 }
15643 });
15644
15645 cx.simulate_keystroke("l");
15646 cx.executor().run_until_parked();
15647 cx.condition(|editor, _| editor.context_menu_visible())
15648 .await;
15649 cx.update_editor(|editor, _, _| {
15650 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15651 {
15652 assert_eq!(
15653 completion_menu_entries(menu),
15654 &["last"],
15655 "After showing word completions, further editing should filter them and not query the LSP"
15656 );
15657 } else {
15658 panic!("expected completion menu to be open");
15659 }
15660 });
15661}
15662
15663#[gpui::test]
15664async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15665 init_test(cx, |language_settings| {
15666 language_settings.defaults.completions = Some(CompletionSettingsContent {
15667 words_min_length: Some(0),
15668 lsp: Some(false),
15669 lsp_insert_mode: Some(LspInsertMode::Insert),
15670 ..Default::default()
15671 });
15672 });
15673
15674 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15675
15676 cx.set_state(indoc! {"ˇ
15677 0_usize
15678 let
15679 33
15680 4.5f32
15681 "});
15682 cx.update_editor(|editor, window, cx| {
15683 editor.show_completions(&ShowCompletions, window, cx);
15684 });
15685 cx.executor().run_until_parked();
15686 cx.condition(|editor, _| editor.context_menu_visible())
15687 .await;
15688 cx.update_editor(|editor, window, cx| {
15689 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15690 {
15691 assert_eq!(
15692 completion_menu_entries(menu),
15693 &["let"],
15694 "With no digits in the completion query, no digits should be in the word completions"
15695 );
15696 } else {
15697 panic!("expected completion menu to be open");
15698 }
15699 editor.cancel(&Cancel, window, cx);
15700 });
15701
15702 cx.set_state(indoc! {"3ˇ
15703 0_usize
15704 let
15705 3
15706 33.35f32
15707 "});
15708 cx.update_editor(|editor, window, cx| {
15709 editor.show_completions(&ShowCompletions, window, cx);
15710 });
15711 cx.executor().run_until_parked();
15712 cx.condition(|editor, _| editor.context_menu_visible())
15713 .await;
15714 cx.update_editor(|editor, _, _| {
15715 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15716 {
15717 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15718 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15719 } else {
15720 panic!("expected completion menu to be open");
15721 }
15722 });
15723}
15724
15725#[gpui::test]
15726async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15727 init_test(cx, |language_settings| {
15728 language_settings.defaults.completions = Some(CompletionSettingsContent {
15729 words: Some(WordsCompletionMode::Enabled),
15730 words_min_length: Some(3),
15731 lsp_insert_mode: Some(LspInsertMode::Insert),
15732 ..Default::default()
15733 });
15734 });
15735
15736 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15737 cx.set_state(indoc! {"ˇ
15738 wow
15739 wowen
15740 wowser
15741 "});
15742 cx.simulate_keystroke("w");
15743 cx.executor().run_until_parked();
15744 cx.update_editor(|editor, _, _| {
15745 if editor.context_menu.borrow_mut().is_some() {
15746 panic!(
15747 "expected completion menu to be hidden, as words completion threshold is not met"
15748 );
15749 }
15750 });
15751
15752 cx.update_editor(|editor, window, cx| {
15753 editor.show_word_completions(&ShowWordCompletions, window, cx);
15754 });
15755 cx.executor().run_until_parked();
15756 cx.update_editor(|editor, window, cx| {
15757 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15758 {
15759 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");
15760 } else {
15761 panic!("expected completion menu to be open after the word completions are called with an action");
15762 }
15763
15764 editor.cancel(&Cancel, window, cx);
15765 });
15766 cx.update_editor(|editor, _, _| {
15767 if editor.context_menu.borrow_mut().is_some() {
15768 panic!("expected completion menu to be hidden after canceling");
15769 }
15770 });
15771
15772 cx.simulate_keystroke("o");
15773 cx.executor().run_until_parked();
15774 cx.update_editor(|editor, _, _| {
15775 if editor.context_menu.borrow_mut().is_some() {
15776 panic!(
15777 "expected completion menu to be hidden, as words completion threshold is not met still"
15778 );
15779 }
15780 });
15781
15782 cx.simulate_keystroke("w");
15783 cx.executor().run_until_parked();
15784 cx.update_editor(|editor, _, _| {
15785 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15786 {
15787 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15788 } else {
15789 panic!("expected completion menu to be open after the word completions threshold is met");
15790 }
15791 });
15792}
15793
15794#[gpui::test]
15795async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15796 init_test(cx, |language_settings| {
15797 language_settings.defaults.completions = Some(CompletionSettingsContent {
15798 words: Some(WordsCompletionMode::Enabled),
15799 words_min_length: Some(0),
15800 lsp_insert_mode: Some(LspInsertMode::Insert),
15801 ..Default::default()
15802 });
15803 });
15804
15805 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15806 cx.update_editor(|editor, _, _| {
15807 editor.disable_word_completions();
15808 });
15809 cx.set_state(indoc! {"ˇ
15810 wow
15811 wowen
15812 wowser
15813 "});
15814 cx.simulate_keystroke("w");
15815 cx.executor().run_until_parked();
15816 cx.update_editor(|editor, _, _| {
15817 if editor.context_menu.borrow_mut().is_some() {
15818 panic!(
15819 "expected completion menu to be hidden, as words completion are disabled for this editor"
15820 );
15821 }
15822 });
15823
15824 cx.update_editor(|editor, window, cx| {
15825 editor.show_word_completions(&ShowWordCompletions, window, cx);
15826 });
15827 cx.executor().run_until_parked();
15828 cx.update_editor(|editor, _, _| {
15829 if editor.context_menu.borrow_mut().is_some() {
15830 panic!(
15831 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15832 );
15833 }
15834 });
15835}
15836
15837#[gpui::test]
15838async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15839 init_test(cx, |language_settings| {
15840 language_settings.defaults.completions = Some(CompletionSettingsContent {
15841 words: Some(WordsCompletionMode::Disabled),
15842 words_min_length: Some(0),
15843 lsp_insert_mode: Some(LspInsertMode::Insert),
15844 ..Default::default()
15845 });
15846 });
15847
15848 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15849 cx.update_editor(|editor, _, _| {
15850 editor.set_completion_provider(None);
15851 });
15852 cx.set_state(indoc! {"ˇ
15853 wow
15854 wowen
15855 wowser
15856 "});
15857 cx.simulate_keystroke("w");
15858 cx.executor().run_until_parked();
15859 cx.update_editor(|editor, _, _| {
15860 if editor.context_menu.borrow_mut().is_some() {
15861 panic!("expected completion menu to be hidden, as disabled in settings");
15862 }
15863 });
15864}
15865
15866fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15867 let position = || lsp::Position {
15868 line: params.text_document_position.position.line,
15869 character: params.text_document_position.position.character,
15870 };
15871 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15872 range: lsp::Range {
15873 start: position(),
15874 end: position(),
15875 },
15876 new_text: text.to_string(),
15877 }))
15878}
15879
15880#[gpui::test]
15881async fn test_multiline_completion(cx: &mut TestAppContext) {
15882 init_test(cx, |_| {});
15883
15884 let fs = FakeFs::new(cx.executor());
15885 fs.insert_tree(
15886 path!("/a"),
15887 json!({
15888 "main.ts": "a",
15889 }),
15890 )
15891 .await;
15892
15893 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15894 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15895 let typescript_language = Arc::new(Language::new(
15896 LanguageConfig {
15897 name: "TypeScript".into(),
15898 matcher: LanguageMatcher {
15899 path_suffixes: vec!["ts".to_string()],
15900 ..LanguageMatcher::default()
15901 },
15902 line_comments: vec!["// ".into()],
15903 ..LanguageConfig::default()
15904 },
15905 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15906 ));
15907 language_registry.add(typescript_language.clone());
15908 let mut fake_servers = language_registry.register_fake_lsp(
15909 "TypeScript",
15910 FakeLspAdapter {
15911 capabilities: lsp::ServerCapabilities {
15912 completion_provider: Some(lsp::CompletionOptions {
15913 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15914 ..lsp::CompletionOptions::default()
15915 }),
15916 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15917 ..lsp::ServerCapabilities::default()
15918 },
15919 // Emulate vtsls label generation
15920 label_for_completion: Some(Box::new(|item, _| {
15921 let text = if let Some(description) = item
15922 .label_details
15923 .as_ref()
15924 .and_then(|label_details| label_details.description.as_ref())
15925 {
15926 format!("{} {}", item.label, description)
15927 } else if let Some(detail) = &item.detail {
15928 format!("{} {}", item.label, detail)
15929 } else {
15930 item.label.clone()
15931 };
15932 Some(language::CodeLabel::plain(text, None))
15933 })),
15934 ..FakeLspAdapter::default()
15935 },
15936 );
15937 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15938 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15939 let worktree_id = workspace
15940 .update(cx, |workspace, _window, cx| {
15941 workspace.project().update(cx, |project, cx| {
15942 project.worktrees(cx).next().unwrap().read(cx).id()
15943 })
15944 })
15945 .unwrap();
15946 let _buffer = project
15947 .update(cx, |project, cx| {
15948 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15949 })
15950 .await
15951 .unwrap();
15952 let editor = workspace
15953 .update(cx, |workspace, window, cx| {
15954 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15955 })
15956 .unwrap()
15957 .await
15958 .unwrap()
15959 .downcast::<Editor>()
15960 .unwrap();
15961 let fake_server = fake_servers.next().await.unwrap();
15962 cx.run_until_parked();
15963
15964 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15965 let multiline_label_2 = "a\nb\nc\n";
15966 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15967 let multiline_description = "d\ne\nf\n";
15968 let multiline_detail_2 = "g\nh\ni\n";
15969
15970 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15971 move |params, _| async move {
15972 Ok(Some(lsp::CompletionResponse::Array(vec![
15973 lsp::CompletionItem {
15974 label: multiline_label.to_string(),
15975 text_edit: gen_text_edit(¶ms, "new_text_1"),
15976 ..lsp::CompletionItem::default()
15977 },
15978 lsp::CompletionItem {
15979 label: "single line label 1".to_string(),
15980 detail: Some(multiline_detail.to_string()),
15981 text_edit: gen_text_edit(¶ms, "new_text_2"),
15982 ..lsp::CompletionItem::default()
15983 },
15984 lsp::CompletionItem {
15985 label: "single line label 2".to_string(),
15986 label_details: Some(lsp::CompletionItemLabelDetails {
15987 description: Some(multiline_description.to_string()),
15988 detail: None,
15989 }),
15990 text_edit: gen_text_edit(¶ms, "new_text_2"),
15991 ..lsp::CompletionItem::default()
15992 },
15993 lsp::CompletionItem {
15994 label: multiline_label_2.to_string(),
15995 detail: Some(multiline_detail_2.to_string()),
15996 text_edit: gen_text_edit(¶ms, "new_text_3"),
15997 ..lsp::CompletionItem::default()
15998 },
15999 lsp::CompletionItem {
16000 label: "Label with many spaces and \t but without newlines".to_string(),
16001 detail: Some(
16002 "Details with many spaces and \t but without newlines".to_string(),
16003 ),
16004 text_edit: gen_text_edit(¶ms, "new_text_4"),
16005 ..lsp::CompletionItem::default()
16006 },
16007 ])))
16008 },
16009 );
16010
16011 editor.update_in(cx, |editor, window, cx| {
16012 cx.focus_self(window);
16013 editor.move_to_end(&MoveToEnd, window, cx);
16014 editor.handle_input(".", window, cx);
16015 });
16016 cx.run_until_parked();
16017 completion_handle.next().await.unwrap();
16018
16019 editor.update(cx, |editor, _| {
16020 assert!(editor.context_menu_visible());
16021 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16022 {
16023 let completion_labels = menu
16024 .completions
16025 .borrow()
16026 .iter()
16027 .map(|c| c.label.text.clone())
16028 .collect::<Vec<_>>();
16029 assert_eq!(
16030 completion_labels,
16031 &[
16032 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
16033 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
16034 "single line label 2 d e f ",
16035 "a b c g h i ",
16036 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
16037 ],
16038 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
16039 );
16040
16041 for completion in menu
16042 .completions
16043 .borrow()
16044 .iter() {
16045 assert_eq!(
16046 completion.label.filter_range,
16047 0..completion.label.text.len(),
16048 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
16049 );
16050 }
16051 } else {
16052 panic!("expected completion menu to be open");
16053 }
16054 });
16055}
16056
16057#[gpui::test]
16058async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
16059 init_test(cx, |_| {});
16060 let mut cx = EditorLspTestContext::new_rust(
16061 lsp::ServerCapabilities {
16062 completion_provider: Some(lsp::CompletionOptions {
16063 trigger_characters: Some(vec![".".to_string()]),
16064 ..Default::default()
16065 }),
16066 ..Default::default()
16067 },
16068 cx,
16069 )
16070 .await;
16071 cx.lsp
16072 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16073 Ok(Some(lsp::CompletionResponse::Array(vec![
16074 lsp::CompletionItem {
16075 label: "first".into(),
16076 ..Default::default()
16077 },
16078 lsp::CompletionItem {
16079 label: "last".into(),
16080 ..Default::default()
16081 },
16082 ])))
16083 });
16084 cx.set_state("variableˇ");
16085 cx.simulate_keystroke(".");
16086 cx.executor().run_until_parked();
16087
16088 cx.update_editor(|editor, _, _| {
16089 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16090 {
16091 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
16092 } else {
16093 panic!("expected completion menu to be open");
16094 }
16095 });
16096
16097 cx.update_editor(|editor, window, cx| {
16098 editor.move_page_down(&MovePageDown::default(), window, cx);
16099 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16100 {
16101 assert!(
16102 menu.selected_item == 1,
16103 "expected PageDown to select the last item from the context menu"
16104 );
16105 } else {
16106 panic!("expected completion menu to stay open after PageDown");
16107 }
16108 });
16109
16110 cx.update_editor(|editor, window, cx| {
16111 editor.move_page_up(&MovePageUp::default(), window, cx);
16112 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16113 {
16114 assert!(
16115 menu.selected_item == 0,
16116 "expected PageUp to select the first item from the context menu"
16117 );
16118 } else {
16119 panic!("expected completion menu to stay open after PageUp");
16120 }
16121 });
16122}
16123
16124#[gpui::test]
16125async fn test_as_is_completions(cx: &mut TestAppContext) {
16126 init_test(cx, |_| {});
16127 let mut cx = EditorLspTestContext::new_rust(
16128 lsp::ServerCapabilities {
16129 completion_provider: Some(lsp::CompletionOptions {
16130 ..Default::default()
16131 }),
16132 ..Default::default()
16133 },
16134 cx,
16135 )
16136 .await;
16137 cx.lsp
16138 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16139 Ok(Some(lsp::CompletionResponse::Array(vec![
16140 lsp::CompletionItem {
16141 label: "unsafe".into(),
16142 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16143 range: lsp::Range {
16144 start: lsp::Position {
16145 line: 1,
16146 character: 2,
16147 },
16148 end: lsp::Position {
16149 line: 1,
16150 character: 3,
16151 },
16152 },
16153 new_text: "unsafe".to_string(),
16154 })),
16155 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
16156 ..Default::default()
16157 },
16158 ])))
16159 });
16160 cx.set_state("fn a() {}\n nˇ");
16161 cx.executor().run_until_parked();
16162 cx.update_editor(|editor, window, cx| {
16163 editor.trigger_completion_on_input("n", true, window, cx)
16164 });
16165 cx.executor().run_until_parked();
16166
16167 cx.update_editor(|editor, window, cx| {
16168 editor.confirm_completion(&Default::default(), window, cx)
16169 });
16170 cx.executor().run_until_parked();
16171 cx.assert_editor_state("fn a() {}\n unsafeˇ");
16172}
16173
16174#[gpui::test]
16175async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
16176 init_test(cx, |_| {});
16177 let language =
16178 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
16179 let mut cx = EditorLspTestContext::new(
16180 language,
16181 lsp::ServerCapabilities {
16182 completion_provider: Some(lsp::CompletionOptions {
16183 ..lsp::CompletionOptions::default()
16184 }),
16185 ..lsp::ServerCapabilities::default()
16186 },
16187 cx,
16188 )
16189 .await;
16190
16191 cx.set_state(
16192 "#ifndef BAR_H
16193#define BAR_H
16194
16195#include <stdbool.h>
16196
16197int fn_branch(bool do_branch1, bool do_branch2);
16198
16199#endif // BAR_H
16200ˇ",
16201 );
16202 cx.executor().run_until_parked();
16203 cx.update_editor(|editor, window, cx| {
16204 editor.handle_input("#", window, cx);
16205 });
16206 cx.executor().run_until_parked();
16207 cx.update_editor(|editor, window, cx| {
16208 editor.handle_input("i", window, cx);
16209 });
16210 cx.executor().run_until_parked();
16211 cx.update_editor(|editor, window, cx| {
16212 editor.handle_input("n", window, cx);
16213 });
16214 cx.executor().run_until_parked();
16215 cx.assert_editor_state(
16216 "#ifndef BAR_H
16217#define BAR_H
16218
16219#include <stdbool.h>
16220
16221int fn_branch(bool do_branch1, bool do_branch2);
16222
16223#endif // BAR_H
16224#inˇ",
16225 );
16226
16227 cx.lsp
16228 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16229 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16230 is_incomplete: false,
16231 item_defaults: None,
16232 items: vec![lsp::CompletionItem {
16233 kind: Some(lsp::CompletionItemKind::SNIPPET),
16234 label_details: Some(lsp::CompletionItemLabelDetails {
16235 detail: Some("header".to_string()),
16236 description: None,
16237 }),
16238 label: " include".to_string(),
16239 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16240 range: lsp::Range {
16241 start: lsp::Position {
16242 line: 8,
16243 character: 1,
16244 },
16245 end: lsp::Position {
16246 line: 8,
16247 character: 1,
16248 },
16249 },
16250 new_text: "include \"$0\"".to_string(),
16251 })),
16252 sort_text: Some("40b67681include".to_string()),
16253 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16254 filter_text: Some("include".to_string()),
16255 insert_text: Some("include \"$0\"".to_string()),
16256 ..lsp::CompletionItem::default()
16257 }],
16258 })))
16259 });
16260 cx.update_editor(|editor, window, cx| {
16261 editor.show_completions(&ShowCompletions, window, cx);
16262 });
16263 cx.executor().run_until_parked();
16264 cx.update_editor(|editor, window, cx| {
16265 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16266 });
16267 cx.executor().run_until_parked();
16268 cx.assert_editor_state(
16269 "#ifndef BAR_H
16270#define BAR_H
16271
16272#include <stdbool.h>
16273
16274int fn_branch(bool do_branch1, bool do_branch2);
16275
16276#endif // BAR_H
16277#include \"ˇ\"",
16278 );
16279
16280 cx.lsp
16281 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16282 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16283 is_incomplete: true,
16284 item_defaults: None,
16285 items: vec![lsp::CompletionItem {
16286 kind: Some(lsp::CompletionItemKind::FILE),
16287 label: "AGL/".to_string(),
16288 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16289 range: lsp::Range {
16290 start: lsp::Position {
16291 line: 8,
16292 character: 10,
16293 },
16294 end: lsp::Position {
16295 line: 8,
16296 character: 11,
16297 },
16298 },
16299 new_text: "AGL/".to_string(),
16300 })),
16301 sort_text: Some("40b67681AGL/".to_string()),
16302 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16303 filter_text: Some("AGL/".to_string()),
16304 insert_text: Some("AGL/".to_string()),
16305 ..lsp::CompletionItem::default()
16306 }],
16307 })))
16308 });
16309 cx.update_editor(|editor, window, cx| {
16310 editor.show_completions(&ShowCompletions, window, cx);
16311 });
16312 cx.executor().run_until_parked();
16313 cx.update_editor(|editor, window, cx| {
16314 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16315 });
16316 cx.executor().run_until_parked();
16317 cx.assert_editor_state(
16318 r##"#ifndef BAR_H
16319#define BAR_H
16320
16321#include <stdbool.h>
16322
16323int fn_branch(bool do_branch1, bool do_branch2);
16324
16325#endif // BAR_H
16326#include "AGL/ˇ"##,
16327 );
16328
16329 cx.update_editor(|editor, window, cx| {
16330 editor.handle_input("\"", window, cx);
16331 });
16332 cx.executor().run_until_parked();
16333 cx.assert_editor_state(
16334 r##"#ifndef BAR_H
16335#define BAR_H
16336
16337#include <stdbool.h>
16338
16339int fn_branch(bool do_branch1, bool do_branch2);
16340
16341#endif // BAR_H
16342#include "AGL/"ˇ"##,
16343 );
16344}
16345
16346#[gpui::test]
16347async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
16348 init_test(cx, |_| {});
16349
16350 let mut cx = EditorLspTestContext::new_rust(
16351 lsp::ServerCapabilities {
16352 completion_provider: Some(lsp::CompletionOptions {
16353 trigger_characters: Some(vec![".".to_string()]),
16354 resolve_provider: Some(true),
16355 ..Default::default()
16356 }),
16357 ..Default::default()
16358 },
16359 cx,
16360 )
16361 .await;
16362
16363 cx.set_state("fn main() { let a = 2ˇ; }");
16364 cx.simulate_keystroke(".");
16365 let completion_item = lsp::CompletionItem {
16366 label: "Some".into(),
16367 kind: Some(lsp::CompletionItemKind::SNIPPET),
16368 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16369 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16370 kind: lsp::MarkupKind::Markdown,
16371 value: "```rust\nSome(2)\n```".to_string(),
16372 })),
16373 deprecated: Some(false),
16374 sort_text: Some("Some".to_string()),
16375 filter_text: Some("Some".to_string()),
16376 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16377 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16378 range: lsp::Range {
16379 start: lsp::Position {
16380 line: 0,
16381 character: 22,
16382 },
16383 end: lsp::Position {
16384 line: 0,
16385 character: 22,
16386 },
16387 },
16388 new_text: "Some(2)".to_string(),
16389 })),
16390 additional_text_edits: Some(vec![lsp::TextEdit {
16391 range: lsp::Range {
16392 start: lsp::Position {
16393 line: 0,
16394 character: 20,
16395 },
16396 end: lsp::Position {
16397 line: 0,
16398 character: 22,
16399 },
16400 },
16401 new_text: "".to_string(),
16402 }]),
16403 ..Default::default()
16404 };
16405
16406 let closure_completion_item = completion_item.clone();
16407 let counter = Arc::new(AtomicUsize::new(0));
16408 let counter_clone = counter.clone();
16409 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16410 let task_completion_item = closure_completion_item.clone();
16411 counter_clone.fetch_add(1, atomic::Ordering::Release);
16412 async move {
16413 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16414 is_incomplete: true,
16415 item_defaults: None,
16416 items: vec![task_completion_item],
16417 })))
16418 }
16419 });
16420
16421 cx.condition(|editor, _| editor.context_menu_visible())
16422 .await;
16423 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
16424 assert!(request.next().await.is_some());
16425 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16426
16427 cx.simulate_keystrokes("S o m");
16428 cx.condition(|editor, _| editor.context_menu_visible())
16429 .await;
16430 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
16431 assert!(request.next().await.is_some());
16432 assert!(request.next().await.is_some());
16433 assert!(request.next().await.is_some());
16434 request.close();
16435 assert!(request.next().await.is_none());
16436 assert_eq!(
16437 counter.load(atomic::Ordering::Acquire),
16438 4,
16439 "With the completions menu open, only one LSP request should happen per input"
16440 );
16441}
16442
16443#[gpui::test]
16444async fn test_toggle_comment(cx: &mut TestAppContext) {
16445 init_test(cx, |_| {});
16446 let mut cx = EditorTestContext::new(cx).await;
16447 let language = Arc::new(Language::new(
16448 LanguageConfig {
16449 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16450 ..Default::default()
16451 },
16452 Some(tree_sitter_rust::LANGUAGE.into()),
16453 ));
16454 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16455
16456 // If multiple selections intersect a line, the line is only toggled once.
16457 cx.set_state(indoc! {"
16458 fn a() {
16459 «//b();
16460 ˇ»// «c();
16461 //ˇ» d();
16462 }
16463 "});
16464
16465 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16466
16467 cx.assert_editor_state(indoc! {"
16468 fn a() {
16469 «b();
16470 ˇ»«c();
16471 ˇ» d();
16472 }
16473 "});
16474
16475 // The comment prefix is inserted at the same column for every line in a
16476 // selection.
16477 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16478
16479 cx.assert_editor_state(indoc! {"
16480 fn a() {
16481 // «b();
16482 ˇ»// «c();
16483 ˇ» // d();
16484 }
16485 "});
16486
16487 // If a selection ends at the beginning of a line, that line is not toggled.
16488 cx.set_selections_state(indoc! {"
16489 fn a() {
16490 // b();
16491 «// c();
16492 ˇ» // d();
16493 }
16494 "});
16495
16496 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16497
16498 cx.assert_editor_state(indoc! {"
16499 fn a() {
16500 // b();
16501 «c();
16502 ˇ» // d();
16503 }
16504 "});
16505
16506 // If a selection span a single line and is empty, the line is toggled.
16507 cx.set_state(indoc! {"
16508 fn a() {
16509 a();
16510 b();
16511 ˇ
16512 }
16513 "});
16514
16515 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16516
16517 cx.assert_editor_state(indoc! {"
16518 fn a() {
16519 a();
16520 b();
16521 //•ˇ
16522 }
16523 "});
16524
16525 // If a selection span multiple lines, empty lines are not toggled.
16526 cx.set_state(indoc! {"
16527 fn a() {
16528 «a();
16529
16530 c();ˇ»
16531 }
16532 "});
16533
16534 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16535
16536 cx.assert_editor_state(indoc! {"
16537 fn a() {
16538 // «a();
16539
16540 // c();ˇ»
16541 }
16542 "});
16543
16544 // If a selection includes multiple comment prefixes, all lines are uncommented.
16545 cx.set_state(indoc! {"
16546 fn a() {
16547 «// a();
16548 /// b();
16549 //! c();ˇ»
16550 }
16551 "});
16552
16553 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16554
16555 cx.assert_editor_state(indoc! {"
16556 fn a() {
16557 «a();
16558 b();
16559 c();ˇ»
16560 }
16561 "});
16562}
16563
16564#[gpui::test]
16565async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16566 init_test(cx, |_| {});
16567 let mut cx = EditorTestContext::new(cx).await;
16568 let language = Arc::new(Language::new(
16569 LanguageConfig {
16570 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16571 ..Default::default()
16572 },
16573 Some(tree_sitter_rust::LANGUAGE.into()),
16574 ));
16575 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16576
16577 let toggle_comments = &ToggleComments {
16578 advance_downwards: false,
16579 ignore_indent: true,
16580 };
16581
16582 // If multiple selections intersect a line, the line is only toggled once.
16583 cx.set_state(indoc! {"
16584 fn a() {
16585 // «b();
16586 // c();
16587 // ˇ» d();
16588 }
16589 "});
16590
16591 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16592
16593 cx.assert_editor_state(indoc! {"
16594 fn a() {
16595 «b();
16596 c();
16597 ˇ» d();
16598 }
16599 "});
16600
16601 // The comment prefix is inserted at the beginning of each line
16602 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16603
16604 cx.assert_editor_state(indoc! {"
16605 fn a() {
16606 // «b();
16607 // c();
16608 // ˇ» d();
16609 }
16610 "});
16611
16612 // If a selection ends at the beginning of a line, that line is not toggled.
16613 cx.set_selections_state(indoc! {"
16614 fn a() {
16615 // b();
16616 // «c();
16617 ˇ»// d();
16618 }
16619 "});
16620
16621 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16622
16623 cx.assert_editor_state(indoc! {"
16624 fn a() {
16625 // b();
16626 «c();
16627 ˇ»// d();
16628 }
16629 "});
16630
16631 // If a selection span a single line and is empty, the line is toggled.
16632 cx.set_state(indoc! {"
16633 fn a() {
16634 a();
16635 b();
16636 ˇ
16637 }
16638 "});
16639
16640 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16641
16642 cx.assert_editor_state(indoc! {"
16643 fn a() {
16644 a();
16645 b();
16646 //ˇ
16647 }
16648 "});
16649
16650 // If a selection span multiple lines, empty lines are not toggled.
16651 cx.set_state(indoc! {"
16652 fn a() {
16653 «a();
16654
16655 c();ˇ»
16656 }
16657 "});
16658
16659 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16660
16661 cx.assert_editor_state(indoc! {"
16662 fn a() {
16663 // «a();
16664
16665 // c();ˇ»
16666 }
16667 "});
16668
16669 // If a selection includes multiple comment prefixes, all lines are uncommented.
16670 cx.set_state(indoc! {"
16671 fn a() {
16672 // «a();
16673 /// b();
16674 //! c();ˇ»
16675 }
16676 "});
16677
16678 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16679
16680 cx.assert_editor_state(indoc! {"
16681 fn a() {
16682 «a();
16683 b();
16684 c();ˇ»
16685 }
16686 "});
16687}
16688
16689#[gpui::test]
16690async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16691 init_test(cx, |_| {});
16692
16693 let language = Arc::new(Language::new(
16694 LanguageConfig {
16695 line_comments: vec!["// ".into()],
16696 ..Default::default()
16697 },
16698 Some(tree_sitter_rust::LANGUAGE.into()),
16699 ));
16700
16701 let mut cx = EditorTestContext::new(cx).await;
16702
16703 cx.language_registry().add(language.clone());
16704 cx.update_buffer(|buffer, cx| {
16705 buffer.set_language(Some(language), cx);
16706 });
16707
16708 let toggle_comments = &ToggleComments {
16709 advance_downwards: true,
16710 ignore_indent: false,
16711 };
16712
16713 // Single cursor on one line -> advance
16714 // Cursor moves horizontally 3 characters as well on non-blank line
16715 cx.set_state(indoc!(
16716 "fn a() {
16717 ˇdog();
16718 cat();
16719 }"
16720 ));
16721 cx.update_editor(|editor, window, cx| {
16722 editor.toggle_comments(toggle_comments, window, cx);
16723 });
16724 cx.assert_editor_state(indoc!(
16725 "fn a() {
16726 // dog();
16727 catˇ();
16728 }"
16729 ));
16730
16731 // Single selection on one line -> don't advance
16732 cx.set_state(indoc!(
16733 "fn a() {
16734 «dog()ˇ»;
16735 cat();
16736 }"
16737 ));
16738 cx.update_editor(|editor, window, cx| {
16739 editor.toggle_comments(toggle_comments, window, cx);
16740 });
16741 cx.assert_editor_state(indoc!(
16742 "fn a() {
16743 // «dog()ˇ»;
16744 cat();
16745 }"
16746 ));
16747
16748 // Multiple cursors on one line -> advance
16749 cx.set_state(indoc!(
16750 "fn a() {
16751 ˇdˇog();
16752 cat();
16753 }"
16754 ));
16755 cx.update_editor(|editor, window, cx| {
16756 editor.toggle_comments(toggle_comments, window, cx);
16757 });
16758 cx.assert_editor_state(indoc!(
16759 "fn a() {
16760 // dog();
16761 catˇ(ˇ);
16762 }"
16763 ));
16764
16765 // Multiple cursors on one line, with selection -> don't advance
16766 cx.set_state(indoc!(
16767 "fn a() {
16768 ˇdˇog«()ˇ»;
16769 cat();
16770 }"
16771 ));
16772 cx.update_editor(|editor, window, cx| {
16773 editor.toggle_comments(toggle_comments, window, cx);
16774 });
16775 cx.assert_editor_state(indoc!(
16776 "fn a() {
16777 // ˇdˇog«()ˇ»;
16778 cat();
16779 }"
16780 ));
16781
16782 // Single cursor on one line -> advance
16783 // Cursor moves to column 0 on blank line
16784 cx.set_state(indoc!(
16785 "fn a() {
16786 ˇdog();
16787
16788 cat();
16789 }"
16790 ));
16791 cx.update_editor(|editor, window, cx| {
16792 editor.toggle_comments(toggle_comments, window, cx);
16793 });
16794 cx.assert_editor_state(indoc!(
16795 "fn a() {
16796 // dog();
16797 ˇ
16798 cat();
16799 }"
16800 ));
16801
16802 // Single cursor on one line -> advance
16803 // Cursor starts and ends at column 0
16804 cx.set_state(indoc!(
16805 "fn a() {
16806 ˇ dog();
16807 cat();
16808 }"
16809 ));
16810 cx.update_editor(|editor, window, cx| {
16811 editor.toggle_comments(toggle_comments, window, cx);
16812 });
16813 cx.assert_editor_state(indoc!(
16814 "fn a() {
16815 // dog();
16816 ˇ cat();
16817 }"
16818 ));
16819}
16820
16821#[gpui::test]
16822async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16823 init_test(cx, |_| {});
16824
16825 let mut cx = EditorTestContext::new(cx).await;
16826
16827 let html_language = Arc::new(
16828 Language::new(
16829 LanguageConfig {
16830 name: "HTML".into(),
16831 block_comment: Some(BlockCommentConfig {
16832 start: "<!-- ".into(),
16833 prefix: "".into(),
16834 end: " -->".into(),
16835 tab_size: 0,
16836 }),
16837 ..Default::default()
16838 },
16839 Some(tree_sitter_html::LANGUAGE.into()),
16840 )
16841 .with_injection_query(
16842 r#"
16843 (script_element
16844 (raw_text) @injection.content
16845 (#set! injection.language "javascript"))
16846 "#,
16847 )
16848 .unwrap(),
16849 );
16850
16851 let javascript_language = Arc::new(Language::new(
16852 LanguageConfig {
16853 name: "JavaScript".into(),
16854 line_comments: vec!["// ".into()],
16855 ..Default::default()
16856 },
16857 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16858 ));
16859
16860 cx.language_registry().add(html_language.clone());
16861 cx.language_registry().add(javascript_language);
16862 cx.update_buffer(|buffer, cx| {
16863 buffer.set_language(Some(html_language), cx);
16864 });
16865
16866 // Toggle comments for empty selections
16867 cx.set_state(
16868 &r#"
16869 <p>A</p>ˇ
16870 <p>B</p>ˇ
16871 <p>C</p>ˇ
16872 "#
16873 .unindent(),
16874 );
16875 cx.update_editor(|editor, window, cx| {
16876 editor.toggle_comments(&ToggleComments::default(), window, cx)
16877 });
16878 cx.assert_editor_state(
16879 &r#"
16880 <!-- <p>A</p>ˇ -->
16881 <!-- <p>B</p>ˇ -->
16882 <!-- <p>C</p>ˇ -->
16883 "#
16884 .unindent(),
16885 );
16886 cx.update_editor(|editor, window, cx| {
16887 editor.toggle_comments(&ToggleComments::default(), window, cx)
16888 });
16889 cx.assert_editor_state(
16890 &r#"
16891 <p>A</p>ˇ
16892 <p>B</p>ˇ
16893 <p>C</p>ˇ
16894 "#
16895 .unindent(),
16896 );
16897
16898 // Toggle comments for mixture of empty and non-empty selections, where
16899 // multiple selections occupy a given line.
16900 cx.set_state(
16901 &r#"
16902 <p>A«</p>
16903 <p>ˇ»B</p>ˇ
16904 <p>C«</p>
16905 <p>ˇ»D</p>ˇ
16906 "#
16907 .unindent(),
16908 );
16909
16910 cx.update_editor(|editor, window, cx| {
16911 editor.toggle_comments(&ToggleComments::default(), window, cx)
16912 });
16913 cx.assert_editor_state(
16914 &r#"
16915 <!-- <p>A«</p>
16916 <p>ˇ»B</p>ˇ -->
16917 <!-- <p>C«</p>
16918 <p>ˇ»D</p>ˇ -->
16919 "#
16920 .unindent(),
16921 );
16922 cx.update_editor(|editor, window, cx| {
16923 editor.toggle_comments(&ToggleComments::default(), window, cx)
16924 });
16925 cx.assert_editor_state(
16926 &r#"
16927 <p>A«</p>
16928 <p>ˇ»B</p>ˇ
16929 <p>C«</p>
16930 <p>ˇ»D</p>ˇ
16931 "#
16932 .unindent(),
16933 );
16934
16935 // Toggle comments when different languages are active for different
16936 // selections.
16937 cx.set_state(
16938 &r#"
16939 ˇ<script>
16940 ˇvar x = new Y();
16941 ˇ</script>
16942 "#
16943 .unindent(),
16944 );
16945 cx.executor().run_until_parked();
16946 cx.update_editor(|editor, window, cx| {
16947 editor.toggle_comments(&ToggleComments::default(), window, cx)
16948 });
16949 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16950 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16951 cx.assert_editor_state(
16952 &r#"
16953 <!-- ˇ<script> -->
16954 // ˇvar x = new Y();
16955 <!-- ˇ</script> -->
16956 "#
16957 .unindent(),
16958 );
16959}
16960
16961#[gpui::test]
16962fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16963 init_test(cx, |_| {});
16964
16965 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16966 let multibuffer = cx.new(|cx| {
16967 let mut multibuffer = MultiBuffer::new(ReadWrite);
16968 multibuffer.push_excerpts(
16969 buffer.clone(),
16970 [
16971 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16972 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16973 ],
16974 cx,
16975 );
16976 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16977 multibuffer
16978 });
16979
16980 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16981 editor.update_in(cx, |editor, window, cx| {
16982 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16984 s.select_ranges([
16985 Point::new(0, 0)..Point::new(0, 0),
16986 Point::new(1, 0)..Point::new(1, 0),
16987 ])
16988 });
16989
16990 editor.handle_input("X", window, cx);
16991 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16992 assert_eq!(
16993 editor.selections.ranges(&editor.display_snapshot(cx)),
16994 [
16995 Point::new(0, 1)..Point::new(0, 1),
16996 Point::new(1, 1)..Point::new(1, 1),
16997 ]
16998 );
16999
17000 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
17001 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17002 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
17003 });
17004 editor.backspace(&Default::default(), window, cx);
17005 assert_eq!(editor.text(cx), "Xa\nbbb");
17006 assert_eq!(
17007 editor.selections.ranges(&editor.display_snapshot(cx)),
17008 [Point::new(1, 0)..Point::new(1, 0)]
17009 );
17010
17011 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17012 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
17013 });
17014 editor.backspace(&Default::default(), window, cx);
17015 assert_eq!(editor.text(cx), "X\nbb");
17016 assert_eq!(
17017 editor.selections.ranges(&editor.display_snapshot(cx)),
17018 [Point::new(0, 1)..Point::new(0, 1)]
17019 );
17020 });
17021}
17022
17023#[gpui::test]
17024fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
17025 init_test(cx, |_| {});
17026
17027 let markers = vec![('[', ']').into(), ('(', ')').into()];
17028 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
17029 indoc! {"
17030 [aaaa
17031 (bbbb]
17032 cccc)",
17033 },
17034 markers.clone(),
17035 );
17036 let excerpt_ranges = markers.into_iter().map(|marker| {
17037 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
17038 ExcerptRange::new(context)
17039 });
17040 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
17041 let multibuffer = cx.new(|cx| {
17042 let mut multibuffer = MultiBuffer::new(ReadWrite);
17043 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
17044 multibuffer
17045 });
17046
17047 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
17048 editor.update_in(cx, |editor, window, cx| {
17049 let (expected_text, selection_ranges) = marked_text_ranges(
17050 indoc! {"
17051 aaaa
17052 bˇbbb
17053 bˇbbˇb
17054 cccc"
17055 },
17056 true,
17057 );
17058 assert_eq!(editor.text(cx), expected_text);
17059 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17060 s.select_ranges(
17061 selection_ranges
17062 .iter()
17063 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
17064 )
17065 });
17066
17067 editor.handle_input("X", window, cx);
17068
17069 let (expected_text, expected_selections) = marked_text_ranges(
17070 indoc! {"
17071 aaaa
17072 bXˇbbXb
17073 bXˇbbXˇb
17074 cccc"
17075 },
17076 false,
17077 );
17078 assert_eq!(editor.text(cx), expected_text);
17079 assert_eq!(
17080 editor.selections.ranges(&editor.display_snapshot(cx)),
17081 expected_selections
17082 .iter()
17083 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
17084 .collect::<Vec<_>>()
17085 );
17086
17087 editor.newline(&Newline, window, cx);
17088 let (expected_text, expected_selections) = marked_text_ranges(
17089 indoc! {"
17090 aaaa
17091 bX
17092 ˇbbX
17093 b
17094 bX
17095 ˇbbX
17096 ˇb
17097 cccc"
17098 },
17099 false,
17100 );
17101 assert_eq!(editor.text(cx), expected_text);
17102 assert_eq!(
17103 editor.selections.ranges(&editor.display_snapshot(cx)),
17104 expected_selections
17105 .iter()
17106 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
17107 .collect::<Vec<_>>()
17108 );
17109 });
17110}
17111
17112#[gpui::test]
17113fn test_refresh_selections(cx: &mut TestAppContext) {
17114 init_test(cx, |_| {});
17115
17116 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17117 let mut excerpt1_id = None;
17118 let multibuffer = cx.new(|cx| {
17119 let mut multibuffer = MultiBuffer::new(ReadWrite);
17120 excerpt1_id = multibuffer
17121 .push_excerpts(
17122 buffer.clone(),
17123 [
17124 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17125 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17126 ],
17127 cx,
17128 )
17129 .into_iter()
17130 .next();
17131 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17132 multibuffer
17133 });
17134
17135 let editor = cx.add_window(|window, cx| {
17136 let mut editor = build_editor(multibuffer.clone(), window, cx);
17137 let snapshot = editor.snapshot(window, cx);
17138 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17139 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
17140 });
17141 editor.begin_selection(
17142 Point::new(2, 1).to_display_point(&snapshot),
17143 true,
17144 1,
17145 window,
17146 cx,
17147 );
17148 assert_eq!(
17149 editor.selections.ranges(&editor.display_snapshot(cx)),
17150 [
17151 Point::new(1, 3)..Point::new(1, 3),
17152 Point::new(2, 1)..Point::new(2, 1),
17153 ]
17154 );
17155 editor
17156 });
17157
17158 // Refreshing selections is a no-op when excerpts haven't changed.
17159 _ = editor.update(cx, |editor, window, cx| {
17160 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17161 assert_eq!(
17162 editor.selections.ranges(&editor.display_snapshot(cx)),
17163 [
17164 Point::new(1, 3)..Point::new(1, 3),
17165 Point::new(2, 1)..Point::new(2, 1),
17166 ]
17167 );
17168 });
17169
17170 multibuffer.update(cx, |multibuffer, cx| {
17171 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17172 });
17173 _ = editor.update(cx, |editor, window, cx| {
17174 // Removing an excerpt causes the first selection to become degenerate.
17175 assert_eq!(
17176 editor.selections.ranges(&editor.display_snapshot(cx)),
17177 [
17178 Point::new(0, 0)..Point::new(0, 0),
17179 Point::new(0, 1)..Point::new(0, 1)
17180 ]
17181 );
17182
17183 // Refreshing selections will relocate the first selection to the original buffer
17184 // location.
17185 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17186 assert_eq!(
17187 editor.selections.ranges(&editor.display_snapshot(cx)),
17188 [
17189 Point::new(0, 1)..Point::new(0, 1),
17190 Point::new(0, 3)..Point::new(0, 3)
17191 ]
17192 );
17193 assert!(editor.selections.pending_anchor().is_some());
17194 });
17195}
17196
17197#[gpui::test]
17198fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
17199 init_test(cx, |_| {});
17200
17201 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17202 let mut excerpt1_id = None;
17203 let multibuffer = cx.new(|cx| {
17204 let mut multibuffer = MultiBuffer::new(ReadWrite);
17205 excerpt1_id = multibuffer
17206 .push_excerpts(
17207 buffer.clone(),
17208 [
17209 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17210 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17211 ],
17212 cx,
17213 )
17214 .into_iter()
17215 .next();
17216 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17217 multibuffer
17218 });
17219
17220 let editor = cx.add_window(|window, cx| {
17221 let mut editor = build_editor(multibuffer.clone(), window, cx);
17222 let snapshot = editor.snapshot(window, cx);
17223 editor.begin_selection(
17224 Point::new(1, 3).to_display_point(&snapshot),
17225 false,
17226 1,
17227 window,
17228 cx,
17229 );
17230 assert_eq!(
17231 editor.selections.ranges(&editor.display_snapshot(cx)),
17232 [Point::new(1, 3)..Point::new(1, 3)]
17233 );
17234 editor
17235 });
17236
17237 multibuffer.update(cx, |multibuffer, cx| {
17238 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17239 });
17240 _ = editor.update(cx, |editor, window, cx| {
17241 assert_eq!(
17242 editor.selections.ranges(&editor.display_snapshot(cx)),
17243 [Point::new(0, 0)..Point::new(0, 0)]
17244 );
17245
17246 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
17247 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17248 assert_eq!(
17249 editor.selections.ranges(&editor.display_snapshot(cx)),
17250 [Point::new(0, 3)..Point::new(0, 3)]
17251 );
17252 assert!(editor.selections.pending_anchor().is_some());
17253 });
17254}
17255
17256#[gpui::test]
17257async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
17258 init_test(cx, |_| {});
17259
17260 let language = Arc::new(
17261 Language::new(
17262 LanguageConfig {
17263 brackets: BracketPairConfig {
17264 pairs: vec![
17265 BracketPair {
17266 start: "{".to_string(),
17267 end: "}".to_string(),
17268 close: true,
17269 surround: true,
17270 newline: true,
17271 },
17272 BracketPair {
17273 start: "/* ".to_string(),
17274 end: " */".to_string(),
17275 close: true,
17276 surround: true,
17277 newline: true,
17278 },
17279 ],
17280 ..Default::default()
17281 },
17282 ..Default::default()
17283 },
17284 Some(tree_sitter_rust::LANGUAGE.into()),
17285 )
17286 .with_indents_query("")
17287 .unwrap(),
17288 );
17289
17290 let text = concat!(
17291 "{ }\n", //
17292 " x\n", //
17293 " /* */\n", //
17294 "x\n", //
17295 "{{} }\n", //
17296 );
17297
17298 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17299 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17300 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17301 editor
17302 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
17303 .await;
17304
17305 editor.update_in(cx, |editor, window, cx| {
17306 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17307 s.select_display_ranges([
17308 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
17309 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
17310 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
17311 ])
17312 });
17313 editor.newline(&Newline, window, cx);
17314
17315 assert_eq!(
17316 editor.buffer().read(cx).read(cx).text(),
17317 concat!(
17318 "{ \n", // Suppress rustfmt
17319 "\n", //
17320 "}\n", //
17321 " x\n", //
17322 " /* \n", //
17323 " \n", //
17324 " */\n", //
17325 "x\n", //
17326 "{{} \n", //
17327 "}\n", //
17328 )
17329 );
17330 });
17331}
17332
17333#[gpui::test]
17334fn test_highlighted_ranges(cx: &mut TestAppContext) {
17335 init_test(cx, |_| {});
17336
17337 let editor = cx.add_window(|window, cx| {
17338 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
17339 build_editor(buffer, window, cx)
17340 });
17341
17342 _ = editor.update(cx, |editor, window, cx| {
17343 struct Type1;
17344 struct Type2;
17345
17346 let buffer = editor.buffer.read(cx).snapshot(cx);
17347
17348 let anchor_range =
17349 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
17350
17351 editor.highlight_background::<Type1>(
17352 &[
17353 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
17354 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
17355 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
17356 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
17357 ],
17358 |_, _| Hsla::red(),
17359 cx,
17360 );
17361 editor.highlight_background::<Type2>(
17362 &[
17363 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
17364 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
17365 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
17366 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
17367 ],
17368 |_, _| Hsla::green(),
17369 cx,
17370 );
17371
17372 let snapshot = editor.snapshot(window, cx);
17373 let highlighted_ranges = editor.sorted_background_highlights_in_range(
17374 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
17375 &snapshot,
17376 cx.theme(),
17377 );
17378 assert_eq!(
17379 highlighted_ranges,
17380 &[
17381 (
17382 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
17383 Hsla::green(),
17384 ),
17385 (
17386 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
17387 Hsla::red(),
17388 ),
17389 (
17390 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
17391 Hsla::green(),
17392 ),
17393 (
17394 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17395 Hsla::red(),
17396 ),
17397 ]
17398 );
17399 assert_eq!(
17400 editor.sorted_background_highlights_in_range(
17401 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
17402 &snapshot,
17403 cx.theme(),
17404 ),
17405 &[(
17406 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17407 Hsla::red(),
17408 )]
17409 );
17410 });
17411}
17412
17413#[gpui::test]
17414async fn test_following(cx: &mut TestAppContext) {
17415 init_test(cx, |_| {});
17416
17417 let fs = FakeFs::new(cx.executor());
17418 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17419
17420 let buffer = project.update(cx, |project, cx| {
17421 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
17422 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
17423 });
17424 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17425 let follower = cx.update(|cx| {
17426 cx.open_window(
17427 WindowOptions {
17428 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
17429 gpui::Point::new(px(0.), px(0.)),
17430 gpui::Point::new(px(10.), px(80.)),
17431 ))),
17432 ..Default::default()
17433 },
17434 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
17435 )
17436 .unwrap()
17437 });
17438
17439 let is_still_following = Rc::new(RefCell::new(true));
17440 let follower_edit_event_count = Rc::new(RefCell::new(0));
17441 let pending_update = Rc::new(RefCell::new(None));
17442 let leader_entity = leader.root(cx).unwrap();
17443 let follower_entity = follower.root(cx).unwrap();
17444 _ = follower.update(cx, {
17445 let update = pending_update.clone();
17446 let is_still_following = is_still_following.clone();
17447 let follower_edit_event_count = follower_edit_event_count.clone();
17448 |_, window, cx| {
17449 cx.subscribe_in(
17450 &leader_entity,
17451 window,
17452 move |_, leader, event, window, cx| {
17453 leader.read(cx).add_event_to_update_proto(
17454 event,
17455 &mut update.borrow_mut(),
17456 window,
17457 cx,
17458 );
17459 },
17460 )
17461 .detach();
17462
17463 cx.subscribe_in(
17464 &follower_entity,
17465 window,
17466 move |_, _, event: &EditorEvent, _window, _cx| {
17467 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
17468 *is_still_following.borrow_mut() = false;
17469 }
17470
17471 if let EditorEvent::BufferEdited = event {
17472 *follower_edit_event_count.borrow_mut() += 1;
17473 }
17474 },
17475 )
17476 .detach();
17477 }
17478 });
17479
17480 // Update the selections only
17481 _ = leader.update(cx, |leader, window, cx| {
17482 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17483 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17484 });
17485 });
17486 follower
17487 .update(cx, |follower, window, cx| {
17488 follower.apply_update_proto(
17489 &project,
17490 pending_update.borrow_mut().take().unwrap(),
17491 window,
17492 cx,
17493 )
17494 })
17495 .unwrap()
17496 .await
17497 .unwrap();
17498 _ = follower.update(cx, |follower, _, cx| {
17499 assert_eq!(
17500 follower.selections.ranges(&follower.display_snapshot(cx)),
17501 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
17502 );
17503 });
17504 assert!(*is_still_following.borrow());
17505 assert_eq!(*follower_edit_event_count.borrow(), 0);
17506
17507 // Update the scroll position only
17508 _ = leader.update(cx, |leader, window, cx| {
17509 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17510 });
17511 follower
17512 .update(cx, |follower, window, cx| {
17513 follower.apply_update_proto(
17514 &project,
17515 pending_update.borrow_mut().take().unwrap(),
17516 window,
17517 cx,
17518 )
17519 })
17520 .unwrap()
17521 .await
17522 .unwrap();
17523 assert_eq!(
17524 follower
17525 .update(cx, |follower, _, cx| follower.scroll_position(cx))
17526 .unwrap(),
17527 gpui::Point::new(1.5, 3.5)
17528 );
17529 assert!(*is_still_following.borrow());
17530 assert_eq!(*follower_edit_event_count.borrow(), 0);
17531
17532 // Update the selections and scroll position. The follower's scroll position is updated
17533 // via autoscroll, not via the leader's exact scroll position.
17534 _ = leader.update(cx, |leader, window, cx| {
17535 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17536 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
17537 });
17538 leader.request_autoscroll(Autoscroll::newest(), cx);
17539 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17540 });
17541 follower
17542 .update(cx, |follower, window, cx| {
17543 follower.apply_update_proto(
17544 &project,
17545 pending_update.borrow_mut().take().unwrap(),
17546 window,
17547 cx,
17548 )
17549 })
17550 .unwrap()
17551 .await
17552 .unwrap();
17553 _ = follower.update(cx, |follower, _, cx| {
17554 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17555 assert_eq!(
17556 follower.selections.ranges(&follower.display_snapshot(cx)),
17557 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17558 );
17559 });
17560 assert!(*is_still_following.borrow());
17561
17562 // Creating a pending selection that precedes another selection
17563 _ = leader.update(cx, |leader, window, cx| {
17564 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17565 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17566 });
17567 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17568 });
17569 follower
17570 .update(cx, |follower, window, cx| {
17571 follower.apply_update_proto(
17572 &project,
17573 pending_update.borrow_mut().take().unwrap(),
17574 window,
17575 cx,
17576 )
17577 })
17578 .unwrap()
17579 .await
17580 .unwrap();
17581 _ = follower.update(cx, |follower, _, cx| {
17582 assert_eq!(
17583 follower.selections.ranges(&follower.display_snapshot(cx)),
17584 vec![
17585 MultiBufferOffset(0)..MultiBufferOffset(0),
17586 MultiBufferOffset(1)..MultiBufferOffset(1)
17587 ]
17588 );
17589 });
17590 assert!(*is_still_following.borrow());
17591
17592 // Extend the pending selection so that it surrounds another selection
17593 _ = leader.update(cx, |leader, window, cx| {
17594 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17595 });
17596 follower
17597 .update(cx, |follower, window, cx| {
17598 follower.apply_update_proto(
17599 &project,
17600 pending_update.borrow_mut().take().unwrap(),
17601 window,
17602 cx,
17603 )
17604 })
17605 .unwrap()
17606 .await
17607 .unwrap();
17608 _ = follower.update(cx, |follower, _, cx| {
17609 assert_eq!(
17610 follower.selections.ranges(&follower.display_snapshot(cx)),
17611 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17612 );
17613 });
17614
17615 // Scrolling locally breaks the follow
17616 _ = follower.update(cx, |follower, window, cx| {
17617 let top_anchor = follower
17618 .buffer()
17619 .read(cx)
17620 .read(cx)
17621 .anchor_after(MultiBufferOffset(0));
17622 follower.set_scroll_anchor(
17623 ScrollAnchor {
17624 anchor: top_anchor,
17625 offset: gpui::Point::new(0.0, 0.5),
17626 },
17627 window,
17628 cx,
17629 );
17630 });
17631 assert!(!(*is_still_following.borrow()));
17632}
17633
17634#[gpui::test]
17635async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17636 init_test(cx, |_| {});
17637
17638 let fs = FakeFs::new(cx.executor());
17639 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17640 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17641 let pane = workspace
17642 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17643 .unwrap();
17644
17645 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17646
17647 let leader = pane.update_in(cx, |_, window, cx| {
17648 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17649 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17650 });
17651
17652 // Start following the editor when it has no excerpts.
17653 let mut state_message =
17654 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17655 let workspace_entity = workspace.root(cx).unwrap();
17656 let follower_1 = cx
17657 .update_window(*workspace.deref(), |_, window, cx| {
17658 Editor::from_state_proto(
17659 workspace_entity,
17660 ViewId {
17661 creator: CollaboratorId::PeerId(PeerId::default()),
17662 id: 0,
17663 },
17664 &mut state_message,
17665 window,
17666 cx,
17667 )
17668 })
17669 .unwrap()
17670 .unwrap()
17671 .await
17672 .unwrap();
17673
17674 let update_message = Rc::new(RefCell::new(None));
17675 follower_1.update_in(cx, {
17676 let update = update_message.clone();
17677 |_, window, cx| {
17678 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17679 leader.read(cx).add_event_to_update_proto(
17680 event,
17681 &mut update.borrow_mut(),
17682 window,
17683 cx,
17684 );
17685 })
17686 .detach();
17687 }
17688 });
17689
17690 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17691 (
17692 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17693 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17694 )
17695 });
17696
17697 // Insert some excerpts.
17698 leader.update(cx, |leader, cx| {
17699 leader.buffer.update(cx, |multibuffer, cx| {
17700 multibuffer.set_excerpts_for_path(
17701 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17702 buffer_1.clone(),
17703 vec![
17704 Point::row_range(0..3),
17705 Point::row_range(1..6),
17706 Point::row_range(12..15),
17707 ],
17708 0,
17709 cx,
17710 );
17711 multibuffer.set_excerpts_for_path(
17712 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17713 buffer_2.clone(),
17714 vec![Point::row_range(0..6), Point::row_range(8..12)],
17715 0,
17716 cx,
17717 );
17718 });
17719 });
17720
17721 // Apply the update of adding the excerpts.
17722 follower_1
17723 .update_in(cx, |follower, window, cx| {
17724 follower.apply_update_proto(
17725 &project,
17726 update_message.borrow().clone().unwrap(),
17727 window,
17728 cx,
17729 )
17730 })
17731 .await
17732 .unwrap();
17733 assert_eq!(
17734 follower_1.update(cx, |editor, cx| editor.text(cx)),
17735 leader.update(cx, |editor, cx| editor.text(cx))
17736 );
17737 update_message.borrow_mut().take();
17738
17739 // Start following separately after it already has excerpts.
17740 let mut state_message =
17741 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17742 let workspace_entity = workspace.root(cx).unwrap();
17743 let follower_2 = cx
17744 .update_window(*workspace.deref(), |_, window, cx| {
17745 Editor::from_state_proto(
17746 workspace_entity,
17747 ViewId {
17748 creator: CollaboratorId::PeerId(PeerId::default()),
17749 id: 0,
17750 },
17751 &mut state_message,
17752 window,
17753 cx,
17754 )
17755 })
17756 .unwrap()
17757 .unwrap()
17758 .await
17759 .unwrap();
17760 assert_eq!(
17761 follower_2.update(cx, |editor, cx| editor.text(cx)),
17762 leader.update(cx, |editor, cx| editor.text(cx))
17763 );
17764
17765 // Remove some excerpts.
17766 leader.update(cx, |leader, cx| {
17767 leader.buffer.update(cx, |multibuffer, cx| {
17768 let excerpt_ids = multibuffer.excerpt_ids();
17769 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17770 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17771 });
17772 });
17773
17774 // Apply the update of removing the excerpts.
17775 follower_1
17776 .update_in(cx, |follower, window, cx| {
17777 follower.apply_update_proto(
17778 &project,
17779 update_message.borrow().clone().unwrap(),
17780 window,
17781 cx,
17782 )
17783 })
17784 .await
17785 .unwrap();
17786 follower_2
17787 .update_in(cx, |follower, window, cx| {
17788 follower.apply_update_proto(
17789 &project,
17790 update_message.borrow().clone().unwrap(),
17791 window,
17792 cx,
17793 )
17794 })
17795 .await
17796 .unwrap();
17797 update_message.borrow_mut().take();
17798 assert_eq!(
17799 follower_1.update(cx, |editor, cx| editor.text(cx)),
17800 leader.update(cx, |editor, cx| editor.text(cx))
17801 );
17802}
17803
17804#[gpui::test]
17805async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17806 init_test(cx, |_| {});
17807
17808 let mut cx = EditorTestContext::new(cx).await;
17809 let lsp_store =
17810 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17811
17812 cx.set_state(indoc! {"
17813 ˇfn func(abc def: i32) -> u32 {
17814 }
17815 "});
17816
17817 cx.update(|_, cx| {
17818 lsp_store.update(cx, |lsp_store, cx| {
17819 lsp_store
17820 .update_diagnostics(
17821 LanguageServerId(0),
17822 lsp::PublishDiagnosticsParams {
17823 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17824 version: None,
17825 diagnostics: vec![
17826 lsp::Diagnostic {
17827 range: lsp::Range::new(
17828 lsp::Position::new(0, 11),
17829 lsp::Position::new(0, 12),
17830 ),
17831 severity: Some(lsp::DiagnosticSeverity::ERROR),
17832 ..Default::default()
17833 },
17834 lsp::Diagnostic {
17835 range: lsp::Range::new(
17836 lsp::Position::new(0, 12),
17837 lsp::Position::new(0, 15),
17838 ),
17839 severity: Some(lsp::DiagnosticSeverity::ERROR),
17840 ..Default::default()
17841 },
17842 lsp::Diagnostic {
17843 range: lsp::Range::new(
17844 lsp::Position::new(0, 25),
17845 lsp::Position::new(0, 28),
17846 ),
17847 severity: Some(lsp::DiagnosticSeverity::ERROR),
17848 ..Default::default()
17849 },
17850 ],
17851 },
17852 None,
17853 DiagnosticSourceKind::Pushed,
17854 &[],
17855 cx,
17856 )
17857 .unwrap()
17858 });
17859 });
17860
17861 executor.run_until_parked();
17862
17863 cx.update_editor(|editor, window, cx| {
17864 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17865 });
17866
17867 cx.assert_editor_state(indoc! {"
17868 fn func(abc def: i32) -> ˇu32 {
17869 }
17870 "});
17871
17872 cx.update_editor(|editor, window, cx| {
17873 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17874 });
17875
17876 cx.assert_editor_state(indoc! {"
17877 fn func(abc ˇdef: i32) -> u32 {
17878 }
17879 "});
17880
17881 cx.update_editor(|editor, window, cx| {
17882 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17883 });
17884
17885 cx.assert_editor_state(indoc! {"
17886 fn func(abcˇ def: i32) -> u32 {
17887 }
17888 "});
17889
17890 cx.update_editor(|editor, window, cx| {
17891 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17892 });
17893
17894 cx.assert_editor_state(indoc! {"
17895 fn func(abc def: i32) -> ˇu32 {
17896 }
17897 "});
17898}
17899
17900#[gpui::test]
17901async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17902 init_test(cx, |_| {});
17903
17904 let mut cx = EditorTestContext::new(cx).await;
17905
17906 let diff_base = r#"
17907 use some::mod;
17908
17909 const A: u32 = 42;
17910
17911 fn main() {
17912 println!("hello");
17913
17914 println!("world");
17915 }
17916 "#
17917 .unindent();
17918
17919 // Edits are modified, removed, modified, added
17920 cx.set_state(
17921 &r#"
17922 use some::modified;
17923
17924 ˇ
17925 fn main() {
17926 println!("hello there");
17927
17928 println!("around the");
17929 println!("world");
17930 }
17931 "#
17932 .unindent(),
17933 );
17934
17935 cx.set_head_text(&diff_base);
17936 executor.run_until_parked();
17937
17938 cx.update_editor(|editor, window, cx| {
17939 //Wrap around the bottom of the buffer
17940 for _ in 0..3 {
17941 editor.go_to_next_hunk(&GoToHunk, window, cx);
17942 }
17943 });
17944
17945 cx.assert_editor_state(
17946 &r#"
17947 ˇuse some::modified;
17948
17949
17950 fn main() {
17951 println!("hello there");
17952
17953 println!("around the");
17954 println!("world");
17955 }
17956 "#
17957 .unindent(),
17958 );
17959
17960 cx.update_editor(|editor, window, cx| {
17961 //Wrap around the top of the buffer
17962 for _ in 0..2 {
17963 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17964 }
17965 });
17966
17967 cx.assert_editor_state(
17968 &r#"
17969 use some::modified;
17970
17971
17972 fn main() {
17973 ˇ println!("hello there");
17974
17975 println!("around the");
17976 println!("world");
17977 }
17978 "#
17979 .unindent(),
17980 );
17981
17982 cx.update_editor(|editor, window, cx| {
17983 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17984 });
17985
17986 cx.assert_editor_state(
17987 &r#"
17988 use some::modified;
17989
17990 ˇ
17991 fn main() {
17992 println!("hello there");
17993
17994 println!("around the");
17995 println!("world");
17996 }
17997 "#
17998 .unindent(),
17999 );
18000
18001 cx.update_editor(|editor, window, cx| {
18002 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18003 });
18004
18005 cx.assert_editor_state(
18006 &r#"
18007 ˇuse some::modified;
18008
18009
18010 fn main() {
18011 println!("hello there");
18012
18013 println!("around the");
18014 println!("world");
18015 }
18016 "#
18017 .unindent(),
18018 );
18019
18020 cx.update_editor(|editor, window, cx| {
18021 for _ in 0..2 {
18022 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18023 }
18024 });
18025
18026 cx.assert_editor_state(
18027 &r#"
18028 use some::modified;
18029
18030
18031 fn main() {
18032 ˇ println!("hello there");
18033
18034 println!("around the");
18035 println!("world");
18036 }
18037 "#
18038 .unindent(),
18039 );
18040
18041 cx.update_editor(|editor, window, cx| {
18042 editor.fold(&Fold, window, cx);
18043 });
18044
18045 cx.update_editor(|editor, window, cx| {
18046 editor.go_to_next_hunk(&GoToHunk, window, cx);
18047 });
18048
18049 cx.assert_editor_state(
18050 &r#"
18051 ˇuse some::modified;
18052
18053
18054 fn main() {
18055 println!("hello there");
18056
18057 println!("around the");
18058 println!("world");
18059 }
18060 "#
18061 .unindent(),
18062 );
18063}
18064
18065#[test]
18066fn test_split_words() {
18067 fn split(text: &str) -> Vec<&str> {
18068 split_words(text).collect()
18069 }
18070
18071 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
18072 assert_eq!(split("hello_world"), &["hello_", "world"]);
18073 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
18074 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
18075 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
18076 assert_eq!(split("helloworld"), &["helloworld"]);
18077
18078 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
18079}
18080
18081#[test]
18082fn test_split_words_for_snippet_prefix() {
18083 fn split(text: &str) -> Vec<&str> {
18084 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
18085 }
18086
18087 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
18088 assert_eq!(split("hello_world"), &["hello_world"]);
18089 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
18090 assert_eq!(split("Hello_World"), &["Hello_World"]);
18091 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
18092 assert_eq!(split("helloworld"), &["helloworld"]);
18093 assert_eq!(
18094 split("this@is!@#$^many . symbols"),
18095 &[
18096 "symbols",
18097 " symbols",
18098 ". symbols",
18099 " . symbols",
18100 " . symbols",
18101 " . symbols",
18102 "many . symbols",
18103 "^many . symbols",
18104 "$^many . symbols",
18105 "#$^many . symbols",
18106 "@#$^many . symbols",
18107 "!@#$^many . symbols",
18108 "is!@#$^many . symbols",
18109 "@is!@#$^many . symbols",
18110 "this@is!@#$^many . symbols",
18111 ],
18112 );
18113 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
18114}
18115
18116#[gpui::test]
18117async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
18118 init_test(cx, |_| {});
18119
18120 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
18121
18122 #[track_caller]
18123 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
18124 let _state_context = cx.set_state(before);
18125 cx.run_until_parked();
18126 cx.update_editor(|editor, window, cx| {
18127 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
18128 });
18129 cx.run_until_parked();
18130 cx.assert_editor_state(after);
18131 }
18132
18133 // Outside bracket jumps to outside of matching bracket
18134 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
18135 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
18136
18137 // Inside bracket jumps to inside of matching bracket
18138 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
18139 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
18140
18141 // When outside a bracket and inside, favor jumping to the inside bracket
18142 assert(
18143 "console.log('foo', [1, 2, 3]ˇ);",
18144 "console.log('foo', ˇ[1, 2, 3]);",
18145 &mut cx,
18146 );
18147 assert(
18148 "console.log(ˇ'foo', [1, 2, 3]);",
18149 "console.log('foo'ˇ, [1, 2, 3]);",
18150 &mut cx,
18151 );
18152
18153 // Bias forward if two options are equally likely
18154 assert(
18155 "let result = curried_fun()ˇ();",
18156 "let result = curried_fun()()ˇ;",
18157 &mut cx,
18158 );
18159
18160 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
18161 assert(
18162 indoc! {"
18163 function test() {
18164 console.log('test')ˇ
18165 }"},
18166 indoc! {"
18167 function test() {
18168 console.logˇ('test')
18169 }"},
18170 &mut cx,
18171 );
18172}
18173
18174#[gpui::test]
18175async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
18176 init_test(cx, |_| {});
18177 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
18178 language_registry.add(markdown_lang());
18179 language_registry.add(rust_lang());
18180 let buffer = cx.new(|cx| {
18181 let mut buffer = language::Buffer::local(
18182 indoc! {"
18183 ```rs
18184 impl Worktree {
18185 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18186 }
18187 }
18188 ```
18189 "},
18190 cx,
18191 );
18192 buffer.set_language_registry(language_registry.clone());
18193 buffer.set_language(Some(markdown_lang()), cx);
18194 buffer
18195 });
18196 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18197 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
18198 cx.executor().run_until_parked();
18199 _ = editor.update(cx, |editor, window, cx| {
18200 // Case 1: Test outer enclosing brackets
18201 select_ranges(
18202 editor,
18203 &indoc! {"
18204 ```rs
18205 impl Worktree {
18206 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18207 }
18208 }ˇ
18209 ```
18210 "},
18211 window,
18212 cx,
18213 );
18214 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18215 assert_text_with_selections(
18216 editor,
18217 &indoc! {"
18218 ```rs
18219 impl Worktree ˇ{
18220 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18221 }
18222 }
18223 ```
18224 "},
18225 cx,
18226 );
18227 // Case 2: Test inner enclosing brackets
18228 select_ranges(
18229 editor,
18230 &indoc! {"
18231 ```rs
18232 impl Worktree {
18233 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18234 }ˇ
18235 }
18236 ```
18237 "},
18238 window,
18239 cx,
18240 );
18241 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18242 assert_text_with_selections(
18243 editor,
18244 &indoc! {"
18245 ```rs
18246 impl Worktree {
18247 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
18248 }
18249 }
18250 ```
18251 "},
18252 cx,
18253 );
18254 });
18255}
18256
18257#[gpui::test]
18258async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
18259 init_test(cx, |_| {});
18260
18261 let fs = FakeFs::new(cx.executor());
18262 fs.insert_tree(
18263 path!("/a"),
18264 json!({
18265 "main.rs": "fn main() { let a = 5; }",
18266 "other.rs": "// Test file",
18267 }),
18268 )
18269 .await;
18270 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18271
18272 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18273 language_registry.add(Arc::new(Language::new(
18274 LanguageConfig {
18275 name: "Rust".into(),
18276 matcher: LanguageMatcher {
18277 path_suffixes: vec!["rs".to_string()],
18278 ..Default::default()
18279 },
18280 brackets: BracketPairConfig {
18281 pairs: vec![BracketPair {
18282 start: "{".to_string(),
18283 end: "}".to_string(),
18284 close: true,
18285 surround: true,
18286 newline: true,
18287 }],
18288 disabled_scopes_by_bracket_ix: Vec::new(),
18289 },
18290 ..Default::default()
18291 },
18292 Some(tree_sitter_rust::LANGUAGE.into()),
18293 )));
18294 let mut fake_servers = language_registry.register_fake_lsp(
18295 "Rust",
18296 FakeLspAdapter {
18297 capabilities: lsp::ServerCapabilities {
18298 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18299 first_trigger_character: "{".to_string(),
18300 more_trigger_character: None,
18301 }),
18302 ..Default::default()
18303 },
18304 ..Default::default()
18305 },
18306 );
18307
18308 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18309
18310 let cx = &mut VisualTestContext::from_window(*workspace, cx);
18311
18312 let worktree_id = workspace
18313 .update(cx, |workspace, _, cx| {
18314 workspace.project().update(cx, |project, cx| {
18315 project.worktrees(cx).next().unwrap().read(cx).id()
18316 })
18317 })
18318 .unwrap();
18319
18320 let buffer = project
18321 .update(cx, |project, cx| {
18322 project.open_local_buffer(path!("/a/main.rs"), cx)
18323 })
18324 .await
18325 .unwrap();
18326 let editor_handle = workspace
18327 .update(cx, |workspace, window, cx| {
18328 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
18329 })
18330 .unwrap()
18331 .await
18332 .unwrap()
18333 .downcast::<Editor>()
18334 .unwrap();
18335
18336 let fake_server = fake_servers.next().await.unwrap();
18337
18338 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
18339 |params, _| async move {
18340 assert_eq!(
18341 params.text_document_position.text_document.uri,
18342 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
18343 );
18344 assert_eq!(
18345 params.text_document_position.position,
18346 lsp::Position::new(0, 21),
18347 );
18348
18349 Ok(Some(vec![lsp::TextEdit {
18350 new_text: "]".to_string(),
18351 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18352 }]))
18353 },
18354 );
18355
18356 editor_handle.update_in(cx, |editor, window, cx| {
18357 window.focus(&editor.focus_handle(cx), cx);
18358 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18359 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
18360 });
18361 editor.handle_input("{", window, cx);
18362 });
18363
18364 cx.executor().run_until_parked();
18365
18366 buffer.update(cx, |buffer, _| {
18367 assert_eq!(
18368 buffer.text(),
18369 "fn main() { let a = {5}; }",
18370 "No extra braces from on type formatting should appear in the buffer"
18371 )
18372 });
18373}
18374
18375#[gpui::test(iterations = 20, seeds(31))]
18376async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
18377 init_test(cx, |_| {});
18378
18379 let mut cx = EditorLspTestContext::new_rust(
18380 lsp::ServerCapabilities {
18381 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18382 first_trigger_character: ".".to_string(),
18383 more_trigger_character: None,
18384 }),
18385 ..Default::default()
18386 },
18387 cx,
18388 )
18389 .await;
18390
18391 cx.update_buffer(|buffer, _| {
18392 // This causes autoindent to be async.
18393 buffer.set_sync_parse_timeout(None)
18394 });
18395
18396 cx.set_state("fn c() {\n d()ˇ\n}\n");
18397 cx.simulate_keystroke("\n");
18398 cx.run_until_parked();
18399
18400 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
18401 let mut request =
18402 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
18403 let buffer_cloned = buffer_cloned.clone();
18404 async move {
18405 buffer_cloned.update(&mut cx, |buffer, _| {
18406 assert_eq!(
18407 buffer.text(),
18408 "fn c() {\n d()\n .\n}\n",
18409 "OnTypeFormatting should triggered after autoindent applied"
18410 )
18411 });
18412
18413 Ok(Some(vec![]))
18414 }
18415 });
18416
18417 cx.simulate_keystroke(".");
18418 cx.run_until_parked();
18419
18420 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
18421 assert!(request.next().await.is_some());
18422 request.close();
18423 assert!(request.next().await.is_none());
18424}
18425
18426#[gpui::test]
18427async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
18428 init_test(cx, |_| {});
18429
18430 let fs = FakeFs::new(cx.executor());
18431 fs.insert_tree(
18432 path!("/a"),
18433 json!({
18434 "main.rs": "fn main() { let a = 5; }",
18435 "other.rs": "// Test file",
18436 }),
18437 )
18438 .await;
18439
18440 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18441
18442 let server_restarts = Arc::new(AtomicUsize::new(0));
18443 let closure_restarts = Arc::clone(&server_restarts);
18444 let language_server_name = "test language server";
18445 let language_name: LanguageName = "Rust".into();
18446
18447 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18448 language_registry.add(Arc::new(Language::new(
18449 LanguageConfig {
18450 name: language_name.clone(),
18451 matcher: LanguageMatcher {
18452 path_suffixes: vec!["rs".to_string()],
18453 ..Default::default()
18454 },
18455 ..Default::default()
18456 },
18457 Some(tree_sitter_rust::LANGUAGE.into()),
18458 )));
18459 let mut fake_servers = language_registry.register_fake_lsp(
18460 "Rust",
18461 FakeLspAdapter {
18462 name: language_server_name,
18463 initialization_options: Some(json!({
18464 "testOptionValue": true
18465 })),
18466 initializer: Some(Box::new(move |fake_server| {
18467 let task_restarts = Arc::clone(&closure_restarts);
18468 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
18469 task_restarts.fetch_add(1, atomic::Ordering::Release);
18470 futures::future::ready(Ok(()))
18471 });
18472 })),
18473 ..Default::default()
18474 },
18475 );
18476
18477 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18478 let _buffer = project
18479 .update(cx, |project, cx| {
18480 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
18481 })
18482 .await
18483 .unwrap();
18484 let _fake_server = fake_servers.next().await.unwrap();
18485 update_test_language_settings(cx, |language_settings| {
18486 language_settings.languages.0.insert(
18487 language_name.clone().0,
18488 LanguageSettingsContent {
18489 tab_size: NonZeroU32::new(8),
18490 ..Default::default()
18491 },
18492 );
18493 });
18494 cx.executor().run_until_parked();
18495 assert_eq!(
18496 server_restarts.load(atomic::Ordering::Acquire),
18497 0,
18498 "Should not restart LSP server on an unrelated change"
18499 );
18500
18501 update_test_project_settings(cx, |project_settings| {
18502 project_settings.lsp.0.insert(
18503 "Some other server name".into(),
18504 LspSettings {
18505 binary: None,
18506 settings: None,
18507 initialization_options: Some(json!({
18508 "some other init value": false
18509 })),
18510 enable_lsp_tasks: false,
18511 fetch: None,
18512 },
18513 );
18514 });
18515 cx.executor().run_until_parked();
18516 assert_eq!(
18517 server_restarts.load(atomic::Ordering::Acquire),
18518 0,
18519 "Should not restart LSP server on an unrelated LSP settings change"
18520 );
18521
18522 update_test_project_settings(cx, |project_settings| {
18523 project_settings.lsp.0.insert(
18524 language_server_name.into(),
18525 LspSettings {
18526 binary: None,
18527 settings: None,
18528 initialization_options: Some(json!({
18529 "anotherInitValue": false
18530 })),
18531 enable_lsp_tasks: false,
18532 fetch: None,
18533 },
18534 );
18535 });
18536 cx.executor().run_until_parked();
18537 assert_eq!(
18538 server_restarts.load(atomic::Ordering::Acquire),
18539 1,
18540 "Should restart LSP server on a related LSP settings change"
18541 );
18542
18543 update_test_project_settings(cx, |project_settings| {
18544 project_settings.lsp.0.insert(
18545 language_server_name.into(),
18546 LspSettings {
18547 binary: None,
18548 settings: None,
18549 initialization_options: Some(json!({
18550 "anotherInitValue": false
18551 })),
18552 enable_lsp_tasks: false,
18553 fetch: None,
18554 },
18555 );
18556 });
18557 cx.executor().run_until_parked();
18558 assert_eq!(
18559 server_restarts.load(atomic::Ordering::Acquire),
18560 1,
18561 "Should not restart LSP server on a related LSP settings change that is the same"
18562 );
18563
18564 update_test_project_settings(cx, |project_settings| {
18565 project_settings.lsp.0.insert(
18566 language_server_name.into(),
18567 LspSettings {
18568 binary: None,
18569 settings: None,
18570 initialization_options: None,
18571 enable_lsp_tasks: false,
18572 fetch: None,
18573 },
18574 );
18575 });
18576 cx.executor().run_until_parked();
18577 assert_eq!(
18578 server_restarts.load(atomic::Ordering::Acquire),
18579 2,
18580 "Should restart LSP server on another related LSP settings change"
18581 );
18582}
18583
18584#[gpui::test]
18585async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18586 init_test(cx, |_| {});
18587
18588 let mut cx = EditorLspTestContext::new_rust(
18589 lsp::ServerCapabilities {
18590 completion_provider: Some(lsp::CompletionOptions {
18591 trigger_characters: Some(vec![".".to_string()]),
18592 resolve_provider: Some(true),
18593 ..Default::default()
18594 }),
18595 ..Default::default()
18596 },
18597 cx,
18598 )
18599 .await;
18600
18601 cx.set_state("fn main() { let a = 2ˇ; }");
18602 cx.simulate_keystroke(".");
18603 let completion_item = lsp::CompletionItem {
18604 label: "some".into(),
18605 kind: Some(lsp::CompletionItemKind::SNIPPET),
18606 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18607 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18608 kind: lsp::MarkupKind::Markdown,
18609 value: "```rust\nSome(2)\n```".to_string(),
18610 })),
18611 deprecated: Some(false),
18612 sort_text: Some("fffffff2".to_string()),
18613 filter_text: Some("some".to_string()),
18614 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18615 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18616 range: lsp::Range {
18617 start: lsp::Position {
18618 line: 0,
18619 character: 22,
18620 },
18621 end: lsp::Position {
18622 line: 0,
18623 character: 22,
18624 },
18625 },
18626 new_text: "Some(2)".to_string(),
18627 })),
18628 additional_text_edits: Some(vec![lsp::TextEdit {
18629 range: lsp::Range {
18630 start: lsp::Position {
18631 line: 0,
18632 character: 20,
18633 },
18634 end: lsp::Position {
18635 line: 0,
18636 character: 22,
18637 },
18638 },
18639 new_text: "".to_string(),
18640 }]),
18641 ..Default::default()
18642 };
18643
18644 let closure_completion_item = completion_item.clone();
18645 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18646 let task_completion_item = closure_completion_item.clone();
18647 async move {
18648 Ok(Some(lsp::CompletionResponse::Array(vec![
18649 task_completion_item,
18650 ])))
18651 }
18652 });
18653
18654 request.next().await;
18655
18656 cx.condition(|editor, _| editor.context_menu_visible())
18657 .await;
18658 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18659 editor
18660 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18661 .unwrap()
18662 });
18663 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18664
18665 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18666 let task_completion_item = completion_item.clone();
18667 async move { Ok(task_completion_item) }
18668 })
18669 .next()
18670 .await
18671 .unwrap();
18672 apply_additional_edits.await.unwrap();
18673 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18674}
18675
18676#[gpui::test]
18677async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18678 init_test(cx, |_| {});
18679
18680 let mut cx = EditorLspTestContext::new_rust(
18681 lsp::ServerCapabilities {
18682 completion_provider: Some(lsp::CompletionOptions {
18683 trigger_characters: Some(vec![".".to_string()]),
18684 resolve_provider: Some(true),
18685 ..Default::default()
18686 }),
18687 ..Default::default()
18688 },
18689 cx,
18690 )
18691 .await;
18692
18693 cx.set_state("fn main() { let a = 2ˇ; }");
18694 cx.simulate_keystroke(".");
18695
18696 let item1 = lsp::CompletionItem {
18697 label: "method id()".to_string(),
18698 filter_text: Some("id".to_string()),
18699 detail: None,
18700 documentation: None,
18701 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18702 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18703 new_text: ".id".to_string(),
18704 })),
18705 ..lsp::CompletionItem::default()
18706 };
18707
18708 let item2 = lsp::CompletionItem {
18709 label: "other".to_string(),
18710 filter_text: Some("other".to_string()),
18711 detail: None,
18712 documentation: None,
18713 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18714 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18715 new_text: ".other".to_string(),
18716 })),
18717 ..lsp::CompletionItem::default()
18718 };
18719
18720 let item1 = item1.clone();
18721 cx.set_request_handler::<lsp::request::Completion, _, _>({
18722 let item1 = item1.clone();
18723 move |_, _, _| {
18724 let item1 = item1.clone();
18725 let item2 = item2.clone();
18726 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
18727 }
18728 })
18729 .next()
18730 .await;
18731
18732 cx.condition(|editor, _| editor.context_menu_visible())
18733 .await;
18734 cx.update_editor(|editor, _, _| {
18735 let context_menu = editor.context_menu.borrow_mut();
18736 let context_menu = context_menu
18737 .as_ref()
18738 .expect("Should have the context menu deployed");
18739 match context_menu {
18740 CodeContextMenu::Completions(completions_menu) => {
18741 let completions = completions_menu.completions.borrow_mut();
18742 assert_eq!(
18743 completions
18744 .iter()
18745 .map(|completion| &completion.label.text)
18746 .collect::<Vec<_>>(),
18747 vec!["method id()", "other"]
18748 )
18749 }
18750 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18751 }
18752 });
18753
18754 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
18755 let item1 = item1.clone();
18756 move |_, item_to_resolve, _| {
18757 let item1 = item1.clone();
18758 async move {
18759 if item1 == item_to_resolve {
18760 Ok(lsp::CompletionItem {
18761 label: "method id()".to_string(),
18762 filter_text: Some("id".to_string()),
18763 detail: Some("Now resolved!".to_string()),
18764 documentation: Some(lsp::Documentation::String("Docs".to_string())),
18765 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18766 range: lsp::Range::new(
18767 lsp::Position::new(0, 22),
18768 lsp::Position::new(0, 22),
18769 ),
18770 new_text: ".id".to_string(),
18771 })),
18772 ..lsp::CompletionItem::default()
18773 })
18774 } else {
18775 Ok(item_to_resolve)
18776 }
18777 }
18778 }
18779 })
18780 .next()
18781 .await
18782 .unwrap();
18783 cx.run_until_parked();
18784
18785 cx.update_editor(|editor, window, cx| {
18786 editor.context_menu_next(&Default::default(), window, cx);
18787 });
18788 cx.run_until_parked();
18789
18790 cx.update_editor(|editor, _, _| {
18791 let context_menu = editor.context_menu.borrow_mut();
18792 let context_menu = context_menu
18793 .as_ref()
18794 .expect("Should have the context menu deployed");
18795 match context_menu {
18796 CodeContextMenu::Completions(completions_menu) => {
18797 let completions = completions_menu.completions.borrow_mut();
18798 assert_eq!(
18799 completions
18800 .iter()
18801 .map(|completion| &completion.label.text)
18802 .collect::<Vec<_>>(),
18803 vec!["method id() Now resolved!", "other"],
18804 "Should update first completion label, but not second as the filter text did not match."
18805 );
18806 }
18807 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18808 }
18809 });
18810}
18811
18812#[gpui::test]
18813async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
18814 init_test(cx, |_| {});
18815 let mut cx = EditorLspTestContext::new_rust(
18816 lsp::ServerCapabilities {
18817 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
18818 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
18819 completion_provider: Some(lsp::CompletionOptions {
18820 resolve_provider: Some(true),
18821 ..Default::default()
18822 }),
18823 ..Default::default()
18824 },
18825 cx,
18826 )
18827 .await;
18828 cx.set_state(indoc! {"
18829 struct TestStruct {
18830 field: i32
18831 }
18832
18833 fn mainˇ() {
18834 let unused_var = 42;
18835 let test_struct = TestStruct { field: 42 };
18836 }
18837 "});
18838 let symbol_range = cx.lsp_range(indoc! {"
18839 struct TestStruct {
18840 field: i32
18841 }
18842
18843 «fn main»() {
18844 let unused_var = 42;
18845 let test_struct = TestStruct { field: 42 };
18846 }
18847 "});
18848 let mut hover_requests =
18849 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18850 Ok(Some(lsp::Hover {
18851 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18852 kind: lsp::MarkupKind::Markdown,
18853 value: "Function documentation".to_string(),
18854 }),
18855 range: Some(symbol_range),
18856 }))
18857 });
18858
18859 // Case 1: Test that code action menu hide hover popover
18860 cx.dispatch_action(Hover);
18861 hover_requests.next().await;
18862 cx.condition(|editor, _| editor.hover_state.visible()).await;
18863 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18864 move |_, _, _| async move {
18865 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18866 lsp::CodeAction {
18867 title: "Remove unused variable".to_string(),
18868 kind: Some(CodeActionKind::QUICKFIX),
18869 edit: Some(lsp::WorkspaceEdit {
18870 changes: Some(
18871 [(
18872 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18873 vec![lsp::TextEdit {
18874 range: lsp::Range::new(
18875 lsp::Position::new(5, 4),
18876 lsp::Position::new(5, 27),
18877 ),
18878 new_text: "".to_string(),
18879 }],
18880 )]
18881 .into_iter()
18882 .collect(),
18883 ),
18884 ..Default::default()
18885 }),
18886 ..Default::default()
18887 },
18888 )]))
18889 },
18890 );
18891 cx.update_editor(|editor, window, cx| {
18892 editor.toggle_code_actions(
18893 &ToggleCodeActions {
18894 deployed_from: None,
18895 quick_launch: false,
18896 },
18897 window,
18898 cx,
18899 );
18900 });
18901 code_action_requests.next().await;
18902 cx.run_until_parked();
18903 cx.condition(|editor, _| editor.context_menu_visible())
18904 .await;
18905 cx.update_editor(|editor, _, _| {
18906 assert!(
18907 !editor.hover_state.visible(),
18908 "Hover popover should be hidden when code action menu is shown"
18909 );
18910 // Hide code actions
18911 editor.context_menu.take();
18912 });
18913
18914 // Case 2: Test that code completions hide hover popover
18915 cx.dispatch_action(Hover);
18916 hover_requests.next().await;
18917 cx.condition(|editor, _| editor.hover_state.visible()).await;
18918 let counter = Arc::new(AtomicUsize::new(0));
18919 let mut completion_requests =
18920 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18921 let counter = counter.clone();
18922 async move {
18923 counter.fetch_add(1, atomic::Ordering::Release);
18924 Ok(Some(lsp::CompletionResponse::Array(vec![
18925 lsp::CompletionItem {
18926 label: "main".into(),
18927 kind: Some(lsp::CompletionItemKind::FUNCTION),
18928 detail: Some("() -> ()".to_string()),
18929 ..Default::default()
18930 },
18931 lsp::CompletionItem {
18932 label: "TestStruct".into(),
18933 kind: Some(lsp::CompletionItemKind::STRUCT),
18934 detail: Some("struct TestStruct".to_string()),
18935 ..Default::default()
18936 },
18937 ])))
18938 }
18939 });
18940 cx.update_editor(|editor, window, cx| {
18941 editor.show_completions(&ShowCompletions, window, cx);
18942 });
18943 completion_requests.next().await;
18944 cx.condition(|editor, _| editor.context_menu_visible())
18945 .await;
18946 cx.update_editor(|editor, _, _| {
18947 assert!(
18948 !editor.hover_state.visible(),
18949 "Hover popover should be hidden when completion menu is shown"
18950 );
18951 });
18952}
18953
18954#[gpui::test]
18955async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18956 init_test(cx, |_| {});
18957
18958 let mut cx = EditorLspTestContext::new_rust(
18959 lsp::ServerCapabilities {
18960 completion_provider: Some(lsp::CompletionOptions {
18961 trigger_characters: Some(vec![".".to_string()]),
18962 resolve_provider: Some(true),
18963 ..Default::default()
18964 }),
18965 ..Default::default()
18966 },
18967 cx,
18968 )
18969 .await;
18970
18971 cx.set_state("fn main() { let a = 2ˇ; }");
18972 cx.simulate_keystroke(".");
18973
18974 let unresolved_item_1 = lsp::CompletionItem {
18975 label: "id".to_string(),
18976 filter_text: Some("id".to_string()),
18977 detail: None,
18978 documentation: None,
18979 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18980 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18981 new_text: ".id".to_string(),
18982 })),
18983 ..lsp::CompletionItem::default()
18984 };
18985 let resolved_item_1 = lsp::CompletionItem {
18986 additional_text_edits: Some(vec![lsp::TextEdit {
18987 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18988 new_text: "!!".to_string(),
18989 }]),
18990 ..unresolved_item_1.clone()
18991 };
18992 let unresolved_item_2 = lsp::CompletionItem {
18993 label: "other".to_string(),
18994 filter_text: Some("other".to_string()),
18995 detail: None,
18996 documentation: None,
18997 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18998 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18999 new_text: ".other".to_string(),
19000 })),
19001 ..lsp::CompletionItem::default()
19002 };
19003 let resolved_item_2 = lsp::CompletionItem {
19004 additional_text_edits: Some(vec![lsp::TextEdit {
19005 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
19006 new_text: "??".to_string(),
19007 }]),
19008 ..unresolved_item_2.clone()
19009 };
19010
19011 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
19012 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
19013 cx.lsp
19014 .server
19015 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19016 let unresolved_item_1 = unresolved_item_1.clone();
19017 let resolved_item_1 = resolved_item_1.clone();
19018 let unresolved_item_2 = unresolved_item_2.clone();
19019 let resolved_item_2 = resolved_item_2.clone();
19020 let resolve_requests_1 = resolve_requests_1.clone();
19021 let resolve_requests_2 = resolve_requests_2.clone();
19022 move |unresolved_request, _| {
19023 let unresolved_item_1 = unresolved_item_1.clone();
19024 let resolved_item_1 = resolved_item_1.clone();
19025 let unresolved_item_2 = unresolved_item_2.clone();
19026 let resolved_item_2 = resolved_item_2.clone();
19027 let resolve_requests_1 = resolve_requests_1.clone();
19028 let resolve_requests_2 = resolve_requests_2.clone();
19029 async move {
19030 if unresolved_request == unresolved_item_1 {
19031 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
19032 Ok(resolved_item_1.clone())
19033 } else if unresolved_request == unresolved_item_2 {
19034 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
19035 Ok(resolved_item_2.clone())
19036 } else {
19037 panic!("Unexpected completion item {unresolved_request:?}")
19038 }
19039 }
19040 }
19041 })
19042 .detach();
19043
19044 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19045 let unresolved_item_1 = unresolved_item_1.clone();
19046 let unresolved_item_2 = unresolved_item_2.clone();
19047 async move {
19048 Ok(Some(lsp::CompletionResponse::Array(vec![
19049 unresolved_item_1,
19050 unresolved_item_2,
19051 ])))
19052 }
19053 })
19054 .next()
19055 .await;
19056
19057 cx.condition(|editor, _| editor.context_menu_visible())
19058 .await;
19059 cx.update_editor(|editor, _, _| {
19060 let context_menu = editor.context_menu.borrow_mut();
19061 let context_menu = context_menu
19062 .as_ref()
19063 .expect("Should have the context menu deployed");
19064 match context_menu {
19065 CodeContextMenu::Completions(completions_menu) => {
19066 let completions = completions_menu.completions.borrow_mut();
19067 assert_eq!(
19068 completions
19069 .iter()
19070 .map(|completion| &completion.label.text)
19071 .collect::<Vec<_>>(),
19072 vec!["id", "other"]
19073 )
19074 }
19075 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
19076 }
19077 });
19078 cx.run_until_parked();
19079
19080 cx.update_editor(|editor, window, cx| {
19081 editor.context_menu_next(&ContextMenuNext, window, cx);
19082 });
19083 cx.run_until_parked();
19084 cx.update_editor(|editor, window, cx| {
19085 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19086 });
19087 cx.run_until_parked();
19088 cx.update_editor(|editor, window, cx| {
19089 editor.context_menu_next(&ContextMenuNext, window, cx);
19090 });
19091 cx.run_until_parked();
19092 cx.update_editor(|editor, window, cx| {
19093 editor
19094 .compose_completion(&ComposeCompletion::default(), window, cx)
19095 .expect("No task returned")
19096 })
19097 .await
19098 .expect("Completion failed");
19099 cx.run_until_parked();
19100
19101 cx.update_editor(|editor, _, cx| {
19102 assert_eq!(
19103 resolve_requests_1.load(atomic::Ordering::Acquire),
19104 1,
19105 "Should always resolve once despite multiple selections"
19106 );
19107 assert_eq!(
19108 resolve_requests_2.load(atomic::Ordering::Acquire),
19109 1,
19110 "Should always resolve once after multiple selections and applying the completion"
19111 );
19112 assert_eq!(
19113 editor.text(cx),
19114 "fn main() { let a = ??.other; }",
19115 "Should use resolved data when applying the completion"
19116 );
19117 });
19118}
19119
19120#[gpui::test]
19121async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
19122 init_test(cx, |_| {});
19123
19124 let item_0 = lsp::CompletionItem {
19125 label: "abs".into(),
19126 insert_text: Some("abs".into()),
19127 data: Some(json!({ "very": "special"})),
19128 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
19129 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19130 lsp::InsertReplaceEdit {
19131 new_text: "abs".to_string(),
19132 insert: lsp::Range::default(),
19133 replace: lsp::Range::default(),
19134 },
19135 )),
19136 ..lsp::CompletionItem::default()
19137 };
19138 let items = iter::once(item_0.clone())
19139 .chain((11..51).map(|i| lsp::CompletionItem {
19140 label: format!("item_{}", i),
19141 insert_text: Some(format!("item_{}", i)),
19142 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
19143 ..lsp::CompletionItem::default()
19144 }))
19145 .collect::<Vec<_>>();
19146
19147 let default_commit_characters = vec!["?".to_string()];
19148 let default_data = json!({ "default": "data"});
19149 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
19150 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
19151 let default_edit_range = lsp::Range {
19152 start: lsp::Position {
19153 line: 0,
19154 character: 5,
19155 },
19156 end: lsp::Position {
19157 line: 0,
19158 character: 5,
19159 },
19160 };
19161
19162 let mut cx = EditorLspTestContext::new_rust(
19163 lsp::ServerCapabilities {
19164 completion_provider: Some(lsp::CompletionOptions {
19165 trigger_characters: Some(vec![".".to_string()]),
19166 resolve_provider: Some(true),
19167 ..Default::default()
19168 }),
19169 ..Default::default()
19170 },
19171 cx,
19172 )
19173 .await;
19174
19175 cx.set_state("fn main() { let a = 2ˇ; }");
19176 cx.simulate_keystroke(".");
19177
19178 let completion_data = default_data.clone();
19179 let completion_characters = default_commit_characters.clone();
19180 let completion_items = items.clone();
19181 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19182 let default_data = completion_data.clone();
19183 let default_commit_characters = completion_characters.clone();
19184 let items = completion_items.clone();
19185 async move {
19186 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
19187 items,
19188 item_defaults: Some(lsp::CompletionListItemDefaults {
19189 data: Some(default_data.clone()),
19190 commit_characters: Some(default_commit_characters.clone()),
19191 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
19192 default_edit_range,
19193 )),
19194 insert_text_format: Some(default_insert_text_format),
19195 insert_text_mode: Some(default_insert_text_mode),
19196 }),
19197 ..lsp::CompletionList::default()
19198 })))
19199 }
19200 })
19201 .next()
19202 .await;
19203
19204 let resolved_items = Arc::new(Mutex::new(Vec::new()));
19205 cx.lsp
19206 .server
19207 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19208 let closure_resolved_items = resolved_items.clone();
19209 move |item_to_resolve, _| {
19210 let closure_resolved_items = closure_resolved_items.clone();
19211 async move {
19212 closure_resolved_items.lock().push(item_to_resolve.clone());
19213 Ok(item_to_resolve)
19214 }
19215 }
19216 })
19217 .detach();
19218
19219 cx.condition(|editor, _| editor.context_menu_visible())
19220 .await;
19221 cx.run_until_parked();
19222 cx.update_editor(|editor, _, _| {
19223 let menu = editor.context_menu.borrow_mut();
19224 match menu.as_ref().expect("should have the completions menu") {
19225 CodeContextMenu::Completions(completions_menu) => {
19226 assert_eq!(
19227 completions_menu
19228 .entries
19229 .borrow()
19230 .iter()
19231 .map(|mat| mat.string.clone())
19232 .collect::<Vec<String>>(),
19233 items
19234 .iter()
19235 .map(|completion| completion.label.clone())
19236 .collect::<Vec<String>>()
19237 );
19238 }
19239 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
19240 }
19241 });
19242 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
19243 // with 4 from the end.
19244 assert_eq!(
19245 *resolved_items.lock(),
19246 [&items[0..16], &items[items.len() - 4..items.len()]]
19247 .concat()
19248 .iter()
19249 .cloned()
19250 .map(|mut item| {
19251 if item.data.is_none() {
19252 item.data = Some(default_data.clone());
19253 }
19254 item
19255 })
19256 .collect::<Vec<lsp::CompletionItem>>(),
19257 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
19258 );
19259 resolved_items.lock().clear();
19260
19261 cx.update_editor(|editor, window, cx| {
19262 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19263 });
19264 cx.run_until_parked();
19265 // Completions that have already been resolved are skipped.
19266 assert_eq!(
19267 *resolved_items.lock(),
19268 items[items.len() - 17..items.len() - 4]
19269 .iter()
19270 .cloned()
19271 .map(|mut item| {
19272 if item.data.is_none() {
19273 item.data = Some(default_data.clone());
19274 }
19275 item
19276 })
19277 .collect::<Vec<lsp::CompletionItem>>()
19278 );
19279 resolved_items.lock().clear();
19280}
19281
19282#[gpui::test]
19283async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
19284 init_test(cx, |_| {});
19285
19286 let mut cx = EditorLspTestContext::new(
19287 Language::new(
19288 LanguageConfig {
19289 matcher: LanguageMatcher {
19290 path_suffixes: vec!["jsx".into()],
19291 ..Default::default()
19292 },
19293 overrides: [(
19294 "element".into(),
19295 LanguageConfigOverride {
19296 completion_query_characters: Override::Set(['-'].into_iter().collect()),
19297 ..Default::default()
19298 },
19299 )]
19300 .into_iter()
19301 .collect(),
19302 ..Default::default()
19303 },
19304 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
19305 )
19306 .with_override_query("(jsx_self_closing_element) @element")
19307 .unwrap(),
19308 lsp::ServerCapabilities {
19309 completion_provider: Some(lsp::CompletionOptions {
19310 trigger_characters: Some(vec![":".to_string()]),
19311 ..Default::default()
19312 }),
19313 ..Default::default()
19314 },
19315 cx,
19316 )
19317 .await;
19318
19319 cx.lsp
19320 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19321 Ok(Some(lsp::CompletionResponse::Array(vec![
19322 lsp::CompletionItem {
19323 label: "bg-blue".into(),
19324 ..Default::default()
19325 },
19326 lsp::CompletionItem {
19327 label: "bg-red".into(),
19328 ..Default::default()
19329 },
19330 lsp::CompletionItem {
19331 label: "bg-yellow".into(),
19332 ..Default::default()
19333 },
19334 ])))
19335 });
19336
19337 cx.set_state(r#"<p class="bgˇ" />"#);
19338
19339 // Trigger completion when typing a dash, because the dash is an extra
19340 // word character in the 'element' scope, which contains the cursor.
19341 cx.simulate_keystroke("-");
19342 cx.executor().run_until_parked();
19343 cx.update_editor(|editor, _, _| {
19344 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19345 {
19346 assert_eq!(
19347 completion_menu_entries(menu),
19348 &["bg-blue", "bg-red", "bg-yellow"]
19349 );
19350 } else {
19351 panic!("expected completion menu to be open");
19352 }
19353 });
19354
19355 cx.simulate_keystroke("l");
19356 cx.executor().run_until_parked();
19357 cx.update_editor(|editor, _, _| {
19358 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19359 {
19360 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
19361 } else {
19362 panic!("expected completion menu to be open");
19363 }
19364 });
19365
19366 // When filtering completions, consider the character after the '-' to
19367 // be the start of a subword.
19368 cx.set_state(r#"<p class="yelˇ" />"#);
19369 cx.simulate_keystroke("l");
19370 cx.executor().run_until_parked();
19371 cx.update_editor(|editor, _, _| {
19372 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19373 {
19374 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
19375 } else {
19376 panic!("expected completion menu to be open");
19377 }
19378 });
19379}
19380
19381fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
19382 let entries = menu.entries.borrow();
19383 entries.iter().map(|mat| mat.string.clone()).collect()
19384}
19385
19386#[gpui::test]
19387async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
19388 init_test(cx, |settings| {
19389 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19390 });
19391
19392 let fs = FakeFs::new(cx.executor());
19393 fs.insert_file(path!("/file.ts"), Default::default()).await;
19394
19395 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
19396 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19397
19398 language_registry.add(Arc::new(Language::new(
19399 LanguageConfig {
19400 name: "TypeScript".into(),
19401 matcher: LanguageMatcher {
19402 path_suffixes: vec!["ts".to_string()],
19403 ..Default::default()
19404 },
19405 ..Default::default()
19406 },
19407 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19408 )));
19409 update_test_language_settings(cx, |settings| {
19410 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19411 });
19412
19413 let test_plugin = "test_plugin";
19414 let _ = language_registry.register_fake_lsp(
19415 "TypeScript",
19416 FakeLspAdapter {
19417 prettier_plugins: vec![test_plugin],
19418 ..Default::default()
19419 },
19420 );
19421
19422 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19423 let buffer = project
19424 .update(cx, |project, cx| {
19425 project.open_local_buffer(path!("/file.ts"), cx)
19426 })
19427 .await
19428 .unwrap();
19429
19430 let buffer_text = "one\ntwo\nthree\n";
19431 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19432 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19433 editor.update_in(cx, |editor, window, cx| {
19434 editor.set_text(buffer_text, window, cx)
19435 });
19436
19437 editor
19438 .update_in(cx, |editor, window, cx| {
19439 editor.perform_format(
19440 project.clone(),
19441 FormatTrigger::Manual,
19442 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19443 window,
19444 cx,
19445 )
19446 })
19447 .unwrap()
19448 .await;
19449 assert_eq!(
19450 editor.update(cx, |editor, cx| editor.text(cx)),
19451 buffer_text.to_string() + prettier_format_suffix,
19452 "Test prettier formatting was not applied to the original buffer text",
19453 );
19454
19455 update_test_language_settings(cx, |settings| {
19456 settings.defaults.formatter = Some(FormatterList::default())
19457 });
19458 let format = editor.update_in(cx, |editor, window, cx| {
19459 editor.perform_format(
19460 project.clone(),
19461 FormatTrigger::Manual,
19462 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19463 window,
19464 cx,
19465 )
19466 });
19467 format.await.unwrap();
19468 assert_eq!(
19469 editor.update(cx, |editor, cx| editor.text(cx)),
19470 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
19471 "Autoformatting (via test prettier) was not applied to the original buffer text",
19472 );
19473}
19474
19475#[gpui::test]
19476async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
19477 init_test(cx, |settings| {
19478 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19479 });
19480
19481 let fs = FakeFs::new(cx.executor());
19482 fs.insert_file(path!("/file.settings"), Default::default())
19483 .await;
19484
19485 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
19486 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19487
19488 let ts_lang = Arc::new(Language::new(
19489 LanguageConfig {
19490 name: "TypeScript".into(),
19491 matcher: LanguageMatcher {
19492 path_suffixes: vec!["ts".to_string()],
19493 ..LanguageMatcher::default()
19494 },
19495 prettier_parser_name: Some("typescript".to_string()),
19496 ..LanguageConfig::default()
19497 },
19498 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19499 ));
19500
19501 language_registry.add(ts_lang.clone());
19502
19503 update_test_language_settings(cx, |settings| {
19504 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19505 });
19506
19507 let test_plugin = "test_plugin";
19508 let _ = language_registry.register_fake_lsp(
19509 "TypeScript",
19510 FakeLspAdapter {
19511 prettier_plugins: vec![test_plugin],
19512 ..Default::default()
19513 },
19514 );
19515
19516 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19517 let buffer = project
19518 .update(cx, |project, cx| {
19519 project.open_local_buffer(path!("/file.settings"), cx)
19520 })
19521 .await
19522 .unwrap();
19523
19524 project.update(cx, |project, cx| {
19525 project.set_language_for_buffer(&buffer, ts_lang, cx)
19526 });
19527
19528 let buffer_text = "one\ntwo\nthree\n";
19529 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19530 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19531 editor.update_in(cx, |editor, window, cx| {
19532 editor.set_text(buffer_text, window, cx)
19533 });
19534
19535 editor
19536 .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 .unwrap()
19546 .await;
19547 assert_eq!(
19548 editor.update(cx, |editor, cx| editor.text(cx)),
19549 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
19550 "Test prettier formatting was not applied to the original buffer text",
19551 );
19552
19553 update_test_language_settings(cx, |settings| {
19554 settings.defaults.formatter = Some(FormatterList::default())
19555 });
19556 let format = editor.update_in(cx, |editor, window, cx| {
19557 editor.perform_format(
19558 project.clone(),
19559 FormatTrigger::Manual,
19560 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19561 window,
19562 cx,
19563 )
19564 });
19565 format.await.unwrap();
19566
19567 assert_eq!(
19568 editor.update(cx, |editor, cx| editor.text(cx)),
19569 buffer_text.to_string()
19570 + prettier_format_suffix
19571 + "\ntypescript\n"
19572 + prettier_format_suffix
19573 + "\ntypescript",
19574 "Autoformatting (via test prettier) was not applied to the original buffer text",
19575 );
19576}
19577
19578#[gpui::test]
19579async fn test_addition_reverts(cx: &mut TestAppContext) {
19580 init_test(cx, |_| {});
19581 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19582 let base_text = indoc! {r#"
19583 struct Row;
19584 struct Row1;
19585 struct Row2;
19586
19587 struct Row4;
19588 struct Row5;
19589 struct Row6;
19590
19591 struct Row8;
19592 struct Row9;
19593 struct Row10;"#};
19594
19595 // When addition hunks are not adjacent to carets, no hunk revert is performed
19596 assert_hunk_revert(
19597 indoc! {r#"struct Row;
19598 struct Row1;
19599 struct Row1.1;
19600 struct Row1.2;
19601 struct Row2;ˇ
19602
19603 struct Row4;
19604 struct Row5;
19605 struct Row6;
19606
19607 struct Row8;
19608 ˇstruct Row9;
19609 struct Row9.1;
19610 struct Row9.2;
19611 struct Row9.3;
19612 struct Row10;"#},
19613 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19614 indoc! {r#"struct Row;
19615 struct Row1;
19616 struct Row1.1;
19617 struct Row1.2;
19618 struct Row2;ˇ
19619
19620 struct Row4;
19621 struct Row5;
19622 struct Row6;
19623
19624 struct Row8;
19625 ˇstruct Row9;
19626 struct Row9.1;
19627 struct Row9.2;
19628 struct Row9.3;
19629 struct Row10;"#},
19630 base_text,
19631 &mut cx,
19632 );
19633 // Same for selections
19634 assert_hunk_revert(
19635 indoc! {r#"struct Row;
19636 struct Row1;
19637 struct Row2;
19638 struct Row2.1;
19639 struct Row2.2;
19640 «ˇ
19641 struct Row4;
19642 struct» Row5;
19643 «struct Row6;
19644 ˇ»
19645 struct Row9.1;
19646 struct Row9.2;
19647 struct Row9.3;
19648 struct Row8;
19649 struct Row9;
19650 struct Row10;"#},
19651 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19652 indoc! {r#"struct Row;
19653 struct Row1;
19654 struct Row2;
19655 struct Row2.1;
19656 struct Row2.2;
19657 «ˇ
19658 struct Row4;
19659 struct» Row5;
19660 «struct Row6;
19661 ˇ»
19662 struct Row9.1;
19663 struct Row9.2;
19664 struct Row9.3;
19665 struct Row8;
19666 struct Row9;
19667 struct Row10;"#},
19668 base_text,
19669 &mut cx,
19670 );
19671
19672 // When carets and selections intersect the addition hunks, those are reverted.
19673 // Adjacent carets got merged.
19674 assert_hunk_revert(
19675 indoc! {r#"struct Row;
19676 ˇ// something on the top
19677 struct Row1;
19678 struct Row2;
19679 struct Roˇw3.1;
19680 struct Row2.2;
19681 struct Row2.3;ˇ
19682
19683 struct Row4;
19684 struct ˇRow5.1;
19685 struct Row5.2;
19686 struct «Rowˇ»5.3;
19687 struct Row5;
19688 struct Row6;
19689 ˇ
19690 struct Row9.1;
19691 struct «Rowˇ»9.2;
19692 struct «ˇRow»9.3;
19693 struct Row8;
19694 struct Row9;
19695 «ˇ// something on bottom»
19696 struct Row10;"#},
19697 vec![
19698 DiffHunkStatusKind::Added,
19699 DiffHunkStatusKind::Added,
19700 DiffHunkStatusKind::Added,
19701 DiffHunkStatusKind::Added,
19702 DiffHunkStatusKind::Added,
19703 ],
19704 indoc! {r#"struct Row;
19705 ˇstruct Row1;
19706 struct Row2;
19707 ˇ
19708 struct Row4;
19709 ˇstruct Row5;
19710 struct Row6;
19711 ˇ
19712 ˇstruct Row8;
19713 struct Row9;
19714 ˇstruct Row10;"#},
19715 base_text,
19716 &mut cx,
19717 );
19718}
19719
19720#[gpui::test]
19721async fn test_modification_reverts(cx: &mut TestAppContext) {
19722 init_test(cx, |_| {});
19723 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19724 let base_text = indoc! {r#"
19725 struct Row;
19726 struct Row1;
19727 struct Row2;
19728
19729 struct Row4;
19730 struct Row5;
19731 struct Row6;
19732
19733 struct Row8;
19734 struct Row9;
19735 struct Row10;"#};
19736
19737 // Modification hunks behave the same as the addition ones.
19738 assert_hunk_revert(
19739 indoc! {r#"struct Row;
19740 struct Row1;
19741 struct Row33;
19742 ˇ
19743 struct Row4;
19744 struct Row5;
19745 struct Row6;
19746 ˇ
19747 struct Row99;
19748 struct Row9;
19749 struct Row10;"#},
19750 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19751 indoc! {r#"struct Row;
19752 struct Row1;
19753 struct Row33;
19754 ˇ
19755 struct Row4;
19756 struct Row5;
19757 struct Row6;
19758 ˇ
19759 struct Row99;
19760 struct Row9;
19761 struct Row10;"#},
19762 base_text,
19763 &mut cx,
19764 );
19765 assert_hunk_revert(
19766 indoc! {r#"struct Row;
19767 struct Row1;
19768 struct Row33;
19769 «ˇ
19770 struct Row4;
19771 struct» Row5;
19772 «struct Row6;
19773 ˇ»
19774 struct Row99;
19775 struct Row9;
19776 struct Row10;"#},
19777 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19778 indoc! {r#"struct Row;
19779 struct Row1;
19780 struct Row33;
19781 «ˇ
19782 struct Row4;
19783 struct» Row5;
19784 «struct Row6;
19785 ˇ»
19786 struct Row99;
19787 struct Row9;
19788 struct Row10;"#},
19789 base_text,
19790 &mut cx,
19791 );
19792
19793 assert_hunk_revert(
19794 indoc! {r#"ˇstruct Row1.1;
19795 struct Row1;
19796 «ˇstr»uct Row22;
19797
19798 struct ˇRow44;
19799 struct Row5;
19800 struct «Rˇ»ow66;ˇ
19801
19802 «struˇ»ct Row88;
19803 struct Row9;
19804 struct Row1011;ˇ"#},
19805 vec![
19806 DiffHunkStatusKind::Modified,
19807 DiffHunkStatusKind::Modified,
19808 DiffHunkStatusKind::Modified,
19809 DiffHunkStatusKind::Modified,
19810 DiffHunkStatusKind::Modified,
19811 DiffHunkStatusKind::Modified,
19812 ],
19813 indoc! {r#"struct Row;
19814 ˇstruct Row1;
19815 struct Row2;
19816 ˇ
19817 struct Row4;
19818 ˇstruct Row5;
19819 struct Row6;
19820 ˇ
19821 struct Row8;
19822 ˇstruct Row9;
19823 struct Row10;ˇ"#},
19824 base_text,
19825 &mut cx,
19826 );
19827}
19828
19829#[gpui::test]
19830async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
19831 init_test(cx, |_| {});
19832 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19833 let base_text = indoc! {r#"
19834 one
19835
19836 two
19837 three
19838 "#};
19839
19840 cx.set_head_text(base_text);
19841 cx.set_state("\nˇ\n");
19842 cx.executor().run_until_parked();
19843 cx.update_editor(|editor, _window, cx| {
19844 editor.expand_selected_diff_hunks(cx);
19845 });
19846 cx.executor().run_until_parked();
19847 cx.update_editor(|editor, window, cx| {
19848 editor.backspace(&Default::default(), window, cx);
19849 });
19850 cx.run_until_parked();
19851 cx.assert_state_with_diff(
19852 indoc! {r#"
19853
19854 - two
19855 - threeˇ
19856 +
19857 "#}
19858 .to_string(),
19859 );
19860}
19861
19862#[gpui::test]
19863async fn test_deletion_reverts(cx: &mut TestAppContext) {
19864 init_test(cx, |_| {});
19865 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19866 let base_text = indoc! {r#"struct Row;
19867struct Row1;
19868struct Row2;
19869
19870struct Row4;
19871struct Row5;
19872struct Row6;
19873
19874struct Row8;
19875struct Row9;
19876struct Row10;"#};
19877
19878 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
19879 assert_hunk_revert(
19880 indoc! {r#"struct Row;
19881 struct Row2;
19882
19883 ˇstruct Row4;
19884 struct Row5;
19885 struct Row6;
19886 ˇ
19887 struct Row8;
19888 struct Row10;"#},
19889 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19890 indoc! {r#"struct Row;
19891 struct Row2;
19892
19893 ˇstruct Row4;
19894 struct Row5;
19895 struct Row6;
19896 ˇ
19897 struct Row8;
19898 struct Row10;"#},
19899 base_text,
19900 &mut cx,
19901 );
19902 assert_hunk_revert(
19903 indoc! {r#"struct Row;
19904 struct Row2;
19905
19906 «ˇstruct Row4;
19907 struct» Row5;
19908 «struct Row6;
19909 ˇ»
19910 struct Row8;
19911 struct Row10;"#},
19912 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19913 indoc! {r#"struct Row;
19914 struct Row2;
19915
19916 «ˇstruct Row4;
19917 struct» Row5;
19918 «struct Row6;
19919 ˇ»
19920 struct Row8;
19921 struct Row10;"#},
19922 base_text,
19923 &mut cx,
19924 );
19925
19926 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
19927 assert_hunk_revert(
19928 indoc! {r#"struct Row;
19929 ˇstruct Row2;
19930
19931 struct Row4;
19932 struct Row5;
19933 struct Row6;
19934
19935 struct Row8;ˇ
19936 struct Row10;"#},
19937 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19938 indoc! {r#"struct Row;
19939 struct Row1;
19940 ˇstruct Row2;
19941
19942 struct Row4;
19943 struct Row5;
19944 struct Row6;
19945
19946 struct Row8;ˇ
19947 struct Row9;
19948 struct Row10;"#},
19949 base_text,
19950 &mut cx,
19951 );
19952 assert_hunk_revert(
19953 indoc! {r#"struct Row;
19954 struct Row2«ˇ;
19955 struct Row4;
19956 struct» Row5;
19957 «struct Row6;
19958
19959 struct Row8;ˇ»
19960 struct Row10;"#},
19961 vec![
19962 DiffHunkStatusKind::Deleted,
19963 DiffHunkStatusKind::Deleted,
19964 DiffHunkStatusKind::Deleted,
19965 ],
19966 indoc! {r#"struct Row;
19967 struct Row1;
19968 struct Row2«ˇ;
19969
19970 struct Row4;
19971 struct» Row5;
19972 «struct Row6;
19973
19974 struct Row8;ˇ»
19975 struct Row9;
19976 struct Row10;"#},
19977 base_text,
19978 &mut cx,
19979 );
19980}
19981
19982#[gpui::test]
19983async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
19984 init_test(cx, |_| {});
19985
19986 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
19987 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
19988 let base_text_3 =
19989 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
19990
19991 let text_1 = edit_first_char_of_every_line(base_text_1);
19992 let text_2 = edit_first_char_of_every_line(base_text_2);
19993 let text_3 = edit_first_char_of_every_line(base_text_3);
19994
19995 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19996 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19997 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19998
19999 let multibuffer = cx.new(|cx| {
20000 let mut multibuffer = MultiBuffer::new(ReadWrite);
20001 multibuffer.push_excerpts(
20002 buffer_1.clone(),
20003 [
20004 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20005 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20006 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20007 ],
20008 cx,
20009 );
20010 multibuffer.push_excerpts(
20011 buffer_2.clone(),
20012 [
20013 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20014 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20015 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20016 ],
20017 cx,
20018 );
20019 multibuffer.push_excerpts(
20020 buffer_3.clone(),
20021 [
20022 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20023 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20024 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20025 ],
20026 cx,
20027 );
20028 multibuffer
20029 });
20030
20031 let fs = FakeFs::new(cx.executor());
20032 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20033 let (editor, cx) = cx
20034 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
20035 editor.update_in(cx, |editor, _window, cx| {
20036 for (buffer, diff_base) in [
20037 (buffer_1.clone(), base_text_1),
20038 (buffer_2.clone(), base_text_2),
20039 (buffer_3.clone(), base_text_3),
20040 ] {
20041 let diff = cx.new(|cx| {
20042 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
20043 });
20044 editor
20045 .buffer
20046 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20047 }
20048 });
20049 cx.executor().run_until_parked();
20050
20051 editor.update_in(cx, |editor, window, cx| {
20052 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}");
20053 editor.select_all(&SelectAll, window, cx);
20054 editor.git_restore(&Default::default(), window, cx);
20055 });
20056 cx.executor().run_until_parked();
20057
20058 // When all ranges are selected, all buffer hunks are reverted.
20059 editor.update(cx, |editor, cx| {
20060 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");
20061 });
20062 buffer_1.update(cx, |buffer, _| {
20063 assert_eq!(buffer.text(), base_text_1);
20064 });
20065 buffer_2.update(cx, |buffer, _| {
20066 assert_eq!(buffer.text(), base_text_2);
20067 });
20068 buffer_3.update(cx, |buffer, _| {
20069 assert_eq!(buffer.text(), base_text_3);
20070 });
20071
20072 editor.update_in(cx, |editor, window, cx| {
20073 editor.undo(&Default::default(), window, cx);
20074 });
20075
20076 editor.update_in(cx, |editor, window, cx| {
20077 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20078 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
20079 });
20080 editor.git_restore(&Default::default(), window, cx);
20081 });
20082
20083 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
20084 // but not affect buffer_2 and its related excerpts.
20085 editor.update(cx, |editor, cx| {
20086 assert_eq!(
20087 editor.text(cx),
20088 "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}"
20089 );
20090 });
20091 buffer_1.update(cx, |buffer, _| {
20092 assert_eq!(buffer.text(), base_text_1);
20093 });
20094 buffer_2.update(cx, |buffer, _| {
20095 assert_eq!(
20096 buffer.text(),
20097 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
20098 );
20099 });
20100 buffer_3.update(cx, |buffer, _| {
20101 assert_eq!(
20102 buffer.text(),
20103 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
20104 );
20105 });
20106
20107 fn edit_first_char_of_every_line(text: &str) -> String {
20108 text.split('\n')
20109 .map(|line| format!("X{}", &line[1..]))
20110 .collect::<Vec<_>>()
20111 .join("\n")
20112 }
20113}
20114
20115#[gpui::test]
20116async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
20117 init_test(cx, |_| {});
20118
20119 let cols = 4;
20120 let rows = 10;
20121 let sample_text_1 = sample_text(rows, cols, 'a');
20122 assert_eq!(
20123 sample_text_1,
20124 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
20125 );
20126 let sample_text_2 = sample_text(rows, cols, 'l');
20127 assert_eq!(
20128 sample_text_2,
20129 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
20130 );
20131 let sample_text_3 = sample_text(rows, cols, 'v');
20132 assert_eq!(
20133 sample_text_3,
20134 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
20135 );
20136
20137 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
20138 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
20139 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
20140
20141 let multi_buffer = cx.new(|cx| {
20142 let mut multibuffer = MultiBuffer::new(ReadWrite);
20143 multibuffer.push_excerpts(
20144 buffer_1.clone(),
20145 [
20146 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20147 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20148 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20149 ],
20150 cx,
20151 );
20152 multibuffer.push_excerpts(
20153 buffer_2.clone(),
20154 [
20155 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20156 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20157 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20158 ],
20159 cx,
20160 );
20161 multibuffer.push_excerpts(
20162 buffer_3.clone(),
20163 [
20164 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20165 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20166 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20167 ],
20168 cx,
20169 );
20170 multibuffer
20171 });
20172
20173 let fs = FakeFs::new(cx.executor());
20174 fs.insert_tree(
20175 "/a",
20176 json!({
20177 "main.rs": sample_text_1,
20178 "other.rs": sample_text_2,
20179 "lib.rs": sample_text_3,
20180 }),
20181 )
20182 .await;
20183 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20184 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20185 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20186 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20187 Editor::new(
20188 EditorMode::full(),
20189 multi_buffer,
20190 Some(project.clone()),
20191 window,
20192 cx,
20193 )
20194 });
20195 let multibuffer_item_id = workspace
20196 .update(cx, |workspace, window, cx| {
20197 assert!(
20198 workspace.active_item(cx).is_none(),
20199 "active item should be None before the first item is added"
20200 );
20201 workspace.add_item_to_active_pane(
20202 Box::new(multi_buffer_editor.clone()),
20203 None,
20204 true,
20205 window,
20206 cx,
20207 );
20208 let active_item = workspace
20209 .active_item(cx)
20210 .expect("should have an active item after adding the multi buffer");
20211 assert_eq!(
20212 active_item.buffer_kind(cx),
20213 ItemBufferKind::Multibuffer,
20214 "A multi buffer was expected to active after adding"
20215 );
20216 active_item.item_id()
20217 })
20218 .unwrap();
20219 cx.executor().run_until_parked();
20220
20221 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20222 editor.change_selections(
20223 SelectionEffects::scroll(Autoscroll::Next),
20224 window,
20225 cx,
20226 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
20227 );
20228 editor.open_excerpts(&OpenExcerpts, window, cx);
20229 });
20230 cx.executor().run_until_parked();
20231 let first_item_id = workspace
20232 .update(cx, |workspace, window, cx| {
20233 let active_item = workspace
20234 .active_item(cx)
20235 .expect("should have an active item after navigating into the 1st buffer");
20236 let first_item_id = active_item.item_id();
20237 assert_ne!(
20238 first_item_id, multibuffer_item_id,
20239 "Should navigate into the 1st buffer and activate it"
20240 );
20241 assert_eq!(
20242 active_item.buffer_kind(cx),
20243 ItemBufferKind::Singleton,
20244 "New active item should be a singleton buffer"
20245 );
20246 assert_eq!(
20247 active_item
20248 .act_as::<Editor>(cx)
20249 .expect("should have navigated into an editor for the 1st buffer")
20250 .read(cx)
20251 .text(cx),
20252 sample_text_1
20253 );
20254
20255 workspace
20256 .go_back(workspace.active_pane().downgrade(), window, cx)
20257 .detach_and_log_err(cx);
20258
20259 first_item_id
20260 })
20261 .unwrap();
20262 cx.executor().run_until_parked();
20263 workspace
20264 .update(cx, |workspace, _, cx| {
20265 let active_item = workspace
20266 .active_item(cx)
20267 .expect("should have an active item after navigating back");
20268 assert_eq!(
20269 active_item.item_id(),
20270 multibuffer_item_id,
20271 "Should navigate back to the multi buffer"
20272 );
20273 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20274 })
20275 .unwrap();
20276
20277 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20278 editor.change_selections(
20279 SelectionEffects::scroll(Autoscroll::Next),
20280 window,
20281 cx,
20282 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
20283 );
20284 editor.open_excerpts(&OpenExcerpts, window, cx);
20285 });
20286 cx.executor().run_until_parked();
20287 let second_item_id = workspace
20288 .update(cx, |workspace, window, cx| {
20289 let active_item = workspace
20290 .active_item(cx)
20291 .expect("should have an active item after navigating into the 2nd buffer");
20292 let second_item_id = active_item.item_id();
20293 assert_ne!(
20294 second_item_id, multibuffer_item_id,
20295 "Should navigate away from the multibuffer"
20296 );
20297 assert_ne!(
20298 second_item_id, first_item_id,
20299 "Should navigate into the 2nd buffer and activate it"
20300 );
20301 assert_eq!(
20302 active_item.buffer_kind(cx),
20303 ItemBufferKind::Singleton,
20304 "New active item should be a singleton buffer"
20305 );
20306 assert_eq!(
20307 active_item
20308 .act_as::<Editor>(cx)
20309 .expect("should have navigated into an editor")
20310 .read(cx)
20311 .text(cx),
20312 sample_text_2
20313 );
20314
20315 workspace
20316 .go_back(workspace.active_pane().downgrade(), window, cx)
20317 .detach_and_log_err(cx);
20318
20319 second_item_id
20320 })
20321 .unwrap();
20322 cx.executor().run_until_parked();
20323 workspace
20324 .update(cx, |workspace, _, cx| {
20325 let active_item = workspace
20326 .active_item(cx)
20327 .expect("should have an active item after navigating back from the 2nd buffer");
20328 assert_eq!(
20329 active_item.item_id(),
20330 multibuffer_item_id,
20331 "Should navigate back from the 2nd buffer to the multi buffer"
20332 );
20333 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20334 })
20335 .unwrap();
20336
20337 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20338 editor.change_selections(
20339 SelectionEffects::scroll(Autoscroll::Next),
20340 window,
20341 cx,
20342 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
20343 );
20344 editor.open_excerpts(&OpenExcerpts, window, cx);
20345 });
20346 cx.executor().run_until_parked();
20347 workspace
20348 .update(cx, |workspace, window, cx| {
20349 let active_item = workspace
20350 .active_item(cx)
20351 .expect("should have an active item after navigating into the 3rd buffer");
20352 let third_item_id = active_item.item_id();
20353 assert_ne!(
20354 third_item_id, multibuffer_item_id,
20355 "Should navigate into the 3rd buffer and activate it"
20356 );
20357 assert_ne!(third_item_id, first_item_id);
20358 assert_ne!(third_item_id, second_item_id);
20359 assert_eq!(
20360 active_item.buffer_kind(cx),
20361 ItemBufferKind::Singleton,
20362 "New active item should be a singleton buffer"
20363 );
20364 assert_eq!(
20365 active_item
20366 .act_as::<Editor>(cx)
20367 .expect("should have navigated into an editor")
20368 .read(cx)
20369 .text(cx),
20370 sample_text_3
20371 );
20372
20373 workspace
20374 .go_back(workspace.active_pane().downgrade(), window, cx)
20375 .detach_and_log_err(cx);
20376 })
20377 .unwrap();
20378 cx.executor().run_until_parked();
20379 workspace
20380 .update(cx, |workspace, _, cx| {
20381 let active_item = workspace
20382 .active_item(cx)
20383 .expect("should have an active item after navigating back from the 3rd buffer");
20384 assert_eq!(
20385 active_item.item_id(),
20386 multibuffer_item_id,
20387 "Should navigate back from the 3rd buffer to the multi buffer"
20388 );
20389 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20390 })
20391 .unwrap();
20392}
20393
20394#[gpui::test]
20395async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20396 init_test(cx, |_| {});
20397
20398 let mut cx = EditorTestContext::new(cx).await;
20399
20400 let diff_base = r#"
20401 use some::mod;
20402
20403 const A: u32 = 42;
20404
20405 fn main() {
20406 println!("hello");
20407
20408 println!("world");
20409 }
20410 "#
20411 .unindent();
20412
20413 cx.set_state(
20414 &r#"
20415 use some::modified;
20416
20417 ˇ
20418 fn main() {
20419 println!("hello there");
20420
20421 println!("around the");
20422 println!("world");
20423 }
20424 "#
20425 .unindent(),
20426 );
20427
20428 cx.set_head_text(&diff_base);
20429 executor.run_until_parked();
20430
20431 cx.update_editor(|editor, window, cx| {
20432 editor.go_to_next_hunk(&GoToHunk, window, cx);
20433 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20434 });
20435 executor.run_until_parked();
20436 cx.assert_state_with_diff(
20437 r#"
20438 use some::modified;
20439
20440
20441 fn main() {
20442 - println!("hello");
20443 + ˇ println!("hello there");
20444
20445 println!("around the");
20446 println!("world");
20447 }
20448 "#
20449 .unindent(),
20450 );
20451
20452 cx.update_editor(|editor, window, cx| {
20453 for _ in 0..2 {
20454 editor.go_to_next_hunk(&GoToHunk, window, cx);
20455 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20456 }
20457 });
20458 executor.run_until_parked();
20459 cx.assert_state_with_diff(
20460 r#"
20461 - use some::mod;
20462 + ˇuse some::modified;
20463
20464
20465 fn main() {
20466 - println!("hello");
20467 + println!("hello there");
20468
20469 + println!("around the");
20470 println!("world");
20471 }
20472 "#
20473 .unindent(),
20474 );
20475
20476 cx.update_editor(|editor, window, cx| {
20477 editor.go_to_next_hunk(&GoToHunk, window, cx);
20478 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20479 });
20480 executor.run_until_parked();
20481 cx.assert_state_with_diff(
20482 r#"
20483 - use some::mod;
20484 + use some::modified;
20485
20486 - const A: u32 = 42;
20487 ˇ
20488 fn main() {
20489 - println!("hello");
20490 + println!("hello there");
20491
20492 + println!("around the");
20493 println!("world");
20494 }
20495 "#
20496 .unindent(),
20497 );
20498
20499 cx.update_editor(|editor, window, cx| {
20500 editor.cancel(&Cancel, window, cx);
20501 });
20502
20503 cx.assert_state_with_diff(
20504 r#"
20505 use some::modified;
20506
20507 ˇ
20508 fn main() {
20509 println!("hello there");
20510
20511 println!("around the");
20512 println!("world");
20513 }
20514 "#
20515 .unindent(),
20516 );
20517}
20518
20519#[gpui::test]
20520async fn test_diff_base_change_with_expanded_diff_hunks(
20521 executor: BackgroundExecutor,
20522 cx: &mut TestAppContext,
20523) {
20524 init_test(cx, |_| {});
20525
20526 let mut cx = EditorTestContext::new(cx).await;
20527
20528 let diff_base = r#"
20529 use some::mod1;
20530 use some::mod2;
20531
20532 const A: u32 = 42;
20533 const B: u32 = 42;
20534 const C: u32 = 42;
20535
20536 fn main() {
20537 println!("hello");
20538
20539 println!("world");
20540 }
20541 "#
20542 .unindent();
20543
20544 cx.set_state(
20545 &r#"
20546 use some::mod2;
20547
20548 const A: u32 = 42;
20549 const C: u32 = 42;
20550
20551 fn main(ˇ) {
20552 //println!("hello");
20553
20554 println!("world");
20555 //
20556 //
20557 }
20558 "#
20559 .unindent(),
20560 );
20561
20562 cx.set_head_text(&diff_base);
20563 executor.run_until_parked();
20564
20565 cx.update_editor(|editor, window, cx| {
20566 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20567 });
20568 executor.run_until_parked();
20569 cx.assert_state_with_diff(
20570 r#"
20571 - use some::mod1;
20572 use some::mod2;
20573
20574 const A: u32 = 42;
20575 - const B: u32 = 42;
20576 const C: u32 = 42;
20577
20578 fn main(ˇ) {
20579 - println!("hello");
20580 + //println!("hello");
20581
20582 println!("world");
20583 + //
20584 + //
20585 }
20586 "#
20587 .unindent(),
20588 );
20589
20590 cx.set_head_text("new diff base!");
20591 executor.run_until_parked();
20592 cx.assert_state_with_diff(
20593 r#"
20594 - new diff base!
20595 + use some::mod2;
20596 +
20597 + const A: u32 = 42;
20598 + const C: u32 = 42;
20599 +
20600 + fn main(ˇ) {
20601 + //println!("hello");
20602 +
20603 + println!("world");
20604 + //
20605 + //
20606 + }
20607 "#
20608 .unindent(),
20609 );
20610}
20611
20612#[gpui::test]
20613async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
20614 init_test(cx, |_| {});
20615
20616 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20617 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20618 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20619 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20620 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
20621 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
20622
20623 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
20624 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
20625 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
20626
20627 let multi_buffer = cx.new(|cx| {
20628 let mut multibuffer = MultiBuffer::new(ReadWrite);
20629 multibuffer.push_excerpts(
20630 buffer_1.clone(),
20631 [
20632 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20633 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20634 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20635 ],
20636 cx,
20637 );
20638 multibuffer.push_excerpts(
20639 buffer_2.clone(),
20640 [
20641 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20642 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20643 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20644 ],
20645 cx,
20646 );
20647 multibuffer.push_excerpts(
20648 buffer_3.clone(),
20649 [
20650 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20651 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20652 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20653 ],
20654 cx,
20655 );
20656 multibuffer
20657 });
20658
20659 let editor =
20660 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20661 editor
20662 .update(cx, |editor, _window, cx| {
20663 for (buffer, diff_base) in [
20664 (buffer_1.clone(), file_1_old),
20665 (buffer_2.clone(), file_2_old),
20666 (buffer_3.clone(), file_3_old),
20667 ] {
20668 let diff = cx.new(|cx| {
20669 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
20670 });
20671 editor
20672 .buffer
20673 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20674 }
20675 })
20676 .unwrap();
20677
20678 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20679 cx.run_until_parked();
20680
20681 cx.assert_editor_state(
20682 &"
20683 ˇaaa
20684 ccc
20685 ddd
20686
20687 ggg
20688 hhh
20689
20690
20691 lll
20692 mmm
20693 NNN
20694
20695 qqq
20696 rrr
20697
20698 uuu
20699 111
20700 222
20701 333
20702
20703 666
20704 777
20705
20706 000
20707 !!!"
20708 .unindent(),
20709 );
20710
20711 cx.update_editor(|editor, window, cx| {
20712 editor.select_all(&SelectAll, window, cx);
20713 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20714 });
20715 cx.executor().run_until_parked();
20716
20717 cx.assert_state_with_diff(
20718 "
20719 «aaa
20720 - bbb
20721 ccc
20722 ddd
20723
20724 ggg
20725 hhh
20726
20727
20728 lll
20729 mmm
20730 - nnn
20731 + NNN
20732
20733 qqq
20734 rrr
20735
20736 uuu
20737 111
20738 222
20739 333
20740
20741 + 666
20742 777
20743
20744 000
20745 !!!ˇ»"
20746 .unindent(),
20747 );
20748}
20749
20750#[gpui::test]
20751async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
20752 init_test(cx, |_| {});
20753
20754 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
20755 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
20756
20757 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
20758 let multi_buffer = cx.new(|cx| {
20759 let mut multibuffer = MultiBuffer::new(ReadWrite);
20760 multibuffer.push_excerpts(
20761 buffer.clone(),
20762 [
20763 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
20764 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
20765 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
20766 ],
20767 cx,
20768 );
20769 multibuffer
20770 });
20771
20772 let editor =
20773 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20774 editor
20775 .update(cx, |editor, _window, cx| {
20776 let diff = cx.new(|cx| {
20777 BufferDiff::new_with_base_text(base, &buffer.read(cx).text_snapshot(), cx)
20778 });
20779 editor
20780 .buffer
20781 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
20782 })
20783 .unwrap();
20784
20785 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20786 cx.run_until_parked();
20787
20788 cx.update_editor(|editor, window, cx| {
20789 editor.expand_all_diff_hunks(&Default::default(), window, cx)
20790 });
20791 cx.executor().run_until_parked();
20792
20793 // When the start of a hunk coincides with the start of its excerpt,
20794 // the hunk is expanded. When the start of a hunk is earlier than
20795 // the start of its excerpt, the hunk is not expanded.
20796 cx.assert_state_with_diff(
20797 "
20798 ˇaaa
20799 - bbb
20800 + BBB
20801
20802 - ddd
20803 - eee
20804 + DDD
20805 + EEE
20806 fff
20807
20808 iii
20809 "
20810 .unindent(),
20811 );
20812}
20813
20814#[gpui::test]
20815async fn test_edits_around_expanded_insertion_hunks(
20816 executor: BackgroundExecutor,
20817 cx: &mut TestAppContext,
20818) {
20819 init_test(cx, |_| {});
20820
20821 let mut cx = EditorTestContext::new(cx).await;
20822
20823 let diff_base = r#"
20824 use some::mod1;
20825 use some::mod2;
20826
20827 const A: u32 = 42;
20828
20829 fn main() {
20830 println!("hello");
20831
20832 println!("world");
20833 }
20834 "#
20835 .unindent();
20836 executor.run_until_parked();
20837 cx.set_state(
20838 &r#"
20839 use some::mod1;
20840 use some::mod2;
20841
20842 const A: u32 = 42;
20843 const B: u32 = 42;
20844 const C: u32 = 42;
20845 ˇ
20846
20847 fn main() {
20848 println!("hello");
20849
20850 println!("world");
20851 }
20852 "#
20853 .unindent(),
20854 );
20855
20856 cx.set_head_text(&diff_base);
20857 executor.run_until_parked();
20858
20859 cx.update_editor(|editor, window, cx| {
20860 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20861 });
20862 executor.run_until_parked();
20863
20864 cx.assert_state_with_diff(
20865 r#"
20866 use some::mod1;
20867 use some::mod2;
20868
20869 const A: u32 = 42;
20870 + const B: u32 = 42;
20871 + const C: u32 = 42;
20872 + ˇ
20873
20874 fn main() {
20875 println!("hello");
20876
20877 println!("world");
20878 }
20879 "#
20880 .unindent(),
20881 );
20882
20883 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
20884 executor.run_until_parked();
20885
20886 cx.assert_state_with_diff(
20887 r#"
20888 use some::mod1;
20889 use some::mod2;
20890
20891 const A: u32 = 42;
20892 + const B: u32 = 42;
20893 + const C: u32 = 42;
20894 + const D: u32 = 42;
20895 + ˇ
20896
20897 fn main() {
20898 println!("hello");
20899
20900 println!("world");
20901 }
20902 "#
20903 .unindent(),
20904 );
20905
20906 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
20907 executor.run_until_parked();
20908
20909 cx.assert_state_with_diff(
20910 r#"
20911 use some::mod1;
20912 use some::mod2;
20913
20914 const A: u32 = 42;
20915 + const B: u32 = 42;
20916 + const C: u32 = 42;
20917 + const D: u32 = 42;
20918 + const E: u32 = 42;
20919 + ˇ
20920
20921 fn main() {
20922 println!("hello");
20923
20924 println!("world");
20925 }
20926 "#
20927 .unindent(),
20928 );
20929
20930 cx.update_editor(|editor, window, cx| {
20931 editor.delete_line(&DeleteLine, window, cx);
20932 });
20933 executor.run_until_parked();
20934
20935 cx.assert_state_with_diff(
20936 r#"
20937 use some::mod1;
20938 use some::mod2;
20939
20940 const A: u32 = 42;
20941 + const B: u32 = 42;
20942 + const C: u32 = 42;
20943 + const D: u32 = 42;
20944 + const E: u32 = 42;
20945 ˇ
20946 fn main() {
20947 println!("hello");
20948
20949 println!("world");
20950 }
20951 "#
20952 .unindent(),
20953 );
20954
20955 cx.update_editor(|editor, window, cx| {
20956 editor.move_up(&MoveUp, window, cx);
20957 editor.delete_line(&DeleteLine, window, cx);
20958 editor.move_up(&MoveUp, window, cx);
20959 editor.delete_line(&DeleteLine, window, cx);
20960 editor.move_up(&MoveUp, window, cx);
20961 editor.delete_line(&DeleteLine, window, cx);
20962 });
20963 executor.run_until_parked();
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 ˇ
20972 fn main() {
20973 println!("hello");
20974
20975 println!("world");
20976 }
20977 "#
20978 .unindent(),
20979 );
20980
20981 cx.update_editor(|editor, window, cx| {
20982 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
20983 editor.delete_line(&DeleteLine, window, cx);
20984 });
20985 executor.run_until_parked();
20986 cx.assert_state_with_diff(
20987 r#"
20988 ˇ
20989 fn main() {
20990 println!("hello");
20991
20992 println!("world");
20993 }
20994 "#
20995 .unindent(),
20996 );
20997}
20998
20999#[gpui::test]
21000async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
21001 init_test(cx, |_| {});
21002
21003 let mut cx = EditorTestContext::new(cx).await;
21004 cx.set_head_text(indoc! { "
21005 one
21006 two
21007 three
21008 four
21009 five
21010 "
21011 });
21012 cx.set_state(indoc! { "
21013 one
21014 ˇthree
21015 five
21016 "});
21017 cx.run_until_parked();
21018 cx.update_editor(|editor, window, cx| {
21019 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21020 });
21021 cx.assert_state_with_diff(
21022 indoc! { "
21023 one
21024 - two
21025 ˇthree
21026 - four
21027 five
21028 "}
21029 .to_string(),
21030 );
21031 cx.update_editor(|editor, window, cx| {
21032 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21033 });
21034
21035 cx.assert_state_with_diff(
21036 indoc! { "
21037 one
21038 ˇthree
21039 five
21040 "}
21041 .to_string(),
21042 );
21043
21044 cx.update_editor(|editor, window, cx| {
21045 editor.move_up(&MoveUp, window, cx);
21046 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21047 });
21048 cx.assert_state_with_diff(
21049 indoc! { "
21050 ˇone
21051 - two
21052 three
21053 five
21054 "}
21055 .to_string(),
21056 );
21057
21058 cx.update_editor(|editor, window, cx| {
21059 editor.move_down(&MoveDown, window, cx);
21060 editor.move_down(&MoveDown, window, cx);
21061 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21062 });
21063 cx.assert_state_with_diff(
21064 indoc! { "
21065 one
21066 - two
21067 ˇthree
21068 - four
21069 five
21070 "}
21071 .to_string(),
21072 );
21073
21074 cx.set_state(indoc! { "
21075 one
21076 ˇTWO
21077 three
21078 four
21079 five
21080 "});
21081 cx.run_until_parked();
21082 cx.update_editor(|editor, window, cx| {
21083 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21084 });
21085
21086 cx.assert_state_with_diff(
21087 indoc! { "
21088 one
21089 - two
21090 + ˇTWO
21091 three
21092 four
21093 five
21094 "}
21095 .to_string(),
21096 );
21097 cx.update_editor(|editor, window, cx| {
21098 editor.move_up(&Default::default(), window, cx);
21099 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21100 });
21101 cx.assert_state_with_diff(
21102 indoc! { "
21103 one
21104 ˇTWO
21105 three
21106 four
21107 five
21108 "}
21109 .to_string(),
21110 );
21111}
21112
21113#[gpui::test]
21114async fn test_toggling_adjacent_diff_hunks_2(
21115 executor: BackgroundExecutor,
21116 cx: &mut TestAppContext,
21117) {
21118 init_test(cx, |_| {});
21119
21120 let mut cx = EditorTestContext::new(cx).await;
21121
21122 let diff_base = r#"
21123 lineA
21124 lineB
21125 lineC
21126 lineD
21127 "#
21128 .unindent();
21129
21130 cx.set_state(
21131 &r#"
21132 ˇlineA1
21133 lineB
21134 lineD
21135 "#
21136 .unindent(),
21137 );
21138 cx.set_head_text(&diff_base);
21139 executor.run_until_parked();
21140
21141 cx.update_editor(|editor, window, cx| {
21142 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21143 });
21144 executor.run_until_parked();
21145 cx.assert_state_with_diff(
21146 r#"
21147 - lineA
21148 + ˇlineA1
21149 lineB
21150 lineD
21151 "#
21152 .unindent(),
21153 );
21154
21155 cx.update_editor(|editor, window, cx| {
21156 editor.move_down(&MoveDown, window, cx);
21157 editor.move_right(&MoveRight, window, cx);
21158 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21159 });
21160 executor.run_until_parked();
21161 cx.assert_state_with_diff(
21162 r#"
21163 - lineA
21164 + lineA1
21165 lˇineB
21166 - lineC
21167 lineD
21168 "#
21169 .unindent(),
21170 );
21171}
21172
21173#[gpui::test]
21174async fn test_edits_around_expanded_deletion_hunks(
21175 executor: BackgroundExecutor,
21176 cx: &mut TestAppContext,
21177) {
21178 init_test(cx, |_| {});
21179
21180 let mut cx = EditorTestContext::new(cx).await;
21181
21182 let diff_base = r#"
21183 use some::mod1;
21184 use some::mod2;
21185
21186 const A: u32 = 42;
21187 const B: u32 = 42;
21188 const C: u32 = 42;
21189
21190
21191 fn main() {
21192 println!("hello");
21193
21194 println!("world");
21195 }
21196 "#
21197 .unindent();
21198 executor.run_until_parked();
21199 cx.set_state(
21200 &r#"
21201 use some::mod1;
21202 use some::mod2;
21203
21204 ˇconst B: u32 = 42;
21205 const C: u32 = 42;
21206
21207
21208 fn main() {
21209 println!("hello");
21210
21211 println!("world");
21212 }
21213 "#
21214 .unindent(),
21215 );
21216
21217 cx.set_head_text(&diff_base);
21218 executor.run_until_parked();
21219
21220 cx.update_editor(|editor, window, cx| {
21221 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21222 });
21223 executor.run_until_parked();
21224
21225 cx.assert_state_with_diff(
21226 r#"
21227 use some::mod1;
21228 use some::mod2;
21229
21230 - const A: u32 = 42;
21231 ˇconst B: u32 = 42;
21232 const C: u32 = 42;
21233
21234
21235 fn main() {
21236 println!("hello");
21237
21238 println!("world");
21239 }
21240 "#
21241 .unindent(),
21242 );
21243
21244 cx.update_editor(|editor, window, cx| {
21245 editor.delete_line(&DeleteLine, window, cx);
21246 });
21247 executor.run_until_parked();
21248 cx.assert_state_with_diff(
21249 r#"
21250 use some::mod1;
21251 use some::mod2;
21252
21253 - const A: u32 = 42;
21254 - const B: u32 = 42;
21255 ˇconst C: u32 = 42;
21256
21257
21258 fn main() {
21259 println!("hello");
21260
21261 println!("world");
21262 }
21263 "#
21264 .unindent(),
21265 );
21266
21267 cx.update_editor(|editor, window, cx| {
21268 editor.delete_line(&DeleteLine, window, cx);
21269 });
21270 executor.run_until_parked();
21271 cx.assert_state_with_diff(
21272 r#"
21273 use some::mod1;
21274 use some::mod2;
21275
21276 - const A: u32 = 42;
21277 - const B: u32 = 42;
21278 - const C: u32 = 42;
21279 ˇ
21280
21281 fn main() {
21282 println!("hello");
21283
21284 println!("world");
21285 }
21286 "#
21287 .unindent(),
21288 );
21289
21290 cx.update_editor(|editor, window, cx| {
21291 editor.handle_input("replacement", window, cx);
21292 });
21293 executor.run_until_parked();
21294 cx.assert_state_with_diff(
21295 r#"
21296 use some::mod1;
21297 use some::mod2;
21298
21299 - const A: u32 = 42;
21300 - const B: u32 = 42;
21301 - const C: u32 = 42;
21302 -
21303 + replacementˇ
21304
21305 fn main() {
21306 println!("hello");
21307
21308 println!("world");
21309 }
21310 "#
21311 .unindent(),
21312 );
21313}
21314
21315#[gpui::test]
21316async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21317 init_test(cx, |_| {});
21318
21319 let mut cx = EditorTestContext::new(cx).await;
21320
21321 let base_text = r#"
21322 one
21323 two
21324 three
21325 four
21326 five
21327 "#
21328 .unindent();
21329 executor.run_until_parked();
21330 cx.set_state(
21331 &r#"
21332 one
21333 two
21334 fˇour
21335 five
21336 "#
21337 .unindent(),
21338 );
21339
21340 cx.set_head_text(&base_text);
21341 executor.run_until_parked();
21342
21343 cx.update_editor(|editor, window, cx| {
21344 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21345 });
21346 executor.run_until_parked();
21347
21348 cx.assert_state_with_diff(
21349 r#"
21350 one
21351 two
21352 - three
21353 fˇour
21354 five
21355 "#
21356 .unindent(),
21357 );
21358
21359 cx.update_editor(|editor, window, cx| {
21360 editor.backspace(&Backspace, window, cx);
21361 editor.backspace(&Backspace, window, cx);
21362 });
21363 executor.run_until_parked();
21364 cx.assert_state_with_diff(
21365 r#"
21366 one
21367 two
21368 - threeˇ
21369 - four
21370 + our
21371 five
21372 "#
21373 .unindent(),
21374 );
21375}
21376
21377#[gpui::test]
21378async fn test_edit_after_expanded_modification_hunk(
21379 executor: BackgroundExecutor,
21380 cx: &mut TestAppContext,
21381) {
21382 init_test(cx, |_| {});
21383
21384 let mut cx = EditorTestContext::new(cx).await;
21385
21386 let diff_base = r#"
21387 use some::mod1;
21388 use some::mod2;
21389
21390 const A: u32 = 42;
21391 const B: u32 = 42;
21392 const C: u32 = 42;
21393 const D: u32 = 42;
21394
21395
21396 fn main() {
21397 println!("hello");
21398
21399 println!("world");
21400 }"#
21401 .unindent();
21402
21403 cx.set_state(
21404 &r#"
21405 use some::mod1;
21406 use some::mod2;
21407
21408 const A: u32 = 42;
21409 const B: u32 = 42;
21410 const C: u32 = 43ˇ
21411 const D: u32 = 42;
21412
21413
21414 fn main() {
21415 println!("hello");
21416
21417 println!("world");
21418 }"#
21419 .unindent(),
21420 );
21421
21422 cx.set_head_text(&diff_base);
21423 executor.run_until_parked();
21424 cx.update_editor(|editor, window, cx| {
21425 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21426 });
21427 executor.run_until_parked();
21428
21429 cx.assert_state_with_diff(
21430 r#"
21431 use some::mod1;
21432 use some::mod2;
21433
21434 const A: u32 = 42;
21435 const B: u32 = 42;
21436 - const C: u32 = 42;
21437 + const C: u32 = 43ˇ
21438 const D: u32 = 42;
21439
21440
21441 fn main() {
21442 println!("hello");
21443
21444 println!("world");
21445 }"#
21446 .unindent(),
21447 );
21448
21449 cx.update_editor(|editor, window, cx| {
21450 editor.handle_input("\nnew_line\n", window, cx);
21451 });
21452 executor.run_until_parked();
21453
21454 cx.assert_state_with_diff(
21455 r#"
21456 use some::mod1;
21457 use some::mod2;
21458
21459 const A: u32 = 42;
21460 const B: u32 = 42;
21461 - const C: u32 = 42;
21462 + const C: u32 = 43
21463 + new_line
21464 + ˇ
21465 const D: u32 = 42;
21466
21467
21468 fn main() {
21469 println!("hello");
21470
21471 println!("world");
21472 }"#
21473 .unindent(),
21474 );
21475}
21476
21477#[gpui::test]
21478async fn test_stage_and_unstage_added_file_hunk(
21479 executor: BackgroundExecutor,
21480 cx: &mut TestAppContext,
21481) {
21482 init_test(cx, |_| {});
21483
21484 let mut cx = EditorTestContext::new(cx).await;
21485 cx.update_editor(|editor, _, cx| {
21486 editor.set_expand_all_diff_hunks(cx);
21487 });
21488
21489 let working_copy = r#"
21490 ˇfn main() {
21491 println!("hello, world!");
21492 }
21493 "#
21494 .unindent();
21495
21496 cx.set_state(&working_copy);
21497 executor.run_until_parked();
21498
21499 cx.assert_state_with_diff(
21500 r#"
21501 + ˇfn main() {
21502 + println!("hello, world!");
21503 + }
21504 "#
21505 .unindent(),
21506 );
21507 cx.assert_index_text(None);
21508
21509 cx.update_editor(|editor, window, cx| {
21510 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21511 });
21512 executor.run_until_parked();
21513 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
21514 cx.assert_state_with_diff(
21515 r#"
21516 + ˇfn main() {
21517 + println!("hello, world!");
21518 + }
21519 "#
21520 .unindent(),
21521 );
21522
21523 cx.update_editor(|editor, window, cx| {
21524 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21525 });
21526 executor.run_until_parked();
21527 cx.assert_index_text(None);
21528}
21529
21530async fn setup_indent_guides_editor(
21531 text: &str,
21532 cx: &mut TestAppContext,
21533) -> (BufferId, EditorTestContext) {
21534 init_test(cx, |_| {});
21535
21536 let mut cx = EditorTestContext::new(cx).await;
21537
21538 let buffer_id = cx.update_editor(|editor, window, cx| {
21539 editor.set_text(text, window, cx);
21540 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
21541
21542 buffer_ids[0]
21543 });
21544
21545 (buffer_id, cx)
21546}
21547
21548fn assert_indent_guides(
21549 range: Range<u32>,
21550 expected: Vec<IndentGuide>,
21551 active_indices: Option<Vec<usize>>,
21552 cx: &mut EditorTestContext,
21553) {
21554 let indent_guides = cx.update_editor(|editor, window, cx| {
21555 let snapshot = editor.snapshot(window, cx).display_snapshot;
21556 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
21557 editor,
21558 MultiBufferRow(range.start)..MultiBufferRow(range.end),
21559 true,
21560 &snapshot,
21561 cx,
21562 );
21563
21564 indent_guides.sort_by(|a, b| {
21565 a.depth.cmp(&b.depth).then(
21566 a.start_row
21567 .cmp(&b.start_row)
21568 .then(a.end_row.cmp(&b.end_row)),
21569 )
21570 });
21571 indent_guides
21572 });
21573
21574 if let Some(expected) = active_indices {
21575 let active_indices = cx.update_editor(|editor, window, cx| {
21576 let snapshot = editor.snapshot(window, cx).display_snapshot;
21577 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
21578 });
21579
21580 assert_eq!(
21581 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
21582 expected,
21583 "Active indent guide indices do not match"
21584 );
21585 }
21586
21587 assert_eq!(indent_guides, expected, "Indent guides do not match");
21588}
21589
21590fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
21591 IndentGuide {
21592 buffer_id,
21593 start_row: MultiBufferRow(start_row),
21594 end_row: MultiBufferRow(end_row),
21595 depth,
21596 tab_size: 4,
21597 settings: IndentGuideSettings {
21598 enabled: true,
21599 line_width: 1,
21600 active_line_width: 1,
21601 coloring: IndentGuideColoring::default(),
21602 background_coloring: IndentGuideBackgroundColoring::default(),
21603 },
21604 }
21605}
21606
21607#[gpui::test]
21608async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
21609 let (buffer_id, mut cx) = setup_indent_guides_editor(
21610 &"
21611 fn main() {
21612 let a = 1;
21613 }"
21614 .unindent(),
21615 cx,
21616 )
21617 .await;
21618
21619 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21620}
21621
21622#[gpui::test]
21623async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
21624 let (buffer_id, mut cx) = setup_indent_guides_editor(
21625 &"
21626 fn main() {
21627 let a = 1;
21628 let b = 2;
21629 }"
21630 .unindent(),
21631 cx,
21632 )
21633 .await;
21634
21635 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
21636}
21637
21638#[gpui::test]
21639async fn test_indent_guide_nested(cx: &mut TestAppContext) {
21640 let (buffer_id, mut cx) = setup_indent_guides_editor(
21641 &"
21642 fn main() {
21643 let a = 1;
21644 if a == 3 {
21645 let b = 2;
21646 } else {
21647 let c = 3;
21648 }
21649 }"
21650 .unindent(),
21651 cx,
21652 )
21653 .await;
21654
21655 assert_indent_guides(
21656 0..8,
21657 vec![
21658 indent_guide(buffer_id, 1, 6, 0),
21659 indent_guide(buffer_id, 3, 3, 1),
21660 indent_guide(buffer_id, 5, 5, 1),
21661 ],
21662 None,
21663 &mut cx,
21664 );
21665}
21666
21667#[gpui::test]
21668async fn test_indent_guide_tab(cx: &mut TestAppContext) {
21669 let (buffer_id, mut cx) = setup_indent_guides_editor(
21670 &"
21671 fn main() {
21672 let a = 1;
21673 let b = 2;
21674 let c = 3;
21675 }"
21676 .unindent(),
21677 cx,
21678 )
21679 .await;
21680
21681 assert_indent_guides(
21682 0..5,
21683 vec![
21684 indent_guide(buffer_id, 1, 3, 0),
21685 indent_guide(buffer_id, 2, 2, 1),
21686 ],
21687 None,
21688 &mut cx,
21689 );
21690}
21691
21692#[gpui::test]
21693async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
21694 let (buffer_id, mut cx) = setup_indent_guides_editor(
21695 &"
21696 fn main() {
21697 let a = 1;
21698
21699 let c = 3;
21700 }"
21701 .unindent(),
21702 cx,
21703 )
21704 .await;
21705
21706 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
21707}
21708
21709#[gpui::test]
21710async fn test_indent_guide_complex(cx: &mut TestAppContext) {
21711 let (buffer_id, mut cx) = setup_indent_guides_editor(
21712 &"
21713 fn main() {
21714 let a = 1;
21715
21716 let c = 3;
21717
21718 if a == 3 {
21719 let b = 2;
21720 } else {
21721 let c = 3;
21722 }
21723 }"
21724 .unindent(),
21725 cx,
21726 )
21727 .await;
21728
21729 assert_indent_guides(
21730 0..11,
21731 vec![
21732 indent_guide(buffer_id, 1, 9, 0),
21733 indent_guide(buffer_id, 6, 6, 1),
21734 indent_guide(buffer_id, 8, 8, 1),
21735 ],
21736 None,
21737 &mut cx,
21738 );
21739}
21740
21741#[gpui::test]
21742async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
21743 let (buffer_id, mut cx) = setup_indent_guides_editor(
21744 &"
21745 fn main() {
21746 let a = 1;
21747
21748 let c = 3;
21749
21750 if a == 3 {
21751 let b = 2;
21752 } else {
21753 let c = 3;
21754 }
21755 }"
21756 .unindent(),
21757 cx,
21758 )
21759 .await;
21760
21761 assert_indent_guides(
21762 1..11,
21763 vec![
21764 indent_guide(buffer_id, 1, 9, 0),
21765 indent_guide(buffer_id, 6, 6, 1),
21766 indent_guide(buffer_id, 8, 8, 1),
21767 ],
21768 None,
21769 &mut cx,
21770 );
21771}
21772
21773#[gpui::test]
21774async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
21775 let (buffer_id, mut cx) = setup_indent_guides_editor(
21776 &"
21777 fn main() {
21778 let a = 1;
21779
21780 let c = 3;
21781
21782 if a == 3 {
21783 let b = 2;
21784 } else {
21785 let c = 3;
21786 }
21787 }"
21788 .unindent(),
21789 cx,
21790 )
21791 .await;
21792
21793 assert_indent_guides(
21794 1..10,
21795 vec![
21796 indent_guide(buffer_id, 1, 9, 0),
21797 indent_guide(buffer_id, 6, 6, 1),
21798 indent_guide(buffer_id, 8, 8, 1),
21799 ],
21800 None,
21801 &mut cx,
21802 );
21803}
21804
21805#[gpui::test]
21806async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
21807 let (buffer_id, mut cx) = setup_indent_guides_editor(
21808 &"
21809 fn main() {
21810 if a {
21811 b(
21812 c,
21813 d,
21814 )
21815 } else {
21816 e(
21817 f
21818 )
21819 }
21820 }"
21821 .unindent(),
21822 cx,
21823 )
21824 .await;
21825
21826 assert_indent_guides(
21827 0..11,
21828 vec![
21829 indent_guide(buffer_id, 1, 10, 0),
21830 indent_guide(buffer_id, 2, 5, 1),
21831 indent_guide(buffer_id, 7, 9, 1),
21832 indent_guide(buffer_id, 3, 4, 2),
21833 indent_guide(buffer_id, 8, 8, 2),
21834 ],
21835 None,
21836 &mut cx,
21837 );
21838
21839 cx.update_editor(|editor, window, cx| {
21840 editor.fold_at(MultiBufferRow(2), window, cx);
21841 assert_eq!(
21842 editor.display_text(cx),
21843 "
21844 fn main() {
21845 if a {
21846 b(⋯
21847 )
21848 } else {
21849 e(
21850 f
21851 )
21852 }
21853 }"
21854 .unindent()
21855 );
21856 });
21857
21858 assert_indent_guides(
21859 0..11,
21860 vec![
21861 indent_guide(buffer_id, 1, 10, 0),
21862 indent_guide(buffer_id, 2, 5, 1),
21863 indent_guide(buffer_id, 7, 9, 1),
21864 indent_guide(buffer_id, 8, 8, 2),
21865 ],
21866 None,
21867 &mut cx,
21868 );
21869}
21870
21871#[gpui::test]
21872async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
21873 let (buffer_id, mut cx) = setup_indent_guides_editor(
21874 &"
21875 block1
21876 block2
21877 block3
21878 block4
21879 block2
21880 block1
21881 block1"
21882 .unindent(),
21883 cx,
21884 )
21885 .await;
21886
21887 assert_indent_guides(
21888 1..10,
21889 vec![
21890 indent_guide(buffer_id, 1, 4, 0),
21891 indent_guide(buffer_id, 2, 3, 1),
21892 indent_guide(buffer_id, 3, 3, 2),
21893 ],
21894 None,
21895 &mut cx,
21896 );
21897}
21898
21899#[gpui::test]
21900async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
21901 let (buffer_id, mut cx) = setup_indent_guides_editor(
21902 &"
21903 block1
21904 block2
21905 block3
21906
21907 block1
21908 block1"
21909 .unindent(),
21910 cx,
21911 )
21912 .await;
21913
21914 assert_indent_guides(
21915 0..6,
21916 vec![
21917 indent_guide(buffer_id, 1, 2, 0),
21918 indent_guide(buffer_id, 2, 2, 1),
21919 ],
21920 None,
21921 &mut cx,
21922 );
21923}
21924
21925#[gpui::test]
21926async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
21927 let (buffer_id, mut cx) = setup_indent_guides_editor(
21928 &"
21929 function component() {
21930 \treturn (
21931 \t\t\t
21932 \t\t<div>
21933 \t\t\t<abc></abc>
21934 \t\t</div>
21935 \t)
21936 }"
21937 .unindent(),
21938 cx,
21939 )
21940 .await;
21941
21942 assert_indent_guides(
21943 0..8,
21944 vec![
21945 indent_guide(buffer_id, 1, 6, 0),
21946 indent_guide(buffer_id, 2, 5, 1),
21947 indent_guide(buffer_id, 4, 4, 2),
21948 ],
21949 None,
21950 &mut cx,
21951 );
21952}
21953
21954#[gpui::test]
21955async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
21956 let (buffer_id, mut cx) = setup_indent_guides_editor(
21957 &"
21958 function component() {
21959 \treturn (
21960 \t
21961 \t\t<div>
21962 \t\t\t<abc></abc>
21963 \t\t</div>
21964 \t)
21965 }"
21966 .unindent(),
21967 cx,
21968 )
21969 .await;
21970
21971 assert_indent_guides(
21972 0..8,
21973 vec![
21974 indent_guide(buffer_id, 1, 6, 0),
21975 indent_guide(buffer_id, 2, 5, 1),
21976 indent_guide(buffer_id, 4, 4, 2),
21977 ],
21978 None,
21979 &mut cx,
21980 );
21981}
21982
21983#[gpui::test]
21984async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
21985 let (buffer_id, mut cx) = setup_indent_guides_editor(
21986 &"
21987 block1
21988
21989
21990
21991 block2
21992 "
21993 .unindent(),
21994 cx,
21995 )
21996 .await;
21997
21998 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21999}
22000
22001#[gpui::test]
22002async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
22003 let (buffer_id, mut cx) = setup_indent_guides_editor(
22004 &"
22005 def a:
22006 \tb = 3
22007 \tif True:
22008 \t\tc = 4
22009 \t\td = 5
22010 \tprint(b)
22011 "
22012 .unindent(),
22013 cx,
22014 )
22015 .await;
22016
22017 assert_indent_guides(
22018 0..6,
22019 vec![
22020 indent_guide(buffer_id, 1, 5, 0),
22021 indent_guide(buffer_id, 3, 4, 1),
22022 ],
22023 None,
22024 &mut cx,
22025 );
22026}
22027
22028#[gpui::test]
22029async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
22030 let (buffer_id, mut cx) = setup_indent_guides_editor(
22031 &"
22032 fn main() {
22033 let a = 1;
22034 }"
22035 .unindent(),
22036 cx,
22037 )
22038 .await;
22039
22040 cx.update_editor(|editor, window, cx| {
22041 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22042 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22043 });
22044 });
22045
22046 assert_indent_guides(
22047 0..3,
22048 vec![indent_guide(buffer_id, 1, 1, 0)],
22049 Some(vec![0]),
22050 &mut cx,
22051 );
22052}
22053
22054#[gpui::test]
22055async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
22056 let (buffer_id, mut cx) = setup_indent_guides_editor(
22057 &"
22058 fn main() {
22059 if 1 == 2 {
22060 let a = 1;
22061 }
22062 }"
22063 .unindent(),
22064 cx,
22065 )
22066 .await;
22067
22068 cx.update_editor(|editor, window, cx| {
22069 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22070 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22071 });
22072 });
22073
22074 assert_indent_guides(
22075 0..4,
22076 vec![
22077 indent_guide(buffer_id, 1, 3, 0),
22078 indent_guide(buffer_id, 2, 2, 1),
22079 ],
22080 Some(vec![1]),
22081 &mut cx,
22082 );
22083
22084 cx.update_editor(|editor, window, cx| {
22085 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22086 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
22087 });
22088 });
22089
22090 assert_indent_guides(
22091 0..4,
22092 vec![
22093 indent_guide(buffer_id, 1, 3, 0),
22094 indent_guide(buffer_id, 2, 2, 1),
22095 ],
22096 Some(vec![1]),
22097 &mut cx,
22098 );
22099
22100 cx.update_editor(|editor, window, cx| {
22101 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22102 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
22103 });
22104 });
22105
22106 assert_indent_guides(
22107 0..4,
22108 vec![
22109 indent_guide(buffer_id, 1, 3, 0),
22110 indent_guide(buffer_id, 2, 2, 1),
22111 ],
22112 Some(vec![0]),
22113 &mut cx,
22114 );
22115}
22116
22117#[gpui::test]
22118async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
22119 let (buffer_id, mut cx) = setup_indent_guides_editor(
22120 &"
22121 fn main() {
22122 let a = 1;
22123
22124 let b = 2;
22125 }"
22126 .unindent(),
22127 cx,
22128 )
22129 .await;
22130
22131 cx.update_editor(|editor, window, cx| {
22132 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22133 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
22134 });
22135 });
22136
22137 assert_indent_guides(
22138 0..5,
22139 vec![indent_guide(buffer_id, 1, 3, 0)],
22140 Some(vec![0]),
22141 &mut cx,
22142 );
22143}
22144
22145#[gpui::test]
22146async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
22147 let (buffer_id, mut cx) = setup_indent_guides_editor(
22148 &"
22149 def m:
22150 a = 1
22151 pass"
22152 .unindent(),
22153 cx,
22154 )
22155 .await;
22156
22157 cx.update_editor(|editor, window, cx| {
22158 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22159 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22160 });
22161 });
22162
22163 assert_indent_guides(
22164 0..3,
22165 vec![indent_guide(buffer_id, 1, 2, 0)],
22166 Some(vec![0]),
22167 &mut cx,
22168 );
22169}
22170
22171#[gpui::test]
22172async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
22173 init_test(cx, |_| {});
22174 let mut cx = EditorTestContext::new(cx).await;
22175 let text = indoc! {
22176 "
22177 impl A {
22178 fn b() {
22179 0;
22180 3;
22181 5;
22182 6;
22183 7;
22184 }
22185 }
22186 "
22187 };
22188 let base_text = indoc! {
22189 "
22190 impl A {
22191 fn b() {
22192 0;
22193 1;
22194 2;
22195 3;
22196 4;
22197 }
22198 fn c() {
22199 5;
22200 6;
22201 7;
22202 }
22203 }
22204 "
22205 };
22206
22207 cx.update_editor(|editor, window, cx| {
22208 editor.set_text(text, window, cx);
22209
22210 editor.buffer().update(cx, |multibuffer, cx| {
22211 let buffer = multibuffer.as_singleton().unwrap();
22212 let diff = cx.new(|cx| {
22213 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
22214 });
22215
22216 multibuffer.set_all_diff_hunks_expanded(cx);
22217 multibuffer.add_diff(diff, cx);
22218
22219 buffer.read(cx).remote_id()
22220 })
22221 });
22222 cx.run_until_parked();
22223
22224 cx.assert_state_with_diff(
22225 indoc! { "
22226 impl A {
22227 fn b() {
22228 0;
22229 - 1;
22230 - 2;
22231 3;
22232 - 4;
22233 - }
22234 - fn c() {
22235 5;
22236 6;
22237 7;
22238 }
22239 }
22240 ˇ"
22241 }
22242 .to_string(),
22243 );
22244
22245 let mut actual_guides = cx.update_editor(|editor, window, cx| {
22246 editor
22247 .snapshot(window, cx)
22248 .buffer_snapshot()
22249 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
22250 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
22251 .collect::<Vec<_>>()
22252 });
22253 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
22254 assert_eq!(
22255 actual_guides,
22256 vec![
22257 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
22258 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
22259 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
22260 ]
22261 );
22262}
22263
22264#[gpui::test]
22265async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22266 init_test(cx, |_| {});
22267 let mut cx = EditorTestContext::new(cx).await;
22268
22269 let diff_base = r#"
22270 a
22271 b
22272 c
22273 "#
22274 .unindent();
22275
22276 cx.set_state(
22277 &r#"
22278 ˇA
22279 b
22280 C
22281 "#
22282 .unindent(),
22283 );
22284 cx.set_head_text(&diff_base);
22285 cx.update_editor(|editor, window, cx| {
22286 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22287 });
22288 executor.run_until_parked();
22289
22290 let both_hunks_expanded = r#"
22291 - a
22292 + ˇA
22293 b
22294 - c
22295 + C
22296 "#
22297 .unindent();
22298
22299 cx.assert_state_with_diff(both_hunks_expanded.clone());
22300
22301 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22302 let snapshot = editor.snapshot(window, cx);
22303 let hunks = editor
22304 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22305 .collect::<Vec<_>>();
22306 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22307 hunks
22308 .into_iter()
22309 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22310 .collect::<Vec<_>>()
22311 });
22312 assert_eq!(hunk_ranges.len(), 2);
22313
22314 cx.update_editor(|editor, _, cx| {
22315 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22316 });
22317 executor.run_until_parked();
22318
22319 let second_hunk_expanded = r#"
22320 ˇA
22321 b
22322 - c
22323 + C
22324 "#
22325 .unindent();
22326
22327 cx.assert_state_with_diff(second_hunk_expanded);
22328
22329 cx.update_editor(|editor, _, cx| {
22330 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22331 });
22332 executor.run_until_parked();
22333
22334 cx.assert_state_with_diff(both_hunks_expanded.clone());
22335
22336 cx.update_editor(|editor, _, cx| {
22337 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22338 });
22339 executor.run_until_parked();
22340
22341 let first_hunk_expanded = r#"
22342 - a
22343 + ˇA
22344 b
22345 C
22346 "#
22347 .unindent();
22348
22349 cx.assert_state_with_diff(first_hunk_expanded);
22350
22351 cx.update_editor(|editor, _, cx| {
22352 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22353 });
22354 executor.run_until_parked();
22355
22356 cx.assert_state_with_diff(both_hunks_expanded);
22357
22358 cx.set_state(
22359 &r#"
22360 ˇA
22361 b
22362 "#
22363 .unindent(),
22364 );
22365 cx.run_until_parked();
22366
22367 // TODO this cursor position seems bad
22368 cx.assert_state_with_diff(
22369 r#"
22370 - ˇa
22371 + A
22372 b
22373 "#
22374 .unindent(),
22375 );
22376
22377 cx.update_editor(|editor, window, cx| {
22378 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22379 });
22380
22381 cx.assert_state_with_diff(
22382 r#"
22383 - ˇa
22384 + A
22385 b
22386 - c
22387 "#
22388 .unindent(),
22389 );
22390
22391 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22392 let snapshot = editor.snapshot(window, cx);
22393 let hunks = editor
22394 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22395 .collect::<Vec<_>>();
22396 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22397 hunks
22398 .into_iter()
22399 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22400 .collect::<Vec<_>>()
22401 });
22402 assert_eq!(hunk_ranges.len(), 2);
22403
22404 cx.update_editor(|editor, _, cx| {
22405 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22406 });
22407 executor.run_until_parked();
22408
22409 cx.assert_state_with_diff(
22410 r#"
22411 - ˇa
22412 + A
22413 b
22414 "#
22415 .unindent(),
22416 );
22417}
22418
22419#[gpui::test]
22420async fn test_toggle_deletion_hunk_at_start_of_file(
22421 executor: BackgroundExecutor,
22422 cx: &mut TestAppContext,
22423) {
22424 init_test(cx, |_| {});
22425 let mut cx = EditorTestContext::new(cx).await;
22426
22427 let diff_base = r#"
22428 a
22429 b
22430 c
22431 "#
22432 .unindent();
22433
22434 cx.set_state(
22435 &r#"
22436 ˇb
22437 c
22438 "#
22439 .unindent(),
22440 );
22441 cx.set_head_text(&diff_base);
22442 cx.update_editor(|editor, window, cx| {
22443 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22444 });
22445 executor.run_until_parked();
22446
22447 let hunk_expanded = r#"
22448 - a
22449 ˇb
22450 c
22451 "#
22452 .unindent();
22453
22454 cx.assert_state_with_diff(hunk_expanded.clone());
22455
22456 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22457 let snapshot = editor.snapshot(window, cx);
22458 let hunks = editor
22459 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22460 .collect::<Vec<_>>();
22461 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22462 hunks
22463 .into_iter()
22464 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22465 .collect::<Vec<_>>()
22466 });
22467 assert_eq!(hunk_ranges.len(), 1);
22468
22469 cx.update_editor(|editor, _, cx| {
22470 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22471 });
22472 executor.run_until_parked();
22473
22474 let hunk_collapsed = r#"
22475 ˇb
22476 c
22477 "#
22478 .unindent();
22479
22480 cx.assert_state_with_diff(hunk_collapsed);
22481
22482 cx.update_editor(|editor, _, cx| {
22483 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22484 });
22485 executor.run_until_parked();
22486
22487 cx.assert_state_with_diff(hunk_expanded);
22488}
22489
22490#[gpui::test]
22491async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
22492 executor: BackgroundExecutor,
22493 cx: &mut TestAppContext,
22494) {
22495 init_test(cx, |_| {});
22496 let mut cx = EditorTestContext::new(cx).await;
22497
22498 cx.set_state("ˇnew\nsecond\nthird\n");
22499 cx.set_head_text("old\nsecond\nthird\n");
22500 cx.update_editor(|editor, window, cx| {
22501 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
22502 });
22503 executor.run_until_parked();
22504 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22505
22506 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
22507 cx.update_editor(|editor, window, cx| {
22508 let snapshot = editor.snapshot(window, cx);
22509 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22510 let hunks = editor
22511 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22512 .collect::<Vec<_>>();
22513 assert_eq!(hunks.len(), 1);
22514 let hunk_range = Anchor::range_in_buffer(excerpt_id, hunks[0].buffer_range.clone());
22515 editor.toggle_single_diff_hunk(hunk_range, cx)
22516 });
22517 executor.run_until_parked();
22518 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
22519
22520 // Keep the editor scrolled to the top so the full hunk remains visible.
22521 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22522}
22523
22524#[gpui::test]
22525async fn test_display_diff_hunks(cx: &mut TestAppContext) {
22526 init_test(cx, |_| {});
22527
22528 let fs = FakeFs::new(cx.executor());
22529 fs.insert_tree(
22530 path!("/test"),
22531 json!({
22532 ".git": {},
22533 "file-1": "ONE\n",
22534 "file-2": "TWO\n",
22535 "file-3": "THREE\n",
22536 }),
22537 )
22538 .await;
22539
22540 fs.set_head_for_repo(
22541 path!("/test/.git").as_ref(),
22542 &[
22543 ("file-1", "one\n".into()),
22544 ("file-2", "two\n".into()),
22545 ("file-3", "three\n".into()),
22546 ],
22547 "deadbeef",
22548 );
22549
22550 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
22551 let mut buffers = vec![];
22552 for i in 1..=3 {
22553 let buffer = project
22554 .update(cx, |project, cx| {
22555 let path = format!(path!("/test/file-{}"), i);
22556 project.open_local_buffer(path, cx)
22557 })
22558 .await
22559 .unwrap();
22560 buffers.push(buffer);
22561 }
22562
22563 let multibuffer = cx.new(|cx| {
22564 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
22565 multibuffer.set_all_diff_hunks_expanded(cx);
22566 for buffer in &buffers {
22567 let snapshot = buffer.read(cx).snapshot();
22568 multibuffer.set_excerpts_for_path(
22569 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
22570 buffer.clone(),
22571 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
22572 2,
22573 cx,
22574 );
22575 }
22576 multibuffer
22577 });
22578
22579 let editor = cx.add_window(|window, cx| {
22580 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
22581 });
22582 cx.run_until_parked();
22583
22584 let snapshot = editor
22585 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22586 .unwrap();
22587 let hunks = snapshot
22588 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
22589 .map(|hunk| match hunk {
22590 DisplayDiffHunk::Unfolded {
22591 display_row_range, ..
22592 } => display_row_range,
22593 DisplayDiffHunk::Folded { .. } => unreachable!(),
22594 })
22595 .collect::<Vec<_>>();
22596 assert_eq!(
22597 hunks,
22598 [
22599 DisplayRow(2)..DisplayRow(4),
22600 DisplayRow(7)..DisplayRow(9),
22601 DisplayRow(12)..DisplayRow(14),
22602 ]
22603 );
22604}
22605
22606#[gpui::test]
22607async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
22608 init_test(cx, |_| {});
22609
22610 let mut cx = EditorTestContext::new(cx).await;
22611 cx.set_head_text(indoc! { "
22612 one
22613 two
22614 three
22615 four
22616 five
22617 "
22618 });
22619 cx.set_index_text(indoc! { "
22620 one
22621 two
22622 three
22623 four
22624 five
22625 "
22626 });
22627 cx.set_state(indoc! {"
22628 one
22629 TWO
22630 ˇTHREE
22631 FOUR
22632 five
22633 "});
22634 cx.run_until_parked();
22635 cx.update_editor(|editor, window, cx| {
22636 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22637 });
22638 cx.run_until_parked();
22639 cx.assert_index_text(Some(indoc! {"
22640 one
22641 TWO
22642 THREE
22643 FOUR
22644 five
22645 "}));
22646 cx.set_state(indoc! { "
22647 one
22648 TWO
22649 ˇTHREE-HUNDRED
22650 FOUR
22651 five
22652 "});
22653 cx.run_until_parked();
22654 cx.update_editor(|editor, window, cx| {
22655 let snapshot = editor.snapshot(window, cx);
22656 let hunks = editor
22657 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22658 .collect::<Vec<_>>();
22659 assert_eq!(hunks.len(), 1);
22660 assert_eq!(
22661 hunks[0].status(),
22662 DiffHunkStatus {
22663 kind: DiffHunkStatusKind::Modified,
22664 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
22665 }
22666 );
22667
22668 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22669 });
22670 cx.run_until_parked();
22671 cx.assert_index_text(Some(indoc! {"
22672 one
22673 TWO
22674 THREE-HUNDRED
22675 FOUR
22676 five
22677 "}));
22678}
22679
22680#[gpui::test]
22681fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
22682 init_test(cx, |_| {});
22683
22684 let editor = cx.add_window(|window, cx| {
22685 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
22686 build_editor(buffer, window, cx)
22687 });
22688
22689 let render_args = Arc::new(Mutex::new(None));
22690 let snapshot = editor
22691 .update(cx, |editor, window, cx| {
22692 let snapshot = editor.buffer().read(cx).snapshot(cx);
22693 let range =
22694 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
22695
22696 struct RenderArgs {
22697 row: MultiBufferRow,
22698 folded: bool,
22699 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
22700 }
22701
22702 let crease = Crease::inline(
22703 range,
22704 FoldPlaceholder::test(),
22705 {
22706 let toggle_callback = render_args.clone();
22707 move |row, folded, callback, _window, _cx| {
22708 *toggle_callback.lock() = Some(RenderArgs {
22709 row,
22710 folded,
22711 callback,
22712 });
22713 div()
22714 }
22715 },
22716 |_row, _folded, _window, _cx| div(),
22717 );
22718
22719 editor.insert_creases(Some(crease), cx);
22720 let snapshot = editor.snapshot(window, cx);
22721 let _div =
22722 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
22723 snapshot
22724 })
22725 .unwrap();
22726
22727 let render_args = render_args.lock().take().unwrap();
22728 assert_eq!(render_args.row, MultiBufferRow(1));
22729 assert!(!render_args.folded);
22730 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22731
22732 cx.update_window(*editor, |_, window, cx| {
22733 (render_args.callback)(true, window, cx)
22734 })
22735 .unwrap();
22736 let snapshot = editor
22737 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22738 .unwrap();
22739 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
22740
22741 cx.update_window(*editor, |_, window, cx| {
22742 (render_args.callback)(false, window, cx)
22743 })
22744 .unwrap();
22745 let snapshot = editor
22746 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22747 .unwrap();
22748 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22749}
22750
22751#[gpui::test]
22752async fn test_input_text(cx: &mut TestAppContext) {
22753 init_test(cx, |_| {});
22754 let mut cx = EditorTestContext::new(cx).await;
22755
22756 cx.set_state(
22757 &r#"ˇone
22758 two
22759
22760 three
22761 fourˇ
22762 five
22763
22764 siˇx"#
22765 .unindent(),
22766 );
22767
22768 cx.dispatch_action(HandleInput(String::new()));
22769 cx.assert_editor_state(
22770 &r#"ˇone
22771 two
22772
22773 three
22774 fourˇ
22775 five
22776
22777 siˇx"#
22778 .unindent(),
22779 );
22780
22781 cx.dispatch_action(HandleInput("AAAA".to_string()));
22782 cx.assert_editor_state(
22783 &r#"AAAAˇone
22784 two
22785
22786 three
22787 fourAAAAˇ
22788 five
22789
22790 siAAAAˇx"#
22791 .unindent(),
22792 );
22793}
22794
22795#[gpui::test]
22796async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22797 init_test(cx, |_| {});
22798
22799 let mut cx = EditorTestContext::new(cx).await;
22800 cx.set_state(
22801 r#"let foo = 1;
22802let foo = 2;
22803let foo = 3;
22804let fooˇ = 4;
22805let foo = 5;
22806let foo = 6;
22807let foo = 7;
22808let foo = 8;
22809let foo = 9;
22810let foo = 10;
22811let foo = 11;
22812let foo = 12;
22813let foo = 13;
22814let foo = 14;
22815let foo = 15;"#,
22816 );
22817
22818 cx.update_editor(|e, window, cx| {
22819 assert_eq!(
22820 e.next_scroll_position,
22821 NextScrollCursorCenterTopBottom::Center,
22822 "Default next scroll direction is center",
22823 );
22824
22825 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22826 assert_eq!(
22827 e.next_scroll_position,
22828 NextScrollCursorCenterTopBottom::Top,
22829 "After center, next scroll direction should be top",
22830 );
22831
22832 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22833 assert_eq!(
22834 e.next_scroll_position,
22835 NextScrollCursorCenterTopBottom::Bottom,
22836 "After top, next scroll direction should be bottom",
22837 );
22838
22839 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22840 assert_eq!(
22841 e.next_scroll_position,
22842 NextScrollCursorCenterTopBottom::Center,
22843 "After bottom, scrolling should start over",
22844 );
22845
22846 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22847 assert_eq!(
22848 e.next_scroll_position,
22849 NextScrollCursorCenterTopBottom::Top,
22850 "Scrolling continues if retriggered fast enough"
22851 );
22852 });
22853
22854 cx.executor()
22855 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22856 cx.executor().run_until_parked();
22857 cx.update_editor(|e, _, _| {
22858 assert_eq!(
22859 e.next_scroll_position,
22860 NextScrollCursorCenterTopBottom::Center,
22861 "If scrolling is not triggered fast enough, it should reset"
22862 );
22863 });
22864}
22865
22866#[gpui::test]
22867async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22868 init_test(cx, |_| {});
22869 let mut cx = EditorLspTestContext::new_rust(
22870 lsp::ServerCapabilities {
22871 definition_provider: Some(lsp::OneOf::Left(true)),
22872 references_provider: Some(lsp::OneOf::Left(true)),
22873 ..lsp::ServerCapabilities::default()
22874 },
22875 cx,
22876 )
22877 .await;
22878
22879 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22880 let go_to_definition = cx
22881 .lsp
22882 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22883 move |params, _| async move {
22884 if empty_go_to_definition {
22885 Ok(None)
22886 } else {
22887 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22888 uri: params.text_document_position_params.text_document.uri,
22889 range: lsp::Range::new(
22890 lsp::Position::new(4, 3),
22891 lsp::Position::new(4, 6),
22892 ),
22893 })))
22894 }
22895 },
22896 );
22897 let references = cx
22898 .lsp
22899 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22900 Ok(Some(vec![lsp::Location {
22901 uri: params.text_document_position.text_document.uri,
22902 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22903 }]))
22904 });
22905 (go_to_definition, references)
22906 };
22907
22908 cx.set_state(
22909 &r#"fn one() {
22910 let mut a = ˇtwo();
22911 }
22912
22913 fn two() {}"#
22914 .unindent(),
22915 );
22916 set_up_lsp_handlers(false, &mut cx);
22917 let navigated = cx
22918 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22919 .await
22920 .expect("Failed to navigate to definition");
22921 assert_eq!(
22922 navigated,
22923 Navigated::Yes,
22924 "Should have navigated to definition from the GetDefinition response"
22925 );
22926 cx.assert_editor_state(
22927 &r#"fn one() {
22928 let mut a = two();
22929 }
22930
22931 fn «twoˇ»() {}"#
22932 .unindent(),
22933 );
22934
22935 let editors = cx.update_workspace(|workspace, _, cx| {
22936 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22937 });
22938 cx.update_editor(|_, _, test_editor_cx| {
22939 assert_eq!(
22940 editors.len(),
22941 1,
22942 "Initially, only one, test, editor should be open in the workspace"
22943 );
22944 assert_eq!(
22945 test_editor_cx.entity(),
22946 editors.last().expect("Asserted len is 1").clone()
22947 );
22948 });
22949
22950 set_up_lsp_handlers(true, &mut cx);
22951 let navigated = cx
22952 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22953 .await
22954 .expect("Failed to navigate to lookup references");
22955 assert_eq!(
22956 navigated,
22957 Navigated::Yes,
22958 "Should have navigated to references as a fallback after empty GoToDefinition response"
22959 );
22960 // We should not change the selections in the existing file,
22961 // if opening another milti buffer with the references
22962 cx.assert_editor_state(
22963 &r#"fn one() {
22964 let mut a = two();
22965 }
22966
22967 fn «twoˇ»() {}"#
22968 .unindent(),
22969 );
22970 let editors = cx.update_workspace(|workspace, _, cx| {
22971 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22972 });
22973 cx.update_editor(|_, _, test_editor_cx| {
22974 assert_eq!(
22975 editors.len(),
22976 2,
22977 "After falling back to references search, we open a new editor with the results"
22978 );
22979 let references_fallback_text = editors
22980 .into_iter()
22981 .find(|new_editor| *new_editor != test_editor_cx.entity())
22982 .expect("Should have one non-test editor now")
22983 .read(test_editor_cx)
22984 .text(test_editor_cx);
22985 assert_eq!(
22986 references_fallback_text, "fn one() {\n let mut a = two();\n}",
22987 "Should use the range from the references response and not the GoToDefinition one"
22988 );
22989 });
22990}
22991
22992#[gpui::test]
22993async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
22994 init_test(cx, |_| {});
22995 cx.update(|cx| {
22996 let mut editor_settings = EditorSettings::get_global(cx).clone();
22997 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
22998 EditorSettings::override_global(editor_settings, cx);
22999 });
23000 let mut cx = EditorLspTestContext::new_rust(
23001 lsp::ServerCapabilities {
23002 definition_provider: Some(lsp::OneOf::Left(true)),
23003 references_provider: Some(lsp::OneOf::Left(true)),
23004 ..lsp::ServerCapabilities::default()
23005 },
23006 cx,
23007 )
23008 .await;
23009 let original_state = r#"fn one() {
23010 let mut a = ˇtwo();
23011 }
23012
23013 fn two() {}"#
23014 .unindent();
23015 cx.set_state(&original_state);
23016
23017 let mut go_to_definition = cx
23018 .lsp
23019 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
23020 move |_, _| async move { Ok(None) },
23021 );
23022 let _references = cx
23023 .lsp
23024 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
23025 panic!("Should not call for references with no go to definition fallback")
23026 });
23027
23028 let navigated = cx
23029 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
23030 .await
23031 .expect("Failed to navigate to lookup references");
23032 go_to_definition
23033 .next()
23034 .await
23035 .expect("Should have called the go_to_definition handler");
23036
23037 assert_eq!(
23038 navigated,
23039 Navigated::No,
23040 "Should have navigated to references as a fallback after empty GoToDefinition response"
23041 );
23042 cx.assert_editor_state(&original_state);
23043 let editors = cx.update_workspace(|workspace, _, cx| {
23044 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23045 });
23046 cx.update_editor(|_, _, _| {
23047 assert_eq!(
23048 editors.len(),
23049 1,
23050 "After unsuccessful fallback, no other editor should have been opened"
23051 );
23052 });
23053}
23054
23055#[gpui::test]
23056async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
23057 init_test(cx, |_| {});
23058 let mut cx = EditorLspTestContext::new_rust(
23059 lsp::ServerCapabilities {
23060 references_provider: Some(lsp::OneOf::Left(true)),
23061 ..lsp::ServerCapabilities::default()
23062 },
23063 cx,
23064 )
23065 .await;
23066
23067 cx.set_state(
23068 &r#"
23069 fn one() {
23070 let mut a = two();
23071 }
23072
23073 fn ˇtwo() {}"#
23074 .unindent(),
23075 );
23076 cx.lsp
23077 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23078 Ok(Some(vec![
23079 lsp::Location {
23080 uri: params.text_document_position.text_document.uri.clone(),
23081 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
23082 },
23083 lsp::Location {
23084 uri: params.text_document_position.text_document.uri,
23085 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
23086 },
23087 ]))
23088 });
23089 let navigated = cx
23090 .update_editor(|editor, window, cx| {
23091 editor.find_all_references(&FindAllReferences::default(), window, cx)
23092 })
23093 .unwrap()
23094 .await
23095 .expect("Failed to navigate to references");
23096 assert_eq!(
23097 navigated,
23098 Navigated::Yes,
23099 "Should have navigated to references from the FindAllReferences response"
23100 );
23101 cx.assert_editor_state(
23102 &r#"fn one() {
23103 let mut a = two();
23104 }
23105
23106 fn ˇtwo() {}"#
23107 .unindent(),
23108 );
23109
23110 let editors = cx.update_workspace(|workspace, _, cx| {
23111 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23112 });
23113 cx.update_editor(|_, _, _| {
23114 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
23115 });
23116
23117 cx.set_state(
23118 &r#"fn one() {
23119 let mut a = ˇtwo();
23120 }
23121
23122 fn two() {}"#
23123 .unindent(),
23124 );
23125 let navigated = cx
23126 .update_editor(|editor, window, cx| {
23127 editor.find_all_references(&FindAllReferences::default(), window, cx)
23128 })
23129 .unwrap()
23130 .await
23131 .expect("Failed to navigate to references");
23132 assert_eq!(
23133 navigated,
23134 Navigated::Yes,
23135 "Should have navigated to references from the FindAllReferences response"
23136 );
23137 cx.assert_editor_state(
23138 &r#"fn one() {
23139 let mut a = ˇtwo();
23140 }
23141
23142 fn two() {}"#
23143 .unindent(),
23144 );
23145 let editors = cx.update_workspace(|workspace, _, cx| {
23146 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23147 });
23148 cx.update_editor(|_, _, _| {
23149 assert_eq!(
23150 editors.len(),
23151 2,
23152 "should have re-used the previous multibuffer"
23153 );
23154 });
23155
23156 cx.set_state(
23157 &r#"fn one() {
23158 let mut a = ˇtwo();
23159 }
23160 fn three() {}
23161 fn two() {}"#
23162 .unindent(),
23163 );
23164 cx.lsp
23165 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23166 Ok(Some(vec![
23167 lsp::Location {
23168 uri: params.text_document_position.text_document.uri.clone(),
23169 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
23170 },
23171 lsp::Location {
23172 uri: params.text_document_position.text_document.uri,
23173 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
23174 },
23175 ]))
23176 });
23177 let navigated = cx
23178 .update_editor(|editor, window, cx| {
23179 editor.find_all_references(&FindAllReferences::default(), window, cx)
23180 })
23181 .unwrap()
23182 .await
23183 .expect("Failed to navigate to references");
23184 assert_eq!(
23185 navigated,
23186 Navigated::Yes,
23187 "Should have navigated to references from the FindAllReferences response"
23188 );
23189 cx.assert_editor_state(
23190 &r#"fn one() {
23191 let mut a = ˇtwo();
23192 }
23193 fn three() {}
23194 fn two() {}"#
23195 .unindent(),
23196 );
23197 let editors = cx.update_workspace(|workspace, _, cx| {
23198 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23199 });
23200 cx.update_editor(|_, _, _| {
23201 assert_eq!(
23202 editors.len(),
23203 3,
23204 "should have used a new multibuffer as offsets changed"
23205 );
23206 });
23207}
23208#[gpui::test]
23209async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
23210 init_test(cx, |_| {});
23211
23212 let language = Arc::new(Language::new(
23213 LanguageConfig::default(),
23214 Some(tree_sitter_rust::LANGUAGE.into()),
23215 ));
23216
23217 let text = r#"
23218 #[cfg(test)]
23219 mod tests() {
23220 #[test]
23221 fn runnable_1() {
23222 let a = 1;
23223 }
23224
23225 #[test]
23226 fn runnable_2() {
23227 let a = 1;
23228 let b = 2;
23229 }
23230 }
23231 "#
23232 .unindent();
23233
23234 let fs = FakeFs::new(cx.executor());
23235 fs.insert_file("/file.rs", Default::default()).await;
23236
23237 let project = Project::test(fs, ["/a".as_ref()], cx).await;
23238 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23239 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23240 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
23241 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
23242
23243 let editor = cx.new_window_entity(|window, cx| {
23244 Editor::new(
23245 EditorMode::full(),
23246 multi_buffer,
23247 Some(project.clone()),
23248 window,
23249 cx,
23250 )
23251 });
23252
23253 editor.update_in(cx, |editor, window, cx| {
23254 let snapshot = editor.buffer().read(cx).snapshot(cx);
23255 editor.tasks.insert(
23256 (buffer.read(cx).remote_id(), 3),
23257 RunnableTasks {
23258 templates: vec![],
23259 offset: snapshot.anchor_before(MultiBufferOffset(43)),
23260 column: 0,
23261 extra_variables: HashMap::default(),
23262 context_range: BufferOffset(43)..BufferOffset(85),
23263 },
23264 );
23265 editor.tasks.insert(
23266 (buffer.read(cx).remote_id(), 8),
23267 RunnableTasks {
23268 templates: vec![],
23269 offset: snapshot.anchor_before(MultiBufferOffset(86)),
23270 column: 0,
23271 extra_variables: HashMap::default(),
23272 context_range: BufferOffset(86)..BufferOffset(191),
23273 },
23274 );
23275
23276 // Test finding task when cursor is inside function body
23277 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23278 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
23279 });
23280 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23281 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
23282
23283 // Test finding task when cursor is on function name
23284 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23285 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
23286 });
23287 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23288 assert_eq!(row, 8, "Should find task when cursor is on function name");
23289 });
23290}
23291
23292#[gpui::test]
23293async fn test_folding_buffers(cx: &mut TestAppContext) {
23294 init_test(cx, |_| {});
23295
23296 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23297 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
23298 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
23299
23300 let fs = FakeFs::new(cx.executor());
23301 fs.insert_tree(
23302 path!("/a"),
23303 json!({
23304 "first.rs": sample_text_1,
23305 "second.rs": sample_text_2,
23306 "third.rs": sample_text_3,
23307 }),
23308 )
23309 .await;
23310 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23311 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23312 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23313 let worktree = project.update(cx, |project, cx| {
23314 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23315 assert_eq!(worktrees.len(), 1);
23316 worktrees.pop().unwrap()
23317 });
23318 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23319
23320 let buffer_1 = project
23321 .update(cx, |project, cx| {
23322 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23323 })
23324 .await
23325 .unwrap();
23326 let buffer_2 = project
23327 .update(cx, |project, cx| {
23328 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23329 })
23330 .await
23331 .unwrap();
23332 let buffer_3 = project
23333 .update(cx, |project, cx| {
23334 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23335 })
23336 .await
23337 .unwrap();
23338
23339 let multi_buffer = cx.new(|cx| {
23340 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23341 multi_buffer.push_excerpts(
23342 buffer_1.clone(),
23343 [
23344 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23345 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23346 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23347 ],
23348 cx,
23349 );
23350 multi_buffer.push_excerpts(
23351 buffer_2.clone(),
23352 [
23353 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23354 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23355 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23356 ],
23357 cx,
23358 );
23359 multi_buffer.push_excerpts(
23360 buffer_3.clone(),
23361 [
23362 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23363 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23364 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23365 ],
23366 cx,
23367 );
23368 multi_buffer
23369 });
23370 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23371 Editor::new(
23372 EditorMode::full(),
23373 multi_buffer.clone(),
23374 Some(project.clone()),
23375 window,
23376 cx,
23377 )
23378 });
23379
23380 assert_eq!(
23381 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23382 "\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",
23383 );
23384
23385 multi_buffer_editor.update(cx, |editor, cx| {
23386 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23387 });
23388 assert_eq!(
23389 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23390 "\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",
23391 "After folding the first buffer, its text should not be displayed"
23392 );
23393
23394 multi_buffer_editor.update(cx, |editor, cx| {
23395 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23396 });
23397 assert_eq!(
23398 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23399 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
23400 "After folding the second buffer, its text should not be displayed"
23401 );
23402
23403 multi_buffer_editor.update(cx, |editor, cx| {
23404 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23405 });
23406 assert_eq!(
23407 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23408 "\n\n\n\n\n",
23409 "After folding the third buffer, its text should not be displayed"
23410 );
23411
23412 // Emulate selection inside the fold logic, that should work
23413 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23414 editor
23415 .snapshot(window, cx)
23416 .next_line_boundary(Point::new(0, 4));
23417 });
23418
23419 multi_buffer_editor.update(cx, |editor, cx| {
23420 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23421 });
23422 assert_eq!(
23423 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23424 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
23425 "After unfolding the second buffer, its text should be displayed"
23426 );
23427
23428 // Typing inside of buffer 1 causes that buffer to be unfolded.
23429 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23430 assert_eq!(
23431 multi_buffer
23432 .read(cx)
23433 .snapshot(cx)
23434 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
23435 .collect::<String>(),
23436 "bbbb"
23437 );
23438 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23439 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
23440 });
23441 editor.handle_input("B", window, cx);
23442 });
23443
23444 assert_eq!(
23445 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23446 "\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",
23447 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
23448 );
23449
23450 multi_buffer_editor.update(cx, |editor, cx| {
23451 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23452 });
23453 assert_eq!(
23454 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23455 "\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",
23456 "After unfolding the all buffers, all original text should be displayed"
23457 );
23458}
23459
23460#[gpui::test]
23461async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
23462 init_test(cx, |_| {});
23463
23464 let sample_text_1 = "1111\n2222\n3333".to_string();
23465 let sample_text_2 = "4444\n5555\n6666".to_string();
23466 let sample_text_3 = "7777\n8888\n9999".to_string();
23467
23468 let fs = FakeFs::new(cx.executor());
23469 fs.insert_tree(
23470 path!("/a"),
23471 json!({
23472 "first.rs": sample_text_1,
23473 "second.rs": sample_text_2,
23474 "third.rs": sample_text_3,
23475 }),
23476 )
23477 .await;
23478 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23479 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23480 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23481 let worktree = project.update(cx, |project, cx| {
23482 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23483 assert_eq!(worktrees.len(), 1);
23484 worktrees.pop().unwrap()
23485 });
23486 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23487
23488 let buffer_1 = project
23489 .update(cx, |project, cx| {
23490 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23491 })
23492 .await
23493 .unwrap();
23494 let buffer_2 = project
23495 .update(cx, |project, cx| {
23496 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23497 })
23498 .await
23499 .unwrap();
23500 let buffer_3 = project
23501 .update(cx, |project, cx| {
23502 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23503 })
23504 .await
23505 .unwrap();
23506
23507 let multi_buffer = cx.new(|cx| {
23508 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23509 multi_buffer.push_excerpts(
23510 buffer_1.clone(),
23511 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23512 cx,
23513 );
23514 multi_buffer.push_excerpts(
23515 buffer_2.clone(),
23516 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23517 cx,
23518 );
23519 multi_buffer.push_excerpts(
23520 buffer_3.clone(),
23521 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23522 cx,
23523 );
23524 multi_buffer
23525 });
23526
23527 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23528 Editor::new(
23529 EditorMode::full(),
23530 multi_buffer,
23531 Some(project.clone()),
23532 window,
23533 cx,
23534 )
23535 });
23536
23537 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
23538 assert_eq!(
23539 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23540 full_text,
23541 );
23542
23543 multi_buffer_editor.update(cx, |editor, cx| {
23544 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23545 });
23546 assert_eq!(
23547 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23548 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
23549 "After folding the first buffer, its text should not be displayed"
23550 );
23551
23552 multi_buffer_editor.update(cx, |editor, cx| {
23553 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23554 });
23555
23556 assert_eq!(
23557 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23558 "\n\n\n\n\n\n7777\n8888\n9999",
23559 "After folding the second buffer, its text should not be displayed"
23560 );
23561
23562 multi_buffer_editor.update(cx, |editor, cx| {
23563 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23564 });
23565 assert_eq!(
23566 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23567 "\n\n\n\n\n",
23568 "After folding the third buffer, its text should not be displayed"
23569 );
23570
23571 multi_buffer_editor.update(cx, |editor, cx| {
23572 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23573 });
23574 assert_eq!(
23575 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23576 "\n\n\n\n4444\n5555\n6666\n\n",
23577 "After unfolding the second buffer, its text should be displayed"
23578 );
23579
23580 multi_buffer_editor.update(cx, |editor, cx| {
23581 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
23582 });
23583 assert_eq!(
23584 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23585 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
23586 "After unfolding the first buffer, its text should be displayed"
23587 );
23588
23589 multi_buffer_editor.update(cx, |editor, cx| {
23590 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23591 });
23592 assert_eq!(
23593 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23594 full_text,
23595 "After unfolding all buffers, all original text should be displayed"
23596 );
23597}
23598
23599#[gpui::test]
23600async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
23601 init_test(cx, |_| {});
23602
23603 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23604
23605 let fs = FakeFs::new(cx.executor());
23606 fs.insert_tree(
23607 path!("/a"),
23608 json!({
23609 "main.rs": sample_text,
23610 }),
23611 )
23612 .await;
23613 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23614 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23615 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23616 let worktree = project.update(cx, |project, cx| {
23617 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23618 assert_eq!(worktrees.len(), 1);
23619 worktrees.pop().unwrap()
23620 });
23621 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23622
23623 let buffer_1 = project
23624 .update(cx, |project, cx| {
23625 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23626 })
23627 .await
23628 .unwrap();
23629
23630 let multi_buffer = cx.new(|cx| {
23631 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23632 multi_buffer.push_excerpts(
23633 buffer_1.clone(),
23634 [ExcerptRange::new(
23635 Point::new(0, 0)
23636 ..Point::new(
23637 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
23638 0,
23639 ),
23640 )],
23641 cx,
23642 );
23643 multi_buffer
23644 });
23645 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23646 Editor::new(
23647 EditorMode::full(),
23648 multi_buffer,
23649 Some(project.clone()),
23650 window,
23651 cx,
23652 )
23653 });
23654
23655 let selection_range = Point::new(1, 0)..Point::new(2, 0);
23656 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23657 enum TestHighlight {}
23658 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
23659 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
23660 editor.highlight_text::<TestHighlight>(
23661 vec![highlight_range.clone()],
23662 HighlightStyle::color(Hsla::green()),
23663 cx,
23664 );
23665 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23666 s.select_ranges(Some(highlight_range))
23667 });
23668 });
23669
23670 let full_text = format!("\n\n{sample_text}");
23671 assert_eq!(
23672 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23673 full_text,
23674 );
23675}
23676
23677#[gpui::test]
23678async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
23679 init_test(cx, |_| {});
23680 cx.update(|cx| {
23681 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
23682 "keymaps/default-linux.json",
23683 cx,
23684 )
23685 .unwrap();
23686 cx.bind_keys(default_key_bindings);
23687 });
23688
23689 let (editor, cx) = cx.add_window_view(|window, cx| {
23690 let multi_buffer = MultiBuffer::build_multi(
23691 [
23692 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
23693 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
23694 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
23695 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
23696 ],
23697 cx,
23698 );
23699 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
23700
23701 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
23702 // fold all but the second buffer, so that we test navigating between two
23703 // adjacent folded buffers, as well as folded buffers at the start and
23704 // end the multibuffer
23705 editor.fold_buffer(buffer_ids[0], cx);
23706 editor.fold_buffer(buffer_ids[2], cx);
23707 editor.fold_buffer(buffer_ids[3], cx);
23708
23709 editor
23710 });
23711 cx.simulate_resize(size(px(1000.), px(1000.)));
23712
23713 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
23714 cx.assert_excerpts_with_selections(indoc! {"
23715 [EXCERPT]
23716 ˇ[FOLDED]
23717 [EXCERPT]
23718 a1
23719 b1
23720 [EXCERPT]
23721 [FOLDED]
23722 [EXCERPT]
23723 [FOLDED]
23724 "
23725 });
23726 cx.simulate_keystroke("down");
23727 cx.assert_excerpts_with_selections(indoc! {"
23728 [EXCERPT]
23729 [FOLDED]
23730 [EXCERPT]
23731 ˇa1
23732 b1
23733 [EXCERPT]
23734 [FOLDED]
23735 [EXCERPT]
23736 [FOLDED]
23737 "
23738 });
23739 cx.simulate_keystroke("down");
23740 cx.assert_excerpts_with_selections(indoc! {"
23741 [EXCERPT]
23742 [FOLDED]
23743 [EXCERPT]
23744 a1
23745 ˇb1
23746 [EXCERPT]
23747 [FOLDED]
23748 [EXCERPT]
23749 [FOLDED]
23750 "
23751 });
23752 cx.simulate_keystroke("down");
23753 cx.assert_excerpts_with_selections(indoc! {"
23754 [EXCERPT]
23755 [FOLDED]
23756 [EXCERPT]
23757 a1
23758 b1
23759 ˇ[EXCERPT]
23760 [FOLDED]
23761 [EXCERPT]
23762 [FOLDED]
23763 "
23764 });
23765 cx.simulate_keystroke("down");
23766 cx.assert_excerpts_with_selections(indoc! {"
23767 [EXCERPT]
23768 [FOLDED]
23769 [EXCERPT]
23770 a1
23771 b1
23772 [EXCERPT]
23773 ˇ[FOLDED]
23774 [EXCERPT]
23775 [FOLDED]
23776 "
23777 });
23778 for _ in 0..5 {
23779 cx.simulate_keystroke("down");
23780 cx.assert_excerpts_with_selections(indoc! {"
23781 [EXCERPT]
23782 [FOLDED]
23783 [EXCERPT]
23784 a1
23785 b1
23786 [EXCERPT]
23787 [FOLDED]
23788 [EXCERPT]
23789 ˇ[FOLDED]
23790 "
23791 });
23792 }
23793
23794 cx.simulate_keystroke("up");
23795 cx.assert_excerpts_with_selections(indoc! {"
23796 [EXCERPT]
23797 [FOLDED]
23798 [EXCERPT]
23799 a1
23800 b1
23801 [EXCERPT]
23802 ˇ[FOLDED]
23803 [EXCERPT]
23804 [FOLDED]
23805 "
23806 });
23807 cx.simulate_keystroke("up");
23808 cx.assert_excerpts_with_selections(indoc! {"
23809 [EXCERPT]
23810 [FOLDED]
23811 [EXCERPT]
23812 a1
23813 b1
23814 ˇ[EXCERPT]
23815 [FOLDED]
23816 [EXCERPT]
23817 [FOLDED]
23818 "
23819 });
23820 cx.simulate_keystroke("up");
23821 cx.assert_excerpts_with_selections(indoc! {"
23822 [EXCERPT]
23823 [FOLDED]
23824 [EXCERPT]
23825 a1
23826 ˇb1
23827 [EXCERPT]
23828 [FOLDED]
23829 [EXCERPT]
23830 [FOLDED]
23831 "
23832 });
23833 cx.simulate_keystroke("up");
23834 cx.assert_excerpts_with_selections(indoc! {"
23835 [EXCERPT]
23836 [FOLDED]
23837 [EXCERPT]
23838 ˇa1
23839 b1
23840 [EXCERPT]
23841 [FOLDED]
23842 [EXCERPT]
23843 [FOLDED]
23844 "
23845 });
23846 for _ in 0..5 {
23847 cx.simulate_keystroke("up");
23848 cx.assert_excerpts_with_selections(indoc! {"
23849 [EXCERPT]
23850 ˇ[FOLDED]
23851 [EXCERPT]
23852 a1
23853 b1
23854 [EXCERPT]
23855 [FOLDED]
23856 [EXCERPT]
23857 [FOLDED]
23858 "
23859 });
23860 }
23861}
23862
23863#[gpui::test]
23864async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23865 init_test(cx, |_| {});
23866
23867 // Simple insertion
23868 assert_highlighted_edits(
23869 "Hello, world!",
23870 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23871 true,
23872 cx,
23873 |highlighted_edits, cx| {
23874 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23875 assert_eq!(highlighted_edits.highlights.len(), 1);
23876 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23877 assert_eq!(
23878 highlighted_edits.highlights[0].1.background_color,
23879 Some(cx.theme().status().created_background)
23880 );
23881 },
23882 )
23883 .await;
23884
23885 // Replacement
23886 assert_highlighted_edits(
23887 "This is a test.",
23888 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23889 false,
23890 cx,
23891 |highlighted_edits, cx| {
23892 assert_eq!(highlighted_edits.text, "That is a test.");
23893 assert_eq!(highlighted_edits.highlights.len(), 1);
23894 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23895 assert_eq!(
23896 highlighted_edits.highlights[0].1.background_color,
23897 Some(cx.theme().status().created_background)
23898 );
23899 },
23900 )
23901 .await;
23902
23903 // Multiple edits
23904 assert_highlighted_edits(
23905 "Hello, world!",
23906 vec![
23907 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23908 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23909 ],
23910 false,
23911 cx,
23912 |highlighted_edits, cx| {
23913 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23914 assert_eq!(highlighted_edits.highlights.len(), 2);
23915 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23916 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23917 assert_eq!(
23918 highlighted_edits.highlights[0].1.background_color,
23919 Some(cx.theme().status().created_background)
23920 );
23921 assert_eq!(
23922 highlighted_edits.highlights[1].1.background_color,
23923 Some(cx.theme().status().created_background)
23924 );
23925 },
23926 )
23927 .await;
23928
23929 // Multiple lines with edits
23930 assert_highlighted_edits(
23931 "First line\nSecond line\nThird line\nFourth line",
23932 vec![
23933 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
23934 (
23935 Point::new(2, 0)..Point::new(2, 10),
23936 "New third line".to_string(),
23937 ),
23938 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
23939 ],
23940 false,
23941 cx,
23942 |highlighted_edits, cx| {
23943 assert_eq!(
23944 highlighted_edits.text,
23945 "Second modified\nNew third line\nFourth updated line"
23946 );
23947 assert_eq!(highlighted_edits.highlights.len(), 3);
23948 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
23949 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
23950 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
23951 for highlight in &highlighted_edits.highlights {
23952 assert_eq!(
23953 highlight.1.background_color,
23954 Some(cx.theme().status().created_background)
23955 );
23956 }
23957 },
23958 )
23959 .await;
23960}
23961
23962#[gpui::test]
23963async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
23964 init_test(cx, |_| {});
23965
23966 // Deletion
23967 assert_highlighted_edits(
23968 "Hello, world!",
23969 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
23970 true,
23971 cx,
23972 |highlighted_edits, cx| {
23973 assert_eq!(highlighted_edits.text, "Hello, world!");
23974 assert_eq!(highlighted_edits.highlights.len(), 1);
23975 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
23976 assert_eq!(
23977 highlighted_edits.highlights[0].1.background_color,
23978 Some(cx.theme().status().deleted_background)
23979 );
23980 },
23981 )
23982 .await;
23983
23984 // Insertion
23985 assert_highlighted_edits(
23986 "Hello, world!",
23987 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
23988 true,
23989 cx,
23990 |highlighted_edits, cx| {
23991 assert_eq!(highlighted_edits.highlights.len(), 1);
23992 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
23993 assert_eq!(
23994 highlighted_edits.highlights[0].1.background_color,
23995 Some(cx.theme().status().created_background)
23996 );
23997 },
23998 )
23999 .await;
24000}
24001
24002async fn assert_highlighted_edits(
24003 text: &str,
24004 edits: Vec<(Range<Point>, String)>,
24005 include_deletions: bool,
24006 cx: &mut TestAppContext,
24007 assertion_fn: impl Fn(HighlightedText, &App),
24008) {
24009 let window = cx.add_window(|window, cx| {
24010 let buffer = MultiBuffer::build_simple(text, cx);
24011 Editor::new(EditorMode::full(), buffer, None, window, cx)
24012 });
24013 let cx = &mut VisualTestContext::from_window(*window, cx);
24014
24015 let (buffer, snapshot) = window
24016 .update(cx, |editor, _window, cx| {
24017 (
24018 editor.buffer().clone(),
24019 editor.buffer().read(cx).snapshot(cx),
24020 )
24021 })
24022 .unwrap();
24023
24024 let edits = edits
24025 .into_iter()
24026 .map(|(range, edit)| {
24027 (
24028 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
24029 edit,
24030 )
24031 })
24032 .collect::<Vec<_>>();
24033
24034 let text_anchor_edits = edits
24035 .clone()
24036 .into_iter()
24037 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
24038 .collect::<Vec<_>>();
24039
24040 let edit_preview = window
24041 .update(cx, |_, _window, cx| {
24042 buffer
24043 .read(cx)
24044 .as_singleton()
24045 .unwrap()
24046 .read(cx)
24047 .preview_edits(text_anchor_edits.into(), cx)
24048 })
24049 .unwrap()
24050 .await;
24051
24052 cx.update(|_window, cx| {
24053 let highlighted_edits = edit_prediction_edit_text(
24054 snapshot.as_singleton().unwrap().2,
24055 &edits,
24056 &edit_preview,
24057 include_deletions,
24058 cx,
24059 );
24060 assertion_fn(highlighted_edits, cx)
24061 });
24062}
24063
24064#[track_caller]
24065fn assert_breakpoint(
24066 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
24067 path: &Arc<Path>,
24068 expected: Vec<(u32, Breakpoint)>,
24069) {
24070 if expected.is_empty() {
24071 assert!(!breakpoints.contains_key(path), "{}", path.display());
24072 } else {
24073 let mut breakpoint = breakpoints
24074 .get(path)
24075 .unwrap()
24076 .iter()
24077 .map(|breakpoint| {
24078 (
24079 breakpoint.row,
24080 Breakpoint {
24081 message: breakpoint.message.clone(),
24082 state: breakpoint.state,
24083 condition: breakpoint.condition.clone(),
24084 hit_condition: breakpoint.hit_condition.clone(),
24085 },
24086 )
24087 })
24088 .collect::<Vec<_>>();
24089
24090 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
24091
24092 assert_eq!(expected, breakpoint);
24093 }
24094}
24095
24096fn add_log_breakpoint_at_cursor(
24097 editor: &mut Editor,
24098 log_message: &str,
24099 window: &mut Window,
24100 cx: &mut Context<Editor>,
24101) {
24102 let (anchor, bp) = editor
24103 .breakpoints_at_cursors(window, cx)
24104 .first()
24105 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
24106 .unwrap_or_else(|| {
24107 let snapshot = editor.snapshot(window, cx);
24108 let cursor_position: Point =
24109 editor.selections.newest(&snapshot.display_snapshot).head();
24110
24111 let breakpoint_position = snapshot
24112 .buffer_snapshot()
24113 .anchor_before(Point::new(cursor_position.row, 0));
24114
24115 (breakpoint_position, Breakpoint::new_log(log_message))
24116 });
24117
24118 editor.edit_breakpoint_at_anchor(
24119 anchor,
24120 bp,
24121 BreakpointEditAction::EditLogMessage(log_message.into()),
24122 cx,
24123 );
24124}
24125
24126#[gpui::test]
24127async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
24128 init_test(cx, |_| {});
24129
24130 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24131 let fs = FakeFs::new(cx.executor());
24132 fs.insert_tree(
24133 path!("/a"),
24134 json!({
24135 "main.rs": sample_text,
24136 }),
24137 )
24138 .await;
24139 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24140 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24141 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24142
24143 let fs = FakeFs::new(cx.executor());
24144 fs.insert_tree(
24145 path!("/a"),
24146 json!({
24147 "main.rs": sample_text,
24148 }),
24149 )
24150 .await;
24151 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24152 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24153 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24154 let worktree_id = workspace
24155 .update(cx, |workspace, _window, cx| {
24156 workspace.project().update(cx, |project, cx| {
24157 project.worktrees(cx).next().unwrap().read(cx).id()
24158 })
24159 })
24160 .unwrap();
24161
24162 let buffer = project
24163 .update(cx, |project, cx| {
24164 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24165 })
24166 .await
24167 .unwrap();
24168
24169 let (editor, cx) = cx.add_window_view(|window, cx| {
24170 Editor::new(
24171 EditorMode::full(),
24172 MultiBuffer::build_from_buffer(buffer, cx),
24173 Some(project.clone()),
24174 window,
24175 cx,
24176 )
24177 });
24178
24179 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24180 let abs_path = project.read_with(cx, |project, cx| {
24181 project
24182 .absolute_path(&project_path, cx)
24183 .map(Arc::from)
24184 .unwrap()
24185 });
24186
24187 // assert we can add breakpoint on the first line
24188 editor.update_in(cx, |editor, window, cx| {
24189 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24190 editor.move_to_end(&MoveToEnd, window, cx);
24191 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24192 });
24193
24194 let breakpoints = editor.update(cx, |editor, cx| {
24195 editor
24196 .breakpoint_store()
24197 .as_ref()
24198 .unwrap()
24199 .read(cx)
24200 .all_source_breakpoints(cx)
24201 });
24202
24203 assert_eq!(1, breakpoints.len());
24204 assert_breakpoint(
24205 &breakpoints,
24206 &abs_path,
24207 vec![
24208 (0, Breakpoint::new_standard()),
24209 (3, Breakpoint::new_standard()),
24210 ],
24211 );
24212
24213 editor.update_in(cx, |editor, window, cx| {
24214 editor.move_to_beginning(&MoveToBeginning, window, cx);
24215 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24216 });
24217
24218 let breakpoints = editor.update(cx, |editor, cx| {
24219 editor
24220 .breakpoint_store()
24221 .as_ref()
24222 .unwrap()
24223 .read(cx)
24224 .all_source_breakpoints(cx)
24225 });
24226
24227 assert_eq!(1, breakpoints.len());
24228 assert_breakpoint(
24229 &breakpoints,
24230 &abs_path,
24231 vec![(3, Breakpoint::new_standard())],
24232 );
24233
24234 editor.update_in(cx, |editor, window, cx| {
24235 editor.move_to_end(&MoveToEnd, window, cx);
24236 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24237 });
24238
24239 let breakpoints = editor.update(cx, |editor, cx| {
24240 editor
24241 .breakpoint_store()
24242 .as_ref()
24243 .unwrap()
24244 .read(cx)
24245 .all_source_breakpoints(cx)
24246 });
24247
24248 assert_eq!(0, breakpoints.len());
24249 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24250}
24251
24252#[gpui::test]
24253async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
24254 init_test(cx, |_| {});
24255
24256 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24257
24258 let fs = FakeFs::new(cx.executor());
24259 fs.insert_tree(
24260 path!("/a"),
24261 json!({
24262 "main.rs": sample_text,
24263 }),
24264 )
24265 .await;
24266 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24267 let (workspace, cx) =
24268 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24269
24270 let worktree_id = workspace.update(cx, |workspace, cx| {
24271 workspace.project().update(cx, |project, cx| {
24272 project.worktrees(cx).next().unwrap().read(cx).id()
24273 })
24274 });
24275
24276 let buffer = project
24277 .update(cx, |project, cx| {
24278 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24279 })
24280 .await
24281 .unwrap();
24282
24283 let (editor, cx) = cx.add_window_view(|window, cx| {
24284 Editor::new(
24285 EditorMode::full(),
24286 MultiBuffer::build_from_buffer(buffer, cx),
24287 Some(project.clone()),
24288 window,
24289 cx,
24290 )
24291 });
24292
24293 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24294 let abs_path = project.read_with(cx, |project, cx| {
24295 project
24296 .absolute_path(&project_path, cx)
24297 .map(Arc::from)
24298 .unwrap()
24299 });
24300
24301 editor.update_in(cx, |editor, window, cx| {
24302 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24303 });
24304
24305 let breakpoints = editor.update(cx, |editor, cx| {
24306 editor
24307 .breakpoint_store()
24308 .as_ref()
24309 .unwrap()
24310 .read(cx)
24311 .all_source_breakpoints(cx)
24312 });
24313
24314 assert_breakpoint(
24315 &breakpoints,
24316 &abs_path,
24317 vec![(0, Breakpoint::new_log("hello world"))],
24318 );
24319
24320 // Removing a log message from a log breakpoint should remove it
24321 editor.update_in(cx, |editor, window, cx| {
24322 add_log_breakpoint_at_cursor(editor, "", window, cx);
24323 });
24324
24325 let breakpoints = editor.update(cx, |editor, cx| {
24326 editor
24327 .breakpoint_store()
24328 .as_ref()
24329 .unwrap()
24330 .read(cx)
24331 .all_source_breakpoints(cx)
24332 });
24333
24334 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24335
24336 editor.update_in(cx, |editor, window, cx| {
24337 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24338 editor.move_to_end(&MoveToEnd, window, cx);
24339 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24340 // Not adding a log message to a standard breakpoint shouldn't remove it
24341 add_log_breakpoint_at_cursor(editor, "", window, cx);
24342 });
24343
24344 let breakpoints = editor.update(cx, |editor, cx| {
24345 editor
24346 .breakpoint_store()
24347 .as_ref()
24348 .unwrap()
24349 .read(cx)
24350 .all_source_breakpoints(cx)
24351 });
24352
24353 assert_breakpoint(
24354 &breakpoints,
24355 &abs_path,
24356 vec![
24357 (0, Breakpoint::new_standard()),
24358 (3, Breakpoint::new_standard()),
24359 ],
24360 );
24361
24362 editor.update_in(cx, |editor, window, cx| {
24363 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24364 });
24365
24366 let breakpoints = editor.update(cx, |editor, cx| {
24367 editor
24368 .breakpoint_store()
24369 .as_ref()
24370 .unwrap()
24371 .read(cx)
24372 .all_source_breakpoints(cx)
24373 });
24374
24375 assert_breakpoint(
24376 &breakpoints,
24377 &abs_path,
24378 vec![
24379 (0, Breakpoint::new_standard()),
24380 (3, Breakpoint::new_log("hello world")),
24381 ],
24382 );
24383
24384 editor.update_in(cx, |editor, window, cx| {
24385 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
24386 });
24387
24388 let breakpoints = editor.update(cx, |editor, cx| {
24389 editor
24390 .breakpoint_store()
24391 .as_ref()
24392 .unwrap()
24393 .read(cx)
24394 .all_source_breakpoints(cx)
24395 });
24396
24397 assert_breakpoint(
24398 &breakpoints,
24399 &abs_path,
24400 vec![
24401 (0, Breakpoint::new_standard()),
24402 (3, Breakpoint::new_log("hello Earth!!")),
24403 ],
24404 );
24405}
24406
24407/// This also tests that Editor::breakpoint_at_cursor_head is working properly
24408/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
24409/// or when breakpoints were placed out of order. This tests for a regression too
24410#[gpui::test]
24411async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
24412 init_test(cx, |_| {});
24413
24414 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24415 let fs = FakeFs::new(cx.executor());
24416 fs.insert_tree(
24417 path!("/a"),
24418 json!({
24419 "main.rs": sample_text,
24420 }),
24421 )
24422 .await;
24423 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24424 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24425 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24426
24427 let fs = FakeFs::new(cx.executor());
24428 fs.insert_tree(
24429 path!("/a"),
24430 json!({
24431 "main.rs": sample_text,
24432 }),
24433 )
24434 .await;
24435 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24436 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24437 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24438 let worktree_id = workspace
24439 .update(cx, |workspace, _window, cx| {
24440 workspace.project().update(cx, |project, cx| {
24441 project.worktrees(cx).next().unwrap().read(cx).id()
24442 })
24443 })
24444 .unwrap();
24445
24446 let buffer = project
24447 .update(cx, |project, cx| {
24448 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24449 })
24450 .await
24451 .unwrap();
24452
24453 let (editor, cx) = cx.add_window_view(|window, cx| {
24454 Editor::new(
24455 EditorMode::full(),
24456 MultiBuffer::build_from_buffer(buffer, cx),
24457 Some(project.clone()),
24458 window,
24459 cx,
24460 )
24461 });
24462
24463 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24464 let abs_path = project.read_with(cx, |project, cx| {
24465 project
24466 .absolute_path(&project_path, cx)
24467 .map(Arc::from)
24468 .unwrap()
24469 });
24470
24471 // assert we can add breakpoint on the first line
24472 editor.update_in(cx, |editor, window, cx| {
24473 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24474 editor.move_to_end(&MoveToEnd, window, cx);
24475 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24476 editor.move_up(&MoveUp, window, cx);
24477 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24478 });
24479
24480 let breakpoints = editor.update(cx, |editor, cx| {
24481 editor
24482 .breakpoint_store()
24483 .as_ref()
24484 .unwrap()
24485 .read(cx)
24486 .all_source_breakpoints(cx)
24487 });
24488
24489 assert_eq!(1, breakpoints.len());
24490 assert_breakpoint(
24491 &breakpoints,
24492 &abs_path,
24493 vec![
24494 (0, Breakpoint::new_standard()),
24495 (2, Breakpoint::new_standard()),
24496 (3, Breakpoint::new_standard()),
24497 ],
24498 );
24499
24500 editor.update_in(cx, |editor, window, cx| {
24501 editor.move_to_beginning(&MoveToBeginning, window, cx);
24502 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24503 editor.move_to_end(&MoveToEnd, window, cx);
24504 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24505 // Disabling a breakpoint that doesn't exist should do nothing
24506 editor.move_up(&MoveUp, window, cx);
24507 editor.move_up(&MoveUp, window, cx);
24508 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24509 });
24510
24511 let breakpoints = editor.update(cx, |editor, cx| {
24512 editor
24513 .breakpoint_store()
24514 .as_ref()
24515 .unwrap()
24516 .read(cx)
24517 .all_source_breakpoints(cx)
24518 });
24519
24520 let disable_breakpoint = {
24521 let mut bp = Breakpoint::new_standard();
24522 bp.state = BreakpointState::Disabled;
24523 bp
24524 };
24525
24526 assert_eq!(1, breakpoints.len());
24527 assert_breakpoint(
24528 &breakpoints,
24529 &abs_path,
24530 vec![
24531 (0, disable_breakpoint.clone()),
24532 (2, Breakpoint::new_standard()),
24533 (3, disable_breakpoint.clone()),
24534 ],
24535 );
24536
24537 editor.update_in(cx, |editor, window, cx| {
24538 editor.move_to_beginning(&MoveToBeginning, window, cx);
24539 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24540 editor.move_to_end(&MoveToEnd, window, cx);
24541 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24542 editor.move_up(&MoveUp, window, cx);
24543 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24544 });
24545
24546 let breakpoints = editor.update(cx, |editor, cx| {
24547 editor
24548 .breakpoint_store()
24549 .as_ref()
24550 .unwrap()
24551 .read(cx)
24552 .all_source_breakpoints(cx)
24553 });
24554
24555 assert_eq!(1, breakpoints.len());
24556 assert_breakpoint(
24557 &breakpoints,
24558 &abs_path,
24559 vec![
24560 (0, Breakpoint::new_standard()),
24561 (2, disable_breakpoint),
24562 (3, Breakpoint::new_standard()),
24563 ],
24564 );
24565}
24566
24567#[gpui::test]
24568async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
24569 init_test(cx, |_| {});
24570 let capabilities = lsp::ServerCapabilities {
24571 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
24572 prepare_provider: Some(true),
24573 work_done_progress_options: Default::default(),
24574 })),
24575 ..Default::default()
24576 };
24577 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24578
24579 cx.set_state(indoc! {"
24580 struct Fˇoo {}
24581 "});
24582
24583 cx.update_editor(|editor, _, cx| {
24584 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24585 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24586 editor.highlight_background::<DocumentHighlightRead>(
24587 &[highlight_range],
24588 |_, theme| theme.colors().editor_document_highlight_read_background,
24589 cx,
24590 );
24591 });
24592
24593 let mut prepare_rename_handler = cx
24594 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
24595 move |_, _, _| async move {
24596 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
24597 start: lsp::Position {
24598 line: 0,
24599 character: 7,
24600 },
24601 end: lsp::Position {
24602 line: 0,
24603 character: 10,
24604 },
24605 })))
24606 },
24607 );
24608 let prepare_rename_task = cx
24609 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24610 .expect("Prepare rename was not started");
24611 prepare_rename_handler.next().await.unwrap();
24612 prepare_rename_task.await.expect("Prepare rename failed");
24613
24614 let mut rename_handler =
24615 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24616 let edit = lsp::TextEdit {
24617 range: lsp::Range {
24618 start: lsp::Position {
24619 line: 0,
24620 character: 7,
24621 },
24622 end: lsp::Position {
24623 line: 0,
24624 character: 10,
24625 },
24626 },
24627 new_text: "FooRenamed".to_string(),
24628 };
24629 Ok(Some(lsp::WorkspaceEdit::new(
24630 // Specify the same edit twice
24631 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
24632 )))
24633 });
24634 let rename_task = cx
24635 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24636 .expect("Confirm rename was not started");
24637 rename_handler.next().await.unwrap();
24638 rename_task.await.expect("Confirm rename failed");
24639 cx.run_until_parked();
24640
24641 // Despite two edits, only one is actually applied as those are identical
24642 cx.assert_editor_state(indoc! {"
24643 struct FooRenamedˇ {}
24644 "});
24645}
24646
24647#[gpui::test]
24648async fn test_rename_without_prepare(cx: &mut TestAppContext) {
24649 init_test(cx, |_| {});
24650 // These capabilities indicate that the server does not support prepare rename.
24651 let capabilities = lsp::ServerCapabilities {
24652 rename_provider: Some(lsp::OneOf::Left(true)),
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, _window, 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 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24672 .expect("Prepare rename was not started")
24673 .await
24674 .expect("Prepare rename failed");
24675
24676 let mut rename_handler =
24677 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24678 let edit = lsp::TextEdit {
24679 range: lsp::Range {
24680 start: lsp::Position {
24681 line: 0,
24682 character: 7,
24683 },
24684 end: lsp::Position {
24685 line: 0,
24686 character: 10,
24687 },
24688 },
24689 new_text: "FooRenamed".to_string(),
24690 };
24691 Ok(Some(lsp::WorkspaceEdit::new(
24692 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
24693 )))
24694 });
24695 let rename_task = cx
24696 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24697 .expect("Confirm rename was not started");
24698 rename_handler.next().await.unwrap();
24699 rename_task.await.expect("Confirm rename failed");
24700 cx.run_until_parked();
24701
24702 // Correct range is renamed, as `surrounding_word` is used to find it.
24703 cx.assert_editor_state(indoc! {"
24704 struct FooRenamedˇ {}
24705 "});
24706}
24707
24708#[gpui::test]
24709async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
24710 init_test(cx, |_| {});
24711 let mut cx = EditorTestContext::new(cx).await;
24712
24713 let language = Arc::new(
24714 Language::new(
24715 LanguageConfig::default(),
24716 Some(tree_sitter_html::LANGUAGE.into()),
24717 )
24718 .with_brackets_query(
24719 r#"
24720 ("<" @open "/>" @close)
24721 ("</" @open ">" @close)
24722 ("<" @open ">" @close)
24723 ("\"" @open "\"" @close)
24724 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
24725 "#,
24726 )
24727 .unwrap(),
24728 );
24729 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24730
24731 cx.set_state(indoc! {"
24732 <span>ˇ</span>
24733 "});
24734 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24735 cx.assert_editor_state(indoc! {"
24736 <span>
24737 ˇ
24738 </span>
24739 "});
24740
24741 cx.set_state(indoc! {"
24742 <span><span></span>ˇ</span>
24743 "});
24744 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24745 cx.assert_editor_state(indoc! {"
24746 <span><span></span>
24747 ˇ</span>
24748 "});
24749
24750 cx.set_state(indoc! {"
24751 <span>ˇ
24752 </span>
24753 "});
24754 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24755 cx.assert_editor_state(indoc! {"
24756 <span>
24757 ˇ
24758 </span>
24759 "});
24760}
24761
24762#[gpui::test(iterations = 10)]
24763async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
24764 init_test(cx, |_| {});
24765
24766 let fs = FakeFs::new(cx.executor());
24767 fs.insert_tree(
24768 path!("/dir"),
24769 json!({
24770 "a.ts": "a",
24771 }),
24772 )
24773 .await;
24774
24775 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
24776 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24777 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24778
24779 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24780 language_registry.add(Arc::new(Language::new(
24781 LanguageConfig {
24782 name: "TypeScript".into(),
24783 matcher: LanguageMatcher {
24784 path_suffixes: vec!["ts".to_string()],
24785 ..Default::default()
24786 },
24787 ..Default::default()
24788 },
24789 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24790 )));
24791 let mut fake_language_servers = language_registry.register_fake_lsp(
24792 "TypeScript",
24793 FakeLspAdapter {
24794 capabilities: lsp::ServerCapabilities {
24795 code_lens_provider: Some(lsp::CodeLensOptions {
24796 resolve_provider: Some(true),
24797 }),
24798 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24799 commands: vec!["_the/command".to_string()],
24800 ..lsp::ExecuteCommandOptions::default()
24801 }),
24802 ..lsp::ServerCapabilities::default()
24803 },
24804 ..FakeLspAdapter::default()
24805 },
24806 );
24807
24808 let editor = workspace
24809 .update(cx, |workspace, window, cx| {
24810 workspace.open_abs_path(
24811 PathBuf::from(path!("/dir/a.ts")),
24812 OpenOptions::default(),
24813 window,
24814 cx,
24815 )
24816 })
24817 .unwrap()
24818 .await
24819 .unwrap()
24820 .downcast::<Editor>()
24821 .unwrap();
24822 cx.executor().run_until_parked();
24823
24824 let fake_server = fake_language_servers.next().await.unwrap();
24825
24826 let buffer = editor.update(cx, |editor, cx| {
24827 editor
24828 .buffer()
24829 .read(cx)
24830 .as_singleton()
24831 .expect("have opened a single file by path")
24832 });
24833
24834 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24835 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24836 drop(buffer_snapshot);
24837 let actions = cx
24838 .update_window(*workspace, |_, window, cx| {
24839 project.code_actions(&buffer, anchor..anchor, window, cx)
24840 })
24841 .unwrap();
24842
24843 fake_server
24844 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24845 Ok(Some(vec![
24846 lsp::CodeLens {
24847 range: lsp::Range::default(),
24848 command: Some(lsp::Command {
24849 title: "Code lens command".to_owned(),
24850 command: "_the/command".to_owned(),
24851 arguments: None,
24852 }),
24853 data: None,
24854 },
24855 lsp::CodeLens {
24856 range: lsp::Range::default(),
24857 command: Some(lsp::Command {
24858 title: "Command not in capabilities".to_owned(),
24859 command: "not in capabilities".to_owned(),
24860 arguments: None,
24861 }),
24862 data: None,
24863 },
24864 lsp::CodeLens {
24865 range: lsp::Range {
24866 start: lsp::Position {
24867 line: 1,
24868 character: 1,
24869 },
24870 end: lsp::Position {
24871 line: 1,
24872 character: 1,
24873 },
24874 },
24875 command: Some(lsp::Command {
24876 title: "Command not in range".to_owned(),
24877 command: "_the/command".to_owned(),
24878 arguments: None,
24879 }),
24880 data: None,
24881 },
24882 ]))
24883 })
24884 .next()
24885 .await;
24886
24887 let actions = actions.await.unwrap();
24888 assert_eq!(
24889 actions.len(),
24890 1,
24891 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24892 );
24893 let action = actions[0].clone();
24894 let apply = project.update(cx, |project, cx| {
24895 project.apply_code_action(buffer.clone(), action, true, cx)
24896 });
24897
24898 // Resolving the code action does not populate its edits. In absence of
24899 // edits, we must execute the given command.
24900 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24901 |mut lens, _| async move {
24902 let lens_command = lens.command.as_mut().expect("should have a command");
24903 assert_eq!(lens_command.title, "Code lens command");
24904 lens_command.arguments = Some(vec![json!("the-argument")]);
24905 Ok(lens)
24906 },
24907 );
24908
24909 // While executing the command, the language server sends the editor
24910 // a `workspaceEdit` request.
24911 fake_server
24912 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24913 let fake = fake_server.clone();
24914 move |params, _| {
24915 assert_eq!(params.command, "_the/command");
24916 let fake = fake.clone();
24917 async move {
24918 fake.server
24919 .request::<lsp::request::ApplyWorkspaceEdit>(
24920 lsp::ApplyWorkspaceEditParams {
24921 label: None,
24922 edit: lsp::WorkspaceEdit {
24923 changes: Some(
24924 [(
24925 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
24926 vec![lsp::TextEdit {
24927 range: lsp::Range::new(
24928 lsp::Position::new(0, 0),
24929 lsp::Position::new(0, 0),
24930 ),
24931 new_text: "X".into(),
24932 }],
24933 )]
24934 .into_iter()
24935 .collect(),
24936 ),
24937 ..lsp::WorkspaceEdit::default()
24938 },
24939 },
24940 )
24941 .await
24942 .into_response()
24943 .unwrap();
24944 Ok(Some(json!(null)))
24945 }
24946 }
24947 })
24948 .next()
24949 .await;
24950
24951 // Applying the code lens command returns a project transaction containing the edits
24952 // sent by the language server in its `workspaceEdit` request.
24953 let transaction = apply.await.unwrap();
24954 assert!(transaction.0.contains_key(&buffer));
24955 buffer.update(cx, |buffer, cx| {
24956 assert_eq!(buffer.text(), "Xa");
24957 buffer.undo(cx);
24958 assert_eq!(buffer.text(), "a");
24959 });
24960
24961 let actions_after_edits = cx
24962 .update_window(*workspace, |_, window, cx| {
24963 project.code_actions(&buffer, anchor..anchor, window, cx)
24964 })
24965 .unwrap()
24966 .await
24967 .unwrap();
24968 assert_eq!(
24969 actions, actions_after_edits,
24970 "For the same selection, same code lens actions should be returned"
24971 );
24972
24973 let _responses =
24974 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24975 panic!("No more code lens requests are expected");
24976 });
24977 editor.update_in(cx, |editor, window, cx| {
24978 editor.select_all(&SelectAll, window, cx);
24979 });
24980 cx.executor().run_until_parked();
24981 let new_actions = cx
24982 .update_window(*workspace, |_, window, cx| {
24983 project.code_actions(&buffer, anchor..anchor, window, cx)
24984 })
24985 .unwrap()
24986 .await
24987 .unwrap();
24988 assert_eq!(
24989 actions, new_actions,
24990 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
24991 );
24992}
24993
24994#[gpui::test]
24995async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
24996 init_test(cx, |_| {});
24997
24998 let fs = FakeFs::new(cx.executor());
24999 let main_text = r#"fn main() {
25000println!("1");
25001println!("2");
25002println!("3");
25003println!("4");
25004println!("5");
25005}"#;
25006 let lib_text = "mod foo {}";
25007 fs.insert_tree(
25008 path!("/a"),
25009 json!({
25010 "lib.rs": lib_text,
25011 "main.rs": main_text,
25012 }),
25013 )
25014 .await;
25015
25016 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25017 let (workspace, cx) =
25018 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25019 let worktree_id = workspace.update(cx, |workspace, cx| {
25020 workspace.project().update(cx, |project, cx| {
25021 project.worktrees(cx).next().unwrap().read(cx).id()
25022 })
25023 });
25024
25025 let expected_ranges = vec![
25026 Point::new(0, 0)..Point::new(0, 0),
25027 Point::new(1, 0)..Point::new(1, 1),
25028 Point::new(2, 0)..Point::new(2, 2),
25029 Point::new(3, 0)..Point::new(3, 3),
25030 ];
25031
25032 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25033 let editor_1 = workspace
25034 .update_in(cx, |workspace, window, cx| {
25035 workspace.open_path(
25036 (worktree_id, rel_path("main.rs")),
25037 Some(pane_1.downgrade()),
25038 true,
25039 window,
25040 cx,
25041 )
25042 })
25043 .unwrap()
25044 .await
25045 .downcast::<Editor>()
25046 .unwrap();
25047 pane_1.update(cx, |pane, cx| {
25048 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25049 open_editor.update(cx, |editor, cx| {
25050 assert_eq!(
25051 editor.display_text(cx),
25052 main_text,
25053 "Original main.rs text on initial open",
25054 );
25055 assert_eq!(
25056 editor
25057 .selections
25058 .all::<Point>(&editor.display_snapshot(cx))
25059 .into_iter()
25060 .map(|s| s.range())
25061 .collect::<Vec<_>>(),
25062 vec![Point::zero()..Point::zero()],
25063 "Default selections on initial open",
25064 );
25065 })
25066 });
25067 editor_1.update_in(cx, |editor, window, cx| {
25068 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25069 s.select_ranges(expected_ranges.clone());
25070 });
25071 });
25072
25073 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
25074 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
25075 });
25076 let editor_2 = workspace
25077 .update_in(cx, |workspace, window, cx| {
25078 workspace.open_path(
25079 (worktree_id, rel_path("main.rs")),
25080 Some(pane_2.downgrade()),
25081 true,
25082 window,
25083 cx,
25084 )
25085 })
25086 .unwrap()
25087 .await
25088 .downcast::<Editor>()
25089 .unwrap();
25090 pane_2.update(cx, |pane, cx| {
25091 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25092 open_editor.update(cx, |editor, cx| {
25093 assert_eq!(
25094 editor.display_text(cx),
25095 main_text,
25096 "Original main.rs text on initial open in another panel",
25097 );
25098 assert_eq!(
25099 editor
25100 .selections
25101 .all::<Point>(&editor.display_snapshot(cx))
25102 .into_iter()
25103 .map(|s| s.range())
25104 .collect::<Vec<_>>(),
25105 vec![Point::zero()..Point::zero()],
25106 "Default selections on initial open in another panel",
25107 );
25108 })
25109 });
25110
25111 editor_2.update_in(cx, |editor, window, cx| {
25112 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
25113 });
25114
25115 let _other_editor_1 = workspace
25116 .update_in(cx, |workspace, window, cx| {
25117 workspace.open_path(
25118 (worktree_id, rel_path("lib.rs")),
25119 Some(pane_1.downgrade()),
25120 true,
25121 window,
25122 cx,
25123 )
25124 })
25125 .unwrap()
25126 .await
25127 .downcast::<Editor>()
25128 .unwrap();
25129 pane_1
25130 .update_in(cx, |pane, window, cx| {
25131 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
25132 })
25133 .await
25134 .unwrap();
25135 drop(editor_1);
25136 pane_1.update(cx, |pane, cx| {
25137 pane.active_item()
25138 .unwrap()
25139 .downcast::<Editor>()
25140 .unwrap()
25141 .update(cx, |editor, cx| {
25142 assert_eq!(
25143 editor.display_text(cx),
25144 lib_text,
25145 "Other file should be open and active",
25146 );
25147 });
25148 assert_eq!(pane.items().count(), 1, "No other editors should be open");
25149 });
25150
25151 let _other_editor_2 = workspace
25152 .update_in(cx, |workspace, window, cx| {
25153 workspace.open_path(
25154 (worktree_id, rel_path("lib.rs")),
25155 Some(pane_2.downgrade()),
25156 true,
25157 window,
25158 cx,
25159 )
25160 })
25161 .unwrap()
25162 .await
25163 .downcast::<Editor>()
25164 .unwrap();
25165 pane_2
25166 .update_in(cx, |pane, window, cx| {
25167 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
25168 })
25169 .await
25170 .unwrap();
25171 drop(editor_2);
25172 pane_2.update(cx, |pane, cx| {
25173 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25174 open_editor.update(cx, |editor, cx| {
25175 assert_eq!(
25176 editor.display_text(cx),
25177 lib_text,
25178 "Other file should be open and active in another panel too",
25179 );
25180 });
25181 assert_eq!(
25182 pane.items().count(),
25183 1,
25184 "No other editors should be open in another pane",
25185 );
25186 });
25187
25188 let _editor_1_reopened = workspace
25189 .update_in(cx, |workspace, window, cx| {
25190 workspace.open_path(
25191 (worktree_id, rel_path("main.rs")),
25192 Some(pane_1.downgrade()),
25193 true,
25194 window,
25195 cx,
25196 )
25197 })
25198 .unwrap()
25199 .await
25200 .downcast::<Editor>()
25201 .unwrap();
25202 let _editor_2_reopened = workspace
25203 .update_in(cx, |workspace, window, cx| {
25204 workspace.open_path(
25205 (worktree_id, rel_path("main.rs")),
25206 Some(pane_2.downgrade()),
25207 true,
25208 window,
25209 cx,
25210 )
25211 })
25212 .unwrap()
25213 .await
25214 .downcast::<Editor>()
25215 .unwrap();
25216 pane_1.update(cx, |pane, cx| {
25217 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25218 open_editor.update(cx, |editor, cx| {
25219 assert_eq!(
25220 editor.display_text(cx),
25221 main_text,
25222 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
25223 );
25224 assert_eq!(
25225 editor
25226 .selections
25227 .all::<Point>(&editor.display_snapshot(cx))
25228 .into_iter()
25229 .map(|s| s.range())
25230 .collect::<Vec<_>>(),
25231 expected_ranges,
25232 "Previous editor in the 1st panel had selections and should get them restored on reopen",
25233 );
25234 })
25235 });
25236 pane_2.update(cx, |pane, cx| {
25237 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25238 open_editor.update(cx, |editor, cx| {
25239 assert_eq!(
25240 editor.display_text(cx),
25241 r#"fn main() {
25242⋯rintln!("1");
25243⋯intln!("2");
25244⋯ntln!("3");
25245println!("4");
25246println!("5");
25247}"#,
25248 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
25249 );
25250 assert_eq!(
25251 editor
25252 .selections
25253 .all::<Point>(&editor.display_snapshot(cx))
25254 .into_iter()
25255 .map(|s| s.range())
25256 .collect::<Vec<_>>(),
25257 vec![Point::zero()..Point::zero()],
25258 "Previous editor in the 2nd pane had no selections changed hence should restore none",
25259 );
25260 })
25261 });
25262}
25263
25264#[gpui::test]
25265async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
25266 init_test(cx, |_| {});
25267
25268 let fs = FakeFs::new(cx.executor());
25269 let main_text = r#"fn main() {
25270println!("1");
25271println!("2");
25272println!("3");
25273println!("4");
25274println!("5");
25275}"#;
25276 let lib_text = "mod foo {}";
25277 fs.insert_tree(
25278 path!("/a"),
25279 json!({
25280 "lib.rs": lib_text,
25281 "main.rs": main_text,
25282 }),
25283 )
25284 .await;
25285
25286 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25287 let (workspace, cx) =
25288 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25289 let worktree_id = workspace.update(cx, |workspace, cx| {
25290 workspace.project().update(cx, |project, cx| {
25291 project.worktrees(cx).next().unwrap().read(cx).id()
25292 })
25293 });
25294
25295 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25296 let editor = workspace
25297 .update_in(cx, |workspace, window, cx| {
25298 workspace.open_path(
25299 (worktree_id, rel_path("main.rs")),
25300 Some(pane.downgrade()),
25301 true,
25302 window,
25303 cx,
25304 )
25305 })
25306 .unwrap()
25307 .await
25308 .downcast::<Editor>()
25309 .unwrap();
25310 pane.update(cx, |pane, cx| {
25311 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25312 open_editor.update(cx, |editor, cx| {
25313 assert_eq!(
25314 editor.display_text(cx),
25315 main_text,
25316 "Original main.rs text on initial open",
25317 );
25318 })
25319 });
25320 editor.update_in(cx, |editor, window, cx| {
25321 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
25322 });
25323
25324 cx.update_global(|store: &mut SettingsStore, cx| {
25325 store.update_user_settings(cx, |s| {
25326 s.workspace.restore_on_file_reopen = Some(false);
25327 });
25328 });
25329 editor.update_in(cx, |editor, window, cx| {
25330 editor.fold_ranges(
25331 vec![
25332 Point::new(1, 0)..Point::new(1, 1),
25333 Point::new(2, 0)..Point::new(2, 2),
25334 Point::new(3, 0)..Point::new(3, 3),
25335 ],
25336 false,
25337 window,
25338 cx,
25339 );
25340 });
25341 pane.update_in(cx, |pane, window, cx| {
25342 pane.close_all_items(&CloseAllItems::default(), window, cx)
25343 })
25344 .await
25345 .unwrap();
25346 pane.update(cx, |pane, _| {
25347 assert!(pane.active_item().is_none());
25348 });
25349 cx.update_global(|store: &mut SettingsStore, cx| {
25350 store.update_user_settings(cx, |s| {
25351 s.workspace.restore_on_file_reopen = Some(true);
25352 });
25353 });
25354
25355 let _editor_reopened = workspace
25356 .update_in(cx, |workspace, window, cx| {
25357 workspace.open_path(
25358 (worktree_id, rel_path("main.rs")),
25359 Some(pane.downgrade()),
25360 true,
25361 window,
25362 cx,
25363 )
25364 })
25365 .unwrap()
25366 .await
25367 .downcast::<Editor>()
25368 .unwrap();
25369 pane.update(cx, |pane, cx| {
25370 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25371 open_editor.update(cx, |editor, cx| {
25372 assert_eq!(
25373 editor.display_text(cx),
25374 main_text,
25375 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
25376 );
25377 })
25378 });
25379}
25380
25381#[gpui::test]
25382async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
25383 struct EmptyModalView {
25384 focus_handle: gpui::FocusHandle,
25385 }
25386 impl EventEmitter<DismissEvent> for EmptyModalView {}
25387 impl Render for EmptyModalView {
25388 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
25389 div()
25390 }
25391 }
25392 impl Focusable for EmptyModalView {
25393 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
25394 self.focus_handle.clone()
25395 }
25396 }
25397 impl workspace::ModalView for EmptyModalView {}
25398 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
25399 EmptyModalView {
25400 focus_handle: cx.focus_handle(),
25401 }
25402 }
25403
25404 init_test(cx, |_| {});
25405
25406 let fs = FakeFs::new(cx.executor());
25407 let project = Project::test(fs, [], cx).await;
25408 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25409 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
25410 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
25411 let editor = cx.new_window_entity(|window, cx| {
25412 Editor::new(
25413 EditorMode::full(),
25414 buffer,
25415 Some(project.clone()),
25416 window,
25417 cx,
25418 )
25419 });
25420 workspace
25421 .update(cx, |workspace, window, cx| {
25422 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
25423 })
25424 .unwrap();
25425 editor.update_in(cx, |editor, window, cx| {
25426 editor.open_context_menu(&OpenContextMenu, window, cx);
25427 assert!(editor.mouse_context_menu.is_some());
25428 });
25429 workspace
25430 .update(cx, |workspace, window, cx| {
25431 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
25432 })
25433 .unwrap();
25434 cx.read(|cx| {
25435 assert!(editor.read(cx).mouse_context_menu.is_none());
25436 });
25437}
25438
25439fn set_linked_edit_ranges(
25440 opening: (Point, Point),
25441 closing: (Point, Point),
25442 editor: &mut Editor,
25443 cx: &mut Context<Editor>,
25444) {
25445 let Some((buffer, _)) = editor
25446 .buffer
25447 .read(cx)
25448 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
25449 else {
25450 panic!("Failed to get buffer for selection position");
25451 };
25452 let buffer = buffer.read(cx);
25453 let buffer_id = buffer.remote_id();
25454 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
25455 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
25456 let mut linked_ranges = HashMap::default();
25457 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
25458 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
25459}
25460
25461#[gpui::test]
25462async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
25463 init_test(cx, |_| {});
25464
25465 let fs = FakeFs::new(cx.executor());
25466 fs.insert_file(path!("/file.html"), Default::default())
25467 .await;
25468
25469 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
25470
25471 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25472 let html_language = Arc::new(Language::new(
25473 LanguageConfig {
25474 name: "HTML".into(),
25475 matcher: LanguageMatcher {
25476 path_suffixes: vec!["html".to_string()],
25477 ..LanguageMatcher::default()
25478 },
25479 brackets: BracketPairConfig {
25480 pairs: vec![BracketPair {
25481 start: "<".into(),
25482 end: ">".into(),
25483 close: true,
25484 ..Default::default()
25485 }],
25486 ..Default::default()
25487 },
25488 ..Default::default()
25489 },
25490 Some(tree_sitter_html::LANGUAGE.into()),
25491 ));
25492 language_registry.add(html_language);
25493 let mut fake_servers = language_registry.register_fake_lsp(
25494 "HTML",
25495 FakeLspAdapter {
25496 capabilities: lsp::ServerCapabilities {
25497 completion_provider: Some(lsp::CompletionOptions {
25498 resolve_provider: Some(true),
25499 ..Default::default()
25500 }),
25501 ..Default::default()
25502 },
25503 ..Default::default()
25504 },
25505 );
25506
25507 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25508 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25509
25510 let worktree_id = workspace
25511 .update(cx, |workspace, _window, cx| {
25512 workspace.project().update(cx, |project, cx| {
25513 project.worktrees(cx).next().unwrap().read(cx).id()
25514 })
25515 })
25516 .unwrap();
25517 project
25518 .update(cx, |project, cx| {
25519 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
25520 })
25521 .await
25522 .unwrap();
25523 let editor = workspace
25524 .update(cx, |workspace, window, cx| {
25525 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
25526 })
25527 .unwrap()
25528 .await
25529 .unwrap()
25530 .downcast::<Editor>()
25531 .unwrap();
25532
25533 let fake_server = fake_servers.next().await.unwrap();
25534 cx.run_until_parked();
25535 editor.update_in(cx, |editor, window, cx| {
25536 editor.set_text("<ad></ad>", window, cx);
25537 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
25538 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
25539 });
25540 set_linked_edit_ranges(
25541 (Point::new(0, 1), Point::new(0, 3)),
25542 (Point::new(0, 6), Point::new(0, 8)),
25543 editor,
25544 cx,
25545 );
25546 });
25547 let mut completion_handle =
25548 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25549 Ok(Some(lsp::CompletionResponse::Array(vec![
25550 lsp::CompletionItem {
25551 label: "head".to_string(),
25552 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25553 lsp::InsertReplaceEdit {
25554 new_text: "head".to_string(),
25555 insert: lsp::Range::new(
25556 lsp::Position::new(0, 1),
25557 lsp::Position::new(0, 3),
25558 ),
25559 replace: lsp::Range::new(
25560 lsp::Position::new(0, 1),
25561 lsp::Position::new(0, 3),
25562 ),
25563 },
25564 )),
25565 ..Default::default()
25566 },
25567 ])))
25568 });
25569 editor.update_in(cx, |editor, window, cx| {
25570 editor.show_completions(&ShowCompletions, window, cx);
25571 });
25572 cx.run_until_parked();
25573 completion_handle.next().await.unwrap();
25574 editor.update(cx, |editor, _| {
25575 assert!(
25576 editor.context_menu_visible(),
25577 "Completion menu should be visible"
25578 );
25579 });
25580 editor.update_in(cx, |editor, window, cx| {
25581 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
25582 });
25583 cx.executor().run_until_parked();
25584 editor.update(cx, |editor, cx| {
25585 assert_eq!(editor.text(cx), "<head></head>");
25586 });
25587}
25588
25589#[gpui::test]
25590async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
25591 init_test(cx, |_| {});
25592
25593 let mut cx = EditorTestContext::new(cx).await;
25594 let language = Arc::new(Language::new(
25595 LanguageConfig {
25596 name: "TSX".into(),
25597 matcher: LanguageMatcher {
25598 path_suffixes: vec!["tsx".to_string()],
25599 ..LanguageMatcher::default()
25600 },
25601 brackets: BracketPairConfig {
25602 pairs: vec![BracketPair {
25603 start: "<".into(),
25604 end: ">".into(),
25605 close: true,
25606 ..Default::default()
25607 }],
25608 ..Default::default()
25609 },
25610 linked_edit_characters: HashSet::from_iter(['.']),
25611 ..Default::default()
25612 },
25613 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
25614 ));
25615 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25616
25617 // Test typing > does not extend linked pair
25618 cx.set_state("<divˇ<div></div>");
25619 cx.update_editor(|editor, _, cx| {
25620 set_linked_edit_ranges(
25621 (Point::new(0, 1), Point::new(0, 4)),
25622 (Point::new(0, 11), Point::new(0, 14)),
25623 editor,
25624 cx,
25625 );
25626 });
25627 cx.update_editor(|editor, window, cx| {
25628 editor.handle_input(">", window, cx);
25629 });
25630 cx.assert_editor_state("<div>ˇ<div></div>");
25631
25632 // Test typing . do extend linked pair
25633 cx.set_state("<Animatedˇ></Animated>");
25634 cx.update_editor(|editor, _, cx| {
25635 set_linked_edit_ranges(
25636 (Point::new(0, 1), Point::new(0, 9)),
25637 (Point::new(0, 12), Point::new(0, 20)),
25638 editor,
25639 cx,
25640 );
25641 });
25642 cx.update_editor(|editor, window, cx| {
25643 editor.handle_input(".", window, cx);
25644 });
25645 cx.assert_editor_state("<Animated.ˇ></Animated.>");
25646 cx.update_editor(|editor, _, cx| {
25647 set_linked_edit_ranges(
25648 (Point::new(0, 1), Point::new(0, 10)),
25649 (Point::new(0, 13), Point::new(0, 21)),
25650 editor,
25651 cx,
25652 );
25653 });
25654 cx.update_editor(|editor, window, cx| {
25655 editor.handle_input("V", window, cx);
25656 });
25657 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
25658}
25659
25660#[gpui::test]
25661async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
25662 init_test(cx, |_| {});
25663
25664 let fs = FakeFs::new(cx.executor());
25665 fs.insert_tree(
25666 path!("/root"),
25667 json!({
25668 "a": {
25669 "main.rs": "fn main() {}",
25670 },
25671 "foo": {
25672 "bar": {
25673 "external_file.rs": "pub mod external {}",
25674 }
25675 }
25676 }),
25677 )
25678 .await;
25679
25680 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
25681 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25682 language_registry.add(rust_lang());
25683 let _fake_servers = language_registry.register_fake_lsp(
25684 "Rust",
25685 FakeLspAdapter {
25686 ..FakeLspAdapter::default()
25687 },
25688 );
25689 let (workspace, cx) =
25690 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25691 let worktree_id = workspace.update(cx, |workspace, cx| {
25692 workspace.project().update(cx, |project, cx| {
25693 project.worktrees(cx).next().unwrap().read(cx).id()
25694 })
25695 });
25696
25697 let assert_language_servers_count =
25698 |expected: usize, context: &str, cx: &mut VisualTestContext| {
25699 project.update(cx, |project, cx| {
25700 let current = project
25701 .lsp_store()
25702 .read(cx)
25703 .as_local()
25704 .unwrap()
25705 .language_servers
25706 .len();
25707 assert_eq!(expected, current, "{context}");
25708 });
25709 };
25710
25711 assert_language_servers_count(
25712 0,
25713 "No servers should be running before any file is open",
25714 cx,
25715 );
25716 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25717 let main_editor = workspace
25718 .update_in(cx, |workspace, window, cx| {
25719 workspace.open_path(
25720 (worktree_id, rel_path("main.rs")),
25721 Some(pane.downgrade()),
25722 true,
25723 window,
25724 cx,
25725 )
25726 })
25727 .unwrap()
25728 .await
25729 .downcast::<Editor>()
25730 .unwrap();
25731 pane.update(cx, |pane, cx| {
25732 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25733 open_editor.update(cx, |editor, cx| {
25734 assert_eq!(
25735 editor.display_text(cx),
25736 "fn main() {}",
25737 "Original main.rs text on initial open",
25738 );
25739 });
25740 assert_eq!(open_editor, main_editor);
25741 });
25742 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
25743
25744 let external_editor = workspace
25745 .update_in(cx, |workspace, window, cx| {
25746 workspace.open_abs_path(
25747 PathBuf::from("/root/foo/bar/external_file.rs"),
25748 OpenOptions::default(),
25749 window,
25750 cx,
25751 )
25752 })
25753 .await
25754 .expect("opening external file")
25755 .downcast::<Editor>()
25756 .expect("downcasted external file's open element to editor");
25757 pane.update(cx, |pane, cx| {
25758 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25759 open_editor.update(cx, |editor, cx| {
25760 assert_eq!(
25761 editor.display_text(cx),
25762 "pub mod external {}",
25763 "External file is open now",
25764 );
25765 });
25766 assert_eq!(open_editor, external_editor);
25767 });
25768 assert_language_servers_count(
25769 1,
25770 "Second, external, *.rs file should join the existing server",
25771 cx,
25772 );
25773
25774 pane.update_in(cx, |pane, window, cx| {
25775 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25776 })
25777 .await
25778 .unwrap();
25779 pane.update_in(cx, |pane, window, cx| {
25780 pane.navigate_backward(&Default::default(), window, cx);
25781 });
25782 cx.run_until_parked();
25783 pane.update(cx, |pane, cx| {
25784 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25785 open_editor.update(cx, |editor, cx| {
25786 assert_eq!(
25787 editor.display_text(cx),
25788 "pub mod external {}",
25789 "External file is open now",
25790 );
25791 });
25792 });
25793 assert_language_servers_count(
25794 1,
25795 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25796 cx,
25797 );
25798
25799 cx.update(|_, cx| {
25800 workspace::reload(cx);
25801 });
25802 assert_language_servers_count(
25803 1,
25804 "After reloading the worktree with local and external files opened, only one project should be started",
25805 cx,
25806 );
25807}
25808
25809#[gpui::test]
25810async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25811 init_test(cx, |_| {});
25812
25813 let mut cx = EditorTestContext::new(cx).await;
25814 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25815 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25816
25817 // test cursor move to start of each line on tab
25818 // for `if`, `elif`, `else`, `while`, `with` and `for`
25819 cx.set_state(indoc! {"
25820 def main():
25821 ˇ for item in items:
25822 ˇ while item.active:
25823 ˇ if item.value > 10:
25824 ˇ continue
25825 ˇ elif item.value < 0:
25826 ˇ break
25827 ˇ else:
25828 ˇ with item.context() as ctx:
25829 ˇ yield count
25830 ˇ else:
25831 ˇ log('while else')
25832 ˇ else:
25833 ˇ log('for else')
25834 "});
25835 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25836 cx.wait_for_autoindent_applied().await;
25837 cx.assert_editor_state(indoc! {"
25838 def main():
25839 ˇfor item in items:
25840 ˇwhile item.active:
25841 ˇif item.value > 10:
25842 ˇcontinue
25843 ˇelif item.value < 0:
25844 ˇbreak
25845 ˇelse:
25846 ˇwith item.context() as ctx:
25847 ˇyield count
25848 ˇelse:
25849 ˇlog('while else')
25850 ˇelse:
25851 ˇlog('for else')
25852 "});
25853 // test relative indent is preserved when tab
25854 // for `if`, `elif`, `else`, `while`, `with` and `for`
25855 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25856 cx.wait_for_autoindent_applied().await;
25857 cx.assert_editor_state(indoc! {"
25858 def main():
25859 ˇfor item in items:
25860 ˇwhile item.active:
25861 ˇif item.value > 10:
25862 ˇcontinue
25863 ˇelif item.value < 0:
25864 ˇbreak
25865 ˇelse:
25866 ˇwith item.context() as ctx:
25867 ˇyield count
25868 ˇelse:
25869 ˇlog('while else')
25870 ˇelse:
25871 ˇlog('for else')
25872 "});
25873
25874 // test cursor move to start of each line on tab
25875 // for `try`, `except`, `else`, `finally`, `match` and `def`
25876 cx.set_state(indoc! {"
25877 def main():
25878 ˇ try:
25879 ˇ fetch()
25880 ˇ except ValueError:
25881 ˇ handle_error()
25882 ˇ else:
25883 ˇ match value:
25884 ˇ case _:
25885 ˇ finally:
25886 ˇ def status():
25887 ˇ return 0
25888 "});
25889 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25890 cx.wait_for_autoindent_applied().await;
25891 cx.assert_editor_state(indoc! {"
25892 def main():
25893 ˇtry:
25894 ˇfetch()
25895 ˇexcept ValueError:
25896 ˇhandle_error()
25897 ˇelse:
25898 ˇmatch value:
25899 ˇcase _:
25900 ˇfinally:
25901 ˇdef status():
25902 ˇreturn 0
25903 "});
25904 // test relative indent is preserved when tab
25905 // for `try`, `except`, `else`, `finally`, `match` and `def`
25906 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25907 cx.wait_for_autoindent_applied().await;
25908 cx.assert_editor_state(indoc! {"
25909 def main():
25910 ˇtry:
25911 ˇfetch()
25912 ˇexcept ValueError:
25913 ˇhandle_error()
25914 ˇelse:
25915 ˇmatch value:
25916 ˇcase _:
25917 ˇfinally:
25918 ˇdef status():
25919 ˇreturn 0
25920 "});
25921}
25922
25923#[gpui::test]
25924async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
25925 init_test(cx, |_| {});
25926
25927 let mut cx = EditorTestContext::new(cx).await;
25928 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25929 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25930
25931 // test `else` auto outdents when typed inside `if` block
25932 cx.set_state(indoc! {"
25933 def main():
25934 if i == 2:
25935 return
25936 ˇ
25937 "});
25938 cx.update_editor(|editor, window, cx| {
25939 editor.handle_input("else:", window, cx);
25940 });
25941 cx.wait_for_autoindent_applied().await;
25942 cx.assert_editor_state(indoc! {"
25943 def main():
25944 if i == 2:
25945 return
25946 else:ˇ
25947 "});
25948
25949 // test `except` auto outdents when typed inside `try` block
25950 cx.set_state(indoc! {"
25951 def main():
25952 try:
25953 i = 2
25954 ˇ
25955 "});
25956 cx.update_editor(|editor, window, cx| {
25957 editor.handle_input("except:", window, cx);
25958 });
25959 cx.wait_for_autoindent_applied().await;
25960 cx.assert_editor_state(indoc! {"
25961 def main():
25962 try:
25963 i = 2
25964 except:ˇ
25965 "});
25966
25967 // test `else` auto outdents when typed inside `except` block
25968 cx.set_state(indoc! {"
25969 def main():
25970 try:
25971 i = 2
25972 except:
25973 j = 2
25974 ˇ
25975 "});
25976 cx.update_editor(|editor, window, cx| {
25977 editor.handle_input("else:", window, cx);
25978 });
25979 cx.wait_for_autoindent_applied().await;
25980 cx.assert_editor_state(indoc! {"
25981 def main():
25982 try:
25983 i = 2
25984 except:
25985 j = 2
25986 else:ˇ
25987 "});
25988
25989 // test `finally` auto outdents when typed inside `else` block
25990 cx.set_state(indoc! {"
25991 def main():
25992 try:
25993 i = 2
25994 except:
25995 j = 2
25996 else:
25997 k = 2
25998 ˇ
25999 "});
26000 cx.update_editor(|editor, window, cx| {
26001 editor.handle_input("finally:", window, cx);
26002 });
26003 cx.wait_for_autoindent_applied().await;
26004 cx.assert_editor_state(indoc! {"
26005 def main():
26006 try:
26007 i = 2
26008 except:
26009 j = 2
26010 else:
26011 k = 2
26012 finally:ˇ
26013 "});
26014
26015 // test `else` does not outdents when typed inside `except` block right after for block
26016 cx.set_state(indoc! {"
26017 def main():
26018 try:
26019 i = 2
26020 except:
26021 for i in range(n):
26022 pass
26023 ˇ
26024 "});
26025 cx.update_editor(|editor, window, cx| {
26026 editor.handle_input("else:", window, cx);
26027 });
26028 cx.wait_for_autoindent_applied().await;
26029 cx.assert_editor_state(indoc! {"
26030 def main():
26031 try:
26032 i = 2
26033 except:
26034 for i in range(n):
26035 pass
26036 else:ˇ
26037 "});
26038
26039 // test `finally` auto outdents when typed inside `else` block right after for block
26040 cx.set_state(indoc! {"
26041 def main():
26042 try:
26043 i = 2
26044 except:
26045 j = 2
26046 else:
26047 for i in range(n):
26048 pass
26049 ˇ
26050 "});
26051 cx.update_editor(|editor, window, cx| {
26052 editor.handle_input("finally:", window, cx);
26053 });
26054 cx.wait_for_autoindent_applied().await;
26055 cx.assert_editor_state(indoc! {"
26056 def main():
26057 try:
26058 i = 2
26059 except:
26060 j = 2
26061 else:
26062 for i in range(n):
26063 pass
26064 finally:ˇ
26065 "});
26066
26067 // test `except` outdents to inner "try" block
26068 cx.set_state(indoc! {"
26069 def main():
26070 try:
26071 i = 2
26072 if i == 2:
26073 try:
26074 i = 3
26075 ˇ
26076 "});
26077 cx.update_editor(|editor, window, cx| {
26078 editor.handle_input("except:", window, cx);
26079 });
26080 cx.wait_for_autoindent_applied().await;
26081 cx.assert_editor_state(indoc! {"
26082 def main():
26083 try:
26084 i = 2
26085 if i == 2:
26086 try:
26087 i = 3
26088 except:ˇ
26089 "});
26090
26091 // test `except` outdents to outer "try" block
26092 cx.set_state(indoc! {"
26093 def main():
26094 try:
26095 i = 2
26096 if i == 2:
26097 try:
26098 i = 3
26099 ˇ
26100 "});
26101 cx.update_editor(|editor, window, cx| {
26102 editor.handle_input("except:", window, cx);
26103 });
26104 cx.wait_for_autoindent_applied().await;
26105 cx.assert_editor_state(indoc! {"
26106 def main():
26107 try:
26108 i = 2
26109 if i == 2:
26110 try:
26111 i = 3
26112 except:ˇ
26113 "});
26114
26115 // test `else` stays at correct indent when typed after `for` block
26116 cx.set_state(indoc! {"
26117 def main():
26118 for i in range(10):
26119 if i == 3:
26120 break
26121 ˇ
26122 "});
26123 cx.update_editor(|editor, window, cx| {
26124 editor.handle_input("else:", window, cx);
26125 });
26126 cx.wait_for_autoindent_applied().await;
26127 cx.assert_editor_state(indoc! {"
26128 def main():
26129 for i in range(10):
26130 if i == 3:
26131 break
26132 else:ˇ
26133 "});
26134
26135 // test does not outdent on typing after line with square brackets
26136 cx.set_state(indoc! {"
26137 def f() -> list[str]:
26138 ˇ
26139 "});
26140 cx.update_editor(|editor, window, cx| {
26141 editor.handle_input("a", window, cx);
26142 });
26143 cx.wait_for_autoindent_applied().await;
26144 cx.assert_editor_state(indoc! {"
26145 def f() -> list[str]:
26146 aˇ
26147 "});
26148
26149 // test does not outdent on typing : after case keyword
26150 cx.set_state(indoc! {"
26151 match 1:
26152 caseˇ
26153 "});
26154 cx.update_editor(|editor, window, cx| {
26155 editor.handle_input(":", window, cx);
26156 });
26157 cx.wait_for_autoindent_applied().await;
26158 cx.assert_editor_state(indoc! {"
26159 match 1:
26160 case:ˇ
26161 "});
26162}
26163
26164#[gpui::test]
26165async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
26166 init_test(cx, |_| {});
26167 update_test_language_settings(cx, |settings| {
26168 settings.defaults.extend_comment_on_newline = Some(false);
26169 });
26170 let mut cx = EditorTestContext::new(cx).await;
26171 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26172 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26173
26174 // test correct indent after newline on comment
26175 cx.set_state(indoc! {"
26176 # COMMENT:ˇ
26177 "});
26178 cx.update_editor(|editor, window, cx| {
26179 editor.newline(&Newline, window, cx);
26180 });
26181 cx.wait_for_autoindent_applied().await;
26182 cx.assert_editor_state(indoc! {"
26183 # COMMENT:
26184 ˇ
26185 "});
26186
26187 // test correct indent after newline in brackets
26188 cx.set_state(indoc! {"
26189 {ˇ}
26190 "});
26191 cx.update_editor(|editor, window, cx| {
26192 editor.newline(&Newline, window, cx);
26193 });
26194 cx.wait_for_autoindent_applied().await;
26195 cx.assert_editor_state(indoc! {"
26196 {
26197 ˇ
26198 }
26199 "});
26200
26201 cx.set_state(indoc! {"
26202 (ˇ)
26203 "});
26204 cx.update_editor(|editor, window, cx| {
26205 editor.newline(&Newline, window, cx);
26206 });
26207 cx.run_until_parked();
26208 cx.assert_editor_state(indoc! {"
26209 (
26210 ˇ
26211 )
26212 "});
26213
26214 // do not indent after empty lists or dictionaries
26215 cx.set_state(indoc! {"
26216 a = []ˇ
26217 "});
26218 cx.update_editor(|editor, window, cx| {
26219 editor.newline(&Newline, window, cx);
26220 });
26221 cx.run_until_parked();
26222 cx.assert_editor_state(indoc! {"
26223 a = []
26224 ˇ
26225 "});
26226}
26227
26228#[gpui::test]
26229async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
26230 init_test(cx, |_| {});
26231
26232 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
26233 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
26234 language_registry.add(markdown_lang());
26235 language_registry.add(python_lang);
26236
26237 let mut cx = EditorTestContext::new(cx).await;
26238 cx.update_buffer(|buffer, cx| {
26239 buffer.set_language_registry(language_registry);
26240 buffer.set_language(Some(markdown_lang()), cx);
26241 });
26242
26243 // Test that `else:` correctly outdents to match `if:` inside the Python code block
26244 cx.set_state(indoc! {"
26245 # Heading
26246
26247 ```python
26248 def main():
26249 if condition:
26250 pass
26251 ˇ
26252 ```
26253 "});
26254 cx.update_editor(|editor, window, cx| {
26255 editor.handle_input("else:", window, cx);
26256 });
26257 cx.run_until_parked();
26258 cx.assert_editor_state(indoc! {"
26259 # Heading
26260
26261 ```python
26262 def main():
26263 if condition:
26264 pass
26265 else:ˇ
26266 ```
26267 "});
26268}
26269
26270#[gpui::test]
26271async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
26272 init_test(cx, |_| {});
26273
26274 let mut cx = EditorTestContext::new(cx).await;
26275 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26276 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26277
26278 // test cursor move to start of each line on tab
26279 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
26280 cx.set_state(indoc! {"
26281 function main() {
26282 ˇ for item in $items; do
26283 ˇ while [ -n \"$item\" ]; do
26284 ˇ if [ \"$value\" -gt 10 ]; then
26285 ˇ continue
26286 ˇ elif [ \"$value\" -lt 0 ]; then
26287 ˇ break
26288 ˇ else
26289 ˇ echo \"$item\"
26290 ˇ fi
26291 ˇ done
26292 ˇ done
26293 ˇ}
26294 "});
26295 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26296 cx.wait_for_autoindent_applied().await;
26297 cx.assert_editor_state(indoc! {"
26298 function main() {
26299 ˇfor item in $items; do
26300 ˇwhile [ -n \"$item\" ]; do
26301 ˇif [ \"$value\" -gt 10 ]; then
26302 ˇcontinue
26303 ˇelif [ \"$value\" -lt 0 ]; then
26304 ˇbreak
26305 ˇelse
26306 ˇecho \"$item\"
26307 ˇfi
26308 ˇdone
26309 ˇdone
26310 ˇ}
26311 "});
26312 // test relative indent is preserved when tab
26313 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26314 cx.wait_for_autoindent_applied().await;
26315 cx.assert_editor_state(indoc! {"
26316 function main() {
26317 ˇfor item in $items; do
26318 ˇwhile [ -n \"$item\" ]; do
26319 ˇif [ \"$value\" -gt 10 ]; then
26320 ˇcontinue
26321 ˇelif [ \"$value\" -lt 0 ]; then
26322 ˇbreak
26323 ˇelse
26324 ˇecho \"$item\"
26325 ˇfi
26326 ˇdone
26327 ˇdone
26328 ˇ}
26329 "});
26330
26331 // test cursor move to start of each line on tab
26332 // for `case` statement with patterns
26333 cx.set_state(indoc! {"
26334 function handle() {
26335 ˇ case \"$1\" in
26336 ˇ start)
26337 ˇ echo \"a\"
26338 ˇ ;;
26339 ˇ stop)
26340 ˇ echo \"b\"
26341 ˇ ;;
26342 ˇ *)
26343 ˇ echo \"c\"
26344 ˇ ;;
26345 ˇ esac
26346 ˇ}
26347 "});
26348 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26349 cx.wait_for_autoindent_applied().await;
26350 cx.assert_editor_state(indoc! {"
26351 function handle() {
26352 ˇcase \"$1\" in
26353 ˇstart)
26354 ˇecho \"a\"
26355 ˇ;;
26356 ˇstop)
26357 ˇecho \"b\"
26358 ˇ;;
26359 ˇ*)
26360 ˇecho \"c\"
26361 ˇ;;
26362 ˇesac
26363 ˇ}
26364 "});
26365}
26366
26367#[gpui::test]
26368async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
26369 init_test(cx, |_| {});
26370
26371 let mut cx = EditorTestContext::new(cx).await;
26372 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26373 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26374
26375 // test indents on comment insert
26376 cx.set_state(indoc! {"
26377 function main() {
26378 ˇ for item in $items; do
26379 ˇ while [ -n \"$item\" ]; do
26380 ˇ if [ \"$value\" -gt 10 ]; then
26381 ˇ continue
26382 ˇ elif [ \"$value\" -lt 0 ]; then
26383 ˇ break
26384 ˇ else
26385 ˇ echo \"$item\"
26386 ˇ fi
26387 ˇ done
26388 ˇ done
26389 ˇ}
26390 "});
26391 cx.update_editor(|e, window, cx| e.handle_input("#", 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
26410#[gpui::test]
26411async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
26412 init_test(cx, |_| {});
26413
26414 let mut cx = EditorTestContext::new(cx).await;
26415 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26416 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26417
26418 // test `else` auto outdents when typed inside `if` block
26419 cx.set_state(indoc! {"
26420 if [ \"$1\" = \"test\" ]; then
26421 echo \"foo bar\"
26422 ˇ
26423 "});
26424 cx.update_editor(|editor, window, cx| {
26425 editor.handle_input("else", window, cx);
26426 });
26427 cx.wait_for_autoindent_applied().await;
26428 cx.assert_editor_state(indoc! {"
26429 if [ \"$1\" = \"test\" ]; then
26430 echo \"foo bar\"
26431 elseˇ
26432 "});
26433
26434 // test `elif` auto outdents when typed inside `if` block
26435 cx.set_state(indoc! {"
26436 if [ \"$1\" = \"test\" ]; then
26437 echo \"foo bar\"
26438 ˇ
26439 "});
26440 cx.update_editor(|editor, window, cx| {
26441 editor.handle_input("elif", window, cx);
26442 });
26443 cx.wait_for_autoindent_applied().await;
26444 cx.assert_editor_state(indoc! {"
26445 if [ \"$1\" = \"test\" ]; then
26446 echo \"foo bar\"
26447 elifˇ
26448 "});
26449
26450 // test `fi` auto outdents when typed inside `else` block
26451 cx.set_state(indoc! {"
26452 if [ \"$1\" = \"test\" ]; then
26453 echo \"foo bar\"
26454 else
26455 echo \"bar baz\"
26456 ˇ
26457 "});
26458 cx.update_editor(|editor, window, cx| {
26459 editor.handle_input("fi", window, cx);
26460 });
26461 cx.wait_for_autoindent_applied().await;
26462 cx.assert_editor_state(indoc! {"
26463 if [ \"$1\" = \"test\" ]; then
26464 echo \"foo bar\"
26465 else
26466 echo \"bar baz\"
26467 fiˇ
26468 "});
26469
26470 // test `done` auto outdents when typed inside `while` block
26471 cx.set_state(indoc! {"
26472 while read line; do
26473 echo \"$line\"
26474 ˇ
26475 "});
26476 cx.update_editor(|editor, window, cx| {
26477 editor.handle_input("done", window, cx);
26478 });
26479 cx.wait_for_autoindent_applied().await;
26480 cx.assert_editor_state(indoc! {"
26481 while read line; do
26482 echo \"$line\"
26483 doneˇ
26484 "});
26485
26486 // test `done` auto outdents when typed inside `for` block
26487 cx.set_state(indoc! {"
26488 for file in *.txt; do
26489 cat \"$file\"
26490 ˇ
26491 "});
26492 cx.update_editor(|editor, window, cx| {
26493 editor.handle_input("done", window, cx);
26494 });
26495 cx.wait_for_autoindent_applied().await;
26496 cx.assert_editor_state(indoc! {"
26497 for file in *.txt; do
26498 cat \"$file\"
26499 doneˇ
26500 "});
26501
26502 // test `esac` auto outdents when typed inside `case` block
26503 cx.set_state(indoc! {"
26504 case \"$1\" in
26505 start)
26506 echo \"foo bar\"
26507 ;;
26508 stop)
26509 echo \"bar baz\"
26510 ;;
26511 ˇ
26512 "});
26513 cx.update_editor(|editor, window, cx| {
26514 editor.handle_input("esac", window, cx);
26515 });
26516 cx.wait_for_autoindent_applied().await;
26517 cx.assert_editor_state(indoc! {"
26518 case \"$1\" in
26519 start)
26520 echo \"foo bar\"
26521 ;;
26522 stop)
26523 echo \"bar baz\"
26524 ;;
26525 esacˇ
26526 "});
26527
26528 // test `*)` auto outdents when typed inside `case` block
26529 cx.set_state(indoc! {"
26530 case \"$1\" in
26531 start)
26532 echo \"foo bar\"
26533 ;;
26534 ˇ
26535 "});
26536 cx.update_editor(|editor, window, cx| {
26537 editor.handle_input("*)", window, cx);
26538 });
26539 cx.wait_for_autoindent_applied().await;
26540 cx.assert_editor_state(indoc! {"
26541 case \"$1\" in
26542 start)
26543 echo \"foo bar\"
26544 ;;
26545 *)ˇ
26546 "});
26547
26548 // test `fi` outdents to correct level with nested if blocks
26549 cx.set_state(indoc! {"
26550 if [ \"$1\" = \"test\" ]; then
26551 echo \"outer if\"
26552 if [ \"$2\" = \"debug\" ]; then
26553 echo \"inner if\"
26554 ˇ
26555 "});
26556 cx.update_editor(|editor, window, cx| {
26557 editor.handle_input("fi", window, cx);
26558 });
26559 cx.wait_for_autoindent_applied().await;
26560 cx.assert_editor_state(indoc! {"
26561 if [ \"$1\" = \"test\" ]; then
26562 echo \"outer if\"
26563 if [ \"$2\" = \"debug\" ]; then
26564 echo \"inner if\"
26565 fiˇ
26566 "});
26567}
26568
26569#[gpui::test]
26570async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
26571 init_test(cx, |_| {});
26572 update_test_language_settings(cx, |settings| {
26573 settings.defaults.extend_comment_on_newline = Some(false);
26574 });
26575 let mut cx = EditorTestContext::new(cx).await;
26576 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26577 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26578
26579 // test correct indent after newline on comment
26580 cx.set_state(indoc! {"
26581 # COMMENT:ˇ
26582 "});
26583 cx.update_editor(|editor, window, cx| {
26584 editor.newline(&Newline, window, cx);
26585 });
26586 cx.wait_for_autoindent_applied().await;
26587 cx.assert_editor_state(indoc! {"
26588 # COMMENT:
26589 ˇ
26590 "});
26591
26592 // test correct indent after newline after `then`
26593 cx.set_state(indoc! {"
26594
26595 if [ \"$1\" = \"test\" ]; thenˇ
26596 "});
26597 cx.update_editor(|editor, window, cx| {
26598 editor.newline(&Newline, window, cx);
26599 });
26600 cx.wait_for_autoindent_applied().await;
26601 cx.assert_editor_state(indoc! {"
26602
26603 if [ \"$1\" = \"test\" ]; then
26604 ˇ
26605 "});
26606
26607 // test correct indent after newline after `else`
26608 cx.set_state(indoc! {"
26609 if [ \"$1\" = \"test\" ]; then
26610 elseˇ
26611 "});
26612 cx.update_editor(|editor, window, cx| {
26613 editor.newline(&Newline, window, cx);
26614 });
26615 cx.wait_for_autoindent_applied().await;
26616 cx.assert_editor_state(indoc! {"
26617 if [ \"$1\" = \"test\" ]; then
26618 else
26619 ˇ
26620 "});
26621
26622 // test correct indent after newline after `elif`
26623 cx.set_state(indoc! {"
26624 if [ \"$1\" = \"test\" ]; then
26625 elifˇ
26626 "});
26627 cx.update_editor(|editor, window, cx| {
26628 editor.newline(&Newline, window, cx);
26629 });
26630 cx.wait_for_autoindent_applied().await;
26631 cx.assert_editor_state(indoc! {"
26632 if [ \"$1\" = \"test\" ]; then
26633 elif
26634 ˇ
26635 "});
26636
26637 // test correct indent after newline after `do`
26638 cx.set_state(indoc! {"
26639 for file in *.txt; doˇ
26640 "});
26641 cx.update_editor(|editor, window, cx| {
26642 editor.newline(&Newline, window, cx);
26643 });
26644 cx.wait_for_autoindent_applied().await;
26645 cx.assert_editor_state(indoc! {"
26646 for file in *.txt; do
26647 ˇ
26648 "});
26649
26650 // test correct indent after newline after case pattern
26651 cx.set_state(indoc! {"
26652 case \"$1\" in
26653 start)ˇ
26654 "});
26655 cx.update_editor(|editor, window, cx| {
26656 editor.newline(&Newline, window, cx);
26657 });
26658 cx.wait_for_autoindent_applied().await;
26659 cx.assert_editor_state(indoc! {"
26660 case \"$1\" in
26661 start)
26662 ˇ
26663 "});
26664
26665 // test correct indent after newline after case pattern
26666 cx.set_state(indoc! {"
26667 case \"$1\" in
26668 start)
26669 ;;
26670 *)ˇ
26671 "});
26672 cx.update_editor(|editor, window, cx| {
26673 editor.newline(&Newline, window, cx);
26674 });
26675 cx.wait_for_autoindent_applied().await;
26676 cx.assert_editor_state(indoc! {"
26677 case \"$1\" in
26678 start)
26679 ;;
26680 *)
26681 ˇ
26682 "});
26683
26684 // test correct indent after newline after function opening brace
26685 cx.set_state(indoc! {"
26686 function test() {ˇ}
26687 "});
26688 cx.update_editor(|editor, window, cx| {
26689 editor.newline(&Newline, window, cx);
26690 });
26691 cx.wait_for_autoindent_applied().await;
26692 cx.assert_editor_state(indoc! {"
26693 function test() {
26694 ˇ
26695 }
26696 "});
26697
26698 // test no extra indent after semicolon on same line
26699 cx.set_state(indoc! {"
26700 echo \"test\";ˇ
26701 "});
26702 cx.update_editor(|editor, window, cx| {
26703 editor.newline(&Newline, window, cx);
26704 });
26705 cx.wait_for_autoindent_applied().await;
26706 cx.assert_editor_state(indoc! {"
26707 echo \"test\";
26708 ˇ
26709 "});
26710}
26711
26712fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
26713 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
26714 point..point
26715}
26716
26717#[track_caller]
26718fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
26719 let (text, ranges) = marked_text_ranges(marked_text, true);
26720 assert_eq!(editor.text(cx), text);
26721 assert_eq!(
26722 editor.selections.ranges(&editor.display_snapshot(cx)),
26723 ranges
26724 .iter()
26725 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
26726 .collect::<Vec<_>>(),
26727 "Assert selections are {}",
26728 marked_text
26729 );
26730}
26731
26732pub fn handle_signature_help_request(
26733 cx: &mut EditorLspTestContext,
26734 mocked_response: lsp::SignatureHelp,
26735) -> impl Future<Output = ()> + use<> {
26736 let mut request =
26737 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
26738 let mocked_response = mocked_response.clone();
26739 async move { Ok(Some(mocked_response)) }
26740 });
26741
26742 async move {
26743 request.next().await;
26744 }
26745}
26746
26747#[track_caller]
26748pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
26749 cx.update_editor(|editor, _, _| {
26750 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
26751 let entries = menu.entries.borrow();
26752 let entries = entries
26753 .iter()
26754 .map(|entry| entry.string.as_str())
26755 .collect::<Vec<_>>();
26756 assert_eq!(entries, expected);
26757 } else {
26758 panic!("Expected completions menu");
26759 }
26760 });
26761}
26762
26763#[gpui::test]
26764async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
26765 init_test(cx, |_| {});
26766 let mut cx = EditorLspTestContext::new_rust(
26767 lsp::ServerCapabilities {
26768 completion_provider: Some(lsp::CompletionOptions {
26769 ..Default::default()
26770 }),
26771 ..Default::default()
26772 },
26773 cx,
26774 )
26775 .await;
26776 cx.lsp
26777 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
26778 Ok(Some(lsp::CompletionResponse::Array(vec![
26779 lsp::CompletionItem {
26780 label: "unsafe".into(),
26781 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26782 range: lsp::Range {
26783 start: lsp::Position {
26784 line: 0,
26785 character: 9,
26786 },
26787 end: lsp::Position {
26788 line: 0,
26789 character: 11,
26790 },
26791 },
26792 new_text: "unsafe".to_string(),
26793 })),
26794 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
26795 ..Default::default()
26796 },
26797 ])))
26798 });
26799
26800 cx.update_editor(|editor, _, cx| {
26801 editor.project().unwrap().update(cx, |project, cx| {
26802 project.snippets().update(cx, |snippets, _cx| {
26803 snippets.add_snippet_for_test(
26804 None,
26805 PathBuf::from("test_snippets.json"),
26806 vec![
26807 Arc::new(project::snippet_provider::Snippet {
26808 prefix: vec![
26809 "unlimited word count".to_string(),
26810 "unlimit word count".to_string(),
26811 "unlimited unknown".to_string(),
26812 ],
26813 body: "this is many words".to_string(),
26814 description: Some("description".to_string()),
26815 name: "multi-word snippet test".to_string(),
26816 }),
26817 Arc::new(project::snippet_provider::Snippet {
26818 prefix: vec!["unsnip".to_string(), "@few".to_string()],
26819 body: "fewer words".to_string(),
26820 description: Some("alt description".to_string()),
26821 name: "other name".to_string(),
26822 }),
26823 Arc::new(project::snippet_provider::Snippet {
26824 prefix: vec!["ab aa".to_string()],
26825 body: "abcd".to_string(),
26826 description: None,
26827 name: "alphabet".to_string(),
26828 }),
26829 ],
26830 );
26831 });
26832 })
26833 });
26834
26835 let get_completions = |cx: &mut EditorLspTestContext| {
26836 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
26837 Some(CodeContextMenu::Completions(context_menu)) => {
26838 let entries = context_menu.entries.borrow();
26839 entries
26840 .iter()
26841 .map(|entry| entry.string.clone())
26842 .collect_vec()
26843 }
26844 _ => vec![],
26845 })
26846 };
26847
26848 // snippets:
26849 // @foo
26850 // foo bar
26851 //
26852 // when typing:
26853 //
26854 // when typing:
26855 // - if I type a symbol "open the completions with snippets only"
26856 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
26857 //
26858 // stuff we need:
26859 // - filtering logic change?
26860 // - remember how far back the completion started.
26861
26862 let test_cases: &[(&str, &[&str])] = &[
26863 (
26864 "un",
26865 &[
26866 "unsafe",
26867 "unlimit word count",
26868 "unlimited unknown",
26869 "unlimited word count",
26870 "unsnip",
26871 ],
26872 ),
26873 (
26874 "u ",
26875 &[
26876 "unlimit word count",
26877 "unlimited unknown",
26878 "unlimited word count",
26879 ],
26880 ),
26881 ("u a", &["ab aa", "unsafe"]), // unsAfe
26882 (
26883 "u u",
26884 &[
26885 "unsafe",
26886 "unlimit word count",
26887 "unlimited unknown", // ranked highest among snippets
26888 "unlimited word count",
26889 "unsnip",
26890 ],
26891 ),
26892 ("uw c", &["unlimit word count", "unlimited word count"]),
26893 (
26894 "u w",
26895 &[
26896 "unlimit word count",
26897 "unlimited word count",
26898 "unlimited unknown",
26899 ],
26900 ),
26901 ("u w ", &["unlimit word count", "unlimited word count"]),
26902 (
26903 "u ",
26904 &[
26905 "unlimit word count",
26906 "unlimited unknown",
26907 "unlimited word count",
26908 ],
26909 ),
26910 ("wor", &[]),
26911 ("uf", &["unsafe"]),
26912 ("af", &["unsafe"]),
26913 ("afu", &[]),
26914 (
26915 "ue",
26916 &["unsafe", "unlimited unknown", "unlimited word count"],
26917 ),
26918 ("@", &["@few"]),
26919 ("@few", &["@few"]),
26920 ("@ ", &[]),
26921 ("a@", &["@few"]),
26922 ("a@f", &["@few", "unsafe"]),
26923 ("a@fw", &["@few"]),
26924 ("a", &["ab aa", "unsafe"]),
26925 ("aa", &["ab aa"]),
26926 ("aaa", &["ab aa"]),
26927 ("ab", &["ab aa"]),
26928 ("ab ", &["ab aa"]),
26929 ("ab a", &["ab aa", "unsafe"]),
26930 ("ab ab", &["ab aa"]),
26931 ("ab ab aa", &["ab aa"]),
26932 ];
26933
26934 for &(input_to_simulate, expected_completions) in test_cases {
26935 cx.set_state("fn a() { ˇ }\n");
26936 for c in input_to_simulate.split("") {
26937 cx.simulate_input(c);
26938 cx.run_until_parked();
26939 }
26940 let expected_completions = expected_completions
26941 .iter()
26942 .map(|s| s.to_string())
26943 .collect_vec();
26944 assert_eq!(
26945 get_completions(&mut cx),
26946 expected_completions,
26947 "< actual / expected >, input = {input_to_simulate:?}",
26948 );
26949 }
26950}
26951
26952/// Handle completion request passing a marked string specifying where the completion
26953/// should be triggered from using '|' character, what range should be replaced, and what completions
26954/// should be returned using '<' and '>' to delimit the range.
26955///
26956/// Also see `handle_completion_request_with_insert_and_replace`.
26957#[track_caller]
26958pub fn handle_completion_request(
26959 marked_string: &str,
26960 completions: Vec<&'static str>,
26961 is_incomplete: bool,
26962 counter: Arc<AtomicUsize>,
26963 cx: &mut EditorLspTestContext,
26964) -> impl Future<Output = ()> {
26965 let complete_from_marker: TextRangeMarker = '|'.into();
26966 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26967 let (_, mut marked_ranges) = marked_text_ranges_by(
26968 marked_string,
26969 vec![complete_from_marker.clone(), replace_range_marker.clone()],
26970 );
26971
26972 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26973 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26974 ));
26975 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26976 let replace_range =
26977 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26978
26979 let mut request =
26980 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26981 let completions = completions.clone();
26982 counter.fetch_add(1, atomic::Ordering::Release);
26983 async move {
26984 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26985 assert_eq!(
26986 params.text_document_position.position,
26987 complete_from_position
26988 );
26989 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
26990 is_incomplete,
26991 item_defaults: None,
26992 items: completions
26993 .iter()
26994 .map(|completion_text| lsp::CompletionItem {
26995 label: completion_text.to_string(),
26996 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26997 range: replace_range,
26998 new_text: completion_text.to_string(),
26999 })),
27000 ..Default::default()
27001 })
27002 .collect(),
27003 })))
27004 }
27005 });
27006
27007 async move {
27008 request.next().await;
27009 }
27010}
27011
27012/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
27013/// given instead, which also contains an `insert` range.
27014///
27015/// This function uses markers to define ranges:
27016/// - `|` marks the cursor position
27017/// - `<>` marks the replace range
27018/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
27019pub fn handle_completion_request_with_insert_and_replace(
27020 cx: &mut EditorLspTestContext,
27021 marked_string: &str,
27022 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
27023 counter: Arc<AtomicUsize>,
27024) -> impl Future<Output = ()> {
27025 let complete_from_marker: TextRangeMarker = '|'.into();
27026 let replace_range_marker: TextRangeMarker = ('<', '>').into();
27027 let insert_range_marker: TextRangeMarker = ('{', '}').into();
27028
27029 let (_, mut marked_ranges) = marked_text_ranges_by(
27030 marked_string,
27031 vec![
27032 complete_from_marker.clone(),
27033 replace_range_marker.clone(),
27034 insert_range_marker.clone(),
27035 ],
27036 );
27037
27038 let complete_from_position = cx.to_lsp(MultiBufferOffset(
27039 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
27040 ));
27041 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
27042 let replace_range =
27043 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
27044
27045 let insert_range = match marked_ranges.remove(&insert_range_marker) {
27046 Some(ranges) if !ranges.is_empty() => {
27047 let range1 = ranges[0].clone();
27048 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
27049 }
27050 _ => lsp::Range {
27051 start: replace_range.start,
27052 end: complete_from_position,
27053 },
27054 };
27055
27056 let mut request =
27057 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
27058 let completions = completions.clone();
27059 counter.fetch_add(1, atomic::Ordering::Release);
27060 async move {
27061 assert_eq!(params.text_document_position.text_document.uri, url.clone());
27062 assert_eq!(
27063 params.text_document_position.position, complete_from_position,
27064 "marker `|` position doesn't match",
27065 );
27066 Ok(Some(lsp::CompletionResponse::Array(
27067 completions
27068 .iter()
27069 .map(|(label, new_text)| lsp::CompletionItem {
27070 label: label.to_string(),
27071 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
27072 lsp::InsertReplaceEdit {
27073 insert: insert_range,
27074 replace: replace_range,
27075 new_text: new_text.to_string(),
27076 },
27077 )),
27078 ..Default::default()
27079 })
27080 .collect(),
27081 )))
27082 }
27083 });
27084
27085 async move {
27086 request.next().await;
27087 }
27088}
27089
27090fn handle_resolve_completion_request(
27091 cx: &mut EditorLspTestContext,
27092 edits: Option<Vec<(&'static str, &'static str)>>,
27093) -> impl Future<Output = ()> {
27094 let edits = edits.map(|edits| {
27095 edits
27096 .iter()
27097 .map(|(marked_string, new_text)| {
27098 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
27099 let replace_range = cx.to_lsp_range(
27100 MultiBufferOffset(marked_ranges[0].start)
27101 ..MultiBufferOffset(marked_ranges[0].end),
27102 );
27103 lsp::TextEdit::new(replace_range, new_text.to_string())
27104 })
27105 .collect::<Vec<_>>()
27106 });
27107
27108 let mut request =
27109 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
27110 let edits = edits.clone();
27111 async move {
27112 Ok(lsp::CompletionItem {
27113 additional_text_edits: edits,
27114 ..Default::default()
27115 })
27116 }
27117 });
27118
27119 async move {
27120 request.next().await;
27121 }
27122}
27123
27124pub(crate) fn update_test_language_settings(
27125 cx: &mut TestAppContext,
27126 f: impl Fn(&mut AllLanguageSettingsContent),
27127) {
27128 cx.update(|cx| {
27129 SettingsStore::update_global(cx, |store, cx| {
27130 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
27131 });
27132 });
27133}
27134
27135pub(crate) fn update_test_project_settings(
27136 cx: &mut TestAppContext,
27137 f: impl Fn(&mut ProjectSettingsContent),
27138) {
27139 cx.update(|cx| {
27140 SettingsStore::update_global(cx, |store, cx| {
27141 store.update_user_settings(cx, |settings| f(&mut settings.project));
27142 });
27143 });
27144}
27145
27146pub(crate) fn update_test_editor_settings(
27147 cx: &mut TestAppContext,
27148 f: impl Fn(&mut EditorSettingsContent),
27149) {
27150 cx.update(|cx| {
27151 SettingsStore::update_global(cx, |store, cx| {
27152 store.update_user_settings(cx, |settings| f(&mut settings.editor));
27153 })
27154 })
27155}
27156
27157pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
27158 cx.update(|cx| {
27159 assets::Assets.load_test_fonts(cx);
27160 let store = SettingsStore::test(cx);
27161 cx.set_global(store);
27162 theme::init(theme::LoadThemes::JustBase, cx);
27163 release_channel::init(semver::Version::new(0, 0, 0), cx);
27164 crate::init(cx);
27165 });
27166 zlog::init_test();
27167 update_test_language_settings(cx, f);
27168}
27169
27170#[track_caller]
27171fn assert_hunk_revert(
27172 not_reverted_text_with_selections: &str,
27173 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
27174 expected_reverted_text_with_selections: &str,
27175 base_text: &str,
27176 cx: &mut EditorLspTestContext,
27177) {
27178 cx.set_state(not_reverted_text_with_selections);
27179 cx.set_head_text(base_text);
27180 cx.executor().run_until_parked();
27181
27182 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
27183 let snapshot = editor.snapshot(window, cx);
27184 let reverted_hunk_statuses = snapshot
27185 .buffer_snapshot()
27186 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
27187 .map(|hunk| hunk.status().kind)
27188 .collect::<Vec<_>>();
27189
27190 editor.git_restore(&Default::default(), window, cx);
27191 reverted_hunk_statuses
27192 });
27193 cx.executor().run_until_parked();
27194 cx.assert_editor_state(expected_reverted_text_with_selections);
27195 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
27196}
27197
27198#[gpui::test(iterations = 10)]
27199async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
27200 init_test(cx, |_| {});
27201
27202 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
27203 let counter = diagnostic_requests.clone();
27204
27205 let fs = FakeFs::new(cx.executor());
27206 fs.insert_tree(
27207 path!("/a"),
27208 json!({
27209 "first.rs": "fn main() { let a = 5; }",
27210 "second.rs": "// Test file",
27211 }),
27212 )
27213 .await;
27214
27215 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27216 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27217 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27218
27219 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27220 language_registry.add(rust_lang());
27221 let mut fake_servers = language_registry.register_fake_lsp(
27222 "Rust",
27223 FakeLspAdapter {
27224 capabilities: lsp::ServerCapabilities {
27225 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
27226 lsp::DiagnosticOptions {
27227 identifier: None,
27228 inter_file_dependencies: true,
27229 workspace_diagnostics: true,
27230 work_done_progress_options: Default::default(),
27231 },
27232 )),
27233 ..Default::default()
27234 },
27235 ..Default::default()
27236 },
27237 );
27238
27239 let editor = workspace
27240 .update(cx, |workspace, window, cx| {
27241 workspace.open_abs_path(
27242 PathBuf::from(path!("/a/first.rs")),
27243 OpenOptions::default(),
27244 window,
27245 cx,
27246 )
27247 })
27248 .unwrap()
27249 .await
27250 .unwrap()
27251 .downcast::<Editor>()
27252 .unwrap();
27253 let fake_server = fake_servers.next().await.unwrap();
27254 let server_id = fake_server.server.server_id();
27255 let mut first_request = fake_server
27256 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
27257 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
27258 let result_id = Some(new_result_id.to_string());
27259 assert_eq!(
27260 params.text_document.uri,
27261 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27262 );
27263 async move {
27264 Ok(lsp::DocumentDiagnosticReportResult::Report(
27265 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
27266 related_documents: None,
27267 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
27268 items: Vec::new(),
27269 result_id,
27270 },
27271 }),
27272 ))
27273 }
27274 });
27275
27276 let ensure_result_id = |expected: Option<SharedString>, cx: &mut TestAppContext| {
27277 project.update(cx, |project, cx| {
27278 let buffer_id = editor
27279 .read(cx)
27280 .buffer()
27281 .read(cx)
27282 .as_singleton()
27283 .expect("created a singleton buffer")
27284 .read(cx)
27285 .remote_id();
27286 let buffer_result_id = project
27287 .lsp_store()
27288 .read(cx)
27289 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
27290 assert_eq!(expected, buffer_result_id);
27291 });
27292 };
27293
27294 ensure_result_id(None, cx);
27295 cx.executor().advance_clock(Duration::from_millis(60));
27296 cx.executor().run_until_parked();
27297 assert_eq!(
27298 diagnostic_requests.load(atomic::Ordering::Acquire),
27299 1,
27300 "Opening file should trigger diagnostic request"
27301 );
27302 first_request
27303 .next()
27304 .await
27305 .expect("should have sent the first diagnostics pull request");
27306 ensure_result_id(Some(SharedString::new("1")), cx);
27307
27308 // Editing should trigger diagnostics
27309 editor.update_in(cx, |editor, window, cx| {
27310 editor.handle_input("2", window, cx)
27311 });
27312 cx.executor().advance_clock(Duration::from_millis(60));
27313 cx.executor().run_until_parked();
27314 assert_eq!(
27315 diagnostic_requests.load(atomic::Ordering::Acquire),
27316 2,
27317 "Editing should trigger diagnostic request"
27318 );
27319 ensure_result_id(Some(SharedString::new("2")), cx);
27320
27321 // Moving cursor should not trigger diagnostic request
27322 editor.update_in(cx, |editor, window, cx| {
27323 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27324 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
27325 });
27326 });
27327 cx.executor().advance_clock(Duration::from_millis(60));
27328 cx.executor().run_until_parked();
27329 assert_eq!(
27330 diagnostic_requests.load(atomic::Ordering::Acquire),
27331 2,
27332 "Cursor movement should not trigger diagnostic request"
27333 );
27334 ensure_result_id(Some(SharedString::new("2")), cx);
27335 // Multiple rapid edits should be debounced
27336 for _ in 0..5 {
27337 editor.update_in(cx, |editor, window, cx| {
27338 editor.handle_input("x", window, cx)
27339 });
27340 }
27341 cx.executor().advance_clock(Duration::from_millis(60));
27342 cx.executor().run_until_parked();
27343
27344 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
27345 assert!(
27346 final_requests <= 4,
27347 "Multiple rapid edits should be debounced (got {final_requests} requests)",
27348 );
27349 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
27350}
27351
27352#[gpui::test]
27353async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
27354 // Regression test for issue #11671
27355 // Previously, adding a cursor after moving multiple cursors would reset
27356 // the cursor count instead of adding to the existing cursors.
27357 init_test(cx, |_| {});
27358 let mut cx = EditorTestContext::new(cx).await;
27359
27360 // Create a simple buffer with cursor at start
27361 cx.set_state(indoc! {"
27362 ˇaaaa
27363 bbbb
27364 cccc
27365 dddd
27366 eeee
27367 ffff
27368 gggg
27369 hhhh"});
27370
27371 // Add 2 cursors below (so we have 3 total)
27372 cx.update_editor(|editor, window, cx| {
27373 editor.add_selection_below(&Default::default(), window, cx);
27374 editor.add_selection_below(&Default::default(), window, cx);
27375 });
27376
27377 // Verify we have 3 cursors
27378 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
27379 assert_eq!(
27380 initial_count, 3,
27381 "Should have 3 cursors after adding 2 below"
27382 );
27383
27384 // Move down one line
27385 cx.update_editor(|editor, window, cx| {
27386 editor.move_down(&MoveDown, window, cx);
27387 });
27388
27389 // Add another cursor below
27390 cx.update_editor(|editor, window, cx| {
27391 editor.add_selection_below(&Default::default(), window, cx);
27392 });
27393
27394 // Should now have 4 cursors (3 original + 1 new)
27395 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
27396 assert_eq!(
27397 final_count, 4,
27398 "Should have 4 cursors after moving and adding another"
27399 );
27400}
27401
27402#[gpui::test]
27403async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
27404 init_test(cx, |_| {});
27405
27406 let mut cx = EditorTestContext::new(cx).await;
27407
27408 cx.set_state(indoc!(
27409 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
27410 Second line here"#
27411 ));
27412
27413 cx.update_editor(|editor, window, cx| {
27414 // Enable soft wrapping with a narrow width to force soft wrapping and
27415 // confirm that more than 2 rows are being displayed.
27416 editor.set_wrap_width(Some(100.0.into()), cx);
27417 assert!(editor.display_text(cx).lines().count() > 2);
27418
27419 editor.add_selection_below(
27420 &AddSelectionBelow {
27421 skip_soft_wrap: true,
27422 },
27423 window,
27424 cx,
27425 );
27426
27427 assert_eq!(
27428 display_ranges(editor, cx),
27429 &[
27430 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27431 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
27432 ]
27433 );
27434
27435 editor.add_selection_above(
27436 &AddSelectionAbove {
27437 skip_soft_wrap: true,
27438 },
27439 window,
27440 cx,
27441 );
27442
27443 assert_eq!(
27444 display_ranges(editor, cx),
27445 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27446 );
27447
27448 editor.add_selection_below(
27449 &AddSelectionBelow {
27450 skip_soft_wrap: false,
27451 },
27452 window,
27453 cx,
27454 );
27455
27456 assert_eq!(
27457 display_ranges(editor, cx),
27458 &[
27459 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27460 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
27461 ]
27462 );
27463
27464 editor.add_selection_above(
27465 &AddSelectionAbove {
27466 skip_soft_wrap: false,
27467 },
27468 window,
27469 cx,
27470 );
27471
27472 assert_eq!(
27473 display_ranges(editor, cx),
27474 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27475 );
27476 });
27477
27478 // Set up text where selections are in the middle of a soft-wrapped line.
27479 // When adding selection below with `skip_soft_wrap` set to `true`, the new
27480 // selection should be at the same buffer column, not the same pixel
27481 // position.
27482 cx.set_state(indoc!(
27483 r#"1. Very long line to show «howˇ» a wrapped line would look
27484 2. Very long line to show how a wrapped line would look"#
27485 ));
27486
27487 cx.update_editor(|editor, window, cx| {
27488 // Enable soft wrapping with a narrow width to force soft wrapping and
27489 // confirm that more than 2 rows are being displayed.
27490 editor.set_wrap_width(Some(100.0.into()), cx);
27491 assert!(editor.display_text(cx).lines().count() > 2);
27492
27493 editor.add_selection_below(
27494 &AddSelectionBelow {
27495 skip_soft_wrap: true,
27496 },
27497 window,
27498 cx,
27499 );
27500
27501 // Assert that there's now 2 selections, both selecting the same column
27502 // range in the buffer row.
27503 let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
27504 let selections = editor.selections.all::<Point>(&display_map);
27505 assert_eq!(selections.len(), 2);
27506 assert_eq!(selections[0].start.column, selections[1].start.column);
27507 assert_eq!(selections[0].end.column, selections[1].end.column);
27508 });
27509}
27510
27511#[gpui::test]
27512async fn test_insert_snippet(cx: &mut TestAppContext) {
27513 init_test(cx, |_| {});
27514 let mut cx = EditorTestContext::new(cx).await;
27515
27516 cx.update_editor(|editor, _, cx| {
27517 editor.project().unwrap().update(cx, |project, cx| {
27518 project.snippets().update(cx, |snippets, _cx| {
27519 let snippet = project::snippet_provider::Snippet {
27520 prefix: vec![], // no prefix needed!
27521 body: "an Unspecified".to_string(),
27522 description: Some("shhhh it's a secret".to_string()),
27523 name: "super secret snippet".to_string(),
27524 };
27525 snippets.add_snippet_for_test(
27526 None,
27527 PathBuf::from("test_snippets.json"),
27528 vec![Arc::new(snippet)],
27529 );
27530
27531 let snippet = project::snippet_provider::Snippet {
27532 prefix: vec![], // no prefix needed!
27533 body: " Location".to_string(),
27534 description: Some("the word 'location'".to_string()),
27535 name: "location word".to_string(),
27536 };
27537 snippets.add_snippet_for_test(
27538 Some("Markdown".to_string()),
27539 PathBuf::from("test_snippets.json"),
27540 vec![Arc::new(snippet)],
27541 );
27542 });
27543 })
27544 });
27545
27546 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
27547
27548 cx.update_editor(|editor, window, cx| {
27549 editor.insert_snippet_at_selections(
27550 &InsertSnippet {
27551 language: None,
27552 name: Some("super secret snippet".to_string()),
27553 snippet: None,
27554 },
27555 window,
27556 cx,
27557 );
27558
27559 // Language is specified in the action,
27560 // so the buffer language does not need to match
27561 editor.insert_snippet_at_selections(
27562 &InsertSnippet {
27563 language: Some("Markdown".to_string()),
27564 name: Some("location word".to_string()),
27565 snippet: None,
27566 },
27567 window,
27568 cx,
27569 );
27570
27571 editor.insert_snippet_at_selections(
27572 &InsertSnippet {
27573 language: None,
27574 name: None,
27575 snippet: Some("$0 after".to_string()),
27576 },
27577 window,
27578 cx,
27579 );
27580 });
27581
27582 cx.assert_editor_state(
27583 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
27584 );
27585}
27586
27587#[gpui::test(iterations = 10)]
27588async fn test_document_colors(cx: &mut TestAppContext) {
27589 let expected_color = Rgba {
27590 r: 0.33,
27591 g: 0.33,
27592 b: 0.33,
27593 a: 0.33,
27594 };
27595
27596 init_test(cx, |_| {});
27597
27598 let fs = FakeFs::new(cx.executor());
27599 fs.insert_tree(
27600 path!("/a"),
27601 json!({
27602 "first.rs": "fn main() { let a = 5; }",
27603 }),
27604 )
27605 .await;
27606
27607 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27608 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27609 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27610
27611 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27612 language_registry.add(rust_lang());
27613 let mut fake_servers = language_registry.register_fake_lsp(
27614 "Rust",
27615 FakeLspAdapter {
27616 capabilities: lsp::ServerCapabilities {
27617 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
27618 ..lsp::ServerCapabilities::default()
27619 },
27620 name: "rust-analyzer",
27621 ..FakeLspAdapter::default()
27622 },
27623 );
27624 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
27625 "Rust",
27626 FakeLspAdapter {
27627 capabilities: lsp::ServerCapabilities {
27628 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
27629 ..lsp::ServerCapabilities::default()
27630 },
27631 name: "not-rust-analyzer",
27632 ..FakeLspAdapter::default()
27633 },
27634 );
27635
27636 let editor = workspace
27637 .update(cx, |workspace, window, cx| {
27638 workspace.open_abs_path(
27639 PathBuf::from(path!("/a/first.rs")),
27640 OpenOptions::default(),
27641 window,
27642 cx,
27643 )
27644 })
27645 .unwrap()
27646 .await
27647 .unwrap()
27648 .downcast::<Editor>()
27649 .unwrap();
27650 let fake_language_server = fake_servers.next().await.unwrap();
27651 let fake_language_server_without_capabilities =
27652 fake_servers_without_capabilities.next().await.unwrap();
27653 let requests_made = Arc::new(AtomicUsize::new(0));
27654 let closure_requests_made = Arc::clone(&requests_made);
27655 let mut color_request_handle = fake_language_server
27656 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27657 let requests_made = Arc::clone(&closure_requests_made);
27658 async move {
27659 assert_eq!(
27660 params.text_document.uri,
27661 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27662 );
27663 requests_made.fetch_add(1, atomic::Ordering::Release);
27664 Ok(vec![
27665 lsp::ColorInformation {
27666 range: lsp::Range {
27667 start: lsp::Position {
27668 line: 0,
27669 character: 0,
27670 },
27671 end: lsp::Position {
27672 line: 0,
27673 character: 1,
27674 },
27675 },
27676 color: lsp::Color {
27677 red: 0.33,
27678 green: 0.33,
27679 blue: 0.33,
27680 alpha: 0.33,
27681 },
27682 },
27683 lsp::ColorInformation {
27684 range: lsp::Range {
27685 start: lsp::Position {
27686 line: 0,
27687 character: 0,
27688 },
27689 end: lsp::Position {
27690 line: 0,
27691 character: 1,
27692 },
27693 },
27694 color: lsp::Color {
27695 red: 0.33,
27696 green: 0.33,
27697 blue: 0.33,
27698 alpha: 0.33,
27699 },
27700 },
27701 ])
27702 }
27703 });
27704
27705 let _handle = fake_language_server_without_capabilities
27706 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
27707 panic!("Should not be called");
27708 });
27709 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27710 color_request_handle.next().await.unwrap();
27711 cx.run_until_parked();
27712 assert_eq!(
27713 1,
27714 requests_made.load(atomic::Ordering::Acquire),
27715 "Should query for colors once per editor open"
27716 );
27717 editor.update_in(cx, |editor, _, cx| {
27718 assert_eq!(
27719 vec![expected_color],
27720 extract_color_inlays(editor, cx),
27721 "Should have an initial inlay"
27722 );
27723 });
27724
27725 // opening another file in a split should not influence the LSP query counter
27726 workspace
27727 .update(cx, |workspace, window, cx| {
27728 assert_eq!(
27729 workspace.panes().len(),
27730 1,
27731 "Should have one pane with one editor"
27732 );
27733 workspace.move_item_to_pane_in_direction(
27734 &MoveItemToPaneInDirection {
27735 direction: SplitDirection::Right,
27736 focus: false,
27737 clone: true,
27738 },
27739 window,
27740 cx,
27741 );
27742 })
27743 .unwrap();
27744 cx.run_until_parked();
27745 workspace
27746 .update(cx, |workspace, _, cx| {
27747 let panes = workspace.panes();
27748 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
27749 for pane in panes {
27750 let editor = pane
27751 .read(cx)
27752 .active_item()
27753 .and_then(|item| item.downcast::<Editor>())
27754 .expect("Should have opened an editor in each split");
27755 let editor_file = editor
27756 .read(cx)
27757 .buffer()
27758 .read(cx)
27759 .as_singleton()
27760 .expect("test deals with singleton buffers")
27761 .read(cx)
27762 .file()
27763 .expect("test buffese should have a file")
27764 .path();
27765 assert_eq!(
27766 editor_file.as_ref(),
27767 rel_path("first.rs"),
27768 "Both editors should be opened for the same file"
27769 )
27770 }
27771 })
27772 .unwrap();
27773
27774 cx.executor().advance_clock(Duration::from_millis(500));
27775 let save = editor.update_in(cx, |editor, window, cx| {
27776 editor.move_to_end(&MoveToEnd, window, cx);
27777 editor.handle_input("dirty", window, cx);
27778 editor.save(
27779 SaveOptions {
27780 format: true,
27781 autosave: true,
27782 },
27783 project.clone(),
27784 window,
27785 cx,
27786 )
27787 });
27788 save.await.unwrap();
27789
27790 color_request_handle.next().await.unwrap();
27791 cx.run_until_parked();
27792 assert_eq!(
27793 2,
27794 requests_made.load(atomic::Ordering::Acquire),
27795 "Should query for colors once per save (deduplicated) and once per formatting after save"
27796 );
27797
27798 drop(editor);
27799 let close = workspace
27800 .update(cx, |workspace, window, cx| {
27801 workspace.active_pane().update(cx, |pane, cx| {
27802 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27803 })
27804 })
27805 .unwrap();
27806 close.await.unwrap();
27807 let close = workspace
27808 .update(cx, |workspace, window, cx| {
27809 workspace.active_pane().update(cx, |pane, cx| {
27810 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27811 })
27812 })
27813 .unwrap();
27814 close.await.unwrap();
27815 assert_eq!(
27816 2,
27817 requests_made.load(atomic::Ordering::Acquire),
27818 "After saving and closing all editors, no extra requests should be made"
27819 );
27820 workspace
27821 .update(cx, |workspace, _, cx| {
27822 assert!(
27823 workspace.active_item(cx).is_none(),
27824 "Should close all editors"
27825 )
27826 })
27827 .unwrap();
27828
27829 workspace
27830 .update(cx, |workspace, window, cx| {
27831 workspace.active_pane().update(cx, |pane, cx| {
27832 pane.navigate_backward(&workspace::GoBack, window, cx);
27833 })
27834 })
27835 .unwrap();
27836 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27837 cx.run_until_parked();
27838 let editor = workspace
27839 .update(cx, |workspace, _, cx| {
27840 workspace
27841 .active_item(cx)
27842 .expect("Should have reopened the editor again after navigating back")
27843 .downcast::<Editor>()
27844 .expect("Should be an editor")
27845 })
27846 .unwrap();
27847
27848 assert_eq!(
27849 2,
27850 requests_made.load(atomic::Ordering::Acquire),
27851 "Cache should be reused on buffer close and reopen"
27852 );
27853 editor.update(cx, |editor, cx| {
27854 assert_eq!(
27855 vec![expected_color],
27856 extract_color_inlays(editor, cx),
27857 "Should have an initial inlay"
27858 );
27859 });
27860
27861 drop(color_request_handle);
27862 let closure_requests_made = Arc::clone(&requests_made);
27863 let mut empty_color_request_handle = fake_language_server
27864 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27865 let requests_made = Arc::clone(&closure_requests_made);
27866 async move {
27867 assert_eq!(
27868 params.text_document.uri,
27869 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27870 );
27871 requests_made.fetch_add(1, atomic::Ordering::Release);
27872 Ok(Vec::new())
27873 }
27874 });
27875 let save = editor.update_in(cx, |editor, window, cx| {
27876 editor.move_to_end(&MoveToEnd, window, cx);
27877 editor.handle_input("dirty_again", window, cx);
27878 editor.save(
27879 SaveOptions {
27880 format: false,
27881 autosave: true,
27882 },
27883 project.clone(),
27884 window,
27885 cx,
27886 )
27887 });
27888 save.await.unwrap();
27889
27890 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27891 empty_color_request_handle.next().await.unwrap();
27892 cx.run_until_parked();
27893 assert_eq!(
27894 3,
27895 requests_made.load(atomic::Ordering::Acquire),
27896 "Should query for colors once per save only, as formatting was not requested"
27897 );
27898 editor.update(cx, |editor, cx| {
27899 assert_eq!(
27900 Vec::<Rgba>::new(),
27901 extract_color_inlays(editor, cx),
27902 "Should clear all colors when the server returns an empty response"
27903 );
27904 });
27905}
27906
27907#[gpui::test]
27908async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
27909 init_test(cx, |_| {});
27910 let (editor, cx) = cx.add_window_view(Editor::single_line);
27911 editor.update_in(cx, |editor, window, cx| {
27912 editor.set_text("oops\n\nwow\n", window, cx)
27913 });
27914 cx.run_until_parked();
27915 editor.update(cx, |editor, cx| {
27916 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
27917 });
27918 editor.update(cx, |editor, cx| {
27919 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
27920 });
27921 cx.run_until_parked();
27922 editor.update(cx, |editor, cx| {
27923 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
27924 });
27925}
27926
27927#[gpui::test]
27928async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
27929 init_test(cx, |_| {});
27930
27931 cx.update(|cx| {
27932 register_project_item::<Editor>(cx);
27933 });
27934
27935 let fs = FakeFs::new(cx.executor());
27936 fs.insert_tree("/root1", json!({})).await;
27937 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
27938 .await;
27939
27940 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
27941 let (workspace, cx) =
27942 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
27943
27944 let worktree_id = project.update(cx, |project, cx| {
27945 project.worktrees(cx).next().unwrap().read(cx).id()
27946 });
27947
27948 let handle = workspace
27949 .update_in(cx, |workspace, window, cx| {
27950 let project_path = (worktree_id, rel_path("one.pdf"));
27951 workspace.open_path(project_path, None, true, window, cx)
27952 })
27953 .await
27954 .unwrap();
27955 // The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
27956 // Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
27957 // With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
27958 assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
27959}
27960
27961#[gpui::test]
27962async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
27963 init_test(cx, |_| {});
27964
27965 let language = Arc::new(Language::new(
27966 LanguageConfig::default(),
27967 Some(tree_sitter_rust::LANGUAGE.into()),
27968 ));
27969
27970 // Test hierarchical sibling navigation
27971 let text = r#"
27972 fn outer() {
27973 if condition {
27974 let a = 1;
27975 }
27976 let b = 2;
27977 }
27978
27979 fn another() {
27980 let c = 3;
27981 }
27982 "#;
27983
27984 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
27985 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
27986 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
27987
27988 // Wait for parsing to complete
27989 editor
27990 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
27991 .await;
27992
27993 editor.update_in(cx, |editor, window, cx| {
27994 // Start by selecting "let a = 1;" inside the if block
27995 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27996 s.select_display_ranges([
27997 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
27998 ]);
27999 });
28000
28001 let initial_selection = editor
28002 .selections
28003 .display_ranges(&editor.display_snapshot(cx));
28004 assert_eq!(initial_selection.len(), 1, "Should have one selection");
28005
28006 // Test select next sibling - should move up levels to find the next sibling
28007 // Since "let a = 1;" has no siblings in the if block, it should move up
28008 // to find "let b = 2;" which is a sibling of the if block
28009 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
28010 let next_selection = editor
28011 .selections
28012 .display_ranges(&editor.display_snapshot(cx));
28013
28014 // Should have a selection and it should be different from the initial
28015 assert_eq!(
28016 next_selection.len(),
28017 1,
28018 "Should have one selection after next"
28019 );
28020 assert_ne!(
28021 next_selection[0], initial_selection[0],
28022 "Next sibling selection should be different"
28023 );
28024
28025 // Test hierarchical navigation by going to the end of the current function
28026 // and trying to navigate to the next function
28027 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28028 s.select_display_ranges([
28029 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
28030 ]);
28031 });
28032
28033 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
28034 let function_next_selection = editor
28035 .selections
28036 .display_ranges(&editor.display_snapshot(cx));
28037
28038 // Should move to the next function
28039 assert_eq!(
28040 function_next_selection.len(),
28041 1,
28042 "Should have one selection after function next"
28043 );
28044
28045 // Test select previous sibling navigation
28046 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
28047 let prev_selection = editor
28048 .selections
28049 .display_ranges(&editor.display_snapshot(cx));
28050
28051 // Should have a selection and it should be different
28052 assert_eq!(
28053 prev_selection.len(),
28054 1,
28055 "Should have one selection after prev"
28056 );
28057 assert_ne!(
28058 prev_selection[0], function_next_selection[0],
28059 "Previous sibling selection should be different from next"
28060 );
28061 });
28062}
28063
28064#[gpui::test]
28065async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
28066 init_test(cx, |_| {});
28067
28068 let mut cx = EditorTestContext::new(cx).await;
28069 cx.set_state(
28070 "let ˇvariable = 42;
28071let another = variable + 1;
28072let result = variable * 2;",
28073 );
28074
28075 // Set up document highlights manually (simulating LSP response)
28076 cx.update_editor(|editor, _window, cx| {
28077 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
28078
28079 // Create highlights for "variable" occurrences
28080 let highlight_ranges = [
28081 Point::new(0, 4)..Point::new(0, 12), // First "variable"
28082 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
28083 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
28084 ];
28085
28086 let anchor_ranges: Vec<_> = highlight_ranges
28087 .iter()
28088 .map(|range| range.clone().to_anchors(&buffer_snapshot))
28089 .collect();
28090
28091 editor.highlight_background::<DocumentHighlightRead>(
28092 &anchor_ranges,
28093 |_, theme| theme.colors().editor_document_highlight_read_background,
28094 cx,
28095 );
28096 });
28097
28098 // Go to next highlight - should move to second "variable"
28099 cx.update_editor(|editor, window, cx| {
28100 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28101 });
28102 cx.assert_editor_state(
28103 "let variable = 42;
28104let another = ˇvariable + 1;
28105let result = variable * 2;",
28106 );
28107
28108 // Go to next highlight - should move to third "variable"
28109 cx.update_editor(|editor, window, cx| {
28110 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28111 });
28112 cx.assert_editor_state(
28113 "let variable = 42;
28114let another = variable + 1;
28115let result = ˇvariable * 2;",
28116 );
28117
28118 // Go to next highlight - should stay at third "variable" (no wrap-around)
28119 cx.update_editor(|editor, window, cx| {
28120 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28121 });
28122 cx.assert_editor_state(
28123 "let variable = 42;
28124let another = variable + 1;
28125let result = ˇvariable * 2;",
28126 );
28127
28128 // Now test going backwards from third position
28129 cx.update_editor(|editor, window, cx| {
28130 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28131 });
28132 cx.assert_editor_state(
28133 "let variable = 42;
28134let another = ˇvariable + 1;
28135let result = variable * 2;",
28136 );
28137
28138 // Go to previous highlight - should move to first "variable"
28139 cx.update_editor(|editor, window, cx| {
28140 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28141 });
28142 cx.assert_editor_state(
28143 "let ˇvariable = 42;
28144let another = variable + 1;
28145let result = variable * 2;",
28146 );
28147
28148 // Go to previous highlight - should stay on first "variable"
28149 cx.update_editor(|editor, window, cx| {
28150 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28151 });
28152 cx.assert_editor_state(
28153 "let ˇvariable = 42;
28154let another = variable + 1;
28155let result = variable * 2;",
28156 );
28157}
28158
28159#[gpui::test]
28160async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
28161 cx: &mut gpui::TestAppContext,
28162) {
28163 init_test(cx, |_| {});
28164
28165 let url = "https://zed.dev";
28166
28167 let markdown_language = Arc::new(Language::new(
28168 LanguageConfig {
28169 name: "Markdown".into(),
28170 ..LanguageConfig::default()
28171 },
28172 None,
28173 ));
28174
28175 let mut cx = EditorTestContext::new(cx).await;
28176 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28177 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
28178
28179 cx.update_editor(|editor, window, cx| {
28180 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28181 editor.paste(&Paste, window, cx);
28182 });
28183
28184 cx.assert_editor_state(&format!(
28185 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
28186 ));
28187}
28188
28189#[gpui::test]
28190async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
28191 init_test(cx, |_| {});
28192
28193 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
28194 let mut cx = EditorTestContext::new(cx).await;
28195
28196 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28197
28198 // Case 1: Test if adding a character with multi cursors preserves nested list indents
28199 cx.set_state(&indoc! {"
28200 - [ ] Item 1
28201 - [ ] Item 1.a
28202 - [ˇ] Item 2
28203 - [ˇ] Item 2.a
28204 - [ˇ] Item 2.b
28205 "
28206 });
28207 cx.update_editor(|editor, window, cx| {
28208 editor.handle_input("x", window, cx);
28209 });
28210 cx.run_until_parked();
28211 cx.assert_editor_state(indoc! {"
28212 - [ ] Item 1
28213 - [ ] Item 1.a
28214 - [xˇ] Item 2
28215 - [xˇ] Item 2.a
28216 - [xˇ] Item 2.b
28217 "
28218 });
28219
28220 // Case 2: Test adding new line after nested list continues the list with unchecked task
28221 cx.set_state(&indoc! {"
28222 - [ ] Item 1
28223 - [ ] Item 1.a
28224 - [x] Item 2
28225 - [x] Item 2.a
28226 - [x] Item 2.bˇ"
28227 });
28228 cx.update_editor(|editor, window, cx| {
28229 editor.newline(&Newline, window, cx);
28230 });
28231 cx.assert_editor_state(indoc! {"
28232 - [ ] Item 1
28233 - [ ] Item 1.a
28234 - [x] Item 2
28235 - [x] Item 2.a
28236 - [x] Item 2.b
28237 - [ ] ˇ"
28238 });
28239
28240 // Case 3: Test adding content to continued list item
28241 cx.update_editor(|editor, window, cx| {
28242 editor.handle_input("Item 2.c", window, cx);
28243 });
28244 cx.run_until_parked();
28245 cx.assert_editor_state(indoc! {"
28246 - [ ] Item 1
28247 - [ ] Item 1.a
28248 - [x] Item 2
28249 - [x] Item 2.a
28250 - [x] Item 2.b
28251 - [ ] Item 2.cˇ"
28252 });
28253
28254 // Case 4: Test adding new line after nested ordered list continues with next number
28255 cx.set_state(indoc! {"
28256 1. Item 1
28257 1. Item 1.a
28258 2. Item 2
28259 1. Item 2.a
28260 2. Item 2.bˇ"
28261 });
28262 cx.update_editor(|editor, window, cx| {
28263 editor.newline(&Newline, window, cx);
28264 });
28265 cx.assert_editor_state(indoc! {"
28266 1. Item 1
28267 1. Item 1.a
28268 2. Item 2
28269 1. Item 2.a
28270 2. Item 2.b
28271 3. ˇ"
28272 });
28273
28274 // Case 5: Adding content to continued ordered list item
28275 cx.update_editor(|editor, window, cx| {
28276 editor.handle_input("Item 2.c", window, cx);
28277 });
28278 cx.run_until_parked();
28279 cx.assert_editor_state(indoc! {"
28280 1. Item 1
28281 1. Item 1.a
28282 2. Item 2
28283 1. Item 2.a
28284 2. Item 2.b
28285 3. Item 2.cˇ"
28286 });
28287
28288 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
28289 cx.set_state(indoc! {"
28290 - Item 1
28291 - Item 1.a
28292 - Item 1.a
28293 ˇ"});
28294 cx.update_editor(|editor, window, cx| {
28295 editor.handle_input("-", window, cx);
28296 });
28297 cx.run_until_parked();
28298 cx.assert_editor_state(indoc! {"
28299 - Item 1
28300 - Item 1.a
28301 - Item 1.a
28302 -ˇ"});
28303
28304 // Case 7: Test blockquote newline preserves something
28305 cx.set_state(indoc! {"
28306 > Item 1ˇ"
28307 });
28308 cx.update_editor(|editor, window, cx| {
28309 editor.newline(&Newline, window, cx);
28310 });
28311 cx.assert_editor_state(indoc! {"
28312 > Item 1
28313 ˇ"
28314 });
28315}
28316
28317#[gpui::test]
28318async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
28319 cx: &mut gpui::TestAppContext,
28320) {
28321 init_test(cx, |_| {});
28322
28323 let url = "https://zed.dev";
28324
28325 let markdown_language = Arc::new(Language::new(
28326 LanguageConfig {
28327 name: "Markdown".into(),
28328 ..LanguageConfig::default()
28329 },
28330 None,
28331 ));
28332
28333 let mut cx = EditorTestContext::new(cx).await;
28334 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28335 cx.set_state(&format!(
28336 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
28337 ));
28338
28339 cx.update_editor(|editor, window, cx| {
28340 editor.copy(&Copy, window, cx);
28341 });
28342
28343 cx.set_state(&format!(
28344 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
28345 ));
28346
28347 cx.update_editor(|editor, window, cx| {
28348 editor.paste(&Paste, window, cx);
28349 });
28350
28351 cx.assert_editor_state(&format!(
28352 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
28353 ));
28354}
28355
28356#[gpui::test]
28357async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
28358 cx: &mut gpui::TestAppContext,
28359) {
28360 init_test(cx, |_| {});
28361
28362 let url = "https://zed.dev";
28363
28364 let markdown_language = Arc::new(Language::new(
28365 LanguageConfig {
28366 name: "Markdown".into(),
28367 ..LanguageConfig::default()
28368 },
28369 None,
28370 ));
28371
28372 let mut cx = EditorTestContext::new(cx).await;
28373 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28374 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
28375
28376 cx.update_editor(|editor, window, cx| {
28377 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28378 editor.paste(&Paste, window, cx);
28379 });
28380
28381 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
28382}
28383
28384#[gpui::test]
28385async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
28386 cx: &mut gpui::TestAppContext,
28387) {
28388 init_test(cx, |_| {});
28389
28390 let text = "Awesome";
28391
28392 let markdown_language = Arc::new(Language::new(
28393 LanguageConfig {
28394 name: "Markdown".into(),
28395 ..LanguageConfig::default()
28396 },
28397 None,
28398 ));
28399
28400 let mut cx = EditorTestContext::new(cx).await;
28401 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28402 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
28403
28404 cx.update_editor(|editor, window, cx| {
28405 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
28406 editor.paste(&Paste, window, cx);
28407 });
28408
28409 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
28410}
28411
28412#[gpui::test]
28413async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
28414 cx: &mut gpui::TestAppContext,
28415) {
28416 init_test(cx, |_| {});
28417
28418 let url = "https://zed.dev";
28419
28420 let markdown_language = Arc::new(Language::new(
28421 LanguageConfig {
28422 name: "Rust".into(),
28423 ..LanguageConfig::default()
28424 },
28425 None,
28426 ));
28427
28428 let mut cx = EditorTestContext::new(cx).await;
28429 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28430 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
28431
28432 cx.update_editor(|editor, window, cx| {
28433 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28434 editor.paste(&Paste, window, cx);
28435 });
28436
28437 cx.assert_editor_state(&format!(
28438 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
28439 ));
28440}
28441
28442#[gpui::test]
28443async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
28444 cx: &mut TestAppContext,
28445) {
28446 init_test(cx, |_| {});
28447
28448 let url = "https://zed.dev";
28449
28450 let markdown_language = Arc::new(Language::new(
28451 LanguageConfig {
28452 name: "Markdown".into(),
28453 ..LanguageConfig::default()
28454 },
28455 None,
28456 ));
28457
28458 let (editor, cx) = cx.add_window_view(|window, cx| {
28459 let multi_buffer = MultiBuffer::build_multi(
28460 [
28461 ("this will embed -> link", vec![Point::row_range(0..1)]),
28462 ("this will replace -> link", vec![Point::row_range(0..1)]),
28463 ],
28464 cx,
28465 );
28466 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
28467 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28468 s.select_ranges(vec![
28469 Point::new(0, 19)..Point::new(0, 23),
28470 Point::new(1, 21)..Point::new(1, 25),
28471 ])
28472 });
28473 let first_buffer_id = multi_buffer
28474 .read(cx)
28475 .excerpt_buffer_ids()
28476 .into_iter()
28477 .next()
28478 .unwrap();
28479 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
28480 first_buffer.update(cx, |buffer, cx| {
28481 buffer.set_language(Some(markdown_language.clone()), cx);
28482 });
28483
28484 editor
28485 });
28486 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28487
28488 cx.update_editor(|editor, window, cx| {
28489 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28490 editor.paste(&Paste, window, cx);
28491 });
28492
28493 cx.assert_editor_state(&format!(
28494 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
28495 ));
28496}
28497
28498#[gpui::test]
28499async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
28500 init_test(cx, |_| {});
28501
28502 let fs = FakeFs::new(cx.executor());
28503 fs.insert_tree(
28504 path!("/project"),
28505 json!({
28506 "first.rs": "# First Document\nSome content here.",
28507 "second.rs": "Plain text content for second file.",
28508 }),
28509 )
28510 .await;
28511
28512 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
28513 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
28514 let cx = &mut VisualTestContext::from_window(*workspace, cx);
28515
28516 let language = rust_lang();
28517 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28518 language_registry.add(language.clone());
28519 let mut fake_servers = language_registry.register_fake_lsp(
28520 "Rust",
28521 FakeLspAdapter {
28522 ..FakeLspAdapter::default()
28523 },
28524 );
28525
28526 let buffer1 = project
28527 .update(cx, |project, cx| {
28528 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
28529 })
28530 .await
28531 .unwrap();
28532 let buffer2 = project
28533 .update(cx, |project, cx| {
28534 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
28535 })
28536 .await
28537 .unwrap();
28538
28539 let multi_buffer = cx.new(|cx| {
28540 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
28541 multi_buffer.set_excerpts_for_path(
28542 PathKey::for_buffer(&buffer1, cx),
28543 buffer1.clone(),
28544 [Point::zero()..buffer1.read(cx).max_point()],
28545 3,
28546 cx,
28547 );
28548 multi_buffer.set_excerpts_for_path(
28549 PathKey::for_buffer(&buffer2, cx),
28550 buffer2.clone(),
28551 [Point::zero()..buffer1.read(cx).max_point()],
28552 3,
28553 cx,
28554 );
28555 multi_buffer
28556 });
28557
28558 let (editor, cx) = cx.add_window_view(|window, cx| {
28559 Editor::new(
28560 EditorMode::full(),
28561 multi_buffer,
28562 Some(project.clone()),
28563 window,
28564 cx,
28565 )
28566 });
28567
28568 let fake_language_server = fake_servers.next().await.unwrap();
28569
28570 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
28571
28572 let save = editor.update_in(cx, |editor, window, cx| {
28573 assert!(editor.is_dirty(cx));
28574
28575 editor.save(
28576 SaveOptions {
28577 format: true,
28578 autosave: true,
28579 },
28580 project,
28581 window,
28582 cx,
28583 )
28584 });
28585 let (start_edit_tx, start_edit_rx) = oneshot::channel();
28586 let (done_edit_tx, done_edit_rx) = oneshot::channel();
28587 let mut done_edit_rx = Some(done_edit_rx);
28588 let mut start_edit_tx = Some(start_edit_tx);
28589
28590 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
28591 start_edit_tx.take().unwrap().send(()).unwrap();
28592 let done_edit_rx = done_edit_rx.take().unwrap();
28593 async move {
28594 done_edit_rx.await.unwrap();
28595 Ok(None)
28596 }
28597 });
28598
28599 start_edit_rx.await.unwrap();
28600 buffer2
28601 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
28602 .unwrap();
28603
28604 done_edit_tx.send(()).unwrap();
28605
28606 save.await.unwrap();
28607 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
28608}
28609
28610#[track_caller]
28611fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
28612 editor
28613 .all_inlays(cx)
28614 .into_iter()
28615 .filter_map(|inlay| inlay.get_color())
28616 .map(Rgba::from)
28617 .collect()
28618}
28619
28620#[gpui::test]
28621fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
28622 init_test(cx, |_| {});
28623
28624 let editor = cx.add_window(|window, cx| {
28625 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
28626 build_editor(buffer, window, cx)
28627 });
28628
28629 editor
28630 .update(cx, |editor, window, cx| {
28631 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28632 s.select_display_ranges([
28633 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
28634 ])
28635 });
28636
28637 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
28638
28639 assert_eq!(
28640 editor.display_text(cx),
28641 "line1\nline2\nline2",
28642 "Duplicating last line upward should create duplicate above, not on same line"
28643 );
28644
28645 assert_eq!(
28646 editor
28647 .selections
28648 .display_ranges(&editor.display_snapshot(cx)),
28649 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
28650 "Selection should move to the duplicated line"
28651 );
28652 })
28653 .unwrap();
28654}
28655
28656#[gpui::test]
28657async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
28658 init_test(cx, |_| {});
28659
28660 let mut cx = EditorTestContext::new(cx).await;
28661
28662 cx.set_state("line1\nline2ˇ");
28663
28664 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28665
28666 let clipboard_text = cx
28667 .read_from_clipboard()
28668 .and_then(|item| item.text().as_deref().map(str::to_string));
28669
28670 assert_eq!(
28671 clipboard_text,
28672 Some("line2\n".to_string()),
28673 "Copying a line without trailing newline should include a newline"
28674 );
28675
28676 cx.set_state("line1\nˇ");
28677
28678 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28679
28680 cx.assert_editor_state("line1\nline2\nˇ");
28681}
28682
28683#[gpui::test]
28684async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28685 init_test(cx, |_| {});
28686
28687 let mut cx = EditorTestContext::new(cx).await;
28688
28689 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28690
28691 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28692
28693 let clipboard_text = cx
28694 .read_from_clipboard()
28695 .and_then(|item| item.text().as_deref().map(str::to_string));
28696
28697 assert_eq!(
28698 clipboard_text,
28699 Some("line1\nline2\nline3\n".to_string()),
28700 "Copying multiple lines should include a single newline between lines"
28701 );
28702
28703 cx.set_state("lineA\nˇ");
28704
28705 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28706
28707 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28708}
28709
28710#[gpui::test]
28711async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28712 init_test(cx, |_| {});
28713
28714 let mut cx = EditorTestContext::new(cx).await;
28715
28716 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28717
28718 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
28719
28720 let clipboard_text = cx
28721 .read_from_clipboard()
28722 .and_then(|item| item.text().as_deref().map(str::to_string));
28723
28724 assert_eq!(
28725 clipboard_text,
28726 Some("line1\nline2\nline3\n".to_string()),
28727 "Copying multiple lines should include a single newline between lines"
28728 );
28729
28730 cx.set_state("lineA\nˇ");
28731
28732 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28733
28734 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28735}
28736
28737#[gpui::test]
28738async fn test_end_of_editor_context(cx: &mut TestAppContext) {
28739 init_test(cx, |_| {});
28740
28741 let mut cx = EditorTestContext::new(cx).await;
28742
28743 cx.set_state("line1\nline2ˇ");
28744 cx.update_editor(|e, window, cx| {
28745 e.set_mode(EditorMode::SingleLine);
28746 assert!(e.key_context(window, cx).contains("end_of_input"));
28747 });
28748 cx.set_state("ˇline1\nline2");
28749 cx.update_editor(|e, window, cx| {
28750 assert!(!e.key_context(window, cx).contains("end_of_input"));
28751 });
28752 cx.set_state("line1ˇ\nline2");
28753 cx.update_editor(|e, window, cx| {
28754 assert!(!e.key_context(window, cx).contains("end_of_input"));
28755 });
28756}
28757
28758#[gpui::test]
28759async fn test_sticky_scroll(cx: &mut TestAppContext) {
28760 init_test(cx, |_| {});
28761 let mut cx = EditorTestContext::new(cx).await;
28762
28763 let buffer = indoc! {"
28764 ˇfn foo() {
28765 let abc = 123;
28766 }
28767 struct Bar;
28768 impl Bar {
28769 fn new() -> Self {
28770 Self
28771 }
28772 }
28773 fn baz() {
28774 }
28775 "};
28776 cx.set_state(&buffer);
28777
28778 cx.update_editor(|e, _, cx| {
28779 e.buffer()
28780 .read(cx)
28781 .as_singleton()
28782 .unwrap()
28783 .update(cx, |buffer, cx| {
28784 buffer.set_language(Some(rust_lang()), cx);
28785 })
28786 });
28787
28788 let mut sticky_headers = |offset: ScrollOffset| {
28789 cx.update_editor(|e, window, cx| {
28790 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
28791 let style = e.style(cx).clone();
28792 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), &style, cx)
28793 .into_iter()
28794 .map(
28795 |StickyHeader {
28796 start_point,
28797 offset,
28798 ..
28799 }| { (start_point, offset) },
28800 )
28801 .collect::<Vec<_>>()
28802 })
28803 };
28804
28805 let fn_foo = Point { row: 0, column: 0 };
28806 let impl_bar = Point { row: 4, column: 0 };
28807 let fn_new = Point { row: 5, column: 4 };
28808
28809 assert_eq!(sticky_headers(0.0), vec![]);
28810 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
28811 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
28812 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
28813 assert_eq!(sticky_headers(2.0), vec![]);
28814 assert_eq!(sticky_headers(2.5), vec![]);
28815 assert_eq!(sticky_headers(3.0), vec![]);
28816 assert_eq!(sticky_headers(3.5), vec![]);
28817 assert_eq!(sticky_headers(4.0), vec![]);
28818 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28819 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28820 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
28821 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
28822 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
28823 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
28824 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
28825 assert_eq!(sticky_headers(8.0), vec![]);
28826 assert_eq!(sticky_headers(8.5), vec![]);
28827 assert_eq!(sticky_headers(9.0), vec![]);
28828 assert_eq!(sticky_headers(9.5), vec![]);
28829 assert_eq!(sticky_headers(10.0), vec![]);
28830}
28831
28832#[gpui::test]
28833fn test_relative_line_numbers(cx: &mut TestAppContext) {
28834 init_test(cx, |_| {});
28835
28836 let buffer_1 = cx.new(|cx| Buffer::local("aaaaaaaaaa\nbbb\n", cx));
28837 let buffer_2 = cx.new(|cx| Buffer::local("cccccccccc\nddd\n", cx));
28838 let buffer_3 = cx.new(|cx| Buffer::local("eee\nffffffffff\n", cx));
28839
28840 let multibuffer = cx.new(|cx| {
28841 let mut multibuffer = MultiBuffer::new(ReadWrite);
28842 multibuffer.push_excerpts(
28843 buffer_1.clone(),
28844 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
28845 cx,
28846 );
28847 multibuffer.push_excerpts(
28848 buffer_2.clone(),
28849 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
28850 cx,
28851 );
28852 multibuffer.push_excerpts(
28853 buffer_3.clone(),
28854 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
28855 cx,
28856 );
28857 multibuffer
28858 });
28859
28860 // wrapped contents of multibuffer:
28861 // aaa
28862 // aaa
28863 // aaa
28864 // a
28865 // bbb
28866 //
28867 // ccc
28868 // ccc
28869 // ccc
28870 // c
28871 // ddd
28872 //
28873 // eee
28874 // fff
28875 // fff
28876 // fff
28877 // f
28878
28879 let editor = cx.add_window(|window, cx| build_editor(multibuffer, window, cx));
28880 _ = editor.update(cx, |editor, window, cx| {
28881 editor.set_wrap_width(Some(30.0.into()), cx); // every 3 characters
28882
28883 // includes trailing newlines.
28884 let expected_line_numbers = [2, 6, 7, 10, 14, 15, 18, 19, 23];
28885 let expected_wrapped_line_numbers = [
28886 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23,
28887 ];
28888
28889 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28890 s.select_ranges([
28891 Point::new(7, 0)..Point::new(7, 1), // second row of `ccc`
28892 ]);
28893 });
28894
28895 let snapshot = editor.snapshot(window, cx);
28896
28897 // these are all 0-indexed
28898 let base_display_row = DisplayRow(11);
28899 let base_row = 3;
28900 let wrapped_base_row = 7;
28901
28902 // test not counting wrapped lines
28903 let expected_relative_numbers = expected_line_numbers
28904 .into_iter()
28905 .enumerate()
28906 .map(|(i, row)| (DisplayRow(row), i.abs_diff(base_row) as u32))
28907 .collect_vec();
28908 let actual_relative_numbers = snapshot
28909 .calculate_relative_line_numbers(
28910 &(DisplayRow(0)..DisplayRow(24)),
28911 base_display_row,
28912 false,
28913 )
28914 .into_iter()
28915 .sorted()
28916 .collect_vec();
28917 assert_eq!(expected_relative_numbers, actual_relative_numbers);
28918 // check `calculate_relative_line_numbers()` against `relative_line_delta()` for each line
28919 for (display_row, relative_number) in expected_relative_numbers {
28920 assert_eq!(
28921 relative_number,
28922 snapshot
28923 .relative_line_delta(display_row, base_display_row, false)
28924 .unsigned_abs() as u32,
28925 );
28926 }
28927
28928 // test counting wrapped lines
28929 let expected_wrapped_relative_numbers = expected_wrapped_line_numbers
28930 .into_iter()
28931 .enumerate()
28932 .map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32))
28933 .filter(|(row, _)| *row != base_display_row)
28934 .collect_vec();
28935 let actual_relative_numbers = snapshot
28936 .calculate_relative_line_numbers(
28937 &(DisplayRow(0)..DisplayRow(24)),
28938 base_display_row,
28939 true,
28940 )
28941 .into_iter()
28942 .sorted()
28943 .collect_vec();
28944 assert_eq!(expected_wrapped_relative_numbers, actual_relative_numbers);
28945 // check `calculate_relative_line_numbers()` against `relative_wrapped_line_delta()` for each line
28946 for (display_row, relative_number) in expected_wrapped_relative_numbers {
28947 assert_eq!(
28948 relative_number,
28949 snapshot
28950 .relative_line_delta(display_row, base_display_row, true)
28951 .unsigned_abs() as u32,
28952 );
28953 }
28954 });
28955}
28956
28957#[gpui::test]
28958async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
28959 init_test(cx, |_| {});
28960 cx.update(|cx| {
28961 SettingsStore::update_global(cx, |store, cx| {
28962 store.update_user_settings(cx, |settings| {
28963 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
28964 enabled: Some(true),
28965 })
28966 });
28967 });
28968 });
28969 let mut cx = EditorTestContext::new(cx).await;
28970
28971 let line_height = cx.update_editor(|editor, window, cx| {
28972 editor
28973 .style(cx)
28974 .text
28975 .line_height_in_pixels(window.rem_size())
28976 });
28977
28978 let buffer = indoc! {"
28979 ˇfn foo() {
28980 let abc = 123;
28981 }
28982 struct Bar;
28983 impl Bar {
28984 fn new() -> Self {
28985 Self
28986 }
28987 }
28988 fn baz() {
28989 }
28990 "};
28991 cx.set_state(&buffer);
28992
28993 cx.update_editor(|e, _, cx| {
28994 e.buffer()
28995 .read(cx)
28996 .as_singleton()
28997 .unwrap()
28998 .update(cx, |buffer, cx| {
28999 buffer.set_language(Some(rust_lang()), cx);
29000 })
29001 });
29002
29003 let fn_foo = || empty_range(0, 0);
29004 let impl_bar = || empty_range(4, 0);
29005 let fn_new = || empty_range(5, 4);
29006
29007 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
29008 cx.update_editor(|e, window, cx| {
29009 e.scroll(
29010 gpui::Point {
29011 x: 0.,
29012 y: scroll_offset,
29013 },
29014 None,
29015 window,
29016 cx,
29017 );
29018 });
29019 cx.simulate_click(
29020 gpui::Point {
29021 x: px(0.),
29022 y: click_offset as f32 * line_height,
29023 },
29024 Modifiers::none(),
29025 );
29026 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
29027 };
29028
29029 assert_eq!(
29030 scroll_and_click(
29031 4.5, // impl Bar is halfway off the screen
29032 0.0 // click top of screen
29033 ),
29034 // scrolled to impl Bar
29035 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29036 );
29037
29038 assert_eq!(
29039 scroll_and_click(
29040 4.5, // impl Bar is halfway off the screen
29041 0.25 // click middle of impl Bar
29042 ),
29043 // scrolled to impl Bar
29044 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29045 );
29046
29047 assert_eq!(
29048 scroll_and_click(
29049 4.5, // impl Bar is halfway off the screen
29050 1.5 // click below impl Bar (e.g. fn new())
29051 ),
29052 // scrolled to fn new() - this is below the impl Bar header which has persisted
29053 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
29054 );
29055
29056 assert_eq!(
29057 scroll_and_click(
29058 5.5, // fn new is halfway underneath impl Bar
29059 0.75 // click on the overlap of impl Bar and fn new()
29060 ),
29061 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29062 );
29063
29064 assert_eq!(
29065 scroll_and_click(
29066 5.5, // fn new is halfway underneath impl Bar
29067 1.25 // click on the visible part of fn new()
29068 ),
29069 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
29070 );
29071
29072 assert_eq!(
29073 scroll_and_click(
29074 1.5, // fn foo is halfway off the screen
29075 0.0 // click top of screen
29076 ),
29077 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
29078 );
29079
29080 assert_eq!(
29081 scroll_and_click(
29082 1.5, // fn foo is halfway off the screen
29083 0.75 // click visible part of let abc...
29084 )
29085 .0,
29086 // no change in scroll
29087 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
29088 (gpui::Point { x: 0., y: 1.5 })
29089 );
29090}
29091
29092#[gpui::test]
29093async fn test_next_prev_reference(cx: &mut TestAppContext) {
29094 const CYCLE_POSITIONS: &[&'static str] = &[
29095 indoc! {"
29096 fn foo() {
29097 let ˇabc = 123;
29098 let x = abc + 1;
29099 let y = abc + 2;
29100 let z = abc + 2;
29101 }
29102 "},
29103 indoc! {"
29104 fn foo() {
29105 let abc = 123;
29106 let x = ˇabc + 1;
29107 let y = abc + 2;
29108 let z = abc + 2;
29109 }
29110 "},
29111 indoc! {"
29112 fn foo() {
29113 let abc = 123;
29114 let x = abc + 1;
29115 let y = ˇabc + 2;
29116 let z = abc + 2;
29117 }
29118 "},
29119 indoc! {"
29120 fn foo() {
29121 let abc = 123;
29122 let x = abc + 1;
29123 let y = abc + 2;
29124 let z = ˇabc + 2;
29125 }
29126 "},
29127 ];
29128
29129 init_test(cx, |_| {});
29130
29131 let mut cx = EditorLspTestContext::new_rust(
29132 lsp::ServerCapabilities {
29133 references_provider: Some(lsp::OneOf::Left(true)),
29134 ..Default::default()
29135 },
29136 cx,
29137 )
29138 .await;
29139
29140 // importantly, the cursor is in the middle
29141 cx.set_state(indoc! {"
29142 fn foo() {
29143 let aˇbc = 123;
29144 let x = abc + 1;
29145 let y = abc + 2;
29146 let z = abc + 2;
29147 }
29148 "});
29149
29150 let reference_ranges = [
29151 lsp::Position::new(1, 8),
29152 lsp::Position::new(2, 12),
29153 lsp::Position::new(3, 12),
29154 lsp::Position::new(4, 12),
29155 ]
29156 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
29157
29158 cx.lsp
29159 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
29160 Ok(Some(
29161 reference_ranges
29162 .map(|range| lsp::Location {
29163 uri: params.text_document_position.text_document.uri.clone(),
29164 range,
29165 })
29166 .to_vec(),
29167 ))
29168 });
29169
29170 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
29171 cx.update_editor(|editor, window, cx| {
29172 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
29173 })
29174 .unwrap()
29175 .await
29176 .unwrap()
29177 };
29178
29179 _move(Direction::Next, 1, &mut cx).await;
29180 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29181
29182 _move(Direction::Next, 1, &mut cx).await;
29183 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29184
29185 _move(Direction::Next, 1, &mut cx).await;
29186 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29187
29188 // loops back to the start
29189 _move(Direction::Next, 1, &mut cx).await;
29190 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29191
29192 // loops back to the end
29193 _move(Direction::Prev, 1, &mut cx).await;
29194 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29195
29196 _move(Direction::Prev, 1, &mut cx).await;
29197 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29198
29199 _move(Direction::Prev, 1, &mut cx).await;
29200 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29201
29202 _move(Direction::Prev, 1, &mut cx).await;
29203 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29204
29205 _move(Direction::Next, 3, &mut cx).await;
29206 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29207
29208 _move(Direction::Prev, 2, &mut cx).await;
29209 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29210}
29211
29212#[gpui::test]
29213async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
29214 init_test(cx, |_| {});
29215
29216 let (editor, cx) = cx.add_window_view(|window, cx| {
29217 let multi_buffer = MultiBuffer::build_multi(
29218 [
29219 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29220 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29221 ],
29222 cx,
29223 );
29224 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29225 });
29226
29227 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29228 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
29229
29230 cx.assert_excerpts_with_selections(indoc! {"
29231 [EXCERPT]
29232 ˇ1
29233 2
29234 3
29235 [EXCERPT]
29236 1
29237 2
29238 3
29239 "});
29240
29241 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
29242 cx.update_editor(|editor, window, cx| {
29243 editor.change_selections(None.into(), window, cx, |s| {
29244 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29245 });
29246 });
29247 cx.assert_excerpts_with_selections(indoc! {"
29248 [EXCERPT]
29249 1
29250 2ˇ
29251 3
29252 [EXCERPT]
29253 1
29254 2
29255 3
29256 "});
29257
29258 cx.update_editor(|editor, window, cx| {
29259 editor
29260 .select_all_matches(&SelectAllMatches, window, cx)
29261 .unwrap();
29262 });
29263 cx.assert_excerpts_with_selections(indoc! {"
29264 [EXCERPT]
29265 1
29266 2ˇ
29267 3
29268 [EXCERPT]
29269 1
29270 2ˇ
29271 3
29272 "});
29273
29274 cx.update_editor(|editor, window, cx| {
29275 editor.handle_input("X", window, cx);
29276 });
29277 cx.assert_excerpts_with_selections(indoc! {"
29278 [EXCERPT]
29279 1
29280 Xˇ
29281 3
29282 [EXCERPT]
29283 1
29284 Xˇ
29285 3
29286 "});
29287
29288 // Scenario 2: Select "2", then fold second buffer before insertion
29289 cx.update_multibuffer(|mb, cx| {
29290 for buffer_id in buffer_ids.iter() {
29291 let buffer = mb.buffer(*buffer_id).unwrap();
29292 buffer.update(cx, |buffer, cx| {
29293 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29294 });
29295 }
29296 });
29297
29298 // Select "2" and select all matches
29299 cx.update_editor(|editor, window, cx| {
29300 editor.change_selections(None.into(), window, cx, |s| {
29301 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29302 });
29303 editor
29304 .select_all_matches(&SelectAllMatches, window, cx)
29305 .unwrap();
29306 });
29307
29308 // Fold second buffer - should remove selections from folded buffer
29309 cx.update_editor(|editor, _, cx| {
29310 editor.fold_buffer(buffer_ids[1], cx);
29311 });
29312 cx.assert_excerpts_with_selections(indoc! {"
29313 [EXCERPT]
29314 1
29315 2ˇ
29316 3
29317 [EXCERPT]
29318 [FOLDED]
29319 "});
29320
29321 // Insert text - should only affect first buffer
29322 cx.update_editor(|editor, window, cx| {
29323 editor.handle_input("Y", window, cx);
29324 });
29325 cx.update_editor(|editor, _, cx| {
29326 editor.unfold_buffer(buffer_ids[1], cx);
29327 });
29328 cx.assert_excerpts_with_selections(indoc! {"
29329 [EXCERPT]
29330 1
29331 Yˇ
29332 3
29333 [EXCERPT]
29334 1
29335 2
29336 3
29337 "});
29338
29339 // Scenario 3: Select "2", then fold first buffer before insertion
29340 cx.update_multibuffer(|mb, cx| {
29341 for buffer_id in buffer_ids.iter() {
29342 let buffer = mb.buffer(*buffer_id).unwrap();
29343 buffer.update(cx, |buffer, cx| {
29344 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29345 });
29346 }
29347 });
29348
29349 // Select "2" and select all matches
29350 cx.update_editor(|editor, window, cx| {
29351 editor.change_selections(None.into(), window, cx, |s| {
29352 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29353 });
29354 editor
29355 .select_all_matches(&SelectAllMatches, window, cx)
29356 .unwrap();
29357 });
29358
29359 // Fold first buffer - should remove selections from folded buffer
29360 cx.update_editor(|editor, _, cx| {
29361 editor.fold_buffer(buffer_ids[0], cx);
29362 });
29363 cx.assert_excerpts_with_selections(indoc! {"
29364 [EXCERPT]
29365 [FOLDED]
29366 [EXCERPT]
29367 1
29368 2ˇ
29369 3
29370 "});
29371
29372 // Insert text - should only affect second buffer
29373 cx.update_editor(|editor, window, cx| {
29374 editor.handle_input("Z", window, cx);
29375 });
29376 cx.update_editor(|editor, _, cx| {
29377 editor.unfold_buffer(buffer_ids[0], cx);
29378 });
29379 cx.assert_excerpts_with_selections(indoc! {"
29380 [EXCERPT]
29381 1
29382 2
29383 3
29384 [EXCERPT]
29385 1
29386 Zˇ
29387 3
29388 "});
29389
29390 // Test correct folded header is selected upon fold
29391 cx.update_editor(|editor, _, cx| {
29392 editor.fold_buffer(buffer_ids[0], cx);
29393 editor.fold_buffer(buffer_ids[1], cx);
29394 });
29395 cx.assert_excerpts_with_selections(indoc! {"
29396 [EXCERPT]
29397 [FOLDED]
29398 [EXCERPT]
29399 ˇ[FOLDED]
29400 "});
29401
29402 // Test selection inside folded buffer unfolds it on type
29403 cx.update_editor(|editor, window, cx| {
29404 editor.handle_input("W", window, cx);
29405 });
29406 cx.update_editor(|editor, _, cx| {
29407 editor.unfold_buffer(buffer_ids[0], cx);
29408 });
29409 cx.assert_excerpts_with_selections(indoc! {"
29410 [EXCERPT]
29411 1
29412 2
29413 3
29414 [EXCERPT]
29415 Wˇ1
29416 Z
29417 3
29418 "});
29419}
29420
29421#[gpui::test]
29422async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
29423 init_test(cx, |_| {});
29424
29425 let (editor, cx) = cx.add_window_view(|window, cx| {
29426 let multi_buffer = MultiBuffer::build_multi(
29427 [
29428 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29429 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
29430 ],
29431 cx,
29432 );
29433 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29434 });
29435
29436 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29437
29438 cx.assert_excerpts_with_selections(indoc! {"
29439 [EXCERPT]
29440 ˇ1
29441 2
29442 3
29443 [EXCERPT]
29444 1
29445 2
29446 3
29447 4
29448 5
29449 6
29450 7
29451 8
29452 9
29453 "});
29454
29455 cx.update_editor(|editor, window, cx| {
29456 editor.change_selections(None.into(), window, cx, |s| {
29457 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
29458 });
29459 });
29460
29461 cx.assert_excerpts_with_selections(indoc! {"
29462 [EXCERPT]
29463 1
29464 2
29465 3
29466 [EXCERPT]
29467 1
29468 2
29469 3
29470 4
29471 5
29472 6
29473 ˇ7
29474 8
29475 9
29476 "});
29477
29478 cx.update_editor(|editor, _window, cx| {
29479 editor.set_vertical_scroll_margin(0, cx);
29480 });
29481
29482 cx.update_editor(|editor, window, cx| {
29483 assert_eq!(editor.vertical_scroll_margin(), 0);
29484 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29485 assert_eq!(
29486 editor.snapshot(window, cx).scroll_position(),
29487 gpui::Point::new(0., 12.0)
29488 );
29489 });
29490
29491 cx.update_editor(|editor, _window, cx| {
29492 editor.set_vertical_scroll_margin(3, cx);
29493 });
29494
29495 cx.update_editor(|editor, window, cx| {
29496 assert_eq!(editor.vertical_scroll_margin(), 3);
29497 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29498 assert_eq!(
29499 editor.snapshot(window, cx).scroll_position(),
29500 gpui::Point::new(0., 9.0)
29501 );
29502 });
29503}
29504
29505#[gpui::test]
29506async fn test_find_references_single_case(cx: &mut TestAppContext) {
29507 init_test(cx, |_| {});
29508 let mut cx = EditorLspTestContext::new_rust(
29509 lsp::ServerCapabilities {
29510 references_provider: Some(lsp::OneOf::Left(true)),
29511 ..lsp::ServerCapabilities::default()
29512 },
29513 cx,
29514 )
29515 .await;
29516
29517 let before = indoc!(
29518 r#"
29519 fn main() {
29520 let aˇbc = 123;
29521 let xyz = abc;
29522 }
29523 "#
29524 );
29525 let after = indoc!(
29526 r#"
29527 fn main() {
29528 let abc = 123;
29529 let xyz = ˇabc;
29530 }
29531 "#
29532 );
29533
29534 cx.lsp
29535 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
29536 Ok(Some(vec![
29537 lsp::Location {
29538 uri: params.text_document_position.text_document.uri.clone(),
29539 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
29540 },
29541 lsp::Location {
29542 uri: params.text_document_position.text_document.uri,
29543 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
29544 },
29545 ]))
29546 });
29547
29548 cx.set_state(before);
29549
29550 let action = FindAllReferences {
29551 always_open_multibuffer: false,
29552 };
29553
29554 let navigated = cx
29555 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
29556 .expect("should have spawned a task")
29557 .await
29558 .unwrap();
29559
29560 assert_eq!(navigated, Navigated::No);
29561
29562 cx.run_until_parked();
29563
29564 cx.assert_editor_state(after);
29565}
29566
29567#[gpui::test]
29568async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
29569 init_test(cx, |settings| {
29570 settings.defaults.tab_size = Some(2.try_into().unwrap());
29571 });
29572
29573 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29574 let mut cx = EditorTestContext::new(cx).await;
29575 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29576
29577 // Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
29578 cx.set_state(indoc! {"
29579 - [ ] taskˇ
29580 "});
29581 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29582 cx.wait_for_autoindent_applied().await;
29583 cx.assert_editor_state(indoc! {"
29584 - [ ] task
29585 - [ ] ˇ
29586 "});
29587
29588 // Case 2: Works with checked task items too
29589 cx.set_state(indoc! {"
29590 - [x] completed taskˇ
29591 "});
29592 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29593 cx.wait_for_autoindent_applied().await;
29594 cx.assert_editor_state(indoc! {"
29595 - [x] completed task
29596 - [ ] ˇ
29597 "});
29598
29599 // Case 2.1: Works with uppercase checked marker too
29600 cx.set_state(indoc! {"
29601 - [X] completed taskˇ
29602 "});
29603 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29604 cx.wait_for_autoindent_applied().await;
29605 cx.assert_editor_state(indoc! {"
29606 - [X] completed task
29607 - [ ] ˇ
29608 "});
29609
29610 // Case 3: Cursor position doesn't matter - content after marker is what counts
29611 cx.set_state(indoc! {"
29612 - [ ] taˇsk
29613 "});
29614 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29615 cx.wait_for_autoindent_applied().await;
29616 cx.assert_editor_state(indoc! {"
29617 - [ ] ta
29618 - [ ] ˇsk
29619 "});
29620
29621 // Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
29622 cx.set_state(indoc! {"
29623 - [ ] ˇ
29624 "});
29625 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29626 cx.wait_for_autoindent_applied().await;
29627 cx.assert_editor_state(
29628 indoc! {"
29629 - [ ]$$
29630 ˇ
29631 "}
29632 .replace("$", " ")
29633 .as_str(),
29634 );
29635
29636 // Case 5: Adding newline with content adds marker preserving indentation
29637 cx.set_state(indoc! {"
29638 - [ ] task
29639 - [ ] indentedˇ
29640 "});
29641 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29642 cx.wait_for_autoindent_applied().await;
29643 cx.assert_editor_state(indoc! {"
29644 - [ ] task
29645 - [ ] indented
29646 - [ ] ˇ
29647 "});
29648
29649 // Case 6: Adding newline with cursor right after prefix, unindents
29650 cx.set_state(indoc! {"
29651 - [ ] task
29652 - [ ] sub task
29653 - [ ] ˇ
29654 "});
29655 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29656 cx.wait_for_autoindent_applied().await;
29657 cx.assert_editor_state(indoc! {"
29658 - [ ] task
29659 - [ ] sub task
29660 - [ ] ˇ
29661 "});
29662 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29663 cx.wait_for_autoindent_applied().await;
29664
29665 // Case 7: Adding newline with cursor right after prefix, removes marker
29666 cx.assert_editor_state(indoc! {"
29667 - [ ] task
29668 - [ ] sub task
29669 - [ ] ˇ
29670 "});
29671 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29672 cx.wait_for_autoindent_applied().await;
29673 cx.assert_editor_state(indoc! {"
29674 - [ ] task
29675 - [ ] sub task
29676 ˇ
29677 "});
29678
29679 // Case 8: Cursor before or inside prefix does not add marker
29680 cx.set_state(indoc! {"
29681 ˇ- [ ] task
29682 "});
29683 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29684 cx.wait_for_autoindent_applied().await;
29685 cx.assert_editor_state(indoc! {"
29686
29687 ˇ- [ ] task
29688 "});
29689
29690 cx.set_state(indoc! {"
29691 - [ˇ ] task
29692 "});
29693 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29694 cx.wait_for_autoindent_applied().await;
29695 cx.assert_editor_state(indoc! {"
29696 - [
29697 ˇ
29698 ] task
29699 "});
29700}
29701
29702#[gpui::test]
29703async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
29704 init_test(cx, |settings| {
29705 settings.defaults.tab_size = Some(2.try_into().unwrap());
29706 });
29707
29708 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29709 let mut cx = EditorTestContext::new(cx).await;
29710 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29711
29712 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
29713 cx.set_state(indoc! {"
29714 - itemˇ
29715 "});
29716 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29717 cx.wait_for_autoindent_applied().await;
29718 cx.assert_editor_state(indoc! {"
29719 - item
29720 - ˇ
29721 "});
29722
29723 // Case 2: Works with different markers
29724 cx.set_state(indoc! {"
29725 * starred itemˇ
29726 "});
29727 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29728 cx.wait_for_autoindent_applied().await;
29729 cx.assert_editor_state(indoc! {"
29730 * starred item
29731 * ˇ
29732 "});
29733
29734 cx.set_state(indoc! {"
29735 + plus itemˇ
29736 "});
29737 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29738 cx.wait_for_autoindent_applied().await;
29739 cx.assert_editor_state(indoc! {"
29740 + plus item
29741 + ˇ
29742 "});
29743
29744 // Case 3: Cursor position doesn't matter - content after marker is what counts
29745 cx.set_state(indoc! {"
29746 - itˇem
29747 "});
29748 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29749 cx.wait_for_autoindent_applied().await;
29750 cx.assert_editor_state(indoc! {"
29751 - it
29752 - ˇem
29753 "});
29754
29755 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
29756 cx.set_state(indoc! {"
29757 - ˇ
29758 "});
29759 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29760 cx.wait_for_autoindent_applied().await;
29761 cx.assert_editor_state(
29762 indoc! {"
29763 - $
29764 ˇ
29765 "}
29766 .replace("$", " ")
29767 .as_str(),
29768 );
29769
29770 // Case 5: Adding newline with content adds marker preserving indentation
29771 cx.set_state(indoc! {"
29772 - item
29773 - indentedˇ
29774 "});
29775 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29776 cx.wait_for_autoindent_applied().await;
29777 cx.assert_editor_state(indoc! {"
29778 - item
29779 - indented
29780 - ˇ
29781 "});
29782
29783 // Case 6: Adding newline with cursor right after marker, unindents
29784 cx.set_state(indoc! {"
29785 - item
29786 - sub item
29787 - ˇ
29788 "});
29789 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29790 cx.wait_for_autoindent_applied().await;
29791 cx.assert_editor_state(indoc! {"
29792 - item
29793 - sub item
29794 - ˇ
29795 "});
29796 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29797 cx.wait_for_autoindent_applied().await;
29798
29799 // Case 7: Adding newline with cursor right after marker, removes marker
29800 cx.assert_editor_state(indoc! {"
29801 - item
29802 - sub item
29803 - ˇ
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 - item
29809 - sub item
29810 ˇ
29811 "});
29812
29813 // Case 8: Cursor before or inside prefix does not add marker
29814 cx.set_state(indoc! {"
29815 ˇ- item
29816 "});
29817 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29818 cx.wait_for_autoindent_applied().await;
29819 cx.assert_editor_state(indoc! {"
29820
29821 ˇ- item
29822 "});
29823
29824 cx.set_state(indoc! {"
29825 -ˇ item
29826 "});
29827 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29828 cx.wait_for_autoindent_applied().await;
29829 cx.assert_editor_state(indoc! {"
29830 -
29831 ˇitem
29832 "});
29833}
29834
29835#[gpui::test]
29836async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
29837 init_test(cx, |settings| {
29838 settings.defaults.tab_size = Some(2.try_into().unwrap());
29839 });
29840
29841 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29842 let mut cx = EditorTestContext::new(cx).await;
29843 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29844
29845 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
29846 cx.set_state(indoc! {"
29847 1. first itemˇ
29848 "});
29849 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29850 cx.wait_for_autoindent_applied().await;
29851 cx.assert_editor_state(indoc! {"
29852 1. first item
29853 2. ˇ
29854 "});
29855
29856 // Case 2: Works with larger numbers
29857 cx.set_state(indoc! {"
29858 10. tenth itemˇ
29859 "});
29860 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29861 cx.wait_for_autoindent_applied().await;
29862 cx.assert_editor_state(indoc! {"
29863 10. tenth item
29864 11. ˇ
29865 "});
29866
29867 // Case 3: Cursor position doesn't matter - content after marker is what counts
29868 cx.set_state(indoc! {"
29869 1. itˇem
29870 "});
29871 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29872 cx.wait_for_autoindent_applied().await;
29873 cx.assert_editor_state(indoc! {"
29874 1. it
29875 2. ˇem
29876 "});
29877
29878 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
29879 cx.set_state(indoc! {"
29880 1. ˇ
29881 "});
29882 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29883 cx.wait_for_autoindent_applied().await;
29884 cx.assert_editor_state(
29885 indoc! {"
29886 1. $
29887 ˇ
29888 "}
29889 .replace("$", " ")
29890 .as_str(),
29891 );
29892
29893 // Case 5: Adding newline with content adds marker preserving indentation
29894 cx.set_state(indoc! {"
29895 1. item
29896 2. indentedˇ
29897 "});
29898 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29899 cx.wait_for_autoindent_applied().await;
29900 cx.assert_editor_state(indoc! {"
29901 1. item
29902 2. indented
29903 3. ˇ
29904 "});
29905
29906 // Case 6: Adding newline with cursor right after marker, unindents
29907 cx.set_state(indoc! {"
29908 1. item
29909 2. sub item
29910 3. ˇ
29911 "});
29912 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29913 cx.wait_for_autoindent_applied().await;
29914 cx.assert_editor_state(indoc! {"
29915 1. item
29916 2. sub item
29917 1. ˇ
29918 "});
29919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29920 cx.wait_for_autoindent_applied().await;
29921
29922 // Case 7: Adding newline with cursor right after marker, removes marker
29923 cx.assert_editor_state(indoc! {"
29924 1. item
29925 2. sub item
29926 1. ˇ
29927 "});
29928 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29929 cx.wait_for_autoindent_applied().await;
29930 cx.assert_editor_state(indoc! {"
29931 1. item
29932 2. sub item
29933 ˇ
29934 "});
29935
29936 // Case 8: Cursor before or inside prefix does not add marker
29937 cx.set_state(indoc! {"
29938 ˇ1. item
29939 "});
29940 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29941 cx.wait_for_autoindent_applied().await;
29942 cx.assert_editor_state(indoc! {"
29943
29944 ˇ1. item
29945 "});
29946
29947 cx.set_state(indoc! {"
29948 1ˇ. item
29949 "});
29950 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29951 cx.wait_for_autoindent_applied().await;
29952 cx.assert_editor_state(indoc! {"
29953 1
29954 ˇ. item
29955 "});
29956}
29957
29958#[gpui::test]
29959async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
29960 init_test(cx, |settings| {
29961 settings.defaults.tab_size = Some(2.try_into().unwrap());
29962 });
29963
29964 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29965 let mut cx = EditorTestContext::new(cx).await;
29966 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29967
29968 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
29969 cx.set_state(indoc! {"
29970 1. first item
29971 1. sub first item
29972 2. sub second item
29973 3. ˇ
29974 "});
29975 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29976 cx.wait_for_autoindent_applied().await;
29977 cx.assert_editor_state(indoc! {"
29978 1. first item
29979 1. sub first item
29980 2. sub second item
29981 1. ˇ
29982 "});
29983}
29984
29985#[gpui::test]
29986async fn test_tab_list_indent(cx: &mut TestAppContext) {
29987 init_test(cx, |settings| {
29988 settings.defaults.tab_size = Some(2.try_into().unwrap());
29989 });
29990
29991 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29992 let mut cx = EditorTestContext::new(cx).await;
29993 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29994
29995 // Case 1: Unordered list - cursor after prefix, adds indent before prefix
29996 cx.set_state(indoc! {"
29997 - ˇitem
29998 "});
29999 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30000 cx.wait_for_autoindent_applied().await;
30001 let expected = indoc! {"
30002 $$- ˇitem
30003 "};
30004 cx.assert_editor_state(expected.replace("$", " ").as_str());
30005
30006 // Case 2: Task list - cursor after prefix
30007 cx.set_state(indoc! {"
30008 - [ ] ˇtask
30009 "});
30010 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30011 cx.wait_for_autoindent_applied().await;
30012 let expected = indoc! {"
30013 $$- [ ] ˇtask
30014 "};
30015 cx.assert_editor_state(expected.replace("$", " ").as_str());
30016
30017 // Case 3: Ordered list - cursor after prefix
30018 cx.set_state(indoc! {"
30019 1. ˇfirst
30020 "});
30021 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30022 cx.wait_for_autoindent_applied().await;
30023 let expected = indoc! {"
30024 $$1. ˇfirst
30025 "};
30026 cx.assert_editor_state(expected.replace("$", " ").as_str());
30027
30028 // Case 4: With existing indentation - adds more indent
30029 let initial = indoc! {"
30030 $$- ˇitem
30031 "};
30032 cx.set_state(initial.replace("$", " ").as_str());
30033 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30034 cx.wait_for_autoindent_applied().await;
30035 let expected = indoc! {"
30036 $$$$- ˇitem
30037 "};
30038 cx.assert_editor_state(expected.replace("$", " ").as_str());
30039
30040 // Case 5: Empty list item
30041 cx.set_state(indoc! {"
30042 - ˇ
30043 "});
30044 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30045 cx.wait_for_autoindent_applied().await;
30046 let expected = indoc! {"
30047 $$- ˇ
30048 "};
30049 cx.assert_editor_state(expected.replace("$", " ").as_str());
30050
30051 // Case 6: Cursor at end of line with content
30052 cx.set_state(indoc! {"
30053 - itemˇ
30054 "});
30055 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30056 cx.wait_for_autoindent_applied().await;
30057 let expected = indoc! {"
30058 $$- itemˇ
30059 "};
30060 cx.assert_editor_state(expected.replace("$", " ").as_str());
30061
30062 // Case 7: Cursor at start of list item, indents it
30063 cx.set_state(indoc! {"
30064 - item
30065 ˇ - sub item
30066 "});
30067 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30068 cx.wait_for_autoindent_applied().await;
30069 let expected = indoc! {"
30070 - item
30071 ˇ - sub item
30072 "};
30073 cx.assert_editor_state(expected);
30074
30075 // Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
30076 cx.update_editor(|_, _, cx| {
30077 SettingsStore::update_global(cx, |store, cx| {
30078 store.update_user_settings(cx, |settings| {
30079 settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
30080 });
30081 });
30082 });
30083 cx.set_state(indoc! {"
30084 - item
30085 ˇ - sub item
30086 "});
30087 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30088 cx.wait_for_autoindent_applied().await;
30089 let expected = indoc! {"
30090 - item
30091 ˇ- sub item
30092 "};
30093 cx.assert_editor_state(expected);
30094}
30095
30096#[gpui::test]
30097async fn test_local_worktree_trust(cx: &mut TestAppContext) {
30098 init_test(cx, |_| {});
30099 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), None, None, cx));
30100
30101 cx.update(|cx| {
30102 SettingsStore::update_global(cx, |store, cx| {
30103 store.update_user_settings(cx, |settings| {
30104 settings.project.all_languages.defaults.inlay_hints =
30105 Some(InlayHintSettingsContent {
30106 enabled: Some(true),
30107 ..InlayHintSettingsContent::default()
30108 });
30109 });
30110 });
30111 });
30112
30113 let fs = FakeFs::new(cx.executor());
30114 fs.insert_tree(
30115 path!("/project"),
30116 json!({
30117 ".zed": {
30118 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
30119 },
30120 "main.rs": "fn main() {}"
30121 }),
30122 )
30123 .await;
30124
30125 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
30126 let server_name = "override-rust-analyzer";
30127 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
30128
30129 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
30130 language_registry.add(rust_lang());
30131
30132 let capabilities = lsp::ServerCapabilities {
30133 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
30134 ..lsp::ServerCapabilities::default()
30135 };
30136 let mut fake_language_servers = language_registry.register_fake_lsp(
30137 "Rust",
30138 FakeLspAdapter {
30139 name: server_name,
30140 capabilities,
30141 initializer: Some(Box::new({
30142 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30143 move |fake_server| {
30144 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30145 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
30146 move |_params, _| {
30147 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
30148 async move {
30149 Ok(Some(vec![lsp::InlayHint {
30150 position: lsp::Position::new(0, 0),
30151 label: lsp::InlayHintLabel::String("hint".to_string()),
30152 kind: None,
30153 text_edits: None,
30154 tooltip: None,
30155 padding_left: None,
30156 padding_right: None,
30157 data: None,
30158 }]))
30159 }
30160 },
30161 );
30162 }
30163 })),
30164 ..FakeLspAdapter::default()
30165 },
30166 );
30167
30168 cx.run_until_parked();
30169
30170 let worktree_id = project.read_with(cx, |project, cx| {
30171 project
30172 .worktrees(cx)
30173 .next()
30174 .map(|wt| wt.read(cx).id())
30175 .expect("should have a worktree")
30176 });
30177 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
30178
30179 let trusted_worktrees =
30180 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
30181
30182 let can_trust = trusted_worktrees.update(cx, |store, cx| {
30183 store.can_trust(&worktree_store, worktree_id, cx)
30184 });
30185 assert!(!can_trust, "worktree should be restricted initially");
30186
30187 let buffer_before_approval = project
30188 .update(cx, |project, cx| {
30189 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
30190 })
30191 .await
30192 .unwrap();
30193
30194 let (editor, cx) = cx.add_window_view(|window, cx| {
30195 Editor::new(
30196 EditorMode::full(),
30197 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
30198 Some(project.clone()),
30199 window,
30200 cx,
30201 )
30202 });
30203 cx.run_until_parked();
30204 let fake_language_server = fake_language_servers.next();
30205
30206 cx.read(|cx| {
30207 let file = buffer_before_approval.read(cx).file();
30208 assert_eq!(
30209 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30210 .language_servers,
30211 ["...".to_string()],
30212 "local .zed/settings.json must not apply before trust approval"
30213 )
30214 });
30215
30216 editor.update_in(cx, |editor, window, cx| {
30217 editor.handle_input("1", window, cx);
30218 });
30219 cx.run_until_parked();
30220 cx.executor()
30221 .advance_clock(std::time::Duration::from_secs(1));
30222 assert_eq!(
30223 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
30224 0,
30225 "inlay hints must not be queried before trust approval"
30226 );
30227
30228 trusted_worktrees.update(cx, |store, cx| {
30229 store.trust(
30230 &worktree_store,
30231 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
30232 cx,
30233 );
30234 });
30235 cx.run_until_parked();
30236
30237 cx.read(|cx| {
30238 let file = buffer_before_approval.read(cx).file();
30239 assert_eq!(
30240 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30241 .language_servers,
30242 ["override-rust-analyzer".to_string()],
30243 "local .zed/settings.json should apply after trust approval"
30244 )
30245 });
30246 let _fake_language_server = fake_language_server.await.unwrap();
30247 editor.update_in(cx, |editor, window, cx| {
30248 editor.handle_input("1", window, cx);
30249 });
30250 cx.run_until_parked();
30251 cx.executor()
30252 .advance_clock(std::time::Duration::from_secs(1));
30253 assert!(
30254 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
30255 "inlay hints should be queried after trust approval"
30256 );
30257
30258 let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
30259 store.can_trust(&worktree_store, worktree_id, cx)
30260 });
30261 assert!(can_trust_after, "worktree should be trusted after trust()");
30262}
30263
30264#[gpui::test]
30265fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
30266 // This test reproduces a bug where drawing an editor at a position above the viewport
30267 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
30268 // causes an infinite loop in blocks_in_range.
30269 //
30270 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
30271 // the content mask intersection produces visible_bounds with origin at the viewport top.
30272 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
30273 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
30274 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
30275 init_test(cx, |_| {});
30276
30277 let window = cx.add_window(|_, _| gpui::Empty);
30278 let mut cx = VisualTestContext::from_window(*window, cx);
30279
30280 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
30281 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
30282
30283 // Simulate a small viewport (500x500 pixels at origin 0,0)
30284 cx.simulate_resize(gpui::size(px(500.), px(500.)));
30285
30286 // Draw the editor at a very negative Y position, simulating an editor that's been
30287 // scrolled way above the visible viewport (like in a List that has scrolled past it).
30288 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
30289 // This should NOT hang - it should just render nothing.
30290 cx.draw(
30291 gpui::point(px(0.), px(-10000.)),
30292 gpui::size(px(500.), px(3000.)),
30293 |_, _| editor.clone(),
30294 );
30295
30296 // If we get here without hanging, the test passes
30297}
30298
30299#[gpui::test]
30300async fn test_diff_review_indicator_created_on_gutter_hover(cx: &mut TestAppContext) {
30301 init_test(cx, |_| {});
30302
30303 let fs = FakeFs::new(cx.executor());
30304 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30305 .await;
30306
30307 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30308 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30309 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30310
30311 let editor = workspace
30312 .update(cx, |workspace, window, cx| {
30313 workspace.open_abs_path(
30314 PathBuf::from(path!("/root/file.txt")),
30315 OpenOptions::default(),
30316 window,
30317 cx,
30318 )
30319 })
30320 .unwrap()
30321 .await
30322 .unwrap()
30323 .downcast::<Editor>()
30324 .unwrap();
30325
30326 // Enable diff review button mode
30327 editor.update(cx, |editor, cx| {
30328 editor.set_show_diff_review_button(true, cx);
30329 });
30330
30331 // Initially, no indicator should be present
30332 editor.update(cx, |editor, _cx| {
30333 assert!(
30334 editor.gutter_diff_review_indicator.0.is_none(),
30335 "Indicator should be None initially"
30336 );
30337 });
30338}
30339
30340#[gpui::test]
30341async fn test_diff_review_button_hidden_when_ai_disabled(cx: &mut TestAppContext) {
30342 init_test(cx, |_| {});
30343
30344 // Register DisableAiSettings and set disable_ai to true
30345 cx.update(|cx| {
30346 project::DisableAiSettings::register(cx);
30347 project::DisableAiSettings::override_global(
30348 project::DisableAiSettings { disable_ai: true },
30349 cx,
30350 );
30351 });
30352
30353 let fs = FakeFs::new(cx.executor());
30354 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30355 .await;
30356
30357 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30358 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30359 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30360
30361 let editor = workspace
30362 .update(cx, |workspace, window, cx| {
30363 workspace.open_abs_path(
30364 PathBuf::from(path!("/root/file.txt")),
30365 OpenOptions::default(),
30366 window,
30367 cx,
30368 )
30369 })
30370 .unwrap()
30371 .await
30372 .unwrap()
30373 .downcast::<Editor>()
30374 .unwrap();
30375
30376 // Enable diff review button mode
30377 editor.update(cx, |editor, cx| {
30378 editor.set_show_diff_review_button(true, cx);
30379 });
30380
30381 // Verify AI is disabled
30382 cx.read(|cx| {
30383 assert!(
30384 project::DisableAiSettings::get_global(cx).disable_ai,
30385 "AI should be disabled"
30386 );
30387 });
30388
30389 // The indicator should not be created when AI is disabled
30390 // (The mouse_moved handler checks DisableAiSettings before creating the indicator)
30391 editor.update(cx, |editor, _cx| {
30392 assert!(
30393 editor.gutter_diff_review_indicator.0.is_none(),
30394 "Indicator should be None when AI is disabled"
30395 );
30396 });
30397}
30398
30399#[gpui::test]
30400async fn test_diff_review_button_shown_when_ai_enabled(cx: &mut TestAppContext) {
30401 init_test(cx, |_| {});
30402
30403 // Register DisableAiSettings and set disable_ai to false
30404 cx.update(|cx| {
30405 project::DisableAiSettings::register(cx);
30406 project::DisableAiSettings::override_global(
30407 project::DisableAiSettings { disable_ai: false },
30408 cx,
30409 );
30410 });
30411
30412 let fs = FakeFs::new(cx.executor());
30413 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30414 .await;
30415
30416 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30417 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30418 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30419
30420 let editor = workspace
30421 .update(cx, |workspace, window, cx| {
30422 workspace.open_abs_path(
30423 PathBuf::from(path!("/root/file.txt")),
30424 OpenOptions::default(),
30425 window,
30426 cx,
30427 )
30428 })
30429 .unwrap()
30430 .await
30431 .unwrap()
30432 .downcast::<Editor>()
30433 .unwrap();
30434
30435 // Enable diff review button mode
30436 editor.update(cx, |editor, cx| {
30437 editor.set_show_diff_review_button(true, cx);
30438 });
30439
30440 // Verify AI is enabled
30441 cx.read(|cx| {
30442 assert!(
30443 !project::DisableAiSettings::get_global(cx).disable_ai,
30444 "AI should be enabled"
30445 );
30446 });
30447
30448 // The show_diff_review_button flag should be true
30449 editor.update(cx, |editor, _cx| {
30450 assert!(
30451 editor.show_diff_review_button(),
30452 "show_diff_review_button should be true"
30453 );
30454 });
30455}
30456
30457#[gpui::test]
30458async fn test_move_to_start_end_of_larger_syntax_node_single_cursor(cx: &mut TestAppContext) {
30459 init_test(cx, |_| {});
30460
30461 let language = Arc::new(Language::new(
30462 LanguageConfig::default(),
30463 Some(tree_sitter_rust::LANGUAGE.into()),
30464 ));
30465
30466 let text = r#"
30467 fn main() {
30468 let x = foo(1, 2);
30469 }
30470 "#
30471 .unindent();
30472
30473 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
30474 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
30475 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
30476
30477 editor
30478 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
30479 .await;
30480
30481 // Test case 1: Move to end of syntax nodes
30482 editor.update_in(cx, |editor, window, cx| {
30483 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30484 s.select_display_ranges([
30485 DisplayPoint::new(DisplayRow(1), 16)..DisplayPoint::new(DisplayRow(1), 16)
30486 ]);
30487 });
30488 });
30489 editor.update(cx, |editor, cx| {
30490 assert_text_with_selections(
30491 editor,
30492 indoc! {r#"
30493 fn main() {
30494 let x = foo(ˇ1, 2);
30495 }
30496 "#},
30497 cx,
30498 );
30499 });
30500 editor.update_in(cx, |editor, window, cx| {
30501 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
30502 });
30503 editor.update(cx, |editor, cx| {
30504 assert_text_with_selections(
30505 editor,
30506 indoc! {r#"
30507 fn main() {
30508 let x = foo(1ˇ, 2);
30509 }
30510 "#},
30511 cx,
30512 );
30513 });
30514 editor.update_in(cx, |editor, window, cx| {
30515 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
30516 });
30517 editor.update(cx, |editor, cx| {
30518 assert_text_with_selections(
30519 editor,
30520 indoc! {r#"
30521 fn main() {
30522 let x = foo(1, 2)ˇ;
30523 }
30524 "#},
30525 cx,
30526 );
30527 });
30528 editor.update_in(cx, |editor, window, cx| {
30529 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
30530 });
30531 editor.update(cx, |editor, cx| {
30532 assert_text_with_selections(
30533 editor,
30534 indoc! {r#"
30535 fn main() {
30536 let x = foo(1, 2);ˇ
30537 }
30538 "#},
30539 cx,
30540 );
30541 });
30542 editor.update_in(cx, |editor, window, cx| {
30543 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
30544 });
30545 editor.update(cx, |editor, cx| {
30546 assert_text_with_selections(
30547 editor,
30548 indoc! {r#"
30549 fn main() {
30550 let x = foo(1, 2);
30551 }ˇ
30552 "#},
30553 cx,
30554 );
30555 });
30556
30557 // Test case 2: Move to start of syntax nodes
30558 editor.update_in(cx, |editor, window, cx| {
30559 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30560 s.select_display_ranges([
30561 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20)
30562 ]);
30563 });
30564 });
30565 editor.update(cx, |editor, cx| {
30566 assert_text_with_selections(
30567 editor,
30568 indoc! {r#"
30569 fn main() {
30570 let x = foo(1, 2ˇ);
30571 }
30572 "#},
30573 cx,
30574 );
30575 });
30576 editor.update_in(cx, |editor, window, cx| {
30577 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
30578 });
30579 editor.update(cx, |editor, cx| {
30580 assert_text_with_selections(
30581 editor,
30582 indoc! {r#"
30583 fn main() {
30584 let x = fooˇ(1, 2);
30585 }
30586 "#},
30587 cx,
30588 );
30589 });
30590 editor.update_in(cx, |editor, window, cx| {
30591 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
30592 });
30593 editor.update(cx, |editor, cx| {
30594 assert_text_with_selections(
30595 editor,
30596 indoc! {r#"
30597 fn main() {
30598 let x = ˇfoo(1, 2);
30599 }
30600 "#},
30601 cx,
30602 );
30603 });
30604 editor.update_in(cx, |editor, window, cx| {
30605 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
30606 });
30607 editor.update(cx, |editor, cx| {
30608 assert_text_with_selections(
30609 editor,
30610 indoc! {r#"
30611 fn main() {
30612 ˇlet x = foo(1, 2);
30613 }
30614 "#},
30615 cx,
30616 );
30617 });
30618 editor.update_in(cx, |editor, window, cx| {
30619 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
30620 });
30621 editor.update(cx, |editor, cx| {
30622 assert_text_with_selections(
30623 editor,
30624 indoc! {r#"
30625 fn main() ˇ{
30626 let x = foo(1, 2);
30627 }
30628 "#},
30629 cx,
30630 );
30631 });
30632 editor.update_in(cx, |editor, window, cx| {
30633 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
30634 });
30635 editor.update(cx, |editor, cx| {
30636 assert_text_with_selections(
30637 editor,
30638 indoc! {r#"
30639 ˇfn main() {
30640 let x = foo(1, 2);
30641 }
30642 "#},
30643 cx,
30644 );
30645 });
30646}
30647
30648#[gpui::test]
30649async fn test_move_to_start_end_of_larger_syntax_node_two_cursors(cx: &mut TestAppContext) {
30650 init_test(cx, |_| {});
30651
30652 let language = Arc::new(Language::new(
30653 LanguageConfig::default(),
30654 Some(tree_sitter_rust::LANGUAGE.into()),
30655 ));
30656
30657 let text = r#"
30658 fn main() {
30659 let x = foo(1, 2);
30660 let y = bar(3, 4);
30661 }
30662 "#
30663 .unindent();
30664
30665 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
30666 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
30667 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
30668
30669 editor
30670 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
30671 .await;
30672
30673 // Test case 1: Move to end of syntax nodes with two cursors
30674 editor.update_in(cx, |editor, window, cx| {
30675 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30676 s.select_display_ranges([
30677 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20),
30678 DisplayPoint::new(DisplayRow(2), 20)..DisplayPoint::new(DisplayRow(2), 20),
30679 ]);
30680 });
30681 });
30682 editor.update(cx, |editor, cx| {
30683 assert_text_with_selections(
30684 editor,
30685 indoc! {r#"
30686 fn main() {
30687 let x = foo(1, 2ˇ);
30688 let y = bar(3, 4ˇ);
30689 }
30690 "#},
30691 cx,
30692 );
30693 });
30694 editor.update_in(cx, |editor, window, cx| {
30695 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
30696 });
30697 editor.update(cx, |editor, cx| {
30698 assert_text_with_selections(
30699 editor,
30700 indoc! {r#"
30701 fn main() {
30702 let x = foo(1, 2)ˇ;
30703 let y = bar(3, 4)ˇ;
30704 }
30705 "#},
30706 cx,
30707 );
30708 });
30709 editor.update_in(cx, |editor, window, cx| {
30710 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
30711 });
30712 editor.update(cx, |editor, cx| {
30713 assert_text_with_selections(
30714 editor,
30715 indoc! {r#"
30716 fn main() {
30717 let x = foo(1, 2);ˇ
30718 let y = bar(3, 4);ˇ
30719 }
30720 "#},
30721 cx,
30722 );
30723 });
30724
30725 // Test case 2: Move to start of syntax nodes with two cursors
30726 editor.update_in(cx, |editor, window, cx| {
30727 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30728 s.select_display_ranges([
30729 DisplayPoint::new(DisplayRow(1), 19)..DisplayPoint::new(DisplayRow(1), 19),
30730 DisplayPoint::new(DisplayRow(2), 19)..DisplayPoint::new(DisplayRow(2), 19),
30731 ]);
30732 });
30733 });
30734 editor.update(cx, |editor, cx| {
30735 assert_text_with_selections(
30736 editor,
30737 indoc! {r#"
30738 fn main() {
30739 let x = foo(1, ˇ2);
30740 let y = bar(3, ˇ4);
30741 }
30742 "#},
30743 cx,
30744 );
30745 });
30746 editor.update_in(cx, |editor, window, cx| {
30747 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
30748 });
30749 editor.update(cx, |editor, cx| {
30750 assert_text_with_selections(
30751 editor,
30752 indoc! {r#"
30753 fn main() {
30754 let x = fooˇ(1, 2);
30755 let y = barˇ(3, 4);
30756 }
30757 "#},
30758 cx,
30759 );
30760 });
30761 editor.update_in(cx, |editor, window, cx| {
30762 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
30763 });
30764 editor.update(cx, |editor, cx| {
30765 assert_text_with_selections(
30766 editor,
30767 indoc! {r#"
30768 fn main() {
30769 let x = ˇfoo(1, 2);
30770 let y = ˇbar(3, 4);
30771 }
30772 "#},
30773 cx,
30774 );
30775 });
30776 editor.update_in(cx, |editor, window, cx| {
30777 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
30778 });
30779 editor.update(cx, |editor, cx| {
30780 assert_text_with_selections(
30781 editor,
30782 indoc! {r#"
30783 fn main() {
30784 ˇlet x = foo(1, 2);
30785 ˇlet y = bar(3, 4);
30786 }
30787 "#},
30788 cx,
30789 );
30790 });
30791}
30792
30793#[gpui::test]
30794async fn test_move_to_start_end_of_larger_syntax_node_with_selections_and_strings(
30795 cx: &mut TestAppContext,
30796) {
30797 init_test(cx, |_| {});
30798
30799 let language = Arc::new(Language::new(
30800 LanguageConfig::default(),
30801 Some(tree_sitter_rust::LANGUAGE.into()),
30802 ));
30803
30804 let text = r#"
30805 fn main() {
30806 let x = foo(1, 2);
30807 let msg = "hello world";
30808 }
30809 "#
30810 .unindent();
30811
30812 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
30813 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
30814 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
30815
30816 editor
30817 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
30818 .await;
30819
30820 // Test case 1: With existing selection, move_to_end keeps selection
30821 editor.update_in(cx, |editor, window, cx| {
30822 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30823 s.select_display_ranges([
30824 DisplayPoint::new(DisplayRow(1), 12)..DisplayPoint::new(DisplayRow(1), 21)
30825 ]);
30826 });
30827 });
30828 editor.update(cx, |editor, cx| {
30829 assert_text_with_selections(
30830 editor,
30831 indoc! {r#"
30832 fn main() {
30833 let x = «foo(1, 2)ˇ»;
30834 let msg = "hello world";
30835 }
30836 "#},
30837 cx,
30838 );
30839 });
30840 editor.update_in(cx, |editor, window, cx| {
30841 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
30842 });
30843 editor.update(cx, |editor, cx| {
30844 assert_text_with_selections(
30845 editor,
30846 indoc! {r#"
30847 fn main() {
30848 let x = «foo(1, 2)ˇ»;
30849 let msg = "hello world";
30850 }
30851 "#},
30852 cx,
30853 );
30854 });
30855
30856 // Test case 2: Move to end within a string
30857 editor.update_in(cx, |editor, window, cx| {
30858 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30859 s.select_display_ranges([
30860 DisplayPoint::new(DisplayRow(2), 15)..DisplayPoint::new(DisplayRow(2), 15)
30861 ]);
30862 });
30863 });
30864 editor.update(cx, |editor, cx| {
30865 assert_text_with_selections(
30866 editor,
30867 indoc! {r#"
30868 fn main() {
30869 let x = foo(1, 2);
30870 let msg = "ˇhello world";
30871 }
30872 "#},
30873 cx,
30874 );
30875 });
30876 editor.update_in(cx, |editor, window, cx| {
30877 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
30878 });
30879 editor.update(cx, |editor, cx| {
30880 assert_text_with_selections(
30881 editor,
30882 indoc! {r#"
30883 fn main() {
30884 let x = foo(1, 2);
30885 let msg = "hello worldˇ";
30886 }
30887 "#},
30888 cx,
30889 );
30890 });
30891
30892 // Test case 3: Move to start within a string
30893 editor.update_in(cx, |editor, window, cx| {
30894 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30895 s.select_display_ranges([
30896 DisplayPoint::new(DisplayRow(2), 21)..DisplayPoint::new(DisplayRow(2), 21)
30897 ]);
30898 });
30899 });
30900 editor.update(cx, |editor, cx| {
30901 assert_text_with_selections(
30902 editor,
30903 indoc! {r#"
30904 fn main() {
30905 let x = foo(1, 2);
30906 let msg = "hello ˇworld";
30907 }
30908 "#},
30909 cx,
30910 );
30911 });
30912 editor.update_in(cx, |editor, window, cx| {
30913 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
30914 });
30915 editor.update(cx, |editor, cx| {
30916 assert_text_with_selections(
30917 editor,
30918 indoc! {r#"
30919 fn main() {
30920 let x = foo(1, 2);
30921 let msg = "ˇhello world";
30922 }
30923 "#},
30924 cx,
30925 );
30926 });
30927}