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_next_word_end_or_newline(cx: &mut TestAppContext) {
3033 init_test(cx, |_| {});
3034
3035 let editor = cx.add_window(|window, cx| {
3036 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3037 build_editor(buffer, window, cx)
3038 });
3039 let del_to_next_word_end = DeleteToNextWordEnd {
3040 ignore_newlines: false,
3041 ignore_brackets: false,
3042 };
3043 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
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(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3052 ])
3053 });
3054 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3055 assert_eq!(
3056 editor.buffer.read(cx).read(cx).text(),
3057 "one\n two\nthree\n four"
3058 );
3059 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3060 assert_eq!(
3061 editor.buffer.read(cx).read(cx).text(),
3062 "\n two\nthree\n four"
3063 );
3064 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3065 assert_eq!(
3066 editor.buffer.read(cx).read(cx).text(),
3067 "two\nthree\n four"
3068 );
3069 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3070 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3071 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3072 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3073 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3074 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3075 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3076 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3077 });
3078}
3079
3080#[gpui::test]
3081fn test_newline(cx: &mut TestAppContext) {
3082 init_test(cx, |_| {});
3083
3084 let editor = cx.add_window(|window, cx| {
3085 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3086 build_editor(buffer, window, cx)
3087 });
3088
3089 _ = editor.update(cx, |editor, window, cx| {
3090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3091 s.select_display_ranges([
3092 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3093 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3094 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3095 ])
3096 });
3097
3098 editor.newline(&Newline, window, cx);
3099 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3100 });
3101}
3102
3103#[gpui::test]
3104async fn test_newline_yaml(cx: &mut TestAppContext) {
3105 init_test(cx, |_| {});
3106
3107 let mut cx = EditorTestContext::new(cx).await;
3108 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3109 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3110
3111 // Object (between 2 fields)
3112 cx.set_state(indoc! {"
3113 test:ˇ
3114 hello: bye"});
3115 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3116 cx.assert_editor_state(indoc! {"
3117 test:
3118 ˇ
3119 hello: bye"});
3120
3121 // Object (first and single line)
3122 cx.set_state(indoc! {"
3123 test:ˇ"});
3124 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3125 cx.assert_editor_state(indoc! {"
3126 test:
3127 ˇ"});
3128
3129 // Array with objects (after first element)
3130 cx.set_state(indoc! {"
3131 test:
3132 - foo: barˇ"});
3133 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3134 cx.assert_editor_state(indoc! {"
3135 test:
3136 - foo: bar
3137 ˇ"});
3138
3139 // Array with objects and comment
3140 cx.set_state(indoc! {"
3141 test:
3142 - foo: bar
3143 - bar: # testˇ"});
3144 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3145 cx.assert_editor_state(indoc! {"
3146 test:
3147 - foo: bar
3148 - bar: # test
3149 ˇ"});
3150
3151 // Array with objects (after second element)
3152 cx.set_state(indoc! {"
3153 test:
3154 - foo: bar
3155 - bar: fooˇ"});
3156 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3157 cx.assert_editor_state(indoc! {"
3158 test:
3159 - foo: bar
3160 - bar: foo
3161 ˇ"});
3162
3163 // Array with strings (after first element)
3164 cx.set_state(indoc! {"
3165 test:
3166 - fooˇ"});
3167 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3168 cx.assert_editor_state(indoc! {"
3169 test:
3170 - foo
3171 ˇ"});
3172}
3173
3174#[gpui::test]
3175fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3176 init_test(cx, |_| {});
3177
3178 let editor = cx.add_window(|window, cx| {
3179 let buffer = MultiBuffer::build_simple(
3180 "
3181 a
3182 b(
3183 X
3184 )
3185 c(
3186 X
3187 )
3188 "
3189 .unindent()
3190 .as_str(),
3191 cx,
3192 );
3193 let mut editor = build_editor(buffer, window, cx);
3194 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3195 s.select_ranges([
3196 Point::new(2, 4)..Point::new(2, 5),
3197 Point::new(5, 4)..Point::new(5, 5),
3198 ])
3199 });
3200 editor
3201 });
3202
3203 _ = editor.update(cx, |editor, window, cx| {
3204 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3205 editor.buffer.update(cx, |buffer, cx| {
3206 buffer.edit(
3207 [
3208 (Point::new(1, 2)..Point::new(3, 0), ""),
3209 (Point::new(4, 2)..Point::new(6, 0), ""),
3210 ],
3211 None,
3212 cx,
3213 );
3214 assert_eq!(
3215 buffer.read(cx).text(),
3216 "
3217 a
3218 b()
3219 c()
3220 "
3221 .unindent()
3222 );
3223 });
3224 assert_eq!(
3225 editor.selections.ranges(&editor.display_snapshot(cx)),
3226 &[
3227 Point::new(1, 2)..Point::new(1, 2),
3228 Point::new(2, 2)..Point::new(2, 2),
3229 ],
3230 );
3231
3232 editor.newline(&Newline, window, cx);
3233 assert_eq!(
3234 editor.text(cx),
3235 "
3236 a
3237 b(
3238 )
3239 c(
3240 )
3241 "
3242 .unindent()
3243 );
3244
3245 // The selections are moved after the inserted newlines
3246 assert_eq!(
3247 editor.selections.ranges(&editor.display_snapshot(cx)),
3248 &[
3249 Point::new(2, 0)..Point::new(2, 0),
3250 Point::new(4, 0)..Point::new(4, 0),
3251 ],
3252 );
3253 });
3254}
3255
3256#[gpui::test]
3257async fn test_newline_above(cx: &mut TestAppContext) {
3258 init_test(cx, |settings| {
3259 settings.defaults.tab_size = NonZeroU32::new(4)
3260 });
3261
3262 let language = Arc::new(
3263 Language::new(
3264 LanguageConfig::default(),
3265 Some(tree_sitter_rust::LANGUAGE.into()),
3266 )
3267 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3268 .unwrap(),
3269 );
3270
3271 let mut cx = EditorTestContext::new(cx).await;
3272 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3273 cx.set_state(indoc! {"
3274 const a: ˇA = (
3275 (ˇ
3276 «const_functionˇ»(ˇ),
3277 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3278 )ˇ
3279 ˇ);ˇ
3280 "});
3281
3282 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3283 cx.assert_editor_state(indoc! {"
3284 ˇ
3285 const a: A = (
3286 ˇ
3287 (
3288 ˇ
3289 ˇ
3290 const_function(),
3291 ˇ
3292 ˇ
3293 ˇ
3294 ˇ
3295 something_else,
3296 ˇ
3297 )
3298 ˇ
3299 ˇ
3300 );
3301 "});
3302}
3303
3304#[gpui::test]
3305async fn test_newline_below(cx: &mut TestAppContext) {
3306 init_test(cx, |settings| {
3307 settings.defaults.tab_size = NonZeroU32::new(4)
3308 });
3309
3310 let language = Arc::new(
3311 Language::new(
3312 LanguageConfig::default(),
3313 Some(tree_sitter_rust::LANGUAGE.into()),
3314 )
3315 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3316 .unwrap(),
3317 );
3318
3319 let mut cx = EditorTestContext::new(cx).await;
3320 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3321 cx.set_state(indoc! {"
3322 const a: ˇA = (
3323 (ˇ
3324 «const_functionˇ»(ˇ),
3325 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3326 )ˇ
3327 ˇ);ˇ
3328 "});
3329
3330 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3331 cx.assert_editor_state(indoc! {"
3332 const a: A = (
3333 ˇ
3334 (
3335 ˇ
3336 const_function(),
3337 ˇ
3338 ˇ
3339 something_else,
3340 ˇ
3341 ˇ
3342 ˇ
3343 ˇ
3344 )
3345 ˇ
3346 );
3347 ˇ
3348 ˇ
3349 "});
3350}
3351
3352#[gpui::test]
3353async fn test_newline_comments(cx: &mut TestAppContext) {
3354 init_test(cx, |settings| {
3355 settings.defaults.tab_size = NonZeroU32::new(4)
3356 });
3357
3358 let language = Arc::new(Language::new(
3359 LanguageConfig {
3360 line_comments: vec!["// ".into()],
3361 ..LanguageConfig::default()
3362 },
3363 None,
3364 ));
3365 {
3366 let mut cx = EditorTestContext::new(cx).await;
3367 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3368 cx.set_state(indoc! {"
3369 // Fooˇ
3370 "});
3371
3372 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3373 cx.assert_editor_state(indoc! {"
3374 // Foo
3375 // ˇ
3376 "});
3377 // Ensure that we add comment prefix when existing line contains space
3378 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3379 cx.assert_editor_state(
3380 indoc! {"
3381 // Foo
3382 //s
3383 // ˇ
3384 "}
3385 .replace("s", " ") // s is used as space placeholder to prevent format on save
3386 .as_str(),
3387 );
3388 // Ensure that we add comment prefix when existing line does not contain space
3389 cx.set_state(indoc! {"
3390 // Foo
3391 //ˇ
3392 "});
3393 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3394 cx.assert_editor_state(indoc! {"
3395 // Foo
3396 //
3397 // ˇ
3398 "});
3399 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3400 cx.set_state(indoc! {"
3401 ˇ// Foo
3402 "});
3403 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3404 cx.assert_editor_state(indoc! {"
3405
3406 ˇ// Foo
3407 "});
3408 }
3409 // Ensure that comment continuations can be disabled.
3410 update_test_language_settings(cx, |settings| {
3411 settings.defaults.extend_comment_on_newline = Some(false);
3412 });
3413 let mut cx = EditorTestContext::new(cx).await;
3414 cx.set_state(indoc! {"
3415 // Fooˇ
3416 "});
3417 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3418 cx.assert_editor_state(indoc! {"
3419 // Foo
3420 ˇ
3421 "});
3422}
3423
3424#[gpui::test]
3425async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3426 init_test(cx, |settings| {
3427 settings.defaults.tab_size = NonZeroU32::new(4)
3428 });
3429
3430 let language = Arc::new(Language::new(
3431 LanguageConfig {
3432 line_comments: vec!["// ".into(), "/// ".into()],
3433 ..LanguageConfig::default()
3434 },
3435 None,
3436 ));
3437 {
3438 let mut cx = EditorTestContext::new(cx).await;
3439 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3440 cx.set_state(indoc! {"
3441 //ˇ
3442 "});
3443 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3444 cx.assert_editor_state(indoc! {"
3445 //
3446 // ˇ
3447 "});
3448
3449 cx.set_state(indoc! {"
3450 ///ˇ
3451 "});
3452 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3453 cx.assert_editor_state(indoc! {"
3454 ///
3455 /// ˇ
3456 "});
3457 }
3458}
3459
3460#[gpui::test]
3461async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3462 init_test(cx, |settings| {
3463 settings.defaults.tab_size = NonZeroU32::new(4)
3464 });
3465
3466 let language = Arc::new(
3467 Language::new(
3468 LanguageConfig {
3469 documentation_comment: Some(language::BlockCommentConfig {
3470 start: "/**".into(),
3471 end: "*/".into(),
3472 prefix: "* ".into(),
3473 tab_size: 1,
3474 }),
3475
3476 ..LanguageConfig::default()
3477 },
3478 Some(tree_sitter_rust::LANGUAGE.into()),
3479 )
3480 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3481 .unwrap(),
3482 );
3483
3484 {
3485 let mut cx = EditorTestContext::new(cx).await;
3486 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3487 cx.set_state(indoc! {"
3488 /**ˇ
3489 "});
3490
3491 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3492 cx.assert_editor_state(indoc! {"
3493 /**
3494 * ˇ
3495 "});
3496 // Ensure that if cursor is before the comment start,
3497 // we do not actually insert a comment prefix.
3498 cx.set_state(indoc! {"
3499 ˇ/**
3500 "});
3501 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3502 cx.assert_editor_state(indoc! {"
3503
3504 ˇ/**
3505 "});
3506 // Ensure that if cursor is between it doesn't add comment prefix.
3507 cx.set_state(indoc! {"
3508 /*ˇ*
3509 "});
3510 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3511 cx.assert_editor_state(indoc! {"
3512 /*
3513 ˇ*
3514 "});
3515 // Ensure that if suffix exists on same line after cursor it adds new line.
3516 cx.set_state(indoc! {"
3517 /**ˇ*/
3518 "});
3519 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3520 cx.assert_editor_state(indoc! {"
3521 /**
3522 * ˇ
3523 */
3524 "});
3525 // Ensure that if suffix exists on same line after cursor with space it adds new line.
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 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3536 cx.set_state(indoc! {"
3537 /** ˇ*/
3538 "});
3539 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3540 cx.assert_editor_state(
3541 indoc! {"
3542 /**s
3543 * ˇ
3544 */
3545 "}
3546 .replace("s", " ") // s is used as space placeholder to prevent format on save
3547 .as_str(),
3548 );
3549 // Ensure that delimiter space is preserved when newline on already
3550 // spaced delimiter.
3551 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3552 cx.assert_editor_state(
3553 indoc! {"
3554 /**s
3555 *s
3556 * ˇ
3557 */
3558 "}
3559 .replace("s", " ") // s is used as space placeholder to prevent format on save
3560 .as_str(),
3561 );
3562 // Ensure that delimiter space is preserved when space is not
3563 // on existing delimiter.
3564 cx.set_state(indoc! {"
3565 /**
3566 *ˇ
3567 */
3568 "});
3569 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3570 cx.assert_editor_state(indoc! {"
3571 /**
3572 *
3573 * ˇ
3574 */
3575 "});
3576 // Ensure that if suffix exists on same line after cursor it
3577 // doesn't add extra new line if prefix is not on same line.
3578 cx.set_state(indoc! {"
3579 /**
3580 ˇ*/
3581 "});
3582 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3583 cx.assert_editor_state(indoc! {"
3584 /**
3585
3586 ˇ*/
3587 "});
3588 // Ensure that it detects suffix after existing prefix.
3589 cx.set_state(indoc! {"
3590 /**ˇ/
3591 "});
3592 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3593 cx.assert_editor_state(indoc! {"
3594 /**
3595 ˇ/
3596 "});
3597 // Ensure that if suffix exists on same line before
3598 // cursor it does not add comment prefix.
3599 cx.set_state(indoc! {"
3600 /** */ˇ
3601 "});
3602 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3603 cx.assert_editor_state(indoc! {"
3604 /** */
3605 ˇ
3606 "});
3607 // Ensure that if suffix exists on same line before
3608 // cursor it does not add comment prefix.
3609 cx.set_state(indoc! {"
3610 /**
3611 *
3612 */ˇ
3613 "});
3614 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3615 cx.assert_editor_state(indoc! {"
3616 /**
3617 *
3618 */
3619 ˇ
3620 "});
3621
3622 // Ensure that inline comment followed by code
3623 // doesn't add comment prefix on newline
3624 cx.set_state(indoc! {"
3625 /** */ textˇ
3626 "});
3627 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3628 cx.assert_editor_state(indoc! {"
3629 /** */ text
3630 ˇ
3631 "});
3632
3633 // Ensure that text after comment end tag
3634 // doesn't add comment prefix on newline
3635 cx.set_state(indoc! {"
3636 /**
3637 *
3638 */ˇtext
3639 "});
3640 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3641 cx.assert_editor_state(indoc! {"
3642 /**
3643 *
3644 */
3645 ˇtext
3646 "});
3647
3648 // Ensure if not comment block it doesn't
3649 // add comment prefix on newline
3650 cx.set_state(indoc! {"
3651 * textˇ
3652 "});
3653 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3654 cx.assert_editor_state(indoc! {"
3655 * text
3656 ˇ
3657 "});
3658 }
3659 // Ensure that comment continuations can be disabled.
3660 update_test_language_settings(cx, |settings| {
3661 settings.defaults.extend_comment_on_newline = Some(false);
3662 });
3663 let mut cx = EditorTestContext::new(cx).await;
3664 cx.set_state(indoc! {"
3665 /**ˇ
3666 "});
3667 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3668 cx.assert_editor_state(indoc! {"
3669 /**
3670 ˇ
3671 "});
3672}
3673
3674#[gpui::test]
3675async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3676 init_test(cx, |settings| {
3677 settings.defaults.tab_size = NonZeroU32::new(4)
3678 });
3679
3680 let lua_language = Arc::new(Language::new(
3681 LanguageConfig {
3682 line_comments: vec!["--".into()],
3683 block_comment: Some(language::BlockCommentConfig {
3684 start: "--[[".into(),
3685 prefix: "".into(),
3686 end: "]]".into(),
3687 tab_size: 0,
3688 }),
3689 ..LanguageConfig::default()
3690 },
3691 None,
3692 ));
3693
3694 let mut cx = EditorTestContext::new(cx).await;
3695 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3696
3697 // Line with line comment should extend
3698 cx.set_state(indoc! {"
3699 --ˇ
3700 "});
3701 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3702 cx.assert_editor_state(indoc! {"
3703 --
3704 --ˇ
3705 "});
3706
3707 // Line with block comment that matches line comment should not extend
3708 cx.set_state(indoc! {"
3709 --[[ˇ
3710 "});
3711 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3712 cx.assert_editor_state(indoc! {"
3713 --[[
3714 ˇ
3715 "});
3716}
3717
3718#[gpui::test]
3719fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3720 init_test(cx, |_| {});
3721
3722 let editor = cx.add_window(|window, cx| {
3723 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3724 let mut editor = build_editor(buffer, window, cx);
3725 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3726 s.select_ranges([
3727 MultiBufferOffset(3)..MultiBufferOffset(4),
3728 MultiBufferOffset(11)..MultiBufferOffset(12),
3729 MultiBufferOffset(19)..MultiBufferOffset(20),
3730 ])
3731 });
3732 editor
3733 });
3734
3735 _ = editor.update(cx, |editor, window, cx| {
3736 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3737 editor.buffer.update(cx, |buffer, cx| {
3738 buffer.edit(
3739 [
3740 (MultiBufferOffset(2)..MultiBufferOffset(5), ""),
3741 (MultiBufferOffset(10)..MultiBufferOffset(13), ""),
3742 (MultiBufferOffset(18)..MultiBufferOffset(21), ""),
3743 ],
3744 None,
3745 cx,
3746 );
3747 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3748 });
3749 assert_eq!(
3750 editor.selections.ranges(&editor.display_snapshot(cx)),
3751 &[
3752 MultiBufferOffset(2)..MultiBufferOffset(2),
3753 MultiBufferOffset(7)..MultiBufferOffset(7),
3754 MultiBufferOffset(12)..MultiBufferOffset(12)
3755 ],
3756 );
3757
3758 editor.insert("Z", window, cx);
3759 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3760
3761 // The selections are moved after the inserted characters
3762 assert_eq!(
3763 editor.selections.ranges(&editor.display_snapshot(cx)),
3764 &[
3765 MultiBufferOffset(3)..MultiBufferOffset(3),
3766 MultiBufferOffset(9)..MultiBufferOffset(9),
3767 MultiBufferOffset(15)..MultiBufferOffset(15)
3768 ],
3769 );
3770 });
3771}
3772
3773#[gpui::test]
3774async fn test_tab(cx: &mut TestAppContext) {
3775 init_test(cx, |settings| {
3776 settings.defaults.tab_size = NonZeroU32::new(3)
3777 });
3778
3779 let mut cx = EditorTestContext::new(cx).await;
3780 cx.set_state(indoc! {"
3781 ˇabˇc
3782 ˇ🏀ˇ🏀ˇefg
3783 dˇ
3784 "});
3785 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3786 cx.assert_editor_state(indoc! {"
3787 ˇab ˇc
3788 ˇ🏀 ˇ🏀 ˇefg
3789 d ˇ
3790 "});
3791
3792 cx.set_state(indoc! {"
3793 a
3794 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3795 "});
3796 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3797 cx.assert_editor_state(indoc! {"
3798 a
3799 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3800 "});
3801}
3802
3803#[gpui::test]
3804async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3805 init_test(cx, |_| {});
3806
3807 let mut cx = EditorTestContext::new(cx).await;
3808 let language = Arc::new(
3809 Language::new(
3810 LanguageConfig::default(),
3811 Some(tree_sitter_rust::LANGUAGE.into()),
3812 )
3813 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3814 .unwrap(),
3815 );
3816 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3817
3818 // test when all cursors are not at suggested indent
3819 // then simply move to their suggested indent location
3820 cx.set_state(indoc! {"
3821 const a: B = (
3822 c(
3823 ˇ
3824 ˇ )
3825 );
3826 "});
3827 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3828 cx.assert_editor_state(indoc! {"
3829 const a: B = (
3830 c(
3831 ˇ
3832 ˇ)
3833 );
3834 "});
3835
3836 // test cursor already at suggested indent not moving when
3837 // other cursors are yet to reach their suggested indents
3838 cx.set_state(indoc! {"
3839 ˇ
3840 const a: B = (
3841 c(
3842 d(
3843 ˇ
3844 )
3845 ˇ
3846 ˇ )
3847 );
3848 "});
3849 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3850 cx.assert_editor_state(indoc! {"
3851 ˇ
3852 const a: B = (
3853 c(
3854 d(
3855 ˇ
3856 )
3857 ˇ
3858 ˇ)
3859 );
3860 "});
3861 // test when all cursors are at suggested indent then tab is inserted
3862 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3863 cx.assert_editor_state(indoc! {"
3864 ˇ
3865 const a: B = (
3866 c(
3867 d(
3868 ˇ
3869 )
3870 ˇ
3871 ˇ)
3872 );
3873 "});
3874
3875 // test when current indent is less than suggested indent,
3876 // we adjust line to match suggested indent and move cursor to it
3877 //
3878 // when no other cursor is at word boundary, all of them should move
3879 cx.set_state(indoc! {"
3880 const a: B = (
3881 c(
3882 d(
3883 ˇ
3884 ˇ )
3885 ˇ )
3886 );
3887 "});
3888 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3889 cx.assert_editor_state(indoc! {"
3890 const a: B = (
3891 c(
3892 d(
3893 ˇ
3894 ˇ)
3895 ˇ)
3896 );
3897 "});
3898
3899 // test when current indent is less than suggested indent,
3900 // we adjust line to match suggested indent and move cursor to it
3901 //
3902 // when some other cursor is at word boundary, it should not move
3903 cx.set_state(indoc! {"
3904 const a: B = (
3905 c(
3906 d(
3907 ˇ
3908 ˇ )
3909 ˇ)
3910 );
3911 "});
3912 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3913 cx.assert_editor_state(indoc! {"
3914 const a: B = (
3915 c(
3916 d(
3917 ˇ
3918 ˇ)
3919 ˇ)
3920 );
3921 "});
3922
3923 // test when current indent is more than suggested indent,
3924 // we just move cursor to current indent instead of suggested indent
3925 //
3926 // when no other cursor is at word boundary, all of them should move
3927 cx.set_state(indoc! {"
3928 const a: B = (
3929 c(
3930 d(
3931 ˇ
3932 ˇ )
3933 ˇ )
3934 );
3935 "});
3936 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3937 cx.assert_editor_state(indoc! {"
3938 const a: B = (
3939 c(
3940 d(
3941 ˇ
3942 ˇ)
3943 ˇ)
3944 );
3945 "});
3946 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3947 cx.assert_editor_state(indoc! {"
3948 const a: B = (
3949 c(
3950 d(
3951 ˇ
3952 ˇ)
3953 ˇ)
3954 );
3955 "});
3956
3957 // test when current indent is more than suggested indent,
3958 // we just move cursor to current indent instead of suggested indent
3959 //
3960 // when some other cursor is at word boundary, it doesn't move
3961 cx.set_state(indoc! {"
3962 const a: B = (
3963 c(
3964 d(
3965 ˇ
3966 ˇ )
3967 ˇ)
3968 );
3969 "});
3970 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3971 cx.assert_editor_state(indoc! {"
3972 const a: B = (
3973 c(
3974 d(
3975 ˇ
3976 ˇ)
3977 ˇ)
3978 );
3979 "});
3980
3981 // handle auto-indent when there are multiple cursors on the same line
3982 cx.set_state(indoc! {"
3983 const a: B = (
3984 c(
3985 ˇ ˇ
3986 ˇ )
3987 );
3988 "});
3989 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3990 cx.assert_editor_state(indoc! {"
3991 const a: B = (
3992 c(
3993 ˇ
3994 ˇ)
3995 );
3996 "});
3997}
3998
3999#[gpui::test]
4000async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
4001 init_test(cx, |settings| {
4002 settings.defaults.tab_size = NonZeroU32::new(3)
4003 });
4004
4005 let mut cx = EditorTestContext::new(cx).await;
4006 cx.set_state(indoc! {"
4007 ˇ
4008 \t ˇ
4009 \t ˇ
4010 \t ˇ
4011 \t \t\t \t \t\t \t\t \t \t ˇ
4012 "});
4013
4014 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4015 cx.assert_editor_state(indoc! {"
4016 ˇ
4017 \t ˇ
4018 \t ˇ
4019 \t ˇ
4020 \t \t\t \t \t\t \t\t \t \t ˇ
4021 "});
4022}
4023
4024#[gpui::test]
4025async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
4026 init_test(cx, |settings| {
4027 settings.defaults.tab_size = NonZeroU32::new(4)
4028 });
4029
4030 let language = Arc::new(
4031 Language::new(
4032 LanguageConfig::default(),
4033 Some(tree_sitter_rust::LANGUAGE.into()),
4034 )
4035 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
4036 .unwrap(),
4037 );
4038
4039 let mut cx = EditorTestContext::new(cx).await;
4040 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4041 cx.set_state(indoc! {"
4042 fn a() {
4043 if b {
4044 \t ˇc
4045 }
4046 }
4047 "});
4048
4049 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4050 cx.assert_editor_state(indoc! {"
4051 fn a() {
4052 if b {
4053 ˇc
4054 }
4055 }
4056 "});
4057}
4058
4059#[gpui::test]
4060async fn test_indent_outdent(cx: &mut TestAppContext) {
4061 init_test(cx, |settings| {
4062 settings.defaults.tab_size = NonZeroU32::new(4);
4063 });
4064
4065 let mut cx = EditorTestContext::new(cx).await;
4066
4067 cx.set_state(indoc! {"
4068 «oneˇ» «twoˇ»
4069 three
4070 four
4071 "});
4072 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4073 cx.assert_editor_state(indoc! {"
4074 «oneˇ» «twoˇ»
4075 three
4076 four
4077 "});
4078
4079 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4080 cx.assert_editor_state(indoc! {"
4081 «oneˇ» «twoˇ»
4082 three
4083 four
4084 "});
4085
4086 // select across line ending
4087 cx.set_state(indoc! {"
4088 one two
4089 t«hree
4090 ˇ» four
4091 "});
4092 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4093 cx.assert_editor_state(indoc! {"
4094 one two
4095 t«hree
4096 ˇ» four
4097 "});
4098
4099 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4100 cx.assert_editor_state(indoc! {"
4101 one two
4102 t«hree
4103 ˇ» four
4104 "});
4105
4106 // Ensure that indenting/outdenting works when the cursor is at column 0.
4107 cx.set_state(indoc! {"
4108 one two
4109 ˇthree
4110 four
4111 "});
4112 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4113 cx.assert_editor_state(indoc! {"
4114 one two
4115 ˇthree
4116 four
4117 "});
4118
4119 cx.set_state(indoc! {"
4120 one two
4121 ˇ three
4122 four
4123 "});
4124 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4125 cx.assert_editor_state(indoc! {"
4126 one two
4127 ˇthree
4128 four
4129 "});
4130}
4131
4132#[gpui::test]
4133async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4134 // This is a regression test for issue #33761
4135 init_test(cx, |_| {});
4136
4137 let mut cx = EditorTestContext::new(cx).await;
4138 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4139 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4140
4141 cx.set_state(
4142 r#"ˇ# ingress:
4143ˇ# api:
4144ˇ# enabled: false
4145ˇ# pathType: Prefix
4146ˇ# console:
4147ˇ# enabled: false
4148ˇ# pathType: Prefix
4149"#,
4150 );
4151
4152 // Press tab to indent all lines
4153 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4154
4155 cx.assert_editor_state(
4156 r#" ˇ# ingress:
4157 ˇ# api:
4158 ˇ# enabled: false
4159 ˇ# pathType: Prefix
4160 ˇ# console:
4161 ˇ# enabled: false
4162 ˇ# pathType: Prefix
4163"#,
4164 );
4165}
4166
4167#[gpui::test]
4168async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4169 // This is a test to make sure our fix for issue #33761 didn't break anything
4170 init_test(cx, |_| {});
4171
4172 let mut cx = EditorTestContext::new(cx).await;
4173 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4174 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4175
4176 cx.set_state(
4177 r#"ˇingress:
4178ˇ api:
4179ˇ enabled: false
4180ˇ pathType: Prefix
4181"#,
4182 );
4183
4184 // Press tab to indent all lines
4185 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4186
4187 cx.assert_editor_state(
4188 r#"ˇingress:
4189 ˇapi:
4190 ˇenabled: false
4191 ˇpathType: Prefix
4192"#,
4193 );
4194}
4195
4196#[gpui::test]
4197async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4198 init_test(cx, |settings| {
4199 settings.defaults.hard_tabs = Some(true);
4200 });
4201
4202 let mut cx = EditorTestContext::new(cx).await;
4203
4204 // select two ranges on one line
4205 cx.set_state(indoc! {"
4206 «oneˇ» «twoˇ»
4207 three
4208 four
4209 "});
4210 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4211 cx.assert_editor_state(indoc! {"
4212 \t«oneˇ» «twoˇ»
4213 three
4214 four
4215 "});
4216 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4217 cx.assert_editor_state(indoc! {"
4218 \t\t«oneˇ» «twoˇ»
4219 three
4220 four
4221 "});
4222 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4223 cx.assert_editor_state(indoc! {"
4224 \t«oneˇ» «twoˇ»
4225 three
4226 four
4227 "});
4228 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4229 cx.assert_editor_state(indoc! {"
4230 «oneˇ» «twoˇ»
4231 three
4232 four
4233 "});
4234
4235 // select across a line ending
4236 cx.set_state(indoc! {"
4237 one two
4238 t«hree
4239 ˇ»four
4240 "});
4241 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4242 cx.assert_editor_state(indoc! {"
4243 one two
4244 \tt«hree
4245 ˇ»four
4246 "});
4247 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4248 cx.assert_editor_state(indoc! {"
4249 one two
4250 \t\tt«hree
4251 ˇ»four
4252 "});
4253 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4254 cx.assert_editor_state(indoc! {"
4255 one two
4256 \tt«hree
4257 ˇ»four
4258 "});
4259 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4260 cx.assert_editor_state(indoc! {"
4261 one two
4262 t«hree
4263 ˇ»four
4264 "});
4265
4266 // Ensure that indenting/outdenting works when the cursor is at column 0.
4267 cx.set_state(indoc! {"
4268 one two
4269 ˇthree
4270 four
4271 "});
4272 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4273 cx.assert_editor_state(indoc! {"
4274 one two
4275 ˇthree
4276 four
4277 "});
4278 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4279 cx.assert_editor_state(indoc! {"
4280 one two
4281 \tˇthree
4282 four
4283 "});
4284 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4285 cx.assert_editor_state(indoc! {"
4286 one two
4287 ˇthree
4288 four
4289 "});
4290}
4291
4292#[gpui::test]
4293fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4294 init_test(cx, |settings| {
4295 settings.languages.0.extend([
4296 (
4297 "TOML".into(),
4298 LanguageSettingsContent {
4299 tab_size: NonZeroU32::new(2),
4300 ..Default::default()
4301 },
4302 ),
4303 (
4304 "Rust".into(),
4305 LanguageSettingsContent {
4306 tab_size: NonZeroU32::new(4),
4307 ..Default::default()
4308 },
4309 ),
4310 ]);
4311 });
4312
4313 let toml_language = Arc::new(Language::new(
4314 LanguageConfig {
4315 name: "TOML".into(),
4316 ..Default::default()
4317 },
4318 None,
4319 ));
4320 let rust_language = Arc::new(Language::new(
4321 LanguageConfig {
4322 name: "Rust".into(),
4323 ..Default::default()
4324 },
4325 None,
4326 ));
4327
4328 let toml_buffer =
4329 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4330 let rust_buffer =
4331 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4332 let multibuffer = cx.new(|cx| {
4333 let mut multibuffer = MultiBuffer::new(ReadWrite);
4334 multibuffer.push_excerpts(
4335 toml_buffer.clone(),
4336 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4337 cx,
4338 );
4339 multibuffer.push_excerpts(
4340 rust_buffer.clone(),
4341 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4342 cx,
4343 );
4344 multibuffer
4345 });
4346
4347 cx.add_window(|window, cx| {
4348 let mut editor = build_editor(multibuffer, window, cx);
4349
4350 assert_eq!(
4351 editor.text(cx),
4352 indoc! {"
4353 a = 1
4354 b = 2
4355
4356 const c: usize = 3;
4357 "}
4358 );
4359
4360 select_ranges(
4361 &mut editor,
4362 indoc! {"
4363 «aˇ» = 1
4364 b = 2
4365
4366 «const c:ˇ» usize = 3;
4367 "},
4368 window,
4369 cx,
4370 );
4371
4372 editor.tab(&Tab, window, cx);
4373 assert_text_with_selections(
4374 &mut editor,
4375 indoc! {"
4376 «aˇ» = 1
4377 b = 2
4378
4379 «const c:ˇ» usize = 3;
4380 "},
4381 cx,
4382 );
4383 editor.backtab(&Backtab, window, cx);
4384 assert_text_with_selections(
4385 &mut editor,
4386 indoc! {"
4387 «aˇ» = 1
4388 b = 2
4389
4390 «const c:ˇ» usize = 3;
4391 "},
4392 cx,
4393 );
4394
4395 editor
4396 });
4397}
4398
4399#[gpui::test]
4400async fn test_backspace(cx: &mut TestAppContext) {
4401 init_test(cx, |_| {});
4402
4403 let mut cx = EditorTestContext::new(cx).await;
4404
4405 // Basic backspace
4406 cx.set_state(indoc! {"
4407 onˇe two three
4408 fou«rˇ» five six
4409 seven «ˇeight nine
4410 »ten
4411 "});
4412 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4413 cx.assert_editor_state(indoc! {"
4414 oˇe two three
4415 fouˇ five six
4416 seven ˇten
4417 "});
4418
4419 // Test backspace inside and around indents
4420 cx.set_state(indoc! {"
4421 zero
4422 ˇone
4423 ˇtwo
4424 ˇ ˇ ˇ three
4425 ˇ ˇ four
4426 "});
4427 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4428 cx.assert_editor_state(indoc! {"
4429 zero
4430 ˇone
4431 ˇtwo
4432 ˇ threeˇ four
4433 "});
4434}
4435
4436#[gpui::test]
4437async fn test_delete(cx: &mut TestAppContext) {
4438 init_test(cx, |_| {});
4439
4440 let mut cx = EditorTestContext::new(cx).await;
4441 cx.set_state(indoc! {"
4442 onˇe two three
4443 fou«rˇ» five six
4444 seven «ˇeight nine
4445 »ten
4446 "});
4447 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4448 cx.assert_editor_state(indoc! {"
4449 onˇ two three
4450 fouˇ five six
4451 seven ˇten
4452 "});
4453}
4454
4455#[gpui::test]
4456fn test_delete_line(cx: &mut TestAppContext) {
4457 init_test(cx, |_| {});
4458
4459 let editor = cx.add_window(|window, cx| {
4460 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4461 build_editor(buffer, window, cx)
4462 });
4463 _ = editor.update(cx, |editor, window, cx| {
4464 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4465 s.select_display_ranges([
4466 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4467 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4468 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4469 ])
4470 });
4471 editor.delete_line(&DeleteLine, window, cx);
4472 assert_eq!(editor.display_text(cx), "ghi");
4473 assert_eq!(
4474 display_ranges(editor, cx),
4475 vec![
4476 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4477 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4478 ]
4479 );
4480 });
4481
4482 let editor = cx.add_window(|window, cx| {
4483 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4484 build_editor(buffer, window, cx)
4485 });
4486 _ = editor.update(cx, |editor, window, cx| {
4487 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4488 s.select_display_ranges([
4489 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4490 ])
4491 });
4492 editor.delete_line(&DeleteLine, window, cx);
4493 assert_eq!(editor.display_text(cx), "ghi\n");
4494 assert_eq!(
4495 display_ranges(editor, cx),
4496 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4497 );
4498 });
4499
4500 let editor = cx.add_window(|window, cx| {
4501 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4502 build_editor(buffer, window, cx)
4503 });
4504 _ = editor.update(cx, |editor, window, cx| {
4505 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4506 s.select_display_ranges([
4507 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4508 ])
4509 });
4510 editor.delete_line(&DeleteLine, window, cx);
4511 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4512 assert_eq!(
4513 display_ranges(editor, cx),
4514 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4515 );
4516 });
4517}
4518
4519#[gpui::test]
4520fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4521 init_test(cx, |_| {});
4522
4523 cx.add_window(|window, cx| {
4524 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4525 let mut editor = build_editor(buffer.clone(), window, cx);
4526 let buffer = buffer.read(cx).as_singleton().unwrap();
4527
4528 assert_eq!(
4529 editor
4530 .selections
4531 .ranges::<Point>(&editor.display_snapshot(cx)),
4532 &[Point::new(0, 0)..Point::new(0, 0)]
4533 );
4534
4535 // When on single line, replace newline at end by space
4536 editor.join_lines(&JoinLines, window, cx);
4537 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4538 assert_eq!(
4539 editor
4540 .selections
4541 .ranges::<Point>(&editor.display_snapshot(cx)),
4542 &[Point::new(0, 3)..Point::new(0, 3)]
4543 );
4544
4545 // When multiple lines are selected, remove newlines that are spanned by the selection
4546 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4547 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4548 });
4549 editor.join_lines(&JoinLines, window, cx);
4550 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4551 assert_eq!(
4552 editor
4553 .selections
4554 .ranges::<Point>(&editor.display_snapshot(cx)),
4555 &[Point::new(0, 11)..Point::new(0, 11)]
4556 );
4557
4558 // Undo should be transactional
4559 editor.undo(&Undo, window, cx);
4560 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4561 assert_eq!(
4562 editor
4563 .selections
4564 .ranges::<Point>(&editor.display_snapshot(cx)),
4565 &[Point::new(0, 5)..Point::new(2, 2)]
4566 );
4567
4568 // When joining an empty line don't insert a space
4569 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4570 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4571 });
4572 editor.join_lines(&JoinLines, window, cx);
4573 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4574 assert_eq!(
4575 editor
4576 .selections
4577 .ranges::<Point>(&editor.display_snapshot(cx)),
4578 [Point::new(2, 3)..Point::new(2, 3)]
4579 );
4580
4581 // We can remove trailing newlines
4582 editor.join_lines(&JoinLines, window, cx);
4583 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4584 assert_eq!(
4585 editor
4586 .selections
4587 .ranges::<Point>(&editor.display_snapshot(cx)),
4588 [Point::new(2, 3)..Point::new(2, 3)]
4589 );
4590
4591 // We don't blow up on the last line
4592 editor.join_lines(&JoinLines, window, cx);
4593 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4594 assert_eq!(
4595 editor
4596 .selections
4597 .ranges::<Point>(&editor.display_snapshot(cx)),
4598 [Point::new(2, 3)..Point::new(2, 3)]
4599 );
4600
4601 // reset to test indentation
4602 editor.buffer.update(cx, |buffer, cx| {
4603 buffer.edit(
4604 [
4605 (Point::new(1, 0)..Point::new(1, 2), " "),
4606 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4607 ],
4608 None,
4609 cx,
4610 )
4611 });
4612
4613 // We remove any leading spaces
4614 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4615 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4616 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4617 });
4618 editor.join_lines(&JoinLines, window, cx);
4619 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4620
4621 // We don't insert a space for a line containing only spaces
4622 editor.join_lines(&JoinLines, window, cx);
4623 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4624
4625 // We ignore any leading tabs
4626 editor.join_lines(&JoinLines, window, cx);
4627 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4628
4629 editor
4630 });
4631}
4632
4633#[gpui::test]
4634fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4635 init_test(cx, |_| {});
4636
4637 cx.add_window(|window, cx| {
4638 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4639 let mut editor = build_editor(buffer.clone(), window, cx);
4640 let buffer = buffer.read(cx).as_singleton().unwrap();
4641
4642 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4643 s.select_ranges([
4644 Point::new(0, 2)..Point::new(1, 1),
4645 Point::new(1, 2)..Point::new(1, 2),
4646 Point::new(3, 1)..Point::new(3, 2),
4647 ])
4648 });
4649
4650 editor.join_lines(&JoinLines, window, cx);
4651 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4652
4653 assert_eq!(
4654 editor
4655 .selections
4656 .ranges::<Point>(&editor.display_snapshot(cx)),
4657 [
4658 Point::new(0, 7)..Point::new(0, 7),
4659 Point::new(1, 3)..Point::new(1, 3)
4660 ]
4661 );
4662 editor
4663 });
4664}
4665
4666#[gpui::test]
4667async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4668 init_test(cx, |_| {});
4669
4670 let mut cx = EditorTestContext::new(cx).await;
4671
4672 let diff_base = r#"
4673 Line 0
4674 Line 1
4675 Line 2
4676 Line 3
4677 "#
4678 .unindent();
4679
4680 cx.set_state(
4681 &r#"
4682 ˇLine 0
4683 Line 1
4684 Line 2
4685 Line 3
4686 "#
4687 .unindent(),
4688 );
4689
4690 cx.set_head_text(&diff_base);
4691 executor.run_until_parked();
4692
4693 // Join lines
4694 cx.update_editor(|editor, window, cx| {
4695 editor.join_lines(&JoinLines, window, cx);
4696 });
4697 executor.run_until_parked();
4698
4699 cx.assert_editor_state(
4700 &r#"
4701 Line 0ˇ Line 1
4702 Line 2
4703 Line 3
4704 "#
4705 .unindent(),
4706 );
4707 // Join again
4708 cx.update_editor(|editor, window, cx| {
4709 editor.join_lines(&JoinLines, window, cx);
4710 });
4711 executor.run_until_parked();
4712
4713 cx.assert_editor_state(
4714 &r#"
4715 Line 0 Line 1ˇ Line 2
4716 Line 3
4717 "#
4718 .unindent(),
4719 );
4720}
4721
4722#[gpui::test]
4723async fn test_custom_newlines_cause_no_false_positive_diffs(
4724 executor: BackgroundExecutor,
4725 cx: &mut TestAppContext,
4726) {
4727 init_test(cx, |_| {});
4728 let mut cx = EditorTestContext::new(cx).await;
4729 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4730 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4731 executor.run_until_parked();
4732
4733 cx.update_editor(|editor, window, cx| {
4734 let snapshot = editor.snapshot(window, cx);
4735 assert_eq!(
4736 snapshot
4737 .buffer_snapshot()
4738 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
4739 .collect::<Vec<_>>(),
4740 Vec::new(),
4741 "Should not have any diffs for files with custom newlines"
4742 );
4743 });
4744}
4745
4746#[gpui::test]
4747async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4748 init_test(cx, |_| {});
4749
4750 let mut cx = EditorTestContext::new(cx).await;
4751
4752 // Test sort_lines_case_insensitive()
4753 cx.set_state(indoc! {"
4754 «z
4755 y
4756 x
4757 Z
4758 Y
4759 Xˇ»
4760 "});
4761 cx.update_editor(|e, window, cx| {
4762 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4763 });
4764 cx.assert_editor_state(indoc! {"
4765 «x
4766 X
4767 y
4768 Y
4769 z
4770 Zˇ»
4771 "});
4772
4773 // Test sort_lines_by_length()
4774 //
4775 // Demonstrates:
4776 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4777 // - sort is stable
4778 cx.set_state(indoc! {"
4779 «123
4780 æ
4781 12
4782 ∞
4783 1
4784 æˇ»
4785 "});
4786 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4787 cx.assert_editor_state(indoc! {"
4788 «æ
4789 ∞
4790 1
4791 æ
4792 12
4793 123ˇ»
4794 "});
4795
4796 // Test reverse_lines()
4797 cx.set_state(indoc! {"
4798 «5
4799 4
4800 3
4801 2
4802 1ˇ»
4803 "});
4804 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4805 cx.assert_editor_state(indoc! {"
4806 «1
4807 2
4808 3
4809 4
4810 5ˇ»
4811 "});
4812
4813 // Skip testing shuffle_line()
4814
4815 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4816 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4817
4818 // Don't manipulate when cursor is on single line, but expand the selection
4819 cx.set_state(indoc! {"
4820 ddˇdd
4821 ccc
4822 bb
4823 a
4824 "});
4825 cx.update_editor(|e, window, cx| {
4826 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4827 });
4828 cx.assert_editor_state(indoc! {"
4829 «ddddˇ»
4830 ccc
4831 bb
4832 a
4833 "});
4834
4835 // Basic manipulate case
4836 // Start selection moves to column 0
4837 // End of selection shrinks to fit shorter line
4838 cx.set_state(indoc! {"
4839 dd«d
4840 ccc
4841 bb
4842 aaaaaˇ»
4843 "});
4844 cx.update_editor(|e, window, cx| {
4845 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4846 });
4847 cx.assert_editor_state(indoc! {"
4848 «aaaaa
4849 bb
4850 ccc
4851 dddˇ»
4852 "});
4853
4854 // Manipulate case with newlines
4855 cx.set_state(indoc! {"
4856 dd«d
4857 ccc
4858
4859 bb
4860 aaaaa
4861
4862 ˇ»
4863 "});
4864 cx.update_editor(|e, window, cx| {
4865 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4866 });
4867 cx.assert_editor_state(indoc! {"
4868 «
4869
4870 aaaaa
4871 bb
4872 ccc
4873 dddˇ»
4874
4875 "});
4876
4877 // Adding new line
4878 cx.set_state(indoc! {"
4879 aa«a
4880 bbˇ»b
4881 "});
4882 cx.update_editor(|e, window, cx| {
4883 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4884 });
4885 cx.assert_editor_state(indoc! {"
4886 «aaa
4887 bbb
4888 added_lineˇ»
4889 "});
4890
4891 // Removing line
4892 cx.set_state(indoc! {"
4893 aa«a
4894 bbbˇ»
4895 "});
4896 cx.update_editor(|e, window, cx| {
4897 e.manipulate_immutable_lines(window, cx, |lines| {
4898 lines.pop();
4899 })
4900 });
4901 cx.assert_editor_state(indoc! {"
4902 «aaaˇ»
4903 "});
4904
4905 // Removing all lines
4906 cx.set_state(indoc! {"
4907 aa«a
4908 bbbˇ»
4909 "});
4910 cx.update_editor(|e, window, cx| {
4911 e.manipulate_immutable_lines(window, cx, |lines| {
4912 lines.drain(..);
4913 })
4914 });
4915 cx.assert_editor_state(indoc! {"
4916 ˇ
4917 "});
4918}
4919
4920#[gpui::test]
4921async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4922 init_test(cx, |_| {});
4923
4924 let mut cx = EditorTestContext::new(cx).await;
4925
4926 // Consider continuous selection as single selection
4927 cx.set_state(indoc! {"
4928 Aaa«aa
4929 cˇ»c«c
4930 bb
4931 aaaˇ»aa
4932 "});
4933 cx.update_editor(|e, window, cx| {
4934 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4935 });
4936 cx.assert_editor_state(indoc! {"
4937 «Aaaaa
4938 ccc
4939 bb
4940 aaaaaˇ»
4941 "});
4942
4943 cx.set_state(indoc! {"
4944 Aaa«aa
4945 cˇ»c«c
4946 bb
4947 aaaˇ»aa
4948 "});
4949 cx.update_editor(|e, window, cx| {
4950 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4951 });
4952 cx.assert_editor_state(indoc! {"
4953 «Aaaaa
4954 ccc
4955 bbˇ»
4956 "});
4957
4958 // Consider non continuous selection as distinct dedup operations
4959 cx.set_state(indoc! {"
4960 «aaaaa
4961 bb
4962 aaaaa
4963 aaaaaˇ»
4964
4965 aaa«aaˇ»
4966 "});
4967 cx.update_editor(|e, window, cx| {
4968 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4969 });
4970 cx.assert_editor_state(indoc! {"
4971 «aaaaa
4972 bbˇ»
4973
4974 «aaaaaˇ»
4975 "});
4976}
4977
4978#[gpui::test]
4979async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4980 init_test(cx, |_| {});
4981
4982 let mut cx = EditorTestContext::new(cx).await;
4983
4984 cx.set_state(indoc! {"
4985 «Aaa
4986 aAa
4987 Aaaˇ»
4988 "});
4989 cx.update_editor(|e, window, cx| {
4990 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4991 });
4992 cx.assert_editor_state(indoc! {"
4993 «Aaa
4994 aAaˇ»
4995 "});
4996
4997 cx.set_state(indoc! {"
4998 «Aaa
4999 aAa
5000 aaAˇ»
5001 "});
5002 cx.update_editor(|e, window, cx| {
5003 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5004 });
5005 cx.assert_editor_state(indoc! {"
5006 «Aaaˇ»
5007 "});
5008}
5009
5010#[gpui::test]
5011async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
5012 init_test(cx, |_| {});
5013
5014 let mut cx = EditorTestContext::new(cx).await;
5015
5016 let js_language = Arc::new(Language::new(
5017 LanguageConfig {
5018 name: "JavaScript".into(),
5019 wrap_characters: Some(language::WrapCharactersConfig {
5020 start_prefix: "<".into(),
5021 start_suffix: ">".into(),
5022 end_prefix: "</".into(),
5023 end_suffix: ">".into(),
5024 }),
5025 ..LanguageConfig::default()
5026 },
5027 None,
5028 ));
5029
5030 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5031
5032 cx.set_state(indoc! {"
5033 «testˇ»
5034 "});
5035 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5036 cx.assert_editor_state(indoc! {"
5037 <«ˇ»>test</«ˇ»>
5038 "});
5039
5040 cx.set_state(indoc! {"
5041 «test
5042 testˇ»
5043 "});
5044 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5045 cx.assert_editor_state(indoc! {"
5046 <«ˇ»>test
5047 test</«ˇ»>
5048 "});
5049
5050 cx.set_state(indoc! {"
5051 teˇst
5052 "});
5053 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5054 cx.assert_editor_state(indoc! {"
5055 te<«ˇ»></«ˇ»>st
5056 "});
5057}
5058
5059#[gpui::test]
5060async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5061 init_test(cx, |_| {});
5062
5063 let mut cx = EditorTestContext::new(cx).await;
5064
5065 let js_language = Arc::new(Language::new(
5066 LanguageConfig {
5067 name: "JavaScript".into(),
5068 wrap_characters: Some(language::WrapCharactersConfig {
5069 start_prefix: "<".into(),
5070 start_suffix: ">".into(),
5071 end_prefix: "</".into(),
5072 end_suffix: ">".into(),
5073 }),
5074 ..LanguageConfig::default()
5075 },
5076 None,
5077 ));
5078
5079 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5080
5081 cx.set_state(indoc! {"
5082 «testˇ»
5083 «testˇ» «testˇ»
5084 «testˇ»
5085 "});
5086 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5087 cx.assert_editor_state(indoc! {"
5088 <«ˇ»>test</«ˇ»>
5089 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5090 <«ˇ»>test</«ˇ»>
5091 "});
5092
5093 cx.set_state(indoc! {"
5094 «test
5095 testˇ»
5096 «test
5097 testˇ»
5098 "});
5099 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5100 cx.assert_editor_state(indoc! {"
5101 <«ˇ»>test
5102 test</«ˇ»>
5103 <«ˇ»>test
5104 test</«ˇ»>
5105 "});
5106}
5107
5108#[gpui::test]
5109async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5110 init_test(cx, |_| {});
5111
5112 let mut cx = EditorTestContext::new(cx).await;
5113
5114 let plaintext_language = Arc::new(Language::new(
5115 LanguageConfig {
5116 name: "Plain Text".into(),
5117 ..LanguageConfig::default()
5118 },
5119 None,
5120 ));
5121
5122 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5123
5124 cx.set_state(indoc! {"
5125 «testˇ»
5126 "});
5127 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5128 cx.assert_editor_state(indoc! {"
5129 «testˇ»
5130 "});
5131}
5132
5133#[gpui::test]
5134async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5135 init_test(cx, |_| {});
5136
5137 let mut cx = EditorTestContext::new(cx).await;
5138
5139 // Manipulate with multiple selections on a single line
5140 cx.set_state(indoc! {"
5141 dd«dd
5142 cˇ»c«c
5143 bb
5144 aaaˇ»aa
5145 "});
5146 cx.update_editor(|e, window, cx| {
5147 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5148 });
5149 cx.assert_editor_state(indoc! {"
5150 «aaaaa
5151 bb
5152 ccc
5153 ddddˇ»
5154 "});
5155
5156 // Manipulate with multiple disjoin selections
5157 cx.set_state(indoc! {"
5158 5«
5159 4
5160 3
5161 2
5162 1ˇ»
5163
5164 dd«dd
5165 ccc
5166 bb
5167 aaaˇ»aa
5168 "});
5169 cx.update_editor(|e, window, cx| {
5170 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5171 });
5172 cx.assert_editor_state(indoc! {"
5173 «1
5174 2
5175 3
5176 4
5177 5ˇ»
5178
5179 «aaaaa
5180 bb
5181 ccc
5182 ddddˇ»
5183 "});
5184
5185 // Adding lines on each selection
5186 cx.set_state(indoc! {"
5187 2«
5188 1ˇ»
5189
5190 bb«bb
5191 aaaˇ»aa
5192 "});
5193 cx.update_editor(|e, window, cx| {
5194 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5195 });
5196 cx.assert_editor_state(indoc! {"
5197 «2
5198 1
5199 added lineˇ»
5200
5201 «bbbb
5202 aaaaa
5203 added lineˇ»
5204 "});
5205
5206 // Removing lines on each selection
5207 cx.set_state(indoc! {"
5208 2«
5209 1ˇ»
5210
5211 bb«bb
5212 aaaˇ»aa
5213 "});
5214 cx.update_editor(|e, window, cx| {
5215 e.manipulate_immutable_lines(window, cx, |lines| {
5216 lines.pop();
5217 })
5218 });
5219 cx.assert_editor_state(indoc! {"
5220 «2ˇ»
5221
5222 «bbbbˇ»
5223 "});
5224}
5225
5226#[gpui::test]
5227async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5228 init_test(cx, |settings| {
5229 settings.defaults.tab_size = NonZeroU32::new(3)
5230 });
5231
5232 let mut cx = EditorTestContext::new(cx).await;
5233
5234 // MULTI SELECTION
5235 // Ln.1 "«" tests empty lines
5236 // Ln.9 tests just leading whitespace
5237 cx.set_state(indoc! {"
5238 «
5239 abc // No indentationˇ»
5240 «\tabc // 1 tabˇ»
5241 \t\tabc « ˇ» // 2 tabs
5242 \t ab«c // Tab followed by space
5243 \tabc // Space followed by tab (3 spaces should be the result)
5244 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5245 abˇ»ˇc ˇ ˇ // Already space indented«
5246 \t
5247 \tabc\tdef // Only the leading tab is manipulatedˇ»
5248 "});
5249 cx.update_editor(|e, window, cx| {
5250 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5251 });
5252 cx.assert_editor_state(
5253 indoc! {"
5254 «
5255 abc // No indentation
5256 abc // 1 tab
5257 abc // 2 tabs
5258 abc // Tab followed by space
5259 abc // Space followed by tab (3 spaces should be the result)
5260 abc // Mixed indentation (tab conversion depends on the column)
5261 abc // Already space indented
5262 ·
5263 abc\tdef // Only the leading tab is manipulatedˇ»
5264 "}
5265 .replace("·", "")
5266 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5267 );
5268
5269 // Test on just a few lines, the others should remain unchanged
5270 // Only lines (3, 5, 10, 11) should change
5271 cx.set_state(
5272 indoc! {"
5273 ·
5274 abc // No indentation
5275 \tabcˇ // 1 tab
5276 \t\tabc // 2 tabs
5277 \t abcˇ // Tab followed by space
5278 \tabc // Space followed by tab (3 spaces should be the result)
5279 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5280 abc // Already space indented
5281 «\t
5282 \tabc\tdef // Only the leading tab is manipulatedˇ»
5283 "}
5284 .replace("·", "")
5285 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5286 );
5287 cx.update_editor(|e, window, cx| {
5288 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5289 });
5290 cx.assert_editor_state(
5291 indoc! {"
5292 ·
5293 abc // No indentation
5294 « abc // 1 tabˇ»
5295 \t\tabc // 2 tabs
5296 « abc // Tab followed by spaceˇ»
5297 \tabc // Space followed by tab (3 spaces should be the result)
5298 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5299 abc // Already space indented
5300 « ·
5301 abc\tdef // Only the leading tab is manipulatedˇ»
5302 "}
5303 .replace("·", "")
5304 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5305 );
5306
5307 // SINGLE SELECTION
5308 // Ln.1 "«" tests empty lines
5309 // Ln.9 tests just leading whitespace
5310 cx.set_state(indoc! {"
5311 «
5312 abc // No indentation
5313 \tabc // 1 tab
5314 \t\tabc // 2 tabs
5315 \t abc // Tab followed by space
5316 \tabc // Space followed by tab (3 spaces should be the result)
5317 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5318 abc // Already space indented
5319 \t
5320 \tabc\tdef // Only the leading tab is manipulatedˇ»
5321 "});
5322 cx.update_editor(|e, window, cx| {
5323 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5324 });
5325 cx.assert_editor_state(
5326 indoc! {"
5327 «
5328 abc // No indentation
5329 abc // 1 tab
5330 abc // 2 tabs
5331 abc // Tab followed by space
5332 abc // Space followed by tab (3 spaces should be the result)
5333 abc // Mixed indentation (tab conversion depends on the column)
5334 abc // Already space indented
5335 ·
5336 abc\tdef // Only the leading tab is manipulatedˇ»
5337 "}
5338 .replace("·", "")
5339 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5340 );
5341}
5342
5343#[gpui::test]
5344async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5345 init_test(cx, |settings| {
5346 settings.defaults.tab_size = NonZeroU32::new(3)
5347 });
5348
5349 let mut cx = EditorTestContext::new(cx).await;
5350
5351 // MULTI SELECTION
5352 // Ln.1 "«" tests empty lines
5353 // Ln.11 tests just leading whitespace
5354 cx.set_state(indoc! {"
5355 «
5356 abˇ»ˇc // No indentation
5357 abc ˇ ˇ // 1 space (< 3 so dont convert)
5358 abc « // 2 spaces (< 3 so dont convert)
5359 abc // 3 spaces (convert)
5360 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5361 «\tˇ»\t«\tˇ»abc // Already tab indented
5362 «\t abc // Tab followed by space
5363 \tabc // Space followed by tab (should be consumed due to tab)
5364 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5365 \tˇ» «\t
5366 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5367 "});
5368 cx.update_editor(|e, window, cx| {
5369 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5370 });
5371 cx.assert_editor_state(indoc! {"
5372 «
5373 abc // No indentation
5374 abc // 1 space (< 3 so dont convert)
5375 abc // 2 spaces (< 3 so dont convert)
5376 \tabc // 3 spaces (convert)
5377 \t abc // 5 spaces (1 tab + 2 spaces)
5378 \t\t\tabc // Already tab indented
5379 \t abc // Tab followed by space
5380 \tabc // Space followed by tab (should be consumed due to tab)
5381 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5382 \t\t\t
5383 \tabc \t // Only the leading spaces should be convertedˇ»
5384 "});
5385
5386 // Test on just a few lines, the other should remain unchanged
5387 // Only lines (4, 8, 11, 12) should change
5388 cx.set_state(
5389 indoc! {"
5390 ·
5391 abc // No indentation
5392 abc // 1 space (< 3 so dont convert)
5393 abc // 2 spaces (< 3 so dont convert)
5394 « abc // 3 spaces (convert)ˇ»
5395 abc // 5 spaces (1 tab + 2 spaces)
5396 \t\t\tabc // Already tab indented
5397 \t abc // Tab followed by space
5398 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5399 \t\t \tabc // Mixed indentation
5400 \t \t \t \tabc // Mixed indentation
5401 \t \tˇ
5402 « abc \t // Only the leading spaces should be convertedˇ»
5403 "}
5404 .replace("·", "")
5405 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5406 );
5407 cx.update_editor(|e, window, cx| {
5408 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5409 });
5410 cx.assert_editor_state(
5411 indoc! {"
5412 ·
5413 abc // No indentation
5414 abc // 1 space (< 3 so dont convert)
5415 abc // 2 spaces (< 3 so dont convert)
5416 «\tabc // 3 spaces (convert)ˇ»
5417 abc // 5 spaces (1 tab + 2 spaces)
5418 \t\t\tabc // Already tab indented
5419 \t abc // Tab followed by space
5420 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5421 \t\t \tabc // Mixed indentation
5422 \t \t \t \tabc // Mixed indentation
5423 «\t\t\t
5424 \tabc \t // Only the leading spaces should be convertedˇ»
5425 "}
5426 .replace("·", "")
5427 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5428 );
5429
5430 // SINGLE SELECTION
5431 // Ln.1 "«" tests empty lines
5432 // Ln.11 tests just leading whitespace
5433 cx.set_state(indoc! {"
5434 «
5435 abc // No indentation
5436 abc // 1 space (< 3 so dont convert)
5437 abc // 2 spaces (< 3 so dont convert)
5438 abc // 3 spaces (convert)
5439 abc // 5 spaces (1 tab + 2 spaces)
5440 \t\t\tabc // Already tab indented
5441 \t abc // Tab followed by space
5442 \tabc // Space followed by tab (should be consumed due to tab)
5443 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5444 \t \t
5445 abc \t // Only the leading spaces should be convertedˇ»
5446 "});
5447 cx.update_editor(|e, window, cx| {
5448 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5449 });
5450 cx.assert_editor_state(indoc! {"
5451 «
5452 abc // No indentation
5453 abc // 1 space (< 3 so dont convert)
5454 abc // 2 spaces (< 3 so dont convert)
5455 \tabc // 3 spaces (convert)
5456 \t abc // 5 spaces (1 tab + 2 spaces)
5457 \t\t\tabc // Already tab indented
5458 \t abc // Tab followed by space
5459 \tabc // Space followed by tab (should be consumed due to tab)
5460 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5461 \t\t\t
5462 \tabc \t // Only the leading spaces should be convertedˇ»
5463 "});
5464}
5465
5466#[gpui::test]
5467async fn test_toggle_case(cx: &mut TestAppContext) {
5468 init_test(cx, |_| {});
5469
5470 let mut cx = EditorTestContext::new(cx).await;
5471
5472 // If all lower case -> upper case
5473 cx.set_state(indoc! {"
5474 «hello worldˇ»
5475 "});
5476 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5477 cx.assert_editor_state(indoc! {"
5478 «HELLO WORLDˇ»
5479 "});
5480
5481 // If all upper case -> lower case
5482 cx.set_state(indoc! {"
5483 «HELLO WORLDˇ»
5484 "});
5485 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5486 cx.assert_editor_state(indoc! {"
5487 «hello worldˇ»
5488 "});
5489
5490 // If any upper case characters are identified -> lower case
5491 // This matches JetBrains IDEs
5492 cx.set_state(indoc! {"
5493 «hEllo worldˇ»
5494 "});
5495 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5496 cx.assert_editor_state(indoc! {"
5497 «hello worldˇ»
5498 "});
5499}
5500
5501#[gpui::test]
5502async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5503 init_test(cx, |_| {});
5504
5505 let mut cx = EditorTestContext::new(cx).await;
5506
5507 cx.set_state(indoc! {"
5508 «implement-windows-supportˇ»
5509 "});
5510 cx.update_editor(|e, window, cx| {
5511 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5512 });
5513 cx.assert_editor_state(indoc! {"
5514 «Implement windows supportˇ»
5515 "});
5516}
5517
5518#[gpui::test]
5519async fn test_manipulate_text(cx: &mut TestAppContext) {
5520 init_test(cx, |_| {});
5521
5522 let mut cx = EditorTestContext::new(cx).await;
5523
5524 // Test convert_to_upper_case()
5525 cx.set_state(indoc! {"
5526 «hello worldˇ»
5527 "});
5528 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5529 cx.assert_editor_state(indoc! {"
5530 «HELLO WORLDˇ»
5531 "});
5532
5533 // Test convert_to_lower_case()
5534 cx.set_state(indoc! {"
5535 «HELLO WORLDˇ»
5536 "});
5537 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5538 cx.assert_editor_state(indoc! {"
5539 «hello worldˇ»
5540 "});
5541
5542 // Test multiple line, single selection case
5543 cx.set_state(indoc! {"
5544 «The quick brown
5545 fox jumps over
5546 the lazy dogˇ»
5547 "});
5548 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5549 cx.assert_editor_state(indoc! {"
5550 «The Quick Brown
5551 Fox Jumps Over
5552 The Lazy Dogˇ»
5553 "});
5554
5555 // Test multiple line, single selection case
5556 cx.set_state(indoc! {"
5557 «The quick brown
5558 fox jumps over
5559 the lazy dogˇ»
5560 "});
5561 cx.update_editor(|e, window, cx| {
5562 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5563 });
5564 cx.assert_editor_state(indoc! {"
5565 «TheQuickBrown
5566 FoxJumpsOver
5567 TheLazyDogˇ»
5568 "});
5569
5570 // From here on out, test more complex cases of manipulate_text()
5571
5572 // Test no selection case - should affect words cursors are in
5573 // Cursor at beginning, middle, and end of word
5574 cx.set_state(indoc! {"
5575 ˇhello big beauˇtiful worldˇ
5576 "});
5577 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5578 cx.assert_editor_state(indoc! {"
5579 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5580 "});
5581
5582 // Test multiple selections on a single line and across multiple lines
5583 cx.set_state(indoc! {"
5584 «Theˇ» quick «brown
5585 foxˇ» jumps «overˇ»
5586 the «lazyˇ» dog
5587 "});
5588 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5589 cx.assert_editor_state(indoc! {"
5590 «THEˇ» quick «BROWN
5591 FOXˇ» jumps «OVERˇ»
5592 the «LAZYˇ» dog
5593 "});
5594
5595 // Test case where text length grows
5596 cx.set_state(indoc! {"
5597 «tschüߡ»
5598 "});
5599 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5600 cx.assert_editor_state(indoc! {"
5601 «TSCHÜSSˇ»
5602 "});
5603
5604 // Test to make sure we don't crash when text shrinks
5605 cx.set_state(indoc! {"
5606 aaa_bbbˇ
5607 "});
5608 cx.update_editor(|e, window, cx| {
5609 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5610 });
5611 cx.assert_editor_state(indoc! {"
5612 «aaaBbbˇ»
5613 "});
5614
5615 // Test to make sure we all aware of the fact that each word can grow and shrink
5616 // Final selections should be aware of this fact
5617 cx.set_state(indoc! {"
5618 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5619 "});
5620 cx.update_editor(|e, window, cx| {
5621 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5622 });
5623 cx.assert_editor_state(indoc! {"
5624 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5625 "});
5626
5627 cx.set_state(indoc! {"
5628 «hElLo, WoRld!ˇ»
5629 "});
5630 cx.update_editor(|e, window, cx| {
5631 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5632 });
5633 cx.assert_editor_state(indoc! {"
5634 «HeLlO, wOrLD!ˇ»
5635 "});
5636
5637 // Test selections with `line_mode() = true`.
5638 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5639 cx.set_state(indoc! {"
5640 «The quick brown
5641 fox jumps over
5642 tˇ»he lazy dog
5643 "});
5644 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5645 cx.assert_editor_state(indoc! {"
5646 «THE QUICK BROWN
5647 FOX JUMPS OVER
5648 THE LAZY DOGˇ»
5649 "});
5650}
5651
5652#[gpui::test]
5653fn test_duplicate_line(cx: &mut TestAppContext) {
5654 init_test(cx, |_| {});
5655
5656 let editor = cx.add_window(|window, cx| {
5657 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5658 build_editor(buffer, window, cx)
5659 });
5660 _ = editor.update(cx, |editor, window, cx| {
5661 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5662 s.select_display_ranges([
5663 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5664 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5665 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5666 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5667 ])
5668 });
5669 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5670 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5671 assert_eq!(
5672 display_ranges(editor, cx),
5673 vec![
5674 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5675 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5676 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5677 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5678 ]
5679 );
5680 });
5681
5682 let editor = cx.add_window(|window, cx| {
5683 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5684 build_editor(buffer, window, cx)
5685 });
5686 _ = editor.update(cx, |editor, window, cx| {
5687 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5688 s.select_display_ranges([
5689 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5690 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5691 ])
5692 });
5693 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5694 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5695 assert_eq!(
5696 display_ranges(editor, cx),
5697 vec![
5698 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5699 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5700 ]
5701 );
5702 });
5703
5704 // With `duplicate_line_up` the selections move to the duplicated lines,
5705 // which are inserted above the original lines
5706 let editor = cx.add_window(|window, cx| {
5707 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5708 build_editor(buffer, window, cx)
5709 });
5710 _ = editor.update(cx, |editor, window, cx| {
5711 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5712 s.select_display_ranges([
5713 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5714 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5715 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5716 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5717 ])
5718 });
5719 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5720 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5721 assert_eq!(
5722 display_ranges(editor, cx),
5723 vec![
5724 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5725 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5726 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5727 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5728 ]
5729 );
5730 });
5731
5732 let editor = cx.add_window(|window, cx| {
5733 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5734 build_editor(buffer, window, cx)
5735 });
5736 _ = editor.update(cx, |editor, window, cx| {
5737 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5738 s.select_display_ranges([
5739 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5740 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5741 ])
5742 });
5743 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5744 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5745 assert_eq!(
5746 display_ranges(editor, cx),
5747 vec![
5748 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5749 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5750 ]
5751 );
5752 });
5753
5754 let editor = cx.add_window(|window, cx| {
5755 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5756 build_editor(buffer, window, cx)
5757 });
5758 _ = editor.update(cx, |editor, window, cx| {
5759 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5760 s.select_display_ranges([
5761 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5762 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5763 ])
5764 });
5765 editor.duplicate_selection(&DuplicateSelection, window, cx);
5766 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5767 assert_eq!(
5768 display_ranges(editor, cx),
5769 vec![
5770 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5771 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5772 ]
5773 );
5774 });
5775}
5776
5777#[gpui::test]
5778async fn test_rotate_selections(cx: &mut TestAppContext) {
5779 init_test(cx, |_| {});
5780
5781 let mut cx = EditorTestContext::new(cx).await;
5782
5783 // Rotate text selections (horizontal)
5784 cx.set_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
5785 cx.update_editor(|e, window, cx| {
5786 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5787 });
5788 cx.assert_editor_state("x=«3ˇ», y=«1ˇ», z=«2ˇ»");
5789 cx.update_editor(|e, window, cx| {
5790 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5791 });
5792 cx.assert_editor_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
5793
5794 // Rotate text selections (vertical)
5795 cx.set_state(indoc! {"
5796 x=«1ˇ»
5797 y=«2ˇ»
5798 z=«3ˇ»
5799 "});
5800 cx.update_editor(|e, window, cx| {
5801 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5802 });
5803 cx.assert_editor_state(indoc! {"
5804 x=«3ˇ»
5805 y=«1ˇ»
5806 z=«2ˇ»
5807 "});
5808 cx.update_editor(|e, window, cx| {
5809 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5810 });
5811 cx.assert_editor_state(indoc! {"
5812 x=«1ˇ»
5813 y=«2ˇ»
5814 z=«3ˇ»
5815 "});
5816
5817 // Rotate text selections (vertical, different lengths)
5818 cx.set_state(indoc! {"
5819 x=\"«ˇ»\"
5820 y=\"«aˇ»\"
5821 z=\"«aaˇ»\"
5822 "});
5823 cx.update_editor(|e, window, cx| {
5824 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5825 });
5826 cx.assert_editor_state(indoc! {"
5827 x=\"«aaˇ»\"
5828 y=\"«ˇ»\"
5829 z=\"«aˇ»\"
5830 "});
5831 cx.update_editor(|e, window, cx| {
5832 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5833 });
5834 cx.assert_editor_state(indoc! {"
5835 x=\"«ˇ»\"
5836 y=\"«aˇ»\"
5837 z=\"«aaˇ»\"
5838 "});
5839
5840 // Rotate whole lines (cursor positions preserved)
5841 cx.set_state(indoc! {"
5842 ˇline123
5843 liˇne23
5844 line3ˇ
5845 "});
5846 cx.update_editor(|e, window, cx| {
5847 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5848 });
5849 cx.assert_editor_state(indoc! {"
5850 line3ˇ
5851 ˇline123
5852 liˇne23
5853 "});
5854 cx.update_editor(|e, window, cx| {
5855 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5856 });
5857 cx.assert_editor_state(indoc! {"
5858 ˇline123
5859 liˇne23
5860 line3ˇ
5861 "});
5862
5863 // Rotate whole lines, multiple cursors per line (positions preserved)
5864 cx.set_state(indoc! {"
5865 ˇliˇne123
5866 ˇline23
5867 ˇline3
5868 "});
5869 cx.update_editor(|e, window, cx| {
5870 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5871 });
5872 cx.assert_editor_state(indoc! {"
5873 ˇline3
5874 ˇliˇne123
5875 ˇline23
5876 "});
5877 cx.update_editor(|e, window, cx| {
5878 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5879 });
5880 cx.assert_editor_state(indoc! {"
5881 ˇliˇne123
5882 ˇline23
5883 ˇline3
5884 "});
5885}
5886
5887#[gpui::test]
5888fn test_move_line_up_down(cx: &mut TestAppContext) {
5889 init_test(cx, |_| {});
5890
5891 let editor = cx.add_window(|window, cx| {
5892 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5893 build_editor(buffer, window, cx)
5894 });
5895 _ = editor.update(cx, |editor, window, cx| {
5896 editor.fold_creases(
5897 vec![
5898 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5899 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5900 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5901 ],
5902 true,
5903 window,
5904 cx,
5905 );
5906 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5907 s.select_display_ranges([
5908 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5909 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5910 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5911 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5912 ])
5913 });
5914 assert_eq!(
5915 editor.display_text(cx),
5916 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5917 );
5918
5919 editor.move_line_up(&MoveLineUp, window, cx);
5920 assert_eq!(
5921 editor.display_text(cx),
5922 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5923 );
5924 assert_eq!(
5925 display_ranges(editor, cx),
5926 vec![
5927 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5928 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5929 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5930 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5931 ]
5932 );
5933 });
5934
5935 _ = editor.update(cx, |editor, window, cx| {
5936 editor.move_line_down(&MoveLineDown, window, cx);
5937 assert_eq!(
5938 editor.display_text(cx),
5939 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5940 );
5941 assert_eq!(
5942 display_ranges(editor, cx),
5943 vec![
5944 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5945 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5946 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5947 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5948 ]
5949 );
5950 });
5951
5952 _ = editor.update(cx, |editor, window, cx| {
5953 editor.move_line_down(&MoveLineDown, window, cx);
5954 assert_eq!(
5955 editor.display_text(cx),
5956 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5957 );
5958 assert_eq!(
5959 display_ranges(editor, cx),
5960 vec![
5961 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5962 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5963 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5964 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5965 ]
5966 );
5967 });
5968
5969 _ = editor.update(cx, |editor, window, cx| {
5970 editor.move_line_up(&MoveLineUp, window, cx);
5971 assert_eq!(
5972 editor.display_text(cx),
5973 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5974 );
5975 assert_eq!(
5976 display_ranges(editor, cx),
5977 vec![
5978 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5979 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5980 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5981 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5982 ]
5983 );
5984 });
5985}
5986
5987#[gpui::test]
5988fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5989 init_test(cx, |_| {});
5990 let editor = cx.add_window(|window, cx| {
5991 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5992 build_editor(buffer, window, cx)
5993 });
5994 _ = editor.update(cx, |editor, window, cx| {
5995 editor.fold_creases(
5996 vec![Crease::simple(
5997 Point::new(6, 4)..Point::new(7, 4),
5998 FoldPlaceholder::test(),
5999 )],
6000 true,
6001 window,
6002 cx,
6003 );
6004 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6005 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
6006 });
6007 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
6008 editor.move_line_up(&MoveLineUp, window, cx);
6009 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
6010 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
6011 });
6012}
6013
6014#[gpui::test]
6015fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
6016 init_test(cx, |_| {});
6017
6018 let editor = cx.add_window(|window, cx| {
6019 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
6020 build_editor(buffer, window, cx)
6021 });
6022 _ = editor.update(cx, |editor, window, cx| {
6023 let snapshot = editor.buffer.read(cx).snapshot(cx);
6024 editor.insert_blocks(
6025 [BlockProperties {
6026 style: BlockStyle::Fixed,
6027 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
6028 height: Some(1),
6029 render: Arc::new(|_| div().into_any()),
6030 priority: 0,
6031 }],
6032 Some(Autoscroll::fit()),
6033 cx,
6034 );
6035 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6036 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
6037 });
6038 editor.move_line_down(&MoveLineDown, window, cx);
6039 });
6040}
6041
6042#[gpui::test]
6043async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
6044 init_test(cx, |_| {});
6045
6046 let mut cx = EditorTestContext::new(cx).await;
6047 cx.set_state(
6048 &"
6049 ˇzero
6050 one
6051 two
6052 three
6053 four
6054 five
6055 "
6056 .unindent(),
6057 );
6058
6059 // Create a four-line block that replaces three lines of text.
6060 cx.update_editor(|editor, window, cx| {
6061 let snapshot = editor.snapshot(window, cx);
6062 let snapshot = &snapshot.buffer_snapshot();
6063 let placement = BlockPlacement::Replace(
6064 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
6065 );
6066 editor.insert_blocks(
6067 [BlockProperties {
6068 placement,
6069 height: Some(4),
6070 style: BlockStyle::Sticky,
6071 render: Arc::new(|_| gpui::div().into_any_element()),
6072 priority: 0,
6073 }],
6074 None,
6075 cx,
6076 );
6077 });
6078
6079 // Move down so that the cursor touches the block.
6080 cx.update_editor(|editor, window, cx| {
6081 editor.move_down(&Default::default(), window, cx);
6082 });
6083 cx.assert_editor_state(
6084 &"
6085 zero
6086 «one
6087 two
6088 threeˇ»
6089 four
6090 five
6091 "
6092 .unindent(),
6093 );
6094
6095 // Move down past the block.
6096 cx.update_editor(|editor, window, cx| {
6097 editor.move_down(&Default::default(), window, cx);
6098 });
6099 cx.assert_editor_state(
6100 &"
6101 zero
6102 one
6103 two
6104 three
6105 ˇfour
6106 five
6107 "
6108 .unindent(),
6109 );
6110}
6111
6112#[gpui::test]
6113fn test_transpose(cx: &mut TestAppContext) {
6114 init_test(cx, |_| {});
6115
6116 _ = cx.add_window(|window, cx| {
6117 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
6118 editor.set_style(EditorStyle::default(), window, cx);
6119 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6120 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
6121 });
6122 editor.transpose(&Default::default(), window, cx);
6123 assert_eq!(editor.text(cx), "bac");
6124 assert_eq!(
6125 editor.selections.ranges(&editor.display_snapshot(cx)),
6126 [MultiBufferOffset(2)..MultiBufferOffset(2)]
6127 );
6128
6129 editor.transpose(&Default::default(), window, cx);
6130 assert_eq!(editor.text(cx), "bca");
6131 assert_eq!(
6132 editor.selections.ranges(&editor.display_snapshot(cx)),
6133 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6134 );
6135
6136 editor.transpose(&Default::default(), window, cx);
6137 assert_eq!(editor.text(cx), "bac");
6138 assert_eq!(
6139 editor.selections.ranges(&editor.display_snapshot(cx)),
6140 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6141 );
6142
6143 editor
6144 });
6145
6146 _ = cx.add_window(|window, cx| {
6147 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6148 editor.set_style(EditorStyle::default(), window, cx);
6149 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6150 s.select_ranges([MultiBufferOffset(3)..MultiBufferOffset(3)])
6151 });
6152 editor.transpose(&Default::default(), window, cx);
6153 assert_eq!(editor.text(cx), "acb\nde");
6154 assert_eq!(
6155 editor.selections.ranges(&editor.display_snapshot(cx)),
6156 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6157 );
6158
6159 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6160 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6161 });
6162 editor.transpose(&Default::default(), window, cx);
6163 assert_eq!(editor.text(cx), "acbd\ne");
6164 assert_eq!(
6165 editor.selections.ranges(&editor.display_snapshot(cx)),
6166 [MultiBufferOffset(5)..MultiBufferOffset(5)]
6167 );
6168
6169 editor.transpose(&Default::default(), window, cx);
6170 assert_eq!(editor.text(cx), "acbde\n");
6171 assert_eq!(
6172 editor.selections.ranges(&editor.display_snapshot(cx)),
6173 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6174 );
6175
6176 editor.transpose(&Default::default(), window, cx);
6177 assert_eq!(editor.text(cx), "acbd\ne");
6178 assert_eq!(
6179 editor.selections.ranges(&editor.display_snapshot(cx)),
6180 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6181 );
6182
6183 editor
6184 });
6185
6186 _ = cx.add_window(|window, cx| {
6187 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6188 editor.set_style(EditorStyle::default(), window, cx);
6189 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6190 s.select_ranges([
6191 MultiBufferOffset(1)..MultiBufferOffset(1),
6192 MultiBufferOffset(2)..MultiBufferOffset(2),
6193 MultiBufferOffset(4)..MultiBufferOffset(4),
6194 ])
6195 });
6196 editor.transpose(&Default::default(), window, cx);
6197 assert_eq!(editor.text(cx), "bacd\ne");
6198 assert_eq!(
6199 editor.selections.ranges(&editor.display_snapshot(cx)),
6200 [
6201 MultiBufferOffset(2)..MultiBufferOffset(2),
6202 MultiBufferOffset(3)..MultiBufferOffset(3),
6203 MultiBufferOffset(5)..MultiBufferOffset(5)
6204 ]
6205 );
6206
6207 editor.transpose(&Default::default(), window, cx);
6208 assert_eq!(editor.text(cx), "bcade\n");
6209 assert_eq!(
6210 editor.selections.ranges(&editor.display_snapshot(cx)),
6211 [
6212 MultiBufferOffset(3)..MultiBufferOffset(3),
6213 MultiBufferOffset(4)..MultiBufferOffset(4),
6214 MultiBufferOffset(6)..MultiBufferOffset(6)
6215 ]
6216 );
6217
6218 editor.transpose(&Default::default(), window, cx);
6219 assert_eq!(editor.text(cx), "bcda\ne");
6220 assert_eq!(
6221 editor.selections.ranges(&editor.display_snapshot(cx)),
6222 [
6223 MultiBufferOffset(4)..MultiBufferOffset(4),
6224 MultiBufferOffset(6)..MultiBufferOffset(6)
6225 ]
6226 );
6227
6228 editor.transpose(&Default::default(), window, cx);
6229 assert_eq!(editor.text(cx), "bcade\n");
6230 assert_eq!(
6231 editor.selections.ranges(&editor.display_snapshot(cx)),
6232 [
6233 MultiBufferOffset(4)..MultiBufferOffset(4),
6234 MultiBufferOffset(6)..MultiBufferOffset(6)
6235 ]
6236 );
6237
6238 editor.transpose(&Default::default(), window, cx);
6239 assert_eq!(editor.text(cx), "bcaed\n");
6240 assert_eq!(
6241 editor.selections.ranges(&editor.display_snapshot(cx)),
6242 [
6243 MultiBufferOffset(5)..MultiBufferOffset(5),
6244 MultiBufferOffset(6)..MultiBufferOffset(6)
6245 ]
6246 );
6247
6248 editor
6249 });
6250
6251 _ = cx.add_window(|window, cx| {
6252 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6253 editor.set_style(EditorStyle::default(), window, cx);
6254 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6255 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6256 });
6257 editor.transpose(&Default::default(), window, cx);
6258 assert_eq!(editor.text(cx), "🏀🍐✋");
6259 assert_eq!(
6260 editor.selections.ranges(&editor.display_snapshot(cx)),
6261 [MultiBufferOffset(8)..MultiBufferOffset(8)]
6262 );
6263
6264 editor.transpose(&Default::default(), window, cx);
6265 assert_eq!(editor.text(cx), "🏀✋🍐");
6266 assert_eq!(
6267 editor.selections.ranges(&editor.display_snapshot(cx)),
6268 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6269 );
6270
6271 editor.transpose(&Default::default(), window, cx);
6272 assert_eq!(editor.text(cx), "🏀🍐✋");
6273 assert_eq!(
6274 editor.selections.ranges(&editor.display_snapshot(cx)),
6275 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6276 );
6277
6278 editor
6279 });
6280}
6281
6282#[gpui::test]
6283async fn test_rewrap(cx: &mut TestAppContext) {
6284 init_test(cx, |settings| {
6285 settings.languages.0.extend([
6286 (
6287 "Markdown".into(),
6288 LanguageSettingsContent {
6289 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6290 preferred_line_length: Some(40),
6291 ..Default::default()
6292 },
6293 ),
6294 (
6295 "Plain Text".into(),
6296 LanguageSettingsContent {
6297 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6298 preferred_line_length: Some(40),
6299 ..Default::default()
6300 },
6301 ),
6302 (
6303 "C++".into(),
6304 LanguageSettingsContent {
6305 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6306 preferred_line_length: Some(40),
6307 ..Default::default()
6308 },
6309 ),
6310 (
6311 "Python".into(),
6312 LanguageSettingsContent {
6313 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6314 preferred_line_length: Some(40),
6315 ..Default::default()
6316 },
6317 ),
6318 (
6319 "Rust".into(),
6320 LanguageSettingsContent {
6321 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6322 preferred_line_length: Some(40),
6323 ..Default::default()
6324 },
6325 ),
6326 ])
6327 });
6328
6329 let mut cx = EditorTestContext::new(cx).await;
6330
6331 let cpp_language = Arc::new(Language::new(
6332 LanguageConfig {
6333 name: "C++".into(),
6334 line_comments: vec!["// ".into()],
6335 ..LanguageConfig::default()
6336 },
6337 None,
6338 ));
6339 let python_language = Arc::new(Language::new(
6340 LanguageConfig {
6341 name: "Python".into(),
6342 line_comments: vec!["# ".into()],
6343 ..LanguageConfig::default()
6344 },
6345 None,
6346 ));
6347 let markdown_language = Arc::new(Language::new(
6348 LanguageConfig {
6349 name: "Markdown".into(),
6350 rewrap_prefixes: vec![
6351 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6352 regex::Regex::new("[-*+]\\s+").unwrap(),
6353 ],
6354 ..LanguageConfig::default()
6355 },
6356 None,
6357 ));
6358 let rust_language = Arc::new(
6359 Language::new(
6360 LanguageConfig {
6361 name: "Rust".into(),
6362 line_comments: vec!["// ".into(), "/// ".into()],
6363 ..LanguageConfig::default()
6364 },
6365 Some(tree_sitter_rust::LANGUAGE.into()),
6366 )
6367 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6368 .unwrap(),
6369 );
6370
6371 let plaintext_language = Arc::new(Language::new(
6372 LanguageConfig {
6373 name: "Plain Text".into(),
6374 ..LanguageConfig::default()
6375 },
6376 None,
6377 ));
6378
6379 // Test basic rewrapping of a long line with a cursor
6380 assert_rewrap(
6381 indoc! {"
6382 // ˇThis is a long comment that needs to be wrapped.
6383 "},
6384 indoc! {"
6385 // ˇThis is a long comment that needs to
6386 // be wrapped.
6387 "},
6388 cpp_language.clone(),
6389 &mut cx,
6390 );
6391
6392 // Test rewrapping a full selection
6393 assert_rewrap(
6394 indoc! {"
6395 «// This selected long comment needs to be wrapped.ˇ»"
6396 },
6397 indoc! {"
6398 «// This selected long comment needs to
6399 // be wrapped.ˇ»"
6400 },
6401 cpp_language.clone(),
6402 &mut cx,
6403 );
6404
6405 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6406 assert_rewrap(
6407 indoc! {"
6408 // ˇThis is the first line.
6409 // Thisˇ is the second line.
6410 // This is the thirdˇ line, all part of one paragraph.
6411 "},
6412 indoc! {"
6413 // ˇThis is the first line. Thisˇ is the
6414 // second line. This is the thirdˇ line,
6415 // all part of one paragraph.
6416 "},
6417 cpp_language.clone(),
6418 &mut cx,
6419 );
6420
6421 // Test multiple cursors in different paragraphs trigger separate rewraps
6422 assert_rewrap(
6423 indoc! {"
6424 // ˇThis is the first paragraph, first line.
6425 // ˇThis is the first paragraph, second line.
6426
6427 // ˇThis is the second paragraph, first line.
6428 // ˇThis is the second paragraph, second line.
6429 "},
6430 indoc! {"
6431 // ˇThis is the first paragraph, first
6432 // line. ˇThis is the first paragraph,
6433 // second line.
6434
6435 // ˇThis is the second paragraph, first
6436 // line. ˇThis is the second paragraph,
6437 // second line.
6438 "},
6439 cpp_language.clone(),
6440 &mut cx,
6441 );
6442
6443 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6444 assert_rewrap(
6445 indoc! {"
6446 «// A regular long long comment to be wrapped.
6447 /// A documentation long comment to be wrapped.ˇ»
6448 "},
6449 indoc! {"
6450 «// A regular long long comment to be
6451 // wrapped.
6452 /// A documentation long comment to be
6453 /// wrapped.ˇ»
6454 "},
6455 rust_language.clone(),
6456 &mut cx,
6457 );
6458
6459 // Test that change in indentation level trigger seperate rewraps
6460 assert_rewrap(
6461 indoc! {"
6462 fn foo() {
6463 «// This is a long comment at the base indent.
6464 // This is a long comment at the next indent.ˇ»
6465 }
6466 "},
6467 indoc! {"
6468 fn foo() {
6469 «// This is a long comment at the
6470 // base indent.
6471 // This is a long comment at the
6472 // next indent.ˇ»
6473 }
6474 "},
6475 rust_language.clone(),
6476 &mut cx,
6477 );
6478
6479 // Test that different comment prefix characters (e.g., '#') are handled correctly
6480 assert_rewrap(
6481 indoc! {"
6482 # ˇThis is a long comment using a pound sign.
6483 "},
6484 indoc! {"
6485 # ˇThis is a long comment using a pound
6486 # sign.
6487 "},
6488 python_language,
6489 &mut cx,
6490 );
6491
6492 // Test rewrapping only affects comments, not code even when selected
6493 assert_rewrap(
6494 indoc! {"
6495 «/// This doc comment is long and should be wrapped.
6496 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6497 "},
6498 indoc! {"
6499 «/// This doc comment is long and should
6500 /// be wrapped.
6501 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6502 "},
6503 rust_language.clone(),
6504 &mut cx,
6505 );
6506
6507 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6508 assert_rewrap(
6509 indoc! {"
6510 # Header
6511
6512 A long long long line of markdown text to wrap.ˇ
6513 "},
6514 indoc! {"
6515 # Header
6516
6517 A long long long line of markdown text
6518 to wrap.ˇ
6519 "},
6520 markdown_language.clone(),
6521 &mut cx,
6522 );
6523
6524 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6525 assert_rewrap(
6526 indoc! {"
6527 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6528 2. This is a numbered list item that is very long and needs to be wrapped properly.
6529 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6530 "},
6531 indoc! {"
6532 «1. This is a numbered list item that is
6533 very long and needs to be wrapped
6534 properly.
6535 2. This is a numbered list item that is
6536 very long and needs to be wrapped
6537 properly.
6538 - This is an unordered list item that is
6539 also very long and should not merge
6540 with the numbered item.ˇ»
6541 "},
6542 markdown_language.clone(),
6543 &mut cx,
6544 );
6545
6546 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6547 assert_rewrap(
6548 indoc! {"
6549 «1. This is a numbered list item that is
6550 very long and needs to be wrapped
6551 properly.
6552 2. This is a numbered list item that is
6553 very long and needs to be wrapped
6554 properly.
6555 - This is an unordered list item that is
6556 also very long and should not merge with
6557 the numbered item.ˇ»
6558 "},
6559 indoc! {"
6560 «1. This is a numbered list item that is
6561 very long and needs to be wrapped
6562 properly.
6563 2. This is a numbered list item that is
6564 very long and needs to be wrapped
6565 properly.
6566 - This is an unordered list item that is
6567 also very long and should not merge
6568 with the numbered item.ˇ»
6569 "},
6570 markdown_language.clone(),
6571 &mut cx,
6572 );
6573
6574 // Test that rewrapping maintain indents even when they already exists.
6575 assert_rewrap(
6576 indoc! {"
6577 «1. This is a numbered list
6578 item that is very long and needs to be wrapped properly.
6579 2. This is a numbered list
6580 item that is very long and needs to be wrapped properly.
6581 - This is an unordered list item that is also very long and
6582 should not merge with the numbered item.ˇ»
6583 "},
6584 indoc! {"
6585 «1. This is a numbered list item that is
6586 very long and needs to be wrapped
6587 properly.
6588 2. This is a numbered list item that is
6589 very long and needs to be wrapped
6590 properly.
6591 - This is an unordered list item that is
6592 also very long and should not merge
6593 with the numbered item.ˇ»
6594 "},
6595 markdown_language,
6596 &mut cx,
6597 );
6598
6599 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6600 assert_rewrap(
6601 indoc! {"
6602 ˇThis is a very long line of plain text that will be wrapped.
6603 "},
6604 indoc! {"
6605 ˇThis is a very long line of plain text
6606 that will be wrapped.
6607 "},
6608 plaintext_language.clone(),
6609 &mut cx,
6610 );
6611
6612 // Test that non-commented code acts as a paragraph boundary within a selection
6613 assert_rewrap(
6614 indoc! {"
6615 «// This is the first long comment block to be wrapped.
6616 fn my_func(a: u32);
6617 // This is the second long comment block to be wrapped.ˇ»
6618 "},
6619 indoc! {"
6620 «// This is the first long comment block
6621 // to be wrapped.
6622 fn my_func(a: u32);
6623 // This is the second long comment block
6624 // to be wrapped.ˇ»
6625 "},
6626 rust_language,
6627 &mut cx,
6628 );
6629
6630 // Test rewrapping multiple selections, including ones with blank lines or tabs
6631 assert_rewrap(
6632 indoc! {"
6633 «ˇThis is a very long line that will be wrapped.
6634
6635 This is another paragraph in the same selection.»
6636
6637 «\tThis is a very long indented line that will be wrapped.ˇ»
6638 "},
6639 indoc! {"
6640 «ˇThis is a very long line that will be
6641 wrapped.
6642
6643 This is another paragraph in the same
6644 selection.»
6645
6646 «\tThis is a very long indented line
6647 \tthat will be wrapped.ˇ»
6648 "},
6649 plaintext_language,
6650 &mut cx,
6651 );
6652
6653 // Test that an empty comment line acts as a paragraph boundary
6654 assert_rewrap(
6655 indoc! {"
6656 // ˇThis is a long comment that will be wrapped.
6657 //
6658 // And this is another long comment that will also be wrapped.ˇ
6659 "},
6660 indoc! {"
6661 // ˇThis is a long comment that will be
6662 // wrapped.
6663 //
6664 // And this is another long comment that
6665 // will also be wrapped.ˇ
6666 "},
6667 cpp_language,
6668 &mut cx,
6669 );
6670
6671 #[track_caller]
6672 fn assert_rewrap(
6673 unwrapped_text: &str,
6674 wrapped_text: &str,
6675 language: Arc<Language>,
6676 cx: &mut EditorTestContext,
6677 ) {
6678 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6679 cx.set_state(unwrapped_text);
6680 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6681 cx.assert_editor_state(wrapped_text);
6682 }
6683}
6684
6685#[gpui::test]
6686async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6687 init_test(cx, |settings| {
6688 settings.languages.0.extend([(
6689 "Rust".into(),
6690 LanguageSettingsContent {
6691 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6692 preferred_line_length: Some(40),
6693 ..Default::default()
6694 },
6695 )])
6696 });
6697
6698 let mut cx = EditorTestContext::new(cx).await;
6699
6700 let rust_lang = Arc::new(
6701 Language::new(
6702 LanguageConfig {
6703 name: "Rust".into(),
6704 line_comments: vec!["// ".into()],
6705 block_comment: Some(BlockCommentConfig {
6706 start: "/*".into(),
6707 end: "*/".into(),
6708 prefix: "* ".into(),
6709 tab_size: 1,
6710 }),
6711 documentation_comment: Some(BlockCommentConfig {
6712 start: "/**".into(),
6713 end: "*/".into(),
6714 prefix: "* ".into(),
6715 tab_size: 1,
6716 }),
6717
6718 ..LanguageConfig::default()
6719 },
6720 Some(tree_sitter_rust::LANGUAGE.into()),
6721 )
6722 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6723 .unwrap(),
6724 );
6725
6726 // regular block comment
6727 assert_rewrap(
6728 indoc! {"
6729 /*
6730 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6731 */
6732 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6733 "},
6734 indoc! {"
6735 /*
6736 *ˇ Lorem ipsum dolor sit amet,
6737 * consectetur adipiscing elit.
6738 */
6739 /*
6740 *ˇ Lorem ipsum dolor sit amet,
6741 * consectetur adipiscing elit.
6742 */
6743 "},
6744 rust_lang.clone(),
6745 &mut cx,
6746 );
6747
6748 // indent is respected
6749 assert_rewrap(
6750 indoc! {"
6751 {}
6752 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6753 "},
6754 indoc! {"
6755 {}
6756 /*
6757 *ˇ Lorem ipsum dolor sit amet,
6758 * consectetur adipiscing elit.
6759 */
6760 "},
6761 rust_lang.clone(),
6762 &mut cx,
6763 );
6764
6765 // short block comments with inline delimiters
6766 assert_rewrap(
6767 indoc! {"
6768 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6769 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6770 */
6771 /*
6772 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6773 "},
6774 indoc! {"
6775 /*
6776 *ˇ Lorem ipsum dolor sit amet,
6777 * consectetur adipiscing elit.
6778 */
6779 /*
6780 *ˇ Lorem ipsum dolor sit amet,
6781 * consectetur adipiscing elit.
6782 */
6783 /*
6784 *ˇ Lorem ipsum dolor sit amet,
6785 * consectetur adipiscing elit.
6786 */
6787 "},
6788 rust_lang.clone(),
6789 &mut cx,
6790 );
6791
6792 // multiline block comment with inline start/end delimiters
6793 assert_rewrap(
6794 indoc! {"
6795 /*ˇ Lorem ipsum dolor sit amet,
6796 * consectetur adipiscing elit. */
6797 "},
6798 indoc! {"
6799 /*
6800 *ˇ Lorem ipsum dolor sit amet,
6801 * consectetur adipiscing elit.
6802 */
6803 "},
6804 rust_lang.clone(),
6805 &mut cx,
6806 );
6807
6808 // block comment rewrap still respects paragraph bounds
6809 assert_rewrap(
6810 indoc! {"
6811 /*
6812 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6813 *
6814 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6815 */
6816 "},
6817 indoc! {"
6818 /*
6819 *ˇ Lorem ipsum dolor sit amet,
6820 * consectetur adipiscing elit.
6821 *
6822 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6823 */
6824 "},
6825 rust_lang.clone(),
6826 &mut cx,
6827 );
6828
6829 // documentation comments
6830 assert_rewrap(
6831 indoc! {"
6832 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6833 /**
6834 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6835 */
6836 "},
6837 indoc! {"
6838 /**
6839 *ˇ Lorem ipsum dolor sit amet,
6840 * consectetur adipiscing elit.
6841 */
6842 /**
6843 *ˇ Lorem ipsum dolor sit amet,
6844 * consectetur adipiscing elit.
6845 */
6846 "},
6847 rust_lang.clone(),
6848 &mut cx,
6849 );
6850
6851 // different, adjacent comments
6852 assert_rewrap(
6853 indoc! {"
6854 /**
6855 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6856 */
6857 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
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 //ˇ Lorem ipsum dolor sit amet,
6870 // consectetur adipiscing elit.
6871 "},
6872 rust_lang.clone(),
6873 &mut cx,
6874 );
6875
6876 // selection w/ single short block comment
6877 assert_rewrap(
6878 indoc! {"
6879 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6880 "},
6881 indoc! {"
6882 «/*
6883 * Lorem ipsum dolor sit amet,
6884 * consectetur adipiscing elit.
6885 */ˇ»
6886 "},
6887 rust_lang.clone(),
6888 &mut cx,
6889 );
6890
6891 // rewrapping a single comment w/ abutting comments
6892 assert_rewrap(
6893 indoc! {"
6894 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6895 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6896 "},
6897 indoc! {"
6898 /*
6899 * ˇLorem ipsum dolor sit amet,
6900 * consectetur adipiscing elit.
6901 */
6902 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6903 "},
6904 rust_lang.clone(),
6905 &mut cx,
6906 );
6907
6908 // selection w/ non-abutting short block comments
6909 assert_rewrap(
6910 indoc! {"
6911 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6912
6913 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6914 "},
6915 indoc! {"
6916 «/*
6917 * Lorem ipsum dolor sit amet,
6918 * consectetur adipiscing elit.
6919 */
6920
6921 /*
6922 * Lorem ipsum dolor sit amet,
6923 * consectetur adipiscing elit.
6924 */ˇ»
6925 "},
6926 rust_lang.clone(),
6927 &mut cx,
6928 );
6929
6930 // selection of multiline block comments
6931 assert_rewrap(
6932 indoc! {"
6933 «/* Lorem ipsum dolor sit amet,
6934 * consectetur adipiscing elit. */ˇ»
6935 "},
6936 indoc! {"
6937 «/*
6938 * Lorem ipsum dolor sit amet,
6939 * consectetur adipiscing elit.
6940 */ˇ»
6941 "},
6942 rust_lang.clone(),
6943 &mut cx,
6944 );
6945
6946 // partial selection of multiline block comments
6947 assert_rewrap(
6948 indoc! {"
6949 «/* Lorem ipsum dolor sit amet,ˇ»
6950 * consectetur adipiscing elit. */
6951 /* Lorem ipsum dolor sit amet,
6952 «* consectetur adipiscing elit. */ˇ»
6953 "},
6954 indoc! {"
6955 «/*
6956 * Lorem ipsum dolor sit amet,ˇ»
6957 * consectetur adipiscing elit. */
6958 /* Lorem ipsum dolor sit amet,
6959 «* consectetur adipiscing elit.
6960 */ˇ»
6961 "},
6962 rust_lang.clone(),
6963 &mut cx,
6964 );
6965
6966 // selection w/ abutting short block comments
6967 // TODO: should not be combined; should rewrap as 2 comments
6968 assert_rewrap(
6969 indoc! {"
6970 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6971 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6972 "},
6973 // desired behavior:
6974 // indoc! {"
6975 // «/*
6976 // * Lorem ipsum dolor sit amet,
6977 // * consectetur adipiscing elit.
6978 // */
6979 // /*
6980 // * Lorem ipsum dolor sit amet,
6981 // * consectetur adipiscing elit.
6982 // */ˇ»
6983 // "},
6984 // actual behaviour:
6985 indoc! {"
6986 «/*
6987 * Lorem ipsum dolor sit amet,
6988 * consectetur adipiscing elit. Lorem
6989 * ipsum dolor sit amet, consectetur
6990 * adipiscing elit.
6991 */ˇ»
6992 "},
6993 rust_lang.clone(),
6994 &mut cx,
6995 );
6996
6997 // TODO: same as above, but with delimiters on separate line
6998 // assert_rewrap(
6999 // indoc! {"
7000 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7001 // */
7002 // /*
7003 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7004 // "},
7005 // // desired:
7006 // // indoc! {"
7007 // // «/*
7008 // // * Lorem ipsum dolor sit amet,
7009 // // * consectetur adipiscing elit.
7010 // // */
7011 // // /*
7012 // // * Lorem ipsum dolor sit amet,
7013 // // * consectetur adipiscing elit.
7014 // // */ˇ»
7015 // // "},
7016 // // actual: (but with trailing w/s on the empty lines)
7017 // indoc! {"
7018 // «/*
7019 // * Lorem ipsum dolor sit amet,
7020 // * consectetur adipiscing elit.
7021 // *
7022 // */
7023 // /*
7024 // *
7025 // * Lorem ipsum dolor sit amet,
7026 // * consectetur adipiscing elit.
7027 // */ˇ»
7028 // "},
7029 // rust_lang.clone(),
7030 // &mut cx,
7031 // );
7032
7033 // TODO these are unhandled edge cases; not correct, just documenting known issues
7034 assert_rewrap(
7035 indoc! {"
7036 /*
7037 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7038 */
7039 /*
7040 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7041 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
7042 "},
7043 // desired:
7044 // indoc! {"
7045 // /*
7046 // *ˇ Lorem ipsum dolor sit amet,
7047 // * consectetur adipiscing elit.
7048 // */
7049 // /*
7050 // *ˇ Lorem ipsum dolor sit amet,
7051 // * consectetur adipiscing elit.
7052 // */
7053 // /*
7054 // *ˇ Lorem ipsum dolor sit amet
7055 // */ /* consectetur adipiscing elit. */
7056 // "},
7057 // actual:
7058 indoc! {"
7059 /*
7060 //ˇ Lorem ipsum dolor sit amet,
7061 // consectetur adipiscing elit.
7062 */
7063 /*
7064 * //ˇ Lorem ipsum dolor sit amet,
7065 * consectetur adipiscing elit.
7066 */
7067 /*
7068 *ˇ Lorem ipsum dolor sit amet */ /*
7069 * consectetur adipiscing elit.
7070 */
7071 "},
7072 rust_lang,
7073 &mut cx,
7074 );
7075
7076 #[track_caller]
7077 fn assert_rewrap(
7078 unwrapped_text: &str,
7079 wrapped_text: &str,
7080 language: Arc<Language>,
7081 cx: &mut EditorTestContext,
7082 ) {
7083 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
7084 cx.set_state(unwrapped_text);
7085 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
7086 cx.assert_editor_state(wrapped_text);
7087 }
7088}
7089
7090#[gpui::test]
7091async fn test_hard_wrap(cx: &mut TestAppContext) {
7092 init_test(cx, |_| {});
7093 let mut cx = EditorTestContext::new(cx).await;
7094
7095 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
7096 cx.update_editor(|editor, _, cx| {
7097 editor.set_hard_wrap(Some(14), cx);
7098 });
7099
7100 cx.set_state(indoc!(
7101 "
7102 one two three ˇ
7103 "
7104 ));
7105 cx.simulate_input("four");
7106 cx.run_until_parked();
7107
7108 cx.assert_editor_state(indoc!(
7109 "
7110 one two three
7111 fourˇ
7112 "
7113 ));
7114
7115 cx.update_editor(|editor, window, cx| {
7116 editor.newline(&Default::default(), window, cx);
7117 });
7118 cx.run_until_parked();
7119 cx.assert_editor_state(indoc!(
7120 "
7121 one two three
7122 four
7123 ˇ
7124 "
7125 ));
7126
7127 cx.simulate_input("five");
7128 cx.run_until_parked();
7129 cx.assert_editor_state(indoc!(
7130 "
7131 one two three
7132 four
7133 fiveˇ
7134 "
7135 ));
7136
7137 cx.update_editor(|editor, window, cx| {
7138 editor.newline(&Default::default(), window, cx);
7139 });
7140 cx.run_until_parked();
7141 cx.simulate_input("# ");
7142 cx.run_until_parked();
7143 cx.assert_editor_state(indoc!(
7144 "
7145 one two three
7146 four
7147 five
7148 # ˇ
7149 "
7150 ));
7151
7152 cx.update_editor(|editor, window, cx| {
7153 editor.newline(&Default::default(), window, cx);
7154 });
7155 cx.run_until_parked();
7156 cx.assert_editor_state(indoc!(
7157 "
7158 one two three
7159 four
7160 five
7161 #\x20
7162 #ˇ
7163 "
7164 ));
7165
7166 cx.simulate_input(" 6");
7167 cx.run_until_parked();
7168 cx.assert_editor_state(indoc!(
7169 "
7170 one two three
7171 four
7172 five
7173 #
7174 # 6ˇ
7175 "
7176 ));
7177}
7178
7179#[gpui::test]
7180async fn test_cut_line_ends(cx: &mut TestAppContext) {
7181 init_test(cx, |_| {});
7182
7183 let mut cx = EditorTestContext::new(cx).await;
7184
7185 cx.set_state(indoc! {"The quick brownˇ"});
7186 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7187 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7188
7189 cx.set_state(indoc! {"The emacs foxˇ"});
7190 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7191 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7192
7193 cx.set_state(indoc! {"
7194 The quick« brownˇ»
7195 fox jumps overˇ
7196 the lazy dog"});
7197 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7198 cx.assert_editor_state(indoc! {"
7199 The quickˇ
7200 ˇthe lazy dog"});
7201
7202 cx.set_state(indoc! {"
7203 The quick« brownˇ»
7204 fox jumps overˇ
7205 the lazy dog"});
7206 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7207 cx.assert_editor_state(indoc! {"
7208 The quickˇ
7209 fox jumps overˇthe lazy dog"});
7210
7211 cx.set_state(indoc! {"
7212 The quick« brownˇ»
7213 fox jumps overˇ
7214 the lazy dog"});
7215 cx.update_editor(|e, window, cx| {
7216 e.cut_to_end_of_line(
7217 &CutToEndOfLine {
7218 stop_at_newlines: true,
7219 },
7220 window,
7221 cx,
7222 )
7223 });
7224 cx.assert_editor_state(indoc! {"
7225 The quickˇ
7226 fox jumps overˇ
7227 the lazy dog"});
7228
7229 cx.set_state(indoc! {"
7230 The quick« brownˇ»
7231 fox jumps overˇ
7232 the lazy dog"});
7233 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7234 cx.assert_editor_state(indoc! {"
7235 The quickˇ
7236 fox jumps overˇthe lazy dog"});
7237}
7238
7239#[gpui::test]
7240async fn test_clipboard(cx: &mut TestAppContext) {
7241 init_test(cx, |_| {});
7242
7243 let mut cx = EditorTestContext::new(cx).await;
7244
7245 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7246 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7247 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7248
7249 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7250 cx.set_state("two ˇfour ˇsix ˇ");
7251 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7252 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7253
7254 // Paste again but with only two cursors. Since the number of cursors doesn't
7255 // match the number of slices in the clipboard, the entire clipboard text
7256 // is pasted at each cursor.
7257 cx.set_state("ˇtwo one✅ four three six five ˇ");
7258 cx.update_editor(|e, window, cx| {
7259 e.handle_input("( ", window, cx);
7260 e.paste(&Paste, window, cx);
7261 e.handle_input(") ", window, cx);
7262 });
7263 cx.assert_editor_state(
7264 &([
7265 "( one✅ ",
7266 "three ",
7267 "five ) ˇtwo one✅ four three six five ( one✅ ",
7268 "three ",
7269 "five ) ˇ",
7270 ]
7271 .join("\n")),
7272 );
7273
7274 // Cut with three selections, one of which is full-line.
7275 cx.set_state(indoc! {"
7276 1«2ˇ»3
7277 4ˇ567
7278 «8ˇ»9"});
7279 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7280 cx.assert_editor_state(indoc! {"
7281 1ˇ3
7282 ˇ9"});
7283
7284 // Paste with three selections, noticing how the copied selection that was full-line
7285 // gets inserted before the second cursor.
7286 cx.set_state(indoc! {"
7287 1ˇ3
7288 9ˇ
7289 «oˇ»ne"});
7290 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7291 cx.assert_editor_state(indoc! {"
7292 12ˇ3
7293 4567
7294 9ˇ
7295 8ˇne"});
7296
7297 // Copy with a single cursor only, which writes the whole line into the clipboard.
7298 cx.set_state(indoc! {"
7299 The quick brown
7300 fox juˇmps over
7301 the lazy dog"});
7302 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7303 assert_eq!(
7304 cx.read_from_clipboard()
7305 .and_then(|item| item.text().as_deref().map(str::to_string)),
7306 Some("fox jumps over\n".to_string())
7307 );
7308
7309 // Paste with three selections, noticing how the copied full-line selection is inserted
7310 // before the empty selections but replaces the selection that is non-empty.
7311 cx.set_state(indoc! {"
7312 Tˇhe quick brown
7313 «foˇ»x jumps over
7314 tˇhe lazy dog"});
7315 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7316 cx.assert_editor_state(indoc! {"
7317 fox jumps over
7318 Tˇhe quick brown
7319 fox jumps over
7320 ˇx jumps over
7321 fox jumps over
7322 tˇhe lazy dog"});
7323}
7324
7325#[gpui::test]
7326async fn test_copy_trim(cx: &mut TestAppContext) {
7327 init_test(cx, |_| {});
7328
7329 let mut cx = EditorTestContext::new(cx).await;
7330 cx.set_state(
7331 r#" «for selection in selections.iter() {
7332 let mut start = selection.start;
7333 let mut end = selection.end;
7334 let is_entire_line = selection.is_empty();
7335 if is_entire_line {
7336 start = Point::new(start.row, 0);ˇ»
7337 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7338 }
7339 "#,
7340 );
7341 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7342 assert_eq!(
7343 cx.read_from_clipboard()
7344 .and_then(|item| item.text().as_deref().map(str::to_string)),
7345 Some(
7346 "for selection in selections.iter() {
7347 let mut start = selection.start;
7348 let mut end = selection.end;
7349 let is_entire_line = selection.is_empty();
7350 if is_entire_line {
7351 start = Point::new(start.row, 0);"
7352 .to_string()
7353 ),
7354 "Regular copying preserves all indentation selected",
7355 );
7356 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7357 assert_eq!(
7358 cx.read_from_clipboard()
7359 .and_then(|item| item.text().as_deref().map(str::to_string)),
7360 Some(
7361 "for selection in selections.iter() {
7362let mut start = selection.start;
7363let mut end = selection.end;
7364let is_entire_line = selection.is_empty();
7365if is_entire_line {
7366 start = Point::new(start.row, 0);"
7367 .to_string()
7368 ),
7369 "Copying with stripping should strip all leading whitespaces"
7370 );
7371
7372 cx.set_state(
7373 r#" « for selection in selections.iter() {
7374 let mut start = selection.start;
7375 let mut end = selection.end;
7376 let is_entire_line = selection.is_empty();
7377 if is_entire_line {
7378 start = Point::new(start.row, 0);ˇ»
7379 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7380 }
7381 "#,
7382 );
7383 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7384 assert_eq!(
7385 cx.read_from_clipboard()
7386 .and_then(|item| item.text().as_deref().map(str::to_string)),
7387 Some(
7388 " for selection in selections.iter() {
7389 let mut start = selection.start;
7390 let mut end = selection.end;
7391 let is_entire_line = selection.is_empty();
7392 if is_entire_line {
7393 start = Point::new(start.row, 0);"
7394 .to_string()
7395 ),
7396 "Regular copying preserves all indentation selected",
7397 );
7398 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7399 assert_eq!(
7400 cx.read_from_clipboard()
7401 .and_then(|item| item.text().as_deref().map(str::to_string)),
7402 Some(
7403 "for selection in selections.iter() {
7404let mut start = selection.start;
7405let mut end = selection.end;
7406let is_entire_line = selection.is_empty();
7407if is_entire_line {
7408 start = Point::new(start.row, 0);"
7409 .to_string()
7410 ),
7411 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7412 );
7413
7414 cx.set_state(
7415 r#" «ˇ for selection in selections.iter() {
7416 let mut start = selection.start;
7417 let mut end = selection.end;
7418 let is_entire_line = selection.is_empty();
7419 if is_entire_line {
7420 start = Point::new(start.row, 0);»
7421 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7422 }
7423 "#,
7424 );
7425 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7426 assert_eq!(
7427 cx.read_from_clipboard()
7428 .and_then(|item| item.text().as_deref().map(str::to_string)),
7429 Some(
7430 " for selection in selections.iter() {
7431 let mut start = selection.start;
7432 let mut end = selection.end;
7433 let is_entire_line = selection.is_empty();
7434 if is_entire_line {
7435 start = Point::new(start.row, 0);"
7436 .to_string()
7437 ),
7438 "Regular copying for reverse selection works the same",
7439 );
7440 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7441 assert_eq!(
7442 cx.read_from_clipboard()
7443 .and_then(|item| item.text().as_deref().map(str::to_string)),
7444 Some(
7445 "for selection in selections.iter() {
7446let mut start = selection.start;
7447let mut end = selection.end;
7448let is_entire_line = selection.is_empty();
7449if is_entire_line {
7450 start = Point::new(start.row, 0);"
7451 .to_string()
7452 ),
7453 "Copying with stripping for reverse selection works the same"
7454 );
7455
7456 cx.set_state(
7457 r#" for selection «in selections.iter() {
7458 let mut start = selection.start;
7459 let mut end = selection.end;
7460 let is_entire_line = selection.is_empty();
7461 if is_entire_line {
7462 start = Point::new(start.row, 0);ˇ»
7463 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7464 }
7465 "#,
7466 );
7467 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7468 assert_eq!(
7469 cx.read_from_clipboard()
7470 .and_then(|item| item.text().as_deref().map(str::to_string)),
7471 Some(
7472 "in selections.iter() {
7473 let mut start = selection.start;
7474 let mut end = selection.end;
7475 let is_entire_line = selection.is_empty();
7476 if is_entire_line {
7477 start = Point::new(start.row, 0);"
7478 .to_string()
7479 ),
7480 "When selecting past the indent, the copying works as usual",
7481 );
7482 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7483 assert_eq!(
7484 cx.read_from_clipboard()
7485 .and_then(|item| item.text().as_deref().map(str::to_string)),
7486 Some(
7487 "in selections.iter() {
7488 let mut start = selection.start;
7489 let mut end = selection.end;
7490 let is_entire_line = selection.is_empty();
7491 if is_entire_line {
7492 start = Point::new(start.row, 0);"
7493 .to_string()
7494 ),
7495 "When selecting past the indent, nothing is trimmed"
7496 );
7497
7498 cx.set_state(
7499 r#" «for selection in selections.iter() {
7500 let mut start = selection.start;
7501
7502 let mut end = selection.end;
7503 let is_entire_line = selection.is_empty();
7504 if is_entire_line {
7505 start = Point::new(start.row, 0);
7506ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7507 }
7508 "#,
7509 );
7510 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7511 assert_eq!(
7512 cx.read_from_clipboard()
7513 .and_then(|item| item.text().as_deref().map(str::to_string)),
7514 Some(
7515 "for selection in selections.iter() {
7516let mut start = selection.start;
7517
7518let mut end = selection.end;
7519let is_entire_line = selection.is_empty();
7520if is_entire_line {
7521 start = Point::new(start.row, 0);
7522"
7523 .to_string()
7524 ),
7525 "Copying with stripping should ignore empty lines"
7526 );
7527}
7528
7529#[gpui::test]
7530async fn test_paste_multiline(cx: &mut TestAppContext) {
7531 init_test(cx, |_| {});
7532
7533 let mut cx = EditorTestContext::new(cx).await;
7534 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7535
7536 // Cut an indented block, without the leading whitespace.
7537 cx.set_state(indoc! {"
7538 const a: B = (
7539 c(),
7540 «d(
7541 e,
7542 f
7543 )ˇ»
7544 );
7545 "});
7546 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7547 cx.assert_editor_state(indoc! {"
7548 const a: B = (
7549 c(),
7550 ˇ
7551 );
7552 "});
7553
7554 // Paste it at the same position.
7555 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7556 cx.assert_editor_state(indoc! {"
7557 const a: B = (
7558 c(),
7559 d(
7560 e,
7561 f
7562 )ˇ
7563 );
7564 "});
7565
7566 // Paste it at a line with a lower indent level.
7567 cx.set_state(indoc! {"
7568 ˇ
7569 const a: B = (
7570 c(),
7571 );
7572 "});
7573 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7574 cx.assert_editor_state(indoc! {"
7575 d(
7576 e,
7577 f
7578 )ˇ
7579 const a: B = (
7580 c(),
7581 );
7582 "});
7583
7584 // Cut an indented block, with the leading whitespace.
7585 cx.set_state(indoc! {"
7586 const a: B = (
7587 c(),
7588 « d(
7589 e,
7590 f
7591 )
7592 ˇ»);
7593 "});
7594 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7595 cx.assert_editor_state(indoc! {"
7596 const a: B = (
7597 c(),
7598 ˇ);
7599 "});
7600
7601 // Paste it at the same position.
7602 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7603 cx.assert_editor_state(indoc! {"
7604 const a: B = (
7605 c(),
7606 d(
7607 e,
7608 f
7609 )
7610 ˇ);
7611 "});
7612
7613 // Paste it at a line with a higher indent level.
7614 cx.set_state(indoc! {"
7615 const a: B = (
7616 c(),
7617 d(
7618 e,
7619 fˇ
7620 )
7621 );
7622 "});
7623 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7624 cx.assert_editor_state(indoc! {"
7625 const a: B = (
7626 c(),
7627 d(
7628 e,
7629 f d(
7630 e,
7631 f
7632 )
7633 ˇ
7634 )
7635 );
7636 "});
7637
7638 // Copy an indented block, starting mid-line
7639 cx.set_state(indoc! {"
7640 const a: B = (
7641 c(),
7642 somethin«g(
7643 e,
7644 f
7645 )ˇ»
7646 );
7647 "});
7648 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7649
7650 // Paste it on a line with a lower indent level
7651 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7652 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7653 cx.assert_editor_state(indoc! {"
7654 const a: B = (
7655 c(),
7656 something(
7657 e,
7658 f
7659 )
7660 );
7661 g(
7662 e,
7663 f
7664 )ˇ"});
7665}
7666
7667#[gpui::test]
7668async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7669 init_test(cx, |_| {});
7670
7671 cx.write_to_clipboard(ClipboardItem::new_string(
7672 " d(\n e\n );\n".into(),
7673 ));
7674
7675 let mut cx = EditorTestContext::new(cx).await;
7676 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7677
7678 cx.set_state(indoc! {"
7679 fn a() {
7680 b();
7681 if c() {
7682 ˇ
7683 }
7684 }
7685 "});
7686
7687 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7688 cx.assert_editor_state(indoc! {"
7689 fn a() {
7690 b();
7691 if c() {
7692 d(
7693 e
7694 );
7695 ˇ
7696 }
7697 }
7698 "});
7699
7700 cx.set_state(indoc! {"
7701 fn a() {
7702 b();
7703 ˇ
7704 }
7705 "});
7706
7707 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7708 cx.assert_editor_state(indoc! {"
7709 fn a() {
7710 b();
7711 d(
7712 e
7713 );
7714 ˇ
7715 }
7716 "});
7717}
7718
7719#[gpui::test]
7720fn test_select_all(cx: &mut TestAppContext) {
7721 init_test(cx, |_| {});
7722
7723 let editor = cx.add_window(|window, cx| {
7724 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7725 build_editor(buffer, window, cx)
7726 });
7727 _ = editor.update(cx, |editor, window, cx| {
7728 editor.select_all(&SelectAll, window, cx);
7729 assert_eq!(
7730 display_ranges(editor, cx),
7731 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7732 );
7733 });
7734}
7735
7736#[gpui::test]
7737fn test_select_line(cx: &mut TestAppContext) {
7738 init_test(cx, |_| {});
7739
7740 let editor = cx.add_window(|window, cx| {
7741 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7742 build_editor(buffer, window, cx)
7743 });
7744 _ = editor.update(cx, |editor, window, cx| {
7745 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7746 s.select_display_ranges([
7747 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7748 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7749 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7750 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7751 ])
7752 });
7753 editor.select_line(&SelectLine, window, cx);
7754 // Adjacent line selections should NOT merge (only overlapping ones do)
7755 assert_eq!(
7756 display_ranges(editor, cx),
7757 vec![
7758 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0),
7759 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(2), 0),
7760 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7761 ]
7762 );
7763 });
7764
7765 _ = editor.update(cx, |editor, window, cx| {
7766 editor.select_line(&SelectLine, window, cx);
7767 assert_eq!(
7768 display_ranges(editor, cx),
7769 vec![
7770 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7771 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7772 ]
7773 );
7774 });
7775
7776 _ = editor.update(cx, |editor, window, cx| {
7777 editor.select_line(&SelectLine, window, cx);
7778 // Adjacent but not overlapping, so they stay separate
7779 assert_eq!(
7780 display_ranges(editor, cx),
7781 vec![
7782 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(4), 0),
7783 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7784 ]
7785 );
7786 });
7787}
7788
7789#[gpui::test]
7790async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7791 init_test(cx, |_| {});
7792 let mut cx = EditorTestContext::new(cx).await;
7793
7794 #[track_caller]
7795 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7796 cx.set_state(initial_state);
7797 cx.update_editor(|e, window, cx| {
7798 e.split_selection_into_lines(&Default::default(), window, cx)
7799 });
7800 cx.assert_editor_state(expected_state);
7801 }
7802
7803 // Selection starts and ends at the middle of lines, left-to-right
7804 test(
7805 &mut cx,
7806 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7807 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7808 );
7809 // Same thing, right-to-left
7810 test(
7811 &mut cx,
7812 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7813 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7814 );
7815
7816 // Whole buffer, left-to-right, last line *doesn't* end with newline
7817 test(
7818 &mut cx,
7819 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7820 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7821 );
7822 // Same thing, right-to-left
7823 test(
7824 &mut cx,
7825 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7826 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7827 );
7828
7829 // Whole buffer, left-to-right, last line ends with newline
7830 test(
7831 &mut cx,
7832 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7833 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7834 );
7835 // Same thing, right-to-left
7836 test(
7837 &mut cx,
7838 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7839 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7840 );
7841
7842 // Starts at the end of a line, ends at the start of another
7843 test(
7844 &mut cx,
7845 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7846 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7847 );
7848}
7849
7850#[gpui::test]
7851async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7852 init_test(cx, |_| {});
7853
7854 let editor = cx.add_window(|window, cx| {
7855 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7856 build_editor(buffer, window, cx)
7857 });
7858
7859 // setup
7860 _ = editor.update(cx, |editor, window, cx| {
7861 editor.fold_creases(
7862 vec![
7863 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7864 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7865 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7866 ],
7867 true,
7868 window,
7869 cx,
7870 );
7871 assert_eq!(
7872 editor.display_text(cx),
7873 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7874 );
7875 });
7876
7877 _ = editor.update(cx, |editor, window, cx| {
7878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7879 s.select_display_ranges([
7880 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7881 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7882 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7883 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7884 ])
7885 });
7886 editor.split_selection_into_lines(&Default::default(), window, cx);
7887 assert_eq!(
7888 editor.display_text(cx),
7889 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7890 );
7891 });
7892 EditorTestContext::for_editor(editor, cx)
7893 .await
7894 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7895
7896 _ = editor.update(cx, |editor, window, cx| {
7897 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7898 s.select_display_ranges([
7899 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7900 ])
7901 });
7902 editor.split_selection_into_lines(&Default::default(), window, cx);
7903 assert_eq!(
7904 editor.display_text(cx),
7905 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7906 );
7907 assert_eq!(
7908 display_ranges(editor, cx),
7909 [
7910 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7911 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7912 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7913 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7914 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7915 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7916 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7917 ]
7918 );
7919 });
7920 EditorTestContext::for_editor(editor, cx)
7921 .await
7922 .assert_editor_state(
7923 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7924 );
7925}
7926
7927#[gpui::test]
7928async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7929 init_test(cx, |_| {});
7930
7931 let mut cx = EditorTestContext::new(cx).await;
7932
7933 cx.set_state(indoc!(
7934 r#"abc
7935 defˇghi
7936
7937 jk
7938 nlmo
7939 "#
7940 ));
7941
7942 cx.update_editor(|editor, window, cx| {
7943 editor.add_selection_above(&Default::default(), window, cx);
7944 });
7945
7946 cx.assert_editor_state(indoc!(
7947 r#"abcˇ
7948 defˇghi
7949
7950 jk
7951 nlmo
7952 "#
7953 ));
7954
7955 cx.update_editor(|editor, window, cx| {
7956 editor.add_selection_above(&Default::default(), window, cx);
7957 });
7958
7959 cx.assert_editor_state(indoc!(
7960 r#"abcˇ
7961 defˇghi
7962
7963 jk
7964 nlmo
7965 "#
7966 ));
7967
7968 cx.update_editor(|editor, window, cx| {
7969 editor.add_selection_below(&Default::default(), window, cx);
7970 });
7971
7972 cx.assert_editor_state(indoc!(
7973 r#"abc
7974 defˇghi
7975
7976 jk
7977 nlmo
7978 "#
7979 ));
7980
7981 cx.update_editor(|editor, window, cx| {
7982 editor.undo_selection(&Default::default(), window, cx);
7983 });
7984
7985 cx.assert_editor_state(indoc!(
7986 r#"abcˇ
7987 defˇghi
7988
7989 jk
7990 nlmo
7991 "#
7992 ));
7993
7994 cx.update_editor(|editor, window, cx| {
7995 editor.redo_selection(&Default::default(), window, cx);
7996 });
7997
7998 cx.assert_editor_state(indoc!(
7999 r#"abc
8000 defˇghi
8001
8002 jk
8003 nlmo
8004 "#
8005 ));
8006
8007 cx.update_editor(|editor, window, cx| {
8008 editor.add_selection_below(&Default::default(), window, cx);
8009 });
8010
8011 cx.assert_editor_state(indoc!(
8012 r#"abc
8013 defˇghi
8014 ˇ
8015 jk
8016 nlmo
8017 "#
8018 ));
8019
8020 cx.update_editor(|editor, window, cx| {
8021 editor.add_selection_below(&Default::default(), window, cx);
8022 });
8023
8024 cx.assert_editor_state(indoc!(
8025 r#"abc
8026 defˇghi
8027 ˇ
8028 jkˇ
8029 nlmo
8030 "#
8031 ));
8032
8033 cx.update_editor(|editor, window, cx| {
8034 editor.add_selection_below(&Default::default(), window, cx);
8035 });
8036
8037 cx.assert_editor_state(indoc!(
8038 r#"abc
8039 defˇghi
8040 ˇ
8041 jkˇ
8042 nlmˇo
8043 "#
8044 ));
8045
8046 cx.update_editor(|editor, window, cx| {
8047 editor.add_selection_below(&Default::default(), window, cx);
8048 });
8049
8050 cx.assert_editor_state(indoc!(
8051 r#"abc
8052 defˇghi
8053 ˇ
8054 jkˇ
8055 nlmˇo
8056 ˇ"#
8057 ));
8058
8059 // change selections
8060 cx.set_state(indoc!(
8061 r#"abc
8062 def«ˇg»hi
8063
8064 jk
8065 nlmo
8066 "#
8067 ));
8068
8069 cx.update_editor(|editor, window, cx| {
8070 editor.add_selection_below(&Default::default(), window, cx);
8071 });
8072
8073 cx.assert_editor_state(indoc!(
8074 r#"abc
8075 def«ˇg»hi
8076
8077 jk
8078 nlm«ˇo»
8079 "#
8080 ));
8081
8082 cx.update_editor(|editor, window, cx| {
8083 editor.add_selection_below(&Default::default(), window, cx);
8084 });
8085
8086 cx.assert_editor_state(indoc!(
8087 r#"abc
8088 def«ˇg»hi
8089
8090 jk
8091 nlm«ˇo»
8092 "#
8093 ));
8094
8095 cx.update_editor(|editor, window, cx| {
8096 editor.add_selection_above(&Default::default(), window, cx);
8097 });
8098
8099 cx.assert_editor_state(indoc!(
8100 r#"abc
8101 def«ˇg»hi
8102
8103 jk
8104 nlmo
8105 "#
8106 ));
8107
8108 cx.update_editor(|editor, window, cx| {
8109 editor.add_selection_above(&Default::default(), window, cx);
8110 });
8111
8112 cx.assert_editor_state(indoc!(
8113 r#"abc
8114 def«ˇg»hi
8115
8116 jk
8117 nlmo
8118 "#
8119 ));
8120
8121 // Change selections again
8122 cx.set_state(indoc!(
8123 r#"a«bc
8124 defgˇ»hi
8125
8126 jk
8127 nlmo
8128 "#
8129 ));
8130
8131 cx.update_editor(|editor, window, cx| {
8132 editor.add_selection_below(&Default::default(), window, cx);
8133 });
8134
8135 cx.assert_editor_state(indoc!(
8136 r#"a«bcˇ»
8137 d«efgˇ»hi
8138
8139 j«kˇ»
8140 nlmo
8141 "#
8142 ));
8143
8144 cx.update_editor(|editor, window, cx| {
8145 editor.add_selection_below(&Default::default(), window, cx);
8146 });
8147 cx.assert_editor_state(indoc!(
8148 r#"a«bcˇ»
8149 d«efgˇ»hi
8150
8151 j«kˇ»
8152 n«lmoˇ»
8153 "#
8154 ));
8155 cx.update_editor(|editor, window, cx| {
8156 editor.add_selection_above(&Default::default(), window, cx);
8157 });
8158
8159 cx.assert_editor_state(indoc!(
8160 r#"a«bcˇ»
8161 d«efgˇ»hi
8162
8163 j«kˇ»
8164 nlmo
8165 "#
8166 ));
8167
8168 // Change selections again
8169 cx.set_state(indoc!(
8170 r#"abc
8171 d«ˇefghi
8172
8173 jk
8174 nlm»o
8175 "#
8176 ));
8177
8178 cx.update_editor(|editor, window, cx| {
8179 editor.add_selection_above(&Default::default(), window, cx);
8180 });
8181
8182 cx.assert_editor_state(indoc!(
8183 r#"a«ˇbc»
8184 d«ˇef»ghi
8185
8186 j«ˇk»
8187 n«ˇlm»o
8188 "#
8189 ));
8190
8191 cx.update_editor(|editor, window, cx| {
8192 editor.add_selection_below(&Default::default(), window, cx);
8193 });
8194
8195 cx.assert_editor_state(indoc!(
8196 r#"abc
8197 d«ˇef»ghi
8198
8199 j«ˇk»
8200 n«ˇlm»o
8201 "#
8202 ));
8203}
8204
8205#[gpui::test]
8206async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8207 init_test(cx, |_| {});
8208 let mut cx = EditorTestContext::new(cx).await;
8209
8210 cx.set_state(indoc!(
8211 r#"line onˇe
8212 liˇne two
8213 line three
8214 line four"#
8215 ));
8216
8217 cx.update_editor(|editor, window, cx| {
8218 editor.add_selection_below(&Default::default(), window, cx);
8219 });
8220
8221 // test multiple cursors expand in the same direction
8222 cx.assert_editor_state(indoc!(
8223 r#"line onˇe
8224 liˇne twˇo
8225 liˇne three
8226 line four"#
8227 ));
8228
8229 cx.update_editor(|editor, window, cx| {
8230 editor.add_selection_below(&Default::default(), window, cx);
8231 });
8232
8233 cx.update_editor(|editor, window, cx| {
8234 editor.add_selection_below(&Default::default(), window, cx);
8235 });
8236
8237 // test multiple cursors expand below overflow
8238 cx.assert_editor_state(indoc!(
8239 r#"line onˇe
8240 liˇne twˇo
8241 liˇne thˇree
8242 liˇne foˇur"#
8243 ));
8244
8245 cx.update_editor(|editor, window, cx| {
8246 editor.add_selection_above(&Default::default(), window, cx);
8247 });
8248
8249 // test multiple cursors retrieves back correctly
8250 cx.assert_editor_state(indoc!(
8251 r#"line onˇe
8252 liˇne twˇo
8253 liˇne thˇree
8254 line four"#
8255 ));
8256
8257 cx.update_editor(|editor, window, cx| {
8258 editor.add_selection_above(&Default::default(), window, cx);
8259 });
8260
8261 cx.update_editor(|editor, window, cx| {
8262 editor.add_selection_above(&Default::default(), window, cx);
8263 });
8264
8265 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8266 cx.assert_editor_state(indoc!(
8267 r#"liˇne onˇe
8268 liˇne two
8269 line three
8270 line four"#
8271 ));
8272
8273 cx.update_editor(|editor, window, cx| {
8274 editor.undo_selection(&Default::default(), window, cx);
8275 });
8276
8277 // test undo
8278 cx.assert_editor_state(indoc!(
8279 r#"line onˇe
8280 liˇne twˇo
8281 line three
8282 line four"#
8283 ));
8284
8285 cx.update_editor(|editor, window, cx| {
8286 editor.redo_selection(&Default::default(), window, cx);
8287 });
8288
8289 // test redo
8290 cx.assert_editor_state(indoc!(
8291 r#"liˇne onˇe
8292 liˇne two
8293 line three
8294 line four"#
8295 ));
8296
8297 cx.set_state(indoc!(
8298 r#"abcd
8299 ef«ghˇ»
8300 ijkl
8301 «mˇ»nop"#
8302 ));
8303
8304 cx.update_editor(|editor, window, cx| {
8305 editor.add_selection_above(&Default::default(), window, cx);
8306 });
8307
8308 // test multiple selections expand in the same direction
8309 cx.assert_editor_state(indoc!(
8310 r#"ab«cdˇ»
8311 ef«ghˇ»
8312 «iˇ»jkl
8313 «mˇ»nop"#
8314 ));
8315
8316 cx.update_editor(|editor, window, cx| {
8317 editor.add_selection_above(&Default::default(), window, cx);
8318 });
8319
8320 // test multiple selection upward overflow
8321 cx.assert_editor_state(indoc!(
8322 r#"ab«cdˇ»
8323 «eˇ»f«ghˇ»
8324 «iˇ»jkl
8325 «mˇ»nop"#
8326 ));
8327
8328 cx.update_editor(|editor, window, cx| {
8329 editor.add_selection_below(&Default::default(), window, cx);
8330 });
8331
8332 // test multiple selection retrieves back correctly
8333 cx.assert_editor_state(indoc!(
8334 r#"abcd
8335 ef«ghˇ»
8336 «iˇ»jkl
8337 «mˇ»nop"#
8338 ));
8339
8340 cx.update_editor(|editor, window, cx| {
8341 editor.add_selection_below(&Default::default(), window, cx);
8342 });
8343
8344 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8345 cx.assert_editor_state(indoc!(
8346 r#"abcd
8347 ef«ghˇ»
8348 ij«klˇ»
8349 «mˇ»nop"#
8350 ));
8351
8352 cx.update_editor(|editor, window, cx| {
8353 editor.undo_selection(&Default::default(), window, cx);
8354 });
8355
8356 // test undo
8357 cx.assert_editor_state(indoc!(
8358 r#"abcd
8359 ef«ghˇ»
8360 «iˇ»jkl
8361 «mˇ»nop"#
8362 ));
8363
8364 cx.update_editor(|editor, window, cx| {
8365 editor.redo_selection(&Default::default(), window, cx);
8366 });
8367
8368 // test redo
8369 cx.assert_editor_state(indoc!(
8370 r#"abcd
8371 ef«ghˇ»
8372 ij«klˇ»
8373 «mˇ»nop"#
8374 ));
8375}
8376
8377#[gpui::test]
8378async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8379 init_test(cx, |_| {});
8380 let mut cx = EditorTestContext::new(cx).await;
8381
8382 cx.set_state(indoc!(
8383 r#"line onˇe
8384 liˇne two
8385 line three
8386 line four"#
8387 ));
8388
8389 cx.update_editor(|editor, window, cx| {
8390 editor.add_selection_below(&Default::default(), window, cx);
8391 editor.add_selection_below(&Default::default(), window, cx);
8392 editor.add_selection_below(&Default::default(), window, cx);
8393 });
8394
8395 // initial state with two multi cursor groups
8396 cx.assert_editor_state(indoc!(
8397 r#"line onˇe
8398 liˇne twˇo
8399 liˇne thˇree
8400 liˇne foˇur"#
8401 ));
8402
8403 // add single cursor in middle - simulate opt click
8404 cx.update_editor(|editor, window, cx| {
8405 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8406 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8407 editor.end_selection(window, cx);
8408 });
8409
8410 cx.assert_editor_state(indoc!(
8411 r#"line onˇe
8412 liˇne twˇo
8413 liˇneˇ thˇree
8414 liˇne foˇur"#
8415 ));
8416
8417 cx.update_editor(|editor, window, cx| {
8418 editor.add_selection_above(&Default::default(), window, cx);
8419 });
8420
8421 // test new added selection expands above and existing selection shrinks
8422 cx.assert_editor_state(indoc!(
8423 r#"line onˇe
8424 liˇneˇ twˇo
8425 liˇneˇ thˇree
8426 line four"#
8427 ));
8428
8429 cx.update_editor(|editor, window, cx| {
8430 editor.add_selection_above(&Default::default(), window, cx);
8431 });
8432
8433 // test new added selection expands above and existing selection shrinks
8434 cx.assert_editor_state(indoc!(
8435 r#"lineˇ onˇe
8436 liˇneˇ twˇo
8437 lineˇ three
8438 line four"#
8439 ));
8440
8441 // intial state with two selection groups
8442 cx.set_state(indoc!(
8443 r#"abcd
8444 ef«ghˇ»
8445 ijkl
8446 «mˇ»nop"#
8447 ));
8448
8449 cx.update_editor(|editor, window, cx| {
8450 editor.add_selection_above(&Default::default(), window, cx);
8451 editor.add_selection_above(&Default::default(), window, cx);
8452 });
8453
8454 cx.assert_editor_state(indoc!(
8455 r#"ab«cdˇ»
8456 «eˇ»f«ghˇ»
8457 «iˇ»jkl
8458 «mˇ»nop"#
8459 ));
8460
8461 // add single selection in middle - simulate opt drag
8462 cx.update_editor(|editor, window, cx| {
8463 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8464 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8465 editor.update_selection(
8466 DisplayPoint::new(DisplayRow(2), 4),
8467 0,
8468 gpui::Point::<f32>::default(),
8469 window,
8470 cx,
8471 );
8472 editor.end_selection(window, cx);
8473 });
8474
8475 cx.assert_editor_state(indoc!(
8476 r#"ab«cdˇ»
8477 «eˇ»f«ghˇ»
8478 «iˇ»jk«lˇ»
8479 «mˇ»nop"#
8480 ));
8481
8482 cx.update_editor(|editor, window, cx| {
8483 editor.add_selection_below(&Default::default(), window, cx);
8484 });
8485
8486 // test new added selection expands below, others shrinks from above
8487 cx.assert_editor_state(indoc!(
8488 r#"abcd
8489 ef«ghˇ»
8490 «iˇ»jk«lˇ»
8491 «mˇ»no«pˇ»"#
8492 ));
8493}
8494
8495#[gpui::test]
8496async fn test_select_next(cx: &mut TestAppContext) {
8497 init_test(cx, |_| {});
8498 let mut cx = EditorTestContext::new(cx).await;
8499
8500 // Enable case sensitive search.
8501 update_test_editor_settings(&mut cx, |settings| {
8502 let mut search_settings = SearchSettingsContent::default();
8503 search_settings.case_sensitive = Some(true);
8504 settings.search = Some(search_settings);
8505 });
8506
8507 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8508
8509 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8510 .unwrap();
8511 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8512
8513 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8514 .unwrap();
8515 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8516
8517 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8518 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8519
8520 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8521 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8522
8523 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8524 .unwrap();
8525 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8526
8527 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8528 .unwrap();
8529 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8530
8531 // Test selection direction should be preserved
8532 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8533
8534 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8535 .unwrap();
8536 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8537
8538 // Test case sensitivity
8539 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8540 cx.update_editor(|e, window, cx| {
8541 e.select_next(&SelectNext::default(), window, cx).unwrap();
8542 });
8543 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8544
8545 // Disable case sensitive search.
8546 update_test_editor_settings(&mut cx, |settings| {
8547 let mut search_settings = SearchSettingsContent::default();
8548 search_settings.case_sensitive = Some(false);
8549 settings.search = Some(search_settings);
8550 });
8551
8552 cx.set_state("«ˇfoo»\nFOO\nFoo");
8553 cx.update_editor(|e, window, cx| {
8554 e.select_next(&SelectNext::default(), window, cx).unwrap();
8555 e.select_next(&SelectNext::default(), window, cx).unwrap();
8556 });
8557 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8558}
8559
8560#[gpui::test]
8561async fn test_select_all_matches(cx: &mut TestAppContext) {
8562 init_test(cx, |_| {});
8563 let mut cx = EditorTestContext::new(cx).await;
8564
8565 // Enable case sensitive search.
8566 update_test_editor_settings(&mut cx, |settings| {
8567 let mut search_settings = SearchSettingsContent::default();
8568 search_settings.case_sensitive = Some(true);
8569 settings.search = Some(search_settings);
8570 });
8571
8572 // Test caret-only selections
8573 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8574 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8575 .unwrap();
8576 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8577
8578 // Test left-to-right selections
8579 cx.set_state("abc\n«abcˇ»\nabc");
8580 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8581 .unwrap();
8582 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8583
8584 // Test right-to-left selections
8585 cx.set_state("abc\n«ˇabc»\nabc");
8586 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8587 .unwrap();
8588 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8589
8590 // Test selecting whitespace with caret selection
8591 cx.set_state("abc\nˇ abc\nabc");
8592 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8593 .unwrap();
8594 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8595
8596 // Test selecting whitespace with left-to-right selection
8597 cx.set_state("abc\n«ˇ »abc\nabc");
8598 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8599 .unwrap();
8600 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8601
8602 // Test no matches with right-to-left selection
8603 cx.set_state("abc\n« ˇ»abc\nabc");
8604 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8605 .unwrap();
8606 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8607
8608 // Test with a single word and clip_at_line_ends=true (#29823)
8609 cx.set_state("aˇbc");
8610 cx.update_editor(|e, window, cx| {
8611 e.set_clip_at_line_ends(true, cx);
8612 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8613 e.set_clip_at_line_ends(false, cx);
8614 });
8615 cx.assert_editor_state("«abcˇ»");
8616
8617 // Test case sensitivity
8618 cx.set_state("fˇoo\nFOO\nFoo");
8619 cx.update_editor(|e, window, cx| {
8620 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8621 });
8622 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8623
8624 // Disable case sensitive search.
8625 update_test_editor_settings(&mut cx, |settings| {
8626 let mut search_settings = SearchSettingsContent::default();
8627 search_settings.case_sensitive = Some(false);
8628 settings.search = Some(search_settings);
8629 });
8630
8631 cx.set_state("fˇoo\nFOO\nFoo");
8632 cx.update_editor(|e, window, cx| {
8633 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8634 });
8635 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
8636}
8637
8638#[gpui::test]
8639async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8640 init_test(cx, |_| {});
8641
8642 let mut cx = EditorTestContext::new(cx).await;
8643
8644 let large_body_1 = "\nd".repeat(200);
8645 let large_body_2 = "\ne".repeat(200);
8646
8647 cx.set_state(&format!(
8648 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8649 ));
8650 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8651 let scroll_position = editor.scroll_position(cx);
8652 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8653 scroll_position
8654 });
8655
8656 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8657 .unwrap();
8658 cx.assert_editor_state(&format!(
8659 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8660 ));
8661 let scroll_position_after_selection =
8662 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8663 assert_eq!(
8664 initial_scroll_position, scroll_position_after_selection,
8665 "Scroll position should not change after selecting all matches"
8666 );
8667}
8668
8669#[gpui::test]
8670async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8671 init_test(cx, |_| {});
8672
8673 let mut cx = EditorLspTestContext::new_rust(
8674 lsp::ServerCapabilities {
8675 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8676 ..Default::default()
8677 },
8678 cx,
8679 )
8680 .await;
8681
8682 cx.set_state(indoc! {"
8683 line 1
8684 line 2
8685 linˇe 3
8686 line 4
8687 line 5
8688 "});
8689
8690 // Make an edit
8691 cx.update_editor(|editor, window, cx| {
8692 editor.handle_input("X", window, cx);
8693 });
8694
8695 // Move cursor to a different position
8696 cx.update_editor(|editor, window, cx| {
8697 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8698 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8699 });
8700 });
8701
8702 cx.assert_editor_state(indoc! {"
8703 line 1
8704 line 2
8705 linXe 3
8706 line 4
8707 liˇne 5
8708 "});
8709
8710 cx.lsp
8711 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8712 Ok(Some(vec![lsp::TextEdit::new(
8713 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8714 "PREFIX ".to_string(),
8715 )]))
8716 });
8717
8718 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8719 .unwrap()
8720 .await
8721 .unwrap();
8722
8723 cx.assert_editor_state(indoc! {"
8724 PREFIX line 1
8725 line 2
8726 linXe 3
8727 line 4
8728 liˇne 5
8729 "});
8730
8731 // Undo formatting
8732 cx.update_editor(|editor, window, cx| {
8733 editor.undo(&Default::default(), window, cx);
8734 });
8735
8736 // Verify cursor moved back to position after edit
8737 cx.assert_editor_state(indoc! {"
8738 line 1
8739 line 2
8740 linXˇe 3
8741 line 4
8742 line 5
8743 "});
8744}
8745
8746#[gpui::test]
8747async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8748 init_test(cx, |_| {});
8749
8750 let mut cx = EditorTestContext::new(cx).await;
8751
8752 let provider = cx.new(|_| FakeEditPredictionDelegate::default());
8753 cx.update_editor(|editor, window, cx| {
8754 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8755 });
8756
8757 cx.set_state(indoc! {"
8758 line 1
8759 line 2
8760 linˇe 3
8761 line 4
8762 line 5
8763 line 6
8764 line 7
8765 line 8
8766 line 9
8767 line 10
8768 "});
8769
8770 let snapshot = cx.buffer_snapshot();
8771 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8772
8773 cx.update(|_, cx| {
8774 provider.update(cx, |provider, _| {
8775 provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
8776 id: None,
8777 edits: vec![(edit_position..edit_position, "X".into())],
8778 edit_preview: None,
8779 }))
8780 })
8781 });
8782
8783 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8784 cx.update_editor(|editor, window, cx| {
8785 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8786 });
8787
8788 cx.assert_editor_state(indoc! {"
8789 line 1
8790 line 2
8791 lineXˇ 3
8792 line 4
8793 line 5
8794 line 6
8795 line 7
8796 line 8
8797 line 9
8798 line 10
8799 "});
8800
8801 cx.update_editor(|editor, window, cx| {
8802 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8803 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8804 });
8805 });
8806
8807 cx.assert_editor_state(indoc! {"
8808 line 1
8809 line 2
8810 lineX 3
8811 line 4
8812 line 5
8813 line 6
8814 line 7
8815 line 8
8816 line 9
8817 liˇne 10
8818 "});
8819
8820 cx.update_editor(|editor, window, cx| {
8821 editor.undo(&Default::default(), window, cx);
8822 });
8823
8824 cx.assert_editor_state(indoc! {"
8825 line 1
8826 line 2
8827 lineˇ 3
8828 line 4
8829 line 5
8830 line 6
8831 line 7
8832 line 8
8833 line 9
8834 line 10
8835 "});
8836}
8837
8838#[gpui::test]
8839async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8840 init_test(cx, |_| {});
8841
8842 let mut cx = EditorTestContext::new(cx).await;
8843 cx.set_state(
8844 r#"let foo = 2;
8845lˇet foo = 2;
8846let fooˇ = 2;
8847let foo = 2;
8848let foo = ˇ2;"#,
8849 );
8850
8851 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8852 .unwrap();
8853 cx.assert_editor_state(
8854 r#"let foo = 2;
8855«letˇ» foo = 2;
8856let «fooˇ» = 2;
8857let foo = 2;
8858let foo = «2ˇ»;"#,
8859 );
8860
8861 // noop for multiple selections with different contents
8862 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8863 .unwrap();
8864 cx.assert_editor_state(
8865 r#"let foo = 2;
8866«letˇ» foo = 2;
8867let «fooˇ» = 2;
8868let foo = 2;
8869let foo = «2ˇ»;"#,
8870 );
8871
8872 // Test last selection direction should be preserved
8873 cx.set_state(
8874 r#"let foo = 2;
8875let foo = 2;
8876let «fooˇ» = 2;
8877let «ˇfoo» = 2;
8878let foo = 2;"#,
8879 );
8880
8881 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8882 .unwrap();
8883 cx.assert_editor_state(
8884 r#"let foo = 2;
8885let foo = 2;
8886let «fooˇ» = 2;
8887let «ˇfoo» = 2;
8888let «ˇfoo» = 2;"#,
8889 );
8890}
8891
8892#[gpui::test]
8893async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8894 init_test(cx, |_| {});
8895
8896 let mut cx =
8897 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8898
8899 cx.assert_editor_state(indoc! {"
8900 ˇbbb
8901 ccc
8902
8903 bbb
8904 ccc
8905 "});
8906 cx.dispatch_action(SelectPrevious::default());
8907 cx.assert_editor_state(indoc! {"
8908 «bbbˇ»
8909 ccc
8910
8911 bbb
8912 ccc
8913 "});
8914 cx.dispatch_action(SelectPrevious::default());
8915 cx.assert_editor_state(indoc! {"
8916 «bbbˇ»
8917 ccc
8918
8919 «bbbˇ»
8920 ccc
8921 "});
8922}
8923
8924#[gpui::test]
8925async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8926 init_test(cx, |_| {});
8927
8928 let mut cx = EditorTestContext::new(cx).await;
8929 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8930
8931 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8932 .unwrap();
8933 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8934
8935 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8936 .unwrap();
8937 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8938
8939 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8940 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8941
8942 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8943 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8944
8945 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8946 .unwrap();
8947 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8948
8949 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8950 .unwrap();
8951 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8952}
8953
8954#[gpui::test]
8955async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8956 init_test(cx, |_| {});
8957
8958 let mut cx = EditorTestContext::new(cx).await;
8959 cx.set_state("aˇ");
8960
8961 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8962 .unwrap();
8963 cx.assert_editor_state("«aˇ»");
8964 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8965 .unwrap();
8966 cx.assert_editor_state("«aˇ»");
8967}
8968
8969#[gpui::test]
8970async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8971 init_test(cx, |_| {});
8972
8973 let mut cx = EditorTestContext::new(cx).await;
8974 cx.set_state(
8975 r#"let foo = 2;
8976lˇet foo = 2;
8977let fooˇ = 2;
8978let foo = 2;
8979let foo = ˇ2;"#,
8980 );
8981
8982 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8983 .unwrap();
8984 cx.assert_editor_state(
8985 r#"let foo = 2;
8986«letˇ» foo = 2;
8987let «fooˇ» = 2;
8988let foo = 2;
8989let foo = «2ˇ»;"#,
8990 );
8991
8992 // noop for multiple selections with different contents
8993 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8994 .unwrap();
8995 cx.assert_editor_state(
8996 r#"let foo = 2;
8997«letˇ» foo = 2;
8998let «fooˇ» = 2;
8999let foo = 2;
9000let foo = «2ˇ»;"#,
9001 );
9002}
9003
9004#[gpui::test]
9005async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
9006 init_test(cx, |_| {});
9007 let mut cx = EditorTestContext::new(cx).await;
9008
9009 // Enable case sensitive search.
9010 update_test_editor_settings(&mut cx, |settings| {
9011 let mut search_settings = SearchSettingsContent::default();
9012 search_settings.case_sensitive = Some(true);
9013 settings.search = Some(search_settings);
9014 });
9015
9016 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
9017
9018 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9019 .unwrap();
9020 // selection direction is preserved
9021 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9022
9023 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9024 .unwrap();
9025 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9026
9027 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9028 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9029
9030 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9031 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9032
9033 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9034 .unwrap();
9035 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
9036
9037 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9038 .unwrap();
9039 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
9040
9041 // Test case sensitivity
9042 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
9043 cx.update_editor(|e, window, cx| {
9044 e.select_previous(&SelectPrevious::default(), window, cx)
9045 .unwrap();
9046 e.select_previous(&SelectPrevious::default(), window, cx)
9047 .unwrap();
9048 });
9049 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
9050
9051 // Disable case sensitive search.
9052 update_test_editor_settings(&mut cx, |settings| {
9053 let mut search_settings = SearchSettingsContent::default();
9054 search_settings.case_sensitive = Some(false);
9055 settings.search = Some(search_settings);
9056 });
9057
9058 cx.set_state("foo\nFOO\n«ˇFoo»");
9059 cx.update_editor(|e, window, cx| {
9060 e.select_previous(&SelectPrevious::default(), window, cx)
9061 .unwrap();
9062 e.select_previous(&SelectPrevious::default(), window, cx)
9063 .unwrap();
9064 });
9065 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
9066}
9067
9068#[gpui::test]
9069async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
9070 init_test(cx, |_| {});
9071
9072 let language = Arc::new(Language::new(
9073 LanguageConfig::default(),
9074 Some(tree_sitter_rust::LANGUAGE.into()),
9075 ));
9076
9077 let text = r#"
9078 use mod1::mod2::{mod3, mod4};
9079
9080 fn fn_1(param1: bool, param2: &str) {
9081 let var1 = "text";
9082 }
9083 "#
9084 .unindent();
9085
9086 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9087 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9088 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9089
9090 editor
9091 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9092 .await;
9093
9094 editor.update_in(cx, |editor, window, cx| {
9095 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9096 s.select_display_ranges([
9097 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
9098 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
9099 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
9100 ]);
9101 });
9102 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9103 });
9104 editor.update(cx, |editor, cx| {
9105 assert_text_with_selections(
9106 editor,
9107 indoc! {r#"
9108 use mod1::mod2::{mod3, «mod4ˇ»};
9109
9110 fn fn_1«ˇ(param1: bool, param2: &str)» {
9111 let var1 = "«ˇtext»";
9112 }
9113 "#},
9114 cx,
9115 );
9116 });
9117
9118 editor.update_in(cx, |editor, window, cx| {
9119 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9120 });
9121 editor.update(cx, |editor, cx| {
9122 assert_text_with_selections(
9123 editor,
9124 indoc! {r#"
9125 use mod1::mod2::«{mod3, mod4}ˇ»;
9126
9127 «ˇfn fn_1(param1: bool, param2: &str) {
9128 let var1 = "text";
9129 }»
9130 "#},
9131 cx,
9132 );
9133 });
9134
9135 editor.update_in(cx, |editor, window, cx| {
9136 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9137 });
9138 assert_eq!(
9139 editor.update(cx, |editor, cx| editor
9140 .selections
9141 .display_ranges(&editor.display_snapshot(cx))),
9142 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9143 );
9144
9145 // Trying to expand the selected syntax node one more time has no effect.
9146 editor.update_in(cx, |editor, window, cx| {
9147 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9148 });
9149 assert_eq!(
9150 editor.update(cx, |editor, cx| editor
9151 .selections
9152 .display_ranges(&editor.display_snapshot(cx))),
9153 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9154 );
9155
9156 editor.update_in(cx, |editor, window, cx| {
9157 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9158 });
9159 editor.update(cx, |editor, cx| {
9160 assert_text_with_selections(
9161 editor,
9162 indoc! {r#"
9163 use mod1::mod2::«{mod3, mod4}ˇ»;
9164
9165 «ˇfn fn_1(param1: bool, param2: &str) {
9166 let var1 = "text";
9167 }»
9168 "#},
9169 cx,
9170 );
9171 });
9172
9173 editor.update_in(cx, |editor, window, cx| {
9174 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9175 });
9176 editor.update(cx, |editor, cx| {
9177 assert_text_with_selections(
9178 editor,
9179 indoc! {r#"
9180 use mod1::mod2::{mod3, «mod4ˇ»};
9181
9182 fn fn_1«ˇ(param1: bool, param2: &str)» {
9183 let var1 = "«ˇtext»";
9184 }
9185 "#},
9186 cx,
9187 );
9188 });
9189
9190 editor.update_in(cx, |editor, window, cx| {
9191 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9192 });
9193 editor.update(cx, |editor, cx| {
9194 assert_text_with_selections(
9195 editor,
9196 indoc! {r#"
9197 use mod1::mod2::{mod3, moˇd4};
9198
9199 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9200 let var1 = "teˇxt";
9201 }
9202 "#},
9203 cx,
9204 );
9205 });
9206
9207 // Trying to shrink the selected syntax node one more time has no effect.
9208 editor.update_in(cx, |editor, window, cx| {
9209 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9210 });
9211 editor.update_in(cx, |editor, _, cx| {
9212 assert_text_with_selections(
9213 editor,
9214 indoc! {r#"
9215 use mod1::mod2::{mod3, moˇd4};
9216
9217 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9218 let var1 = "teˇxt";
9219 }
9220 "#},
9221 cx,
9222 );
9223 });
9224
9225 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9226 // a fold.
9227 editor.update_in(cx, |editor, window, cx| {
9228 editor.fold_creases(
9229 vec![
9230 Crease::simple(
9231 Point::new(0, 21)..Point::new(0, 24),
9232 FoldPlaceholder::test(),
9233 ),
9234 Crease::simple(
9235 Point::new(3, 20)..Point::new(3, 22),
9236 FoldPlaceholder::test(),
9237 ),
9238 ],
9239 true,
9240 window,
9241 cx,
9242 );
9243 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9244 });
9245 editor.update(cx, |editor, cx| {
9246 assert_text_with_selections(
9247 editor,
9248 indoc! {r#"
9249 use mod1::mod2::«{mod3, mod4}ˇ»;
9250
9251 fn fn_1«ˇ(param1: bool, param2: &str)» {
9252 let var1 = "«ˇtext»";
9253 }
9254 "#},
9255 cx,
9256 );
9257 });
9258}
9259
9260#[gpui::test]
9261async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9262 init_test(cx, |_| {});
9263
9264 let language = Arc::new(Language::new(
9265 LanguageConfig::default(),
9266 Some(tree_sitter_rust::LANGUAGE.into()),
9267 ));
9268
9269 let text = "let a = 2;";
9270
9271 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9272 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9273 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9274
9275 editor
9276 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9277 .await;
9278
9279 // Test case 1: Cursor at end of word
9280 editor.update_in(cx, |editor, window, cx| {
9281 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9282 s.select_display_ranges([
9283 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9284 ]);
9285 });
9286 });
9287 editor.update(cx, |editor, cx| {
9288 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9289 });
9290 editor.update_in(cx, |editor, window, cx| {
9291 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9292 });
9293 editor.update(cx, |editor, cx| {
9294 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9295 });
9296 editor.update_in(cx, |editor, window, cx| {
9297 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9298 });
9299 editor.update(cx, |editor, cx| {
9300 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9301 });
9302
9303 // Test case 2: Cursor at end of statement
9304 editor.update_in(cx, |editor, window, cx| {
9305 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9306 s.select_display_ranges([
9307 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9308 ]);
9309 });
9310 });
9311 editor.update(cx, |editor, cx| {
9312 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9313 });
9314 editor.update_in(cx, |editor, window, cx| {
9315 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9316 });
9317 editor.update(cx, |editor, cx| {
9318 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9319 });
9320}
9321
9322#[gpui::test]
9323async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9324 init_test(cx, |_| {});
9325
9326 let language = Arc::new(Language::new(
9327 LanguageConfig {
9328 name: "JavaScript".into(),
9329 ..Default::default()
9330 },
9331 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9332 ));
9333
9334 let text = r#"
9335 let a = {
9336 key: "value",
9337 };
9338 "#
9339 .unindent();
9340
9341 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9342 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9343 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9344
9345 editor
9346 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9347 .await;
9348
9349 // Test case 1: Cursor after '{'
9350 editor.update_in(cx, |editor, window, cx| {
9351 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9352 s.select_display_ranges([
9353 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9354 ]);
9355 });
9356 });
9357 editor.update(cx, |editor, cx| {
9358 assert_text_with_selections(
9359 editor,
9360 indoc! {r#"
9361 let a = {ˇ
9362 key: "value",
9363 };
9364 "#},
9365 cx,
9366 );
9367 });
9368 editor.update_in(cx, |editor, window, cx| {
9369 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9370 });
9371 editor.update(cx, |editor, cx| {
9372 assert_text_with_selections(
9373 editor,
9374 indoc! {r#"
9375 let a = «ˇ{
9376 key: "value",
9377 }»;
9378 "#},
9379 cx,
9380 );
9381 });
9382
9383 // Test case 2: Cursor after ':'
9384 editor.update_in(cx, |editor, window, cx| {
9385 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9386 s.select_display_ranges([
9387 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9388 ]);
9389 });
9390 });
9391 editor.update(cx, |editor, cx| {
9392 assert_text_with_selections(
9393 editor,
9394 indoc! {r#"
9395 let a = {
9396 key:ˇ "value",
9397 };
9398 "#},
9399 cx,
9400 );
9401 });
9402 editor.update_in(cx, |editor, window, cx| {
9403 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9404 });
9405 editor.update(cx, |editor, cx| {
9406 assert_text_with_selections(
9407 editor,
9408 indoc! {r#"
9409 let a = {
9410 «ˇkey: "value"»,
9411 };
9412 "#},
9413 cx,
9414 );
9415 });
9416 editor.update_in(cx, |editor, window, cx| {
9417 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9418 });
9419 editor.update(cx, |editor, cx| {
9420 assert_text_with_selections(
9421 editor,
9422 indoc! {r#"
9423 let a = «ˇ{
9424 key: "value",
9425 }»;
9426 "#},
9427 cx,
9428 );
9429 });
9430
9431 // Test case 3: Cursor after ','
9432 editor.update_in(cx, |editor, window, cx| {
9433 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9434 s.select_display_ranges([
9435 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9436 ]);
9437 });
9438 });
9439 editor.update(cx, |editor, cx| {
9440 assert_text_with_selections(
9441 editor,
9442 indoc! {r#"
9443 let a = {
9444 key: "value",ˇ
9445 };
9446 "#},
9447 cx,
9448 );
9449 });
9450 editor.update_in(cx, |editor, window, cx| {
9451 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9452 });
9453 editor.update(cx, |editor, cx| {
9454 assert_text_with_selections(
9455 editor,
9456 indoc! {r#"
9457 let a = «ˇ{
9458 key: "value",
9459 }»;
9460 "#},
9461 cx,
9462 );
9463 });
9464
9465 // Test case 4: Cursor after ';'
9466 editor.update_in(cx, |editor, window, cx| {
9467 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9468 s.select_display_ranges([
9469 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9470 ]);
9471 });
9472 });
9473 editor.update(cx, |editor, cx| {
9474 assert_text_with_selections(
9475 editor,
9476 indoc! {r#"
9477 let a = {
9478 key: "value",
9479 };ˇ
9480 "#},
9481 cx,
9482 );
9483 });
9484 editor.update_in(cx, |editor, window, cx| {
9485 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9486 });
9487 editor.update(cx, |editor, cx| {
9488 assert_text_with_selections(
9489 editor,
9490 indoc! {r#"
9491 «ˇlet a = {
9492 key: "value",
9493 };
9494 »"#},
9495 cx,
9496 );
9497 });
9498}
9499
9500#[gpui::test]
9501async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9502 init_test(cx, |_| {});
9503
9504 let language = Arc::new(Language::new(
9505 LanguageConfig::default(),
9506 Some(tree_sitter_rust::LANGUAGE.into()),
9507 ));
9508
9509 let text = r#"
9510 use mod1::mod2::{mod3, mod4};
9511
9512 fn fn_1(param1: bool, param2: &str) {
9513 let var1 = "hello world";
9514 }
9515 "#
9516 .unindent();
9517
9518 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9519 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9520 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9521
9522 editor
9523 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9524 .await;
9525
9526 // Test 1: Cursor on a letter of a string word
9527 editor.update_in(cx, |editor, window, cx| {
9528 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9529 s.select_display_ranges([
9530 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9531 ]);
9532 });
9533 });
9534 editor.update_in(cx, |editor, window, cx| {
9535 assert_text_with_selections(
9536 editor,
9537 indoc! {r#"
9538 use mod1::mod2::{mod3, mod4};
9539
9540 fn fn_1(param1: bool, param2: &str) {
9541 let var1 = "hˇello world";
9542 }
9543 "#},
9544 cx,
9545 );
9546 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9547 assert_text_with_selections(
9548 editor,
9549 indoc! {r#"
9550 use mod1::mod2::{mod3, mod4};
9551
9552 fn fn_1(param1: bool, param2: &str) {
9553 let var1 = "«ˇhello» world";
9554 }
9555 "#},
9556 cx,
9557 );
9558 });
9559
9560 // Test 2: Partial selection within a word
9561 editor.update_in(cx, |editor, window, cx| {
9562 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9563 s.select_display_ranges([
9564 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9565 ]);
9566 });
9567 });
9568 editor.update_in(cx, |editor, window, cx| {
9569 assert_text_with_selections(
9570 editor,
9571 indoc! {r#"
9572 use mod1::mod2::{mod3, mod4};
9573
9574 fn fn_1(param1: bool, param2: &str) {
9575 let var1 = "h«elˇ»lo world";
9576 }
9577 "#},
9578 cx,
9579 );
9580 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9581 assert_text_with_selections(
9582 editor,
9583 indoc! {r#"
9584 use mod1::mod2::{mod3, mod4};
9585
9586 fn fn_1(param1: bool, param2: &str) {
9587 let var1 = "«ˇhello» world";
9588 }
9589 "#},
9590 cx,
9591 );
9592 });
9593
9594 // Test 3: Complete word already selected
9595 editor.update_in(cx, |editor, window, cx| {
9596 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9597 s.select_display_ranges([
9598 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9599 ]);
9600 });
9601 });
9602 editor.update_in(cx, |editor, window, cx| {
9603 assert_text_with_selections(
9604 editor,
9605 indoc! {r#"
9606 use mod1::mod2::{mod3, mod4};
9607
9608 fn fn_1(param1: bool, param2: &str) {
9609 let var1 = "«helloˇ» world";
9610 }
9611 "#},
9612 cx,
9613 );
9614 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9615 assert_text_with_selections(
9616 editor,
9617 indoc! {r#"
9618 use mod1::mod2::{mod3, mod4};
9619
9620 fn fn_1(param1: bool, param2: &str) {
9621 let var1 = "«hello worldˇ»";
9622 }
9623 "#},
9624 cx,
9625 );
9626 });
9627
9628 // Test 4: Selection spanning across words
9629 editor.update_in(cx, |editor, window, cx| {
9630 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9631 s.select_display_ranges([
9632 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9633 ]);
9634 });
9635 });
9636 editor.update_in(cx, |editor, window, cx| {
9637 assert_text_with_selections(
9638 editor,
9639 indoc! {r#"
9640 use mod1::mod2::{mod3, mod4};
9641
9642 fn fn_1(param1: bool, param2: &str) {
9643 let var1 = "hel«lo woˇ»rld";
9644 }
9645 "#},
9646 cx,
9647 );
9648 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9649 assert_text_with_selections(
9650 editor,
9651 indoc! {r#"
9652 use mod1::mod2::{mod3, mod4};
9653
9654 fn fn_1(param1: bool, param2: &str) {
9655 let var1 = "«ˇhello world»";
9656 }
9657 "#},
9658 cx,
9659 );
9660 });
9661
9662 // Test 5: Expansion beyond string
9663 editor.update_in(cx, |editor, window, cx| {
9664 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9665 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9666 assert_text_with_selections(
9667 editor,
9668 indoc! {r#"
9669 use mod1::mod2::{mod3, mod4};
9670
9671 fn fn_1(param1: bool, param2: &str) {
9672 «ˇlet var1 = "hello world";»
9673 }
9674 "#},
9675 cx,
9676 );
9677 });
9678}
9679
9680#[gpui::test]
9681async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9682 init_test(cx, |_| {});
9683
9684 let mut cx = EditorTestContext::new(cx).await;
9685
9686 let language = Arc::new(Language::new(
9687 LanguageConfig::default(),
9688 Some(tree_sitter_rust::LANGUAGE.into()),
9689 ));
9690
9691 cx.update_buffer(|buffer, cx| {
9692 buffer.set_language(Some(language), cx);
9693 });
9694
9695 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9696 cx.update_editor(|editor, window, cx| {
9697 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9698 });
9699
9700 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9701
9702 cx.set_state(indoc! { r#"fn a() {
9703 // what
9704 // a
9705 // ˇlong
9706 // method
9707 // I
9708 // sure
9709 // hope
9710 // it
9711 // works
9712 }"# });
9713
9714 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9715 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9716 cx.update(|_, cx| {
9717 multi_buffer.update(cx, |multi_buffer, cx| {
9718 multi_buffer.set_excerpts_for_path(
9719 PathKey::for_buffer(&buffer, cx),
9720 buffer,
9721 [Point::new(1, 0)..Point::new(1, 0)],
9722 3,
9723 cx,
9724 );
9725 });
9726 });
9727
9728 let editor2 = cx.new_window_entity(|window, cx| {
9729 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9730 });
9731
9732 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9733 cx.update_editor(|editor, window, cx| {
9734 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9735 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9736 })
9737 });
9738
9739 cx.assert_editor_state(indoc! { "
9740 fn a() {
9741 // what
9742 // a
9743 ˇ // long
9744 // method"});
9745
9746 cx.update_editor(|editor, window, cx| {
9747 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9748 });
9749
9750 // Although we could potentially make the action work when the syntax node
9751 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9752 // did. Maybe we could also expand the excerpt to contain the range?
9753 cx.assert_editor_state(indoc! { "
9754 fn a() {
9755 // what
9756 // a
9757 ˇ // long
9758 // method"});
9759}
9760
9761#[gpui::test]
9762async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9763 init_test(cx, |_| {});
9764
9765 let base_text = r#"
9766 impl A {
9767 // this is an uncommitted comment
9768
9769 fn b() {
9770 c();
9771 }
9772
9773 // this is another uncommitted comment
9774
9775 fn d() {
9776 // e
9777 // f
9778 }
9779 }
9780
9781 fn g() {
9782 // h
9783 }
9784 "#
9785 .unindent();
9786
9787 let text = r#"
9788 ˇimpl A {
9789
9790 fn b() {
9791 c();
9792 }
9793
9794 fn d() {
9795 // e
9796 // f
9797 }
9798 }
9799
9800 fn g() {
9801 // h
9802 }
9803 "#
9804 .unindent();
9805
9806 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9807 cx.set_state(&text);
9808 cx.set_head_text(&base_text);
9809 cx.update_editor(|editor, window, cx| {
9810 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9811 });
9812
9813 cx.assert_state_with_diff(
9814 "
9815 ˇimpl A {
9816 - // this is an uncommitted comment
9817
9818 fn b() {
9819 c();
9820 }
9821
9822 - // this is another uncommitted comment
9823 -
9824 fn d() {
9825 // e
9826 // f
9827 }
9828 }
9829
9830 fn g() {
9831 // h
9832 }
9833 "
9834 .unindent(),
9835 );
9836
9837 let expected_display_text = "
9838 impl A {
9839 // this is an uncommitted comment
9840
9841 fn b() {
9842 ⋯
9843 }
9844
9845 // this is another uncommitted comment
9846
9847 fn d() {
9848 ⋯
9849 }
9850 }
9851
9852 fn g() {
9853 ⋯
9854 }
9855 "
9856 .unindent();
9857
9858 cx.update_editor(|editor, window, cx| {
9859 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9860 assert_eq!(editor.display_text(cx), expected_display_text);
9861 });
9862}
9863
9864#[gpui::test]
9865async fn test_autoindent(cx: &mut TestAppContext) {
9866 init_test(cx, |_| {});
9867
9868 let language = Arc::new(
9869 Language::new(
9870 LanguageConfig {
9871 brackets: BracketPairConfig {
9872 pairs: vec![
9873 BracketPair {
9874 start: "{".to_string(),
9875 end: "}".to_string(),
9876 close: false,
9877 surround: false,
9878 newline: true,
9879 },
9880 BracketPair {
9881 start: "(".to_string(),
9882 end: ")".to_string(),
9883 close: false,
9884 surround: false,
9885 newline: true,
9886 },
9887 ],
9888 ..Default::default()
9889 },
9890 ..Default::default()
9891 },
9892 Some(tree_sitter_rust::LANGUAGE.into()),
9893 )
9894 .with_indents_query(
9895 r#"
9896 (_ "(" ")" @end) @indent
9897 (_ "{" "}" @end) @indent
9898 "#,
9899 )
9900 .unwrap(),
9901 );
9902
9903 let text = "fn a() {}";
9904
9905 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9906 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9907 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9908 editor
9909 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9910 .await;
9911
9912 editor.update_in(cx, |editor, window, cx| {
9913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9914 s.select_ranges([
9915 MultiBufferOffset(5)..MultiBufferOffset(5),
9916 MultiBufferOffset(8)..MultiBufferOffset(8),
9917 MultiBufferOffset(9)..MultiBufferOffset(9),
9918 ])
9919 });
9920 editor.newline(&Newline, window, cx);
9921 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9922 assert_eq!(
9923 editor.selections.ranges(&editor.display_snapshot(cx)),
9924 &[
9925 Point::new(1, 4)..Point::new(1, 4),
9926 Point::new(3, 4)..Point::new(3, 4),
9927 Point::new(5, 0)..Point::new(5, 0)
9928 ]
9929 );
9930 });
9931}
9932
9933#[gpui::test]
9934async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9935 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9936
9937 let language = Arc::new(
9938 Language::new(
9939 LanguageConfig {
9940 brackets: BracketPairConfig {
9941 pairs: vec![
9942 BracketPair {
9943 start: "{".to_string(),
9944 end: "}".to_string(),
9945 close: false,
9946 surround: false,
9947 newline: true,
9948 },
9949 BracketPair {
9950 start: "(".to_string(),
9951 end: ")".to_string(),
9952 close: false,
9953 surround: false,
9954 newline: true,
9955 },
9956 ],
9957 ..Default::default()
9958 },
9959 ..Default::default()
9960 },
9961 Some(tree_sitter_rust::LANGUAGE.into()),
9962 )
9963 .with_indents_query(
9964 r#"
9965 (_ "(" ")" @end) @indent
9966 (_ "{" "}" @end) @indent
9967 "#,
9968 )
9969 .unwrap(),
9970 );
9971
9972 let text = "fn a() {}";
9973
9974 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9975 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9976 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9977 editor
9978 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9979 .await;
9980
9981 editor.update_in(cx, |editor, window, cx| {
9982 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9983 s.select_ranges([
9984 MultiBufferOffset(5)..MultiBufferOffset(5),
9985 MultiBufferOffset(8)..MultiBufferOffset(8),
9986 MultiBufferOffset(9)..MultiBufferOffset(9),
9987 ])
9988 });
9989 editor.newline(&Newline, window, cx);
9990 assert_eq!(
9991 editor.text(cx),
9992 indoc!(
9993 "
9994 fn a(
9995
9996 ) {
9997
9998 }
9999 "
10000 )
10001 );
10002 assert_eq!(
10003 editor.selections.ranges(&editor.display_snapshot(cx)),
10004 &[
10005 Point::new(1, 0)..Point::new(1, 0),
10006 Point::new(3, 0)..Point::new(3, 0),
10007 Point::new(5, 0)..Point::new(5, 0)
10008 ]
10009 );
10010 });
10011}
10012
10013#[gpui::test]
10014async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
10015 init_test(cx, |settings| {
10016 settings.defaults.auto_indent = Some(true);
10017 settings.languages.0.insert(
10018 "python".into(),
10019 LanguageSettingsContent {
10020 auto_indent: Some(false),
10021 ..Default::default()
10022 },
10023 );
10024 });
10025
10026 let mut cx = EditorTestContext::new(cx).await;
10027
10028 let injected_language = Arc::new(
10029 Language::new(
10030 LanguageConfig {
10031 brackets: BracketPairConfig {
10032 pairs: vec![
10033 BracketPair {
10034 start: "{".to_string(),
10035 end: "}".to_string(),
10036 close: false,
10037 surround: false,
10038 newline: true,
10039 },
10040 BracketPair {
10041 start: "(".to_string(),
10042 end: ")".to_string(),
10043 close: true,
10044 surround: false,
10045 newline: true,
10046 },
10047 ],
10048 ..Default::default()
10049 },
10050 name: "python".into(),
10051 ..Default::default()
10052 },
10053 Some(tree_sitter_python::LANGUAGE.into()),
10054 )
10055 .with_indents_query(
10056 r#"
10057 (_ "(" ")" @end) @indent
10058 (_ "{" "}" @end) @indent
10059 "#,
10060 )
10061 .unwrap(),
10062 );
10063
10064 let language = Arc::new(
10065 Language::new(
10066 LanguageConfig {
10067 brackets: BracketPairConfig {
10068 pairs: vec![
10069 BracketPair {
10070 start: "{".to_string(),
10071 end: "}".to_string(),
10072 close: false,
10073 surround: false,
10074 newline: true,
10075 },
10076 BracketPair {
10077 start: "(".to_string(),
10078 end: ")".to_string(),
10079 close: true,
10080 surround: false,
10081 newline: true,
10082 },
10083 ],
10084 ..Default::default()
10085 },
10086 name: LanguageName::new_static("rust"),
10087 ..Default::default()
10088 },
10089 Some(tree_sitter_rust::LANGUAGE.into()),
10090 )
10091 .with_indents_query(
10092 r#"
10093 (_ "(" ")" @end) @indent
10094 (_ "{" "}" @end) @indent
10095 "#,
10096 )
10097 .unwrap()
10098 .with_injection_query(
10099 r#"
10100 (macro_invocation
10101 macro: (identifier) @_macro_name
10102 (token_tree) @injection.content
10103 (#set! injection.language "python"))
10104 "#,
10105 )
10106 .unwrap(),
10107 );
10108
10109 cx.language_registry().add(injected_language);
10110 cx.language_registry().add(language.clone());
10111
10112 cx.update_buffer(|buffer, cx| {
10113 buffer.set_language(Some(language), cx);
10114 });
10115
10116 cx.set_state(r#"struct A {ˇ}"#);
10117
10118 cx.update_editor(|editor, window, cx| {
10119 editor.newline(&Default::default(), window, cx);
10120 });
10121
10122 cx.assert_editor_state(indoc!(
10123 "struct A {
10124 ˇ
10125 }"
10126 ));
10127
10128 cx.set_state(r#"select_biased!(ˇ)"#);
10129
10130 cx.update_editor(|editor, window, cx| {
10131 editor.newline(&Default::default(), window, cx);
10132 editor.handle_input("def ", window, cx);
10133 editor.handle_input("(", window, cx);
10134 editor.newline(&Default::default(), window, cx);
10135 editor.handle_input("a", window, cx);
10136 });
10137
10138 cx.assert_editor_state(indoc!(
10139 "select_biased!(
10140 def (
10141 aˇ
10142 )
10143 )"
10144 ));
10145}
10146
10147#[gpui::test]
10148async fn test_autoindent_selections(cx: &mut TestAppContext) {
10149 init_test(cx, |_| {});
10150
10151 {
10152 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10153 cx.set_state(indoc! {"
10154 impl A {
10155
10156 fn b() {}
10157
10158 «fn c() {
10159
10160 }ˇ»
10161 }
10162 "});
10163
10164 cx.update_editor(|editor, window, cx| {
10165 editor.autoindent(&Default::default(), window, cx);
10166 });
10167
10168 cx.assert_editor_state(indoc! {"
10169 impl A {
10170
10171 fn b() {}
10172
10173 «fn c() {
10174
10175 }ˇ»
10176 }
10177 "});
10178 }
10179
10180 {
10181 let mut cx = EditorTestContext::new_multibuffer(
10182 cx,
10183 [indoc! { "
10184 impl A {
10185 «
10186 // a
10187 fn b(){}
10188 »
10189 «
10190 }
10191 fn c(){}
10192 »
10193 "}],
10194 );
10195
10196 let buffer = cx.update_editor(|editor, _, cx| {
10197 let buffer = editor.buffer().update(cx, |buffer, _| {
10198 buffer.all_buffers().iter().next().unwrap().clone()
10199 });
10200 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10201 buffer
10202 });
10203
10204 cx.run_until_parked();
10205 cx.update_editor(|editor, window, cx| {
10206 editor.select_all(&Default::default(), window, cx);
10207 editor.autoindent(&Default::default(), window, cx)
10208 });
10209 cx.run_until_parked();
10210
10211 cx.update(|_, cx| {
10212 assert_eq!(
10213 buffer.read(cx).text(),
10214 indoc! { "
10215 impl A {
10216
10217 // a
10218 fn b(){}
10219
10220
10221 }
10222 fn c(){}
10223
10224 " }
10225 )
10226 });
10227 }
10228}
10229
10230#[gpui::test]
10231async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10232 init_test(cx, |_| {});
10233
10234 let mut cx = EditorTestContext::new(cx).await;
10235
10236 let language = Arc::new(Language::new(
10237 LanguageConfig {
10238 brackets: BracketPairConfig {
10239 pairs: vec![
10240 BracketPair {
10241 start: "{".to_string(),
10242 end: "}".to_string(),
10243 close: true,
10244 surround: true,
10245 newline: true,
10246 },
10247 BracketPair {
10248 start: "(".to_string(),
10249 end: ")".to_string(),
10250 close: true,
10251 surround: true,
10252 newline: true,
10253 },
10254 BracketPair {
10255 start: "/*".to_string(),
10256 end: " */".to_string(),
10257 close: true,
10258 surround: true,
10259 newline: true,
10260 },
10261 BracketPair {
10262 start: "[".to_string(),
10263 end: "]".to_string(),
10264 close: false,
10265 surround: false,
10266 newline: true,
10267 },
10268 BracketPair {
10269 start: "\"".to_string(),
10270 end: "\"".to_string(),
10271 close: true,
10272 surround: true,
10273 newline: false,
10274 },
10275 BracketPair {
10276 start: "<".to_string(),
10277 end: ">".to_string(),
10278 close: false,
10279 surround: true,
10280 newline: true,
10281 },
10282 ],
10283 ..Default::default()
10284 },
10285 autoclose_before: "})]".to_string(),
10286 ..Default::default()
10287 },
10288 Some(tree_sitter_rust::LANGUAGE.into()),
10289 ));
10290
10291 cx.language_registry().add(language.clone());
10292 cx.update_buffer(|buffer, cx| {
10293 buffer.set_language(Some(language), cx);
10294 });
10295
10296 cx.set_state(
10297 &r#"
10298 🏀ˇ
10299 εˇ
10300 ❤️ˇ
10301 "#
10302 .unindent(),
10303 );
10304
10305 // autoclose multiple nested brackets at multiple cursors
10306 cx.update_editor(|editor, window, cx| {
10307 editor.handle_input("{", window, cx);
10308 editor.handle_input("{", window, cx);
10309 editor.handle_input("{", window, cx);
10310 });
10311 cx.assert_editor_state(
10312 &"
10313 🏀{{{ˇ}}}
10314 ε{{{ˇ}}}
10315 ❤️{{{ˇ}}}
10316 "
10317 .unindent(),
10318 );
10319
10320 // insert a different closing bracket
10321 cx.update_editor(|editor, window, cx| {
10322 editor.handle_input(")", window, cx);
10323 });
10324 cx.assert_editor_state(
10325 &"
10326 🏀{{{)ˇ}}}
10327 ε{{{)ˇ}}}
10328 ❤️{{{)ˇ}}}
10329 "
10330 .unindent(),
10331 );
10332
10333 // skip over the auto-closed brackets when typing a closing bracket
10334 cx.update_editor(|editor, window, cx| {
10335 editor.move_right(&MoveRight, window, cx);
10336 editor.handle_input("}", window, cx);
10337 editor.handle_input("}", window, cx);
10338 editor.handle_input("}", window, cx);
10339 });
10340 cx.assert_editor_state(
10341 &"
10342 🏀{{{)}}}}ˇ
10343 ε{{{)}}}}ˇ
10344 ❤️{{{)}}}}ˇ
10345 "
10346 .unindent(),
10347 );
10348
10349 // autoclose multi-character pairs
10350 cx.set_state(
10351 &"
10352 ˇ
10353 ˇ
10354 "
10355 .unindent(),
10356 );
10357 cx.update_editor(|editor, window, cx| {
10358 editor.handle_input("/", window, cx);
10359 editor.handle_input("*", window, cx);
10360 });
10361 cx.assert_editor_state(
10362 &"
10363 /*ˇ */
10364 /*ˇ */
10365 "
10366 .unindent(),
10367 );
10368
10369 // one cursor autocloses a multi-character pair, one cursor
10370 // does not autoclose.
10371 cx.set_state(
10372 &"
10373 /ˇ
10374 ˇ
10375 "
10376 .unindent(),
10377 );
10378 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10379 cx.assert_editor_state(
10380 &"
10381 /*ˇ */
10382 *ˇ
10383 "
10384 .unindent(),
10385 );
10386
10387 // Don't autoclose if the next character isn't whitespace and isn't
10388 // listed in the language's "autoclose_before" section.
10389 cx.set_state("ˇa b");
10390 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10391 cx.assert_editor_state("{ˇa b");
10392
10393 // Don't autoclose if `close` is false for the bracket pair
10394 cx.set_state("ˇ");
10395 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10396 cx.assert_editor_state("[ˇ");
10397
10398 // Surround with brackets if text is selected
10399 cx.set_state("«aˇ» b");
10400 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10401 cx.assert_editor_state("{«aˇ»} b");
10402
10403 // Autoclose when not immediately after a word character
10404 cx.set_state("a ˇ");
10405 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10406 cx.assert_editor_state("a \"ˇ\"");
10407
10408 // Autoclose pair where the start and end characters are the same
10409 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10410 cx.assert_editor_state("a \"\"ˇ");
10411
10412 // Don't autoclose when immediately after a word character
10413 cx.set_state("aˇ");
10414 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10415 cx.assert_editor_state("a\"ˇ");
10416
10417 // Do autoclose when after a non-word character
10418 cx.set_state("{ˇ");
10419 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10420 cx.assert_editor_state("{\"ˇ\"");
10421
10422 // Non identical pairs autoclose regardless of preceding character
10423 cx.set_state("aˇ");
10424 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10425 cx.assert_editor_state("a{ˇ}");
10426
10427 // Don't autoclose pair if autoclose is disabled
10428 cx.set_state("ˇ");
10429 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10430 cx.assert_editor_state("<ˇ");
10431
10432 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10433 cx.set_state("«aˇ» b");
10434 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10435 cx.assert_editor_state("<«aˇ»> b");
10436}
10437
10438#[gpui::test]
10439async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10440 init_test(cx, |settings| {
10441 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10442 });
10443
10444 let mut cx = EditorTestContext::new(cx).await;
10445
10446 let language = Arc::new(Language::new(
10447 LanguageConfig {
10448 brackets: BracketPairConfig {
10449 pairs: vec![
10450 BracketPair {
10451 start: "{".to_string(),
10452 end: "}".to_string(),
10453 close: true,
10454 surround: true,
10455 newline: true,
10456 },
10457 BracketPair {
10458 start: "(".to_string(),
10459 end: ")".to_string(),
10460 close: true,
10461 surround: true,
10462 newline: true,
10463 },
10464 BracketPair {
10465 start: "[".to_string(),
10466 end: "]".to_string(),
10467 close: false,
10468 surround: false,
10469 newline: true,
10470 },
10471 ],
10472 ..Default::default()
10473 },
10474 autoclose_before: "})]".to_string(),
10475 ..Default::default()
10476 },
10477 Some(tree_sitter_rust::LANGUAGE.into()),
10478 ));
10479
10480 cx.language_registry().add(language.clone());
10481 cx.update_buffer(|buffer, cx| {
10482 buffer.set_language(Some(language), cx);
10483 });
10484
10485 cx.set_state(
10486 &"
10487 ˇ
10488 ˇ
10489 ˇ
10490 "
10491 .unindent(),
10492 );
10493
10494 // ensure only matching closing brackets are skipped over
10495 cx.update_editor(|editor, window, cx| {
10496 editor.handle_input("}", window, cx);
10497 editor.move_left(&MoveLeft, window, cx);
10498 editor.handle_input(")", window, cx);
10499 editor.move_left(&MoveLeft, window, cx);
10500 });
10501 cx.assert_editor_state(
10502 &"
10503 ˇ)}
10504 ˇ)}
10505 ˇ)}
10506 "
10507 .unindent(),
10508 );
10509
10510 // skip-over closing brackets at multiple cursors
10511 cx.update_editor(|editor, window, cx| {
10512 editor.handle_input(")", window, cx);
10513 editor.handle_input("}", window, cx);
10514 });
10515 cx.assert_editor_state(
10516 &"
10517 )}ˇ
10518 )}ˇ
10519 )}ˇ
10520 "
10521 .unindent(),
10522 );
10523
10524 // ignore non-close brackets
10525 cx.update_editor(|editor, window, cx| {
10526 editor.handle_input("]", window, cx);
10527 editor.move_left(&MoveLeft, window, cx);
10528 editor.handle_input("]", window, cx);
10529 });
10530 cx.assert_editor_state(
10531 &"
10532 )}]ˇ]
10533 )}]ˇ]
10534 )}]ˇ]
10535 "
10536 .unindent(),
10537 );
10538}
10539
10540#[gpui::test]
10541async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10542 init_test(cx, |_| {});
10543
10544 let mut cx = EditorTestContext::new(cx).await;
10545
10546 let html_language = Arc::new(
10547 Language::new(
10548 LanguageConfig {
10549 name: "HTML".into(),
10550 brackets: BracketPairConfig {
10551 pairs: vec![
10552 BracketPair {
10553 start: "<".into(),
10554 end: ">".into(),
10555 close: true,
10556 ..Default::default()
10557 },
10558 BracketPair {
10559 start: "{".into(),
10560 end: "}".into(),
10561 close: true,
10562 ..Default::default()
10563 },
10564 BracketPair {
10565 start: "(".into(),
10566 end: ")".into(),
10567 close: true,
10568 ..Default::default()
10569 },
10570 ],
10571 ..Default::default()
10572 },
10573 autoclose_before: "})]>".into(),
10574 ..Default::default()
10575 },
10576 Some(tree_sitter_html::LANGUAGE.into()),
10577 )
10578 .with_injection_query(
10579 r#"
10580 (script_element
10581 (raw_text) @injection.content
10582 (#set! injection.language "javascript"))
10583 "#,
10584 )
10585 .unwrap(),
10586 );
10587
10588 let javascript_language = Arc::new(Language::new(
10589 LanguageConfig {
10590 name: "JavaScript".into(),
10591 brackets: BracketPairConfig {
10592 pairs: vec![
10593 BracketPair {
10594 start: "/*".into(),
10595 end: " */".into(),
10596 close: true,
10597 ..Default::default()
10598 },
10599 BracketPair {
10600 start: "{".into(),
10601 end: "}".into(),
10602 close: true,
10603 ..Default::default()
10604 },
10605 BracketPair {
10606 start: "(".into(),
10607 end: ")".into(),
10608 close: true,
10609 ..Default::default()
10610 },
10611 ],
10612 ..Default::default()
10613 },
10614 autoclose_before: "})]>".into(),
10615 ..Default::default()
10616 },
10617 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10618 ));
10619
10620 cx.language_registry().add(html_language.clone());
10621 cx.language_registry().add(javascript_language);
10622 cx.executor().run_until_parked();
10623
10624 cx.update_buffer(|buffer, cx| {
10625 buffer.set_language(Some(html_language), cx);
10626 });
10627
10628 cx.set_state(
10629 &r#"
10630 <body>ˇ
10631 <script>
10632 var x = 1;ˇ
10633 </script>
10634 </body>ˇ
10635 "#
10636 .unindent(),
10637 );
10638
10639 // Precondition: different languages are active at different locations.
10640 cx.update_editor(|editor, window, cx| {
10641 let snapshot = editor.snapshot(window, cx);
10642 let cursors = editor
10643 .selections
10644 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
10645 let languages = cursors
10646 .iter()
10647 .map(|c| snapshot.language_at(c.start).unwrap().name())
10648 .collect::<Vec<_>>();
10649 assert_eq!(
10650 languages,
10651 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10652 );
10653 });
10654
10655 // Angle brackets autoclose in HTML, but not JavaScript.
10656 cx.update_editor(|editor, window, cx| {
10657 editor.handle_input("<", window, cx);
10658 editor.handle_input("a", window, cx);
10659 });
10660 cx.assert_editor_state(
10661 &r#"
10662 <body><aˇ>
10663 <script>
10664 var x = 1;<aˇ
10665 </script>
10666 </body><aˇ>
10667 "#
10668 .unindent(),
10669 );
10670
10671 // Curly braces and parens autoclose in both HTML and JavaScript.
10672 cx.update_editor(|editor, window, cx| {
10673 editor.handle_input(" b=", window, cx);
10674 editor.handle_input("{", window, cx);
10675 editor.handle_input("c", window, cx);
10676 editor.handle_input("(", window, cx);
10677 });
10678 cx.assert_editor_state(
10679 &r#"
10680 <body><a b={c(ˇ)}>
10681 <script>
10682 var x = 1;<a b={c(ˇ)}
10683 </script>
10684 </body><a b={c(ˇ)}>
10685 "#
10686 .unindent(),
10687 );
10688
10689 // Brackets that were already autoclosed are skipped.
10690 cx.update_editor(|editor, window, cx| {
10691 editor.handle_input(")", window, cx);
10692 editor.handle_input("d", window, cx);
10693 editor.handle_input("}", window, cx);
10694 });
10695 cx.assert_editor_state(
10696 &r#"
10697 <body><a b={c()d}ˇ>
10698 <script>
10699 var x = 1;<a b={c()d}ˇ
10700 </script>
10701 </body><a b={c()d}ˇ>
10702 "#
10703 .unindent(),
10704 );
10705 cx.update_editor(|editor, window, cx| {
10706 editor.handle_input(">", window, cx);
10707 });
10708 cx.assert_editor_state(
10709 &r#"
10710 <body><a b={c()d}>ˇ
10711 <script>
10712 var x = 1;<a b={c()d}>ˇ
10713 </script>
10714 </body><a b={c()d}>ˇ
10715 "#
10716 .unindent(),
10717 );
10718
10719 // Reset
10720 cx.set_state(
10721 &r#"
10722 <body>ˇ
10723 <script>
10724 var x = 1;ˇ
10725 </script>
10726 </body>ˇ
10727 "#
10728 .unindent(),
10729 );
10730
10731 cx.update_editor(|editor, window, cx| {
10732 editor.handle_input("<", window, cx);
10733 });
10734 cx.assert_editor_state(
10735 &r#"
10736 <body><ˇ>
10737 <script>
10738 var x = 1;<ˇ
10739 </script>
10740 </body><ˇ>
10741 "#
10742 .unindent(),
10743 );
10744
10745 // When backspacing, the closing angle brackets are removed.
10746 cx.update_editor(|editor, window, cx| {
10747 editor.backspace(&Backspace, window, cx);
10748 });
10749 cx.assert_editor_state(
10750 &r#"
10751 <body>ˇ
10752 <script>
10753 var x = 1;ˇ
10754 </script>
10755 </body>ˇ
10756 "#
10757 .unindent(),
10758 );
10759
10760 // Block comments autoclose in JavaScript, but not HTML.
10761 cx.update_editor(|editor, window, cx| {
10762 editor.handle_input("/", window, cx);
10763 editor.handle_input("*", window, cx);
10764 });
10765 cx.assert_editor_state(
10766 &r#"
10767 <body>/*ˇ
10768 <script>
10769 var x = 1;/*ˇ */
10770 </script>
10771 </body>/*ˇ
10772 "#
10773 .unindent(),
10774 );
10775}
10776
10777#[gpui::test]
10778async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10779 init_test(cx, |_| {});
10780
10781 let mut cx = EditorTestContext::new(cx).await;
10782
10783 let rust_language = Arc::new(
10784 Language::new(
10785 LanguageConfig {
10786 name: "Rust".into(),
10787 brackets: serde_json::from_value(json!([
10788 { "start": "{", "end": "}", "close": true, "newline": true },
10789 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10790 ]))
10791 .unwrap(),
10792 autoclose_before: "})]>".into(),
10793 ..Default::default()
10794 },
10795 Some(tree_sitter_rust::LANGUAGE.into()),
10796 )
10797 .with_override_query("(string_literal) @string")
10798 .unwrap(),
10799 );
10800
10801 cx.language_registry().add(rust_language.clone());
10802 cx.update_buffer(|buffer, cx| {
10803 buffer.set_language(Some(rust_language), cx);
10804 });
10805
10806 cx.set_state(
10807 &r#"
10808 let x = ˇ
10809 "#
10810 .unindent(),
10811 );
10812
10813 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10814 cx.update_editor(|editor, window, cx| {
10815 editor.handle_input("\"", window, cx);
10816 });
10817 cx.assert_editor_state(
10818 &r#"
10819 let x = "ˇ"
10820 "#
10821 .unindent(),
10822 );
10823
10824 // Inserting another quotation mark. The cursor moves across the existing
10825 // automatically-inserted quotation mark.
10826 cx.update_editor(|editor, window, cx| {
10827 editor.handle_input("\"", window, cx);
10828 });
10829 cx.assert_editor_state(
10830 &r#"
10831 let x = ""ˇ
10832 "#
10833 .unindent(),
10834 );
10835
10836 // Reset
10837 cx.set_state(
10838 &r#"
10839 let x = ˇ
10840 "#
10841 .unindent(),
10842 );
10843
10844 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10845 cx.update_editor(|editor, window, cx| {
10846 editor.handle_input("\"", window, cx);
10847 editor.handle_input(" ", window, cx);
10848 editor.move_left(&Default::default(), window, cx);
10849 editor.handle_input("\\", window, cx);
10850 editor.handle_input("\"", window, cx);
10851 });
10852 cx.assert_editor_state(
10853 &r#"
10854 let x = "\"ˇ "
10855 "#
10856 .unindent(),
10857 );
10858
10859 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10860 // mark. Nothing is inserted.
10861 cx.update_editor(|editor, window, cx| {
10862 editor.move_right(&Default::default(), window, cx);
10863 editor.handle_input("\"", window, cx);
10864 });
10865 cx.assert_editor_state(
10866 &r#"
10867 let x = "\" "ˇ
10868 "#
10869 .unindent(),
10870 );
10871}
10872
10873#[gpui::test]
10874async fn test_autoclose_quotes_with_scope_awareness(cx: &mut TestAppContext) {
10875 init_test(cx, |_| {});
10876
10877 let mut cx = EditorTestContext::new(cx).await;
10878 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
10879
10880 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10881
10882 // Double quote inside single-quoted string
10883 cx.set_state(indoc! {r#"
10884 def main():
10885 items = ['"', ˇ]
10886 "#});
10887 cx.update_editor(|editor, window, cx| {
10888 editor.handle_input("\"", window, cx);
10889 });
10890 cx.assert_editor_state(indoc! {r#"
10891 def main():
10892 items = ['"', "ˇ"]
10893 "#});
10894
10895 // Two double quotes inside single-quoted string
10896 cx.set_state(indoc! {r#"
10897 def main():
10898 items = ['""', ˇ]
10899 "#});
10900 cx.update_editor(|editor, window, cx| {
10901 editor.handle_input("\"", window, cx);
10902 });
10903 cx.assert_editor_state(indoc! {r#"
10904 def main():
10905 items = ['""', "ˇ"]
10906 "#});
10907
10908 // Single quote inside double-quoted string
10909 cx.set_state(indoc! {r#"
10910 def main():
10911 items = ["'", ˇ]
10912 "#});
10913 cx.update_editor(|editor, window, cx| {
10914 editor.handle_input("'", window, cx);
10915 });
10916 cx.assert_editor_state(indoc! {r#"
10917 def main():
10918 items = ["'", 'ˇ']
10919 "#});
10920
10921 // Two single quotes inside double-quoted string
10922 cx.set_state(indoc! {r#"
10923 def main():
10924 items = ["''", ˇ]
10925 "#});
10926 cx.update_editor(|editor, window, cx| {
10927 editor.handle_input("'", window, cx);
10928 });
10929 cx.assert_editor_state(indoc! {r#"
10930 def main():
10931 items = ["''", 'ˇ']
10932 "#});
10933
10934 // Mixed quotes on same line
10935 cx.set_state(indoc! {r#"
10936 def main():
10937 items = ['"""', "'''''", ˇ]
10938 "#});
10939 cx.update_editor(|editor, window, cx| {
10940 editor.handle_input("\"", window, cx);
10941 });
10942 cx.assert_editor_state(indoc! {r#"
10943 def main():
10944 items = ['"""', "'''''", "ˇ"]
10945 "#});
10946 cx.update_editor(|editor, window, cx| {
10947 editor.move_right(&MoveRight, window, cx);
10948 });
10949 cx.update_editor(|editor, window, cx| {
10950 editor.handle_input(", ", window, cx);
10951 });
10952 cx.update_editor(|editor, window, cx| {
10953 editor.handle_input("'", window, cx);
10954 });
10955 cx.assert_editor_state(indoc! {r#"
10956 def main():
10957 items = ['"""', "'''''", "", 'ˇ']
10958 "#});
10959}
10960
10961#[gpui::test]
10962async fn test_autoclose_quotes_with_multibyte_characters(cx: &mut TestAppContext) {
10963 init_test(cx, |_| {});
10964
10965 let mut cx = EditorTestContext::new(cx).await;
10966 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
10967 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10968
10969 cx.set_state(indoc! {r#"
10970 def main():
10971 items = ["🎉", ˇ]
10972 "#});
10973 cx.update_editor(|editor, window, cx| {
10974 editor.handle_input("\"", window, cx);
10975 });
10976 cx.assert_editor_state(indoc! {r#"
10977 def main():
10978 items = ["🎉", "ˇ"]
10979 "#});
10980}
10981
10982#[gpui::test]
10983async fn test_surround_with_pair(cx: &mut TestAppContext) {
10984 init_test(cx, |_| {});
10985
10986 let language = Arc::new(Language::new(
10987 LanguageConfig {
10988 brackets: BracketPairConfig {
10989 pairs: vec![
10990 BracketPair {
10991 start: "{".to_string(),
10992 end: "}".to_string(),
10993 close: true,
10994 surround: true,
10995 newline: true,
10996 },
10997 BracketPair {
10998 start: "/* ".to_string(),
10999 end: "*/".to_string(),
11000 close: true,
11001 surround: true,
11002 ..Default::default()
11003 },
11004 ],
11005 ..Default::default()
11006 },
11007 ..Default::default()
11008 },
11009 Some(tree_sitter_rust::LANGUAGE.into()),
11010 ));
11011
11012 let text = r#"
11013 a
11014 b
11015 c
11016 "#
11017 .unindent();
11018
11019 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11020 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11021 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11022 editor
11023 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11024 .await;
11025
11026 editor.update_in(cx, |editor, window, cx| {
11027 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11028 s.select_display_ranges([
11029 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11030 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11031 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
11032 ])
11033 });
11034
11035 editor.handle_input("{", window, cx);
11036 editor.handle_input("{", window, cx);
11037 editor.handle_input("{", window, cx);
11038 assert_eq!(
11039 editor.text(cx),
11040 "
11041 {{{a}}}
11042 {{{b}}}
11043 {{{c}}}
11044 "
11045 .unindent()
11046 );
11047 assert_eq!(
11048 display_ranges(editor, cx),
11049 [
11050 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
11051 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
11052 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
11053 ]
11054 );
11055
11056 editor.undo(&Undo, window, cx);
11057 editor.undo(&Undo, window, cx);
11058 editor.undo(&Undo, window, cx);
11059 assert_eq!(
11060 editor.text(cx),
11061 "
11062 a
11063 b
11064 c
11065 "
11066 .unindent()
11067 );
11068 assert_eq!(
11069 display_ranges(editor, cx),
11070 [
11071 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11072 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11073 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11074 ]
11075 );
11076
11077 // Ensure inserting the first character of a multi-byte bracket pair
11078 // doesn't surround the selections with the bracket.
11079 editor.handle_input("/", window, cx);
11080 assert_eq!(
11081 editor.text(cx),
11082 "
11083 /
11084 /
11085 /
11086 "
11087 .unindent()
11088 );
11089 assert_eq!(
11090 display_ranges(editor, cx),
11091 [
11092 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11093 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11094 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11095 ]
11096 );
11097
11098 editor.undo(&Undo, window, cx);
11099 assert_eq!(
11100 editor.text(cx),
11101 "
11102 a
11103 b
11104 c
11105 "
11106 .unindent()
11107 );
11108 assert_eq!(
11109 display_ranges(editor, cx),
11110 [
11111 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11112 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11113 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11114 ]
11115 );
11116
11117 // Ensure inserting the last character of a multi-byte bracket pair
11118 // doesn't surround the selections with the bracket.
11119 editor.handle_input("*", window, cx);
11120 assert_eq!(
11121 editor.text(cx),
11122 "
11123 *
11124 *
11125 *
11126 "
11127 .unindent()
11128 );
11129 assert_eq!(
11130 display_ranges(editor, cx),
11131 [
11132 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11133 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11134 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11135 ]
11136 );
11137 });
11138}
11139
11140#[gpui::test]
11141async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
11142 init_test(cx, |_| {});
11143
11144 let language = Arc::new(Language::new(
11145 LanguageConfig {
11146 brackets: BracketPairConfig {
11147 pairs: vec![BracketPair {
11148 start: "{".to_string(),
11149 end: "}".to_string(),
11150 close: true,
11151 surround: true,
11152 newline: true,
11153 }],
11154 ..Default::default()
11155 },
11156 autoclose_before: "}".to_string(),
11157 ..Default::default()
11158 },
11159 Some(tree_sitter_rust::LANGUAGE.into()),
11160 ));
11161
11162 let text = r#"
11163 a
11164 b
11165 c
11166 "#
11167 .unindent();
11168
11169 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11170 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11171 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11172 editor
11173 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11174 .await;
11175
11176 editor.update_in(cx, |editor, window, cx| {
11177 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11178 s.select_ranges([
11179 Point::new(0, 1)..Point::new(0, 1),
11180 Point::new(1, 1)..Point::new(1, 1),
11181 Point::new(2, 1)..Point::new(2, 1),
11182 ])
11183 });
11184
11185 editor.handle_input("{", window, cx);
11186 editor.handle_input("{", window, cx);
11187 editor.handle_input("_", window, cx);
11188 assert_eq!(
11189 editor.text(cx),
11190 "
11191 a{{_}}
11192 b{{_}}
11193 c{{_}}
11194 "
11195 .unindent()
11196 );
11197 assert_eq!(
11198 editor
11199 .selections
11200 .ranges::<Point>(&editor.display_snapshot(cx)),
11201 [
11202 Point::new(0, 4)..Point::new(0, 4),
11203 Point::new(1, 4)..Point::new(1, 4),
11204 Point::new(2, 4)..Point::new(2, 4)
11205 ]
11206 );
11207
11208 editor.backspace(&Default::default(), window, cx);
11209 editor.backspace(&Default::default(), window, cx);
11210 assert_eq!(
11211 editor.text(cx),
11212 "
11213 a{}
11214 b{}
11215 c{}
11216 "
11217 .unindent()
11218 );
11219 assert_eq!(
11220 editor
11221 .selections
11222 .ranges::<Point>(&editor.display_snapshot(cx)),
11223 [
11224 Point::new(0, 2)..Point::new(0, 2),
11225 Point::new(1, 2)..Point::new(1, 2),
11226 Point::new(2, 2)..Point::new(2, 2)
11227 ]
11228 );
11229
11230 editor.delete_to_previous_word_start(&Default::default(), window, cx);
11231 assert_eq!(
11232 editor.text(cx),
11233 "
11234 a
11235 b
11236 c
11237 "
11238 .unindent()
11239 );
11240 assert_eq!(
11241 editor
11242 .selections
11243 .ranges::<Point>(&editor.display_snapshot(cx)),
11244 [
11245 Point::new(0, 1)..Point::new(0, 1),
11246 Point::new(1, 1)..Point::new(1, 1),
11247 Point::new(2, 1)..Point::new(2, 1)
11248 ]
11249 );
11250 });
11251}
11252
11253#[gpui::test]
11254async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
11255 init_test(cx, |settings| {
11256 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11257 });
11258
11259 let mut cx = EditorTestContext::new(cx).await;
11260
11261 let language = Arc::new(Language::new(
11262 LanguageConfig {
11263 brackets: BracketPairConfig {
11264 pairs: vec![
11265 BracketPair {
11266 start: "{".to_string(),
11267 end: "}".to_string(),
11268 close: true,
11269 surround: true,
11270 newline: true,
11271 },
11272 BracketPair {
11273 start: "(".to_string(),
11274 end: ")".to_string(),
11275 close: true,
11276 surround: true,
11277 newline: true,
11278 },
11279 BracketPair {
11280 start: "[".to_string(),
11281 end: "]".to_string(),
11282 close: false,
11283 surround: true,
11284 newline: true,
11285 },
11286 ],
11287 ..Default::default()
11288 },
11289 autoclose_before: "})]".to_string(),
11290 ..Default::default()
11291 },
11292 Some(tree_sitter_rust::LANGUAGE.into()),
11293 ));
11294
11295 cx.language_registry().add(language.clone());
11296 cx.update_buffer(|buffer, cx| {
11297 buffer.set_language(Some(language), cx);
11298 });
11299
11300 cx.set_state(
11301 &"
11302 {(ˇ)}
11303 [[ˇ]]
11304 {(ˇ)}
11305 "
11306 .unindent(),
11307 );
11308
11309 cx.update_editor(|editor, window, cx| {
11310 editor.backspace(&Default::default(), window, cx);
11311 editor.backspace(&Default::default(), window, cx);
11312 });
11313
11314 cx.assert_editor_state(
11315 &"
11316 ˇ
11317 ˇ]]
11318 ˇ
11319 "
11320 .unindent(),
11321 );
11322
11323 cx.update_editor(|editor, window, cx| {
11324 editor.handle_input("{", window, cx);
11325 editor.handle_input("{", window, cx);
11326 editor.move_right(&MoveRight, window, cx);
11327 editor.move_right(&MoveRight, window, cx);
11328 editor.move_left(&MoveLeft, window, cx);
11329 editor.move_left(&MoveLeft, window, cx);
11330 editor.backspace(&Default::default(), window, cx);
11331 });
11332
11333 cx.assert_editor_state(
11334 &"
11335 {ˇ}
11336 {ˇ}]]
11337 {ˇ}
11338 "
11339 .unindent(),
11340 );
11341
11342 cx.update_editor(|editor, window, cx| {
11343 editor.backspace(&Default::default(), window, cx);
11344 });
11345
11346 cx.assert_editor_state(
11347 &"
11348 ˇ
11349 ˇ]]
11350 ˇ
11351 "
11352 .unindent(),
11353 );
11354}
11355
11356#[gpui::test]
11357async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11358 init_test(cx, |_| {});
11359
11360 let language = Arc::new(Language::new(
11361 LanguageConfig::default(),
11362 Some(tree_sitter_rust::LANGUAGE.into()),
11363 ));
11364
11365 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11366 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11367 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11368 editor
11369 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11370 .await;
11371
11372 editor.update_in(cx, |editor, window, cx| {
11373 editor.set_auto_replace_emoji_shortcode(true);
11374
11375 editor.handle_input("Hello ", window, cx);
11376 editor.handle_input(":wave", window, cx);
11377 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11378
11379 editor.handle_input(":", window, cx);
11380 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11381
11382 editor.handle_input(" :smile", window, cx);
11383 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11384
11385 editor.handle_input(":", window, cx);
11386 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11387
11388 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11389 editor.handle_input(":wave", window, cx);
11390 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11391
11392 editor.handle_input(":", window, cx);
11393 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11394
11395 editor.handle_input(":1", window, cx);
11396 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11397
11398 editor.handle_input(":", window, cx);
11399 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11400
11401 // Ensure shortcode does not get replaced when it is part of a word
11402 editor.handle_input(" Test:wave", window, cx);
11403 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11404
11405 editor.handle_input(":", window, cx);
11406 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11407
11408 editor.set_auto_replace_emoji_shortcode(false);
11409
11410 // Ensure shortcode does not get replaced when auto replace is off
11411 editor.handle_input(" :wave", window, cx);
11412 assert_eq!(
11413 editor.text(cx),
11414 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11415 );
11416
11417 editor.handle_input(":", window, cx);
11418 assert_eq!(
11419 editor.text(cx),
11420 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11421 );
11422 });
11423}
11424
11425#[gpui::test]
11426async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11427 init_test(cx, |_| {});
11428
11429 let (text, insertion_ranges) = marked_text_ranges(
11430 indoc! {"
11431 ˇ
11432 "},
11433 false,
11434 );
11435
11436 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11437 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11438
11439 _ = editor.update_in(cx, |editor, window, cx| {
11440 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11441
11442 editor
11443 .insert_snippet(
11444 &insertion_ranges
11445 .iter()
11446 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11447 .collect::<Vec<_>>(),
11448 snippet,
11449 window,
11450 cx,
11451 )
11452 .unwrap();
11453
11454 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11455 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11456 assert_eq!(editor.text(cx), expected_text);
11457 assert_eq!(
11458 editor.selections.ranges(&editor.display_snapshot(cx)),
11459 selection_ranges
11460 .iter()
11461 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11462 .collect::<Vec<_>>()
11463 );
11464 }
11465
11466 assert(
11467 editor,
11468 cx,
11469 indoc! {"
11470 type «» =•
11471 "},
11472 );
11473
11474 assert!(editor.context_menu_visible(), "There should be a matches");
11475 });
11476}
11477
11478#[gpui::test]
11479async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11480 init_test(cx, |_| {});
11481
11482 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11483 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11484 assert_eq!(editor.text(cx), expected_text);
11485 assert_eq!(
11486 editor.selections.ranges(&editor.display_snapshot(cx)),
11487 selection_ranges
11488 .iter()
11489 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11490 .collect::<Vec<_>>()
11491 );
11492 }
11493
11494 let (text, insertion_ranges) = marked_text_ranges(
11495 indoc! {"
11496 ˇ
11497 "},
11498 false,
11499 );
11500
11501 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11502 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11503
11504 _ = editor.update_in(cx, |editor, window, cx| {
11505 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11506
11507 editor
11508 .insert_snippet(
11509 &insertion_ranges
11510 .iter()
11511 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11512 .collect::<Vec<_>>(),
11513 snippet,
11514 window,
11515 cx,
11516 )
11517 .unwrap();
11518
11519 assert_state(
11520 editor,
11521 cx,
11522 indoc! {"
11523 type «» = ;•
11524 "},
11525 );
11526
11527 assert!(
11528 editor.context_menu_visible(),
11529 "Context menu should be visible for placeholder choices"
11530 );
11531
11532 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11533
11534 assert_state(
11535 editor,
11536 cx,
11537 indoc! {"
11538 type = «»;•
11539 "},
11540 );
11541
11542 assert!(
11543 !editor.context_menu_visible(),
11544 "Context menu should be hidden after moving to next tabstop"
11545 );
11546
11547 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11548
11549 assert_state(
11550 editor,
11551 cx,
11552 indoc! {"
11553 type = ; ˇ
11554 "},
11555 );
11556
11557 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11558
11559 assert_state(
11560 editor,
11561 cx,
11562 indoc! {"
11563 type = ; ˇ
11564 "},
11565 );
11566 });
11567
11568 _ = editor.update_in(cx, |editor, window, cx| {
11569 editor.select_all(&SelectAll, window, cx);
11570 editor.backspace(&Backspace, window, cx);
11571
11572 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11573 let insertion_ranges = editor
11574 .selections
11575 .all(&editor.display_snapshot(cx))
11576 .iter()
11577 .map(|s| s.range())
11578 .collect::<Vec<_>>();
11579
11580 editor
11581 .insert_snippet(&insertion_ranges, snippet, window, cx)
11582 .unwrap();
11583
11584 assert_state(editor, cx, "fn «» = value;•");
11585
11586 assert!(
11587 editor.context_menu_visible(),
11588 "Context menu should be visible for placeholder choices"
11589 );
11590
11591 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11592
11593 assert_state(editor, cx, "fn = «valueˇ»;•");
11594
11595 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11596
11597 assert_state(editor, cx, "fn «» = value;•");
11598
11599 assert!(
11600 editor.context_menu_visible(),
11601 "Context menu should be visible again after returning to first tabstop"
11602 );
11603
11604 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11605
11606 assert_state(editor, cx, "fn «» = value;•");
11607 });
11608}
11609
11610#[gpui::test]
11611async fn test_snippets(cx: &mut TestAppContext) {
11612 init_test(cx, |_| {});
11613
11614 let mut cx = EditorTestContext::new(cx).await;
11615
11616 cx.set_state(indoc! {"
11617 a.ˇ b
11618 a.ˇ b
11619 a.ˇ b
11620 "});
11621
11622 cx.update_editor(|editor, window, cx| {
11623 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11624 let insertion_ranges = editor
11625 .selections
11626 .all(&editor.display_snapshot(cx))
11627 .iter()
11628 .map(|s| s.range())
11629 .collect::<Vec<_>>();
11630 editor
11631 .insert_snippet(&insertion_ranges, snippet, window, cx)
11632 .unwrap();
11633 });
11634
11635 cx.assert_editor_state(indoc! {"
11636 a.f(«oneˇ», two, «threeˇ») b
11637 a.f(«oneˇ», two, «threeˇ») b
11638 a.f(«oneˇ», two, «threeˇ») b
11639 "});
11640
11641 // Can't move earlier than the first tab stop
11642 cx.update_editor(|editor, window, cx| {
11643 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11644 });
11645 cx.assert_editor_state(indoc! {"
11646 a.f(«oneˇ», two, «threeˇ») b
11647 a.f(«oneˇ», two, «threeˇ») b
11648 a.f(«oneˇ», two, «threeˇ») b
11649 "});
11650
11651 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11652 cx.assert_editor_state(indoc! {"
11653 a.f(one, «twoˇ», three) b
11654 a.f(one, «twoˇ», three) b
11655 a.f(one, «twoˇ», three) b
11656 "});
11657
11658 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11659 cx.assert_editor_state(indoc! {"
11660 a.f(«oneˇ», two, «threeˇ») b
11661 a.f(«oneˇ», two, «threeˇ») b
11662 a.f(«oneˇ», two, «threeˇ») b
11663 "});
11664
11665 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11666 cx.assert_editor_state(indoc! {"
11667 a.f(one, «twoˇ», three) b
11668 a.f(one, «twoˇ», three) b
11669 a.f(one, «twoˇ», three) b
11670 "});
11671 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11672 cx.assert_editor_state(indoc! {"
11673 a.f(one, two, three)ˇ b
11674 a.f(one, two, three)ˇ b
11675 a.f(one, two, three)ˇ b
11676 "});
11677
11678 // As soon as the last tab stop is reached, snippet state is gone
11679 cx.update_editor(|editor, window, cx| {
11680 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11681 });
11682 cx.assert_editor_state(indoc! {"
11683 a.f(one, two, three)ˇ b
11684 a.f(one, two, three)ˇ b
11685 a.f(one, two, three)ˇ b
11686 "});
11687}
11688
11689#[gpui::test]
11690async fn test_snippet_indentation(cx: &mut TestAppContext) {
11691 init_test(cx, |_| {});
11692
11693 let mut cx = EditorTestContext::new(cx).await;
11694
11695 cx.update_editor(|editor, window, cx| {
11696 let snippet = Snippet::parse(indoc! {"
11697 /*
11698 * Multiline comment with leading indentation
11699 *
11700 * $1
11701 */
11702 $0"})
11703 .unwrap();
11704 let insertion_ranges = editor
11705 .selections
11706 .all(&editor.display_snapshot(cx))
11707 .iter()
11708 .map(|s| s.range())
11709 .collect::<Vec<_>>();
11710 editor
11711 .insert_snippet(&insertion_ranges, snippet, window, cx)
11712 .unwrap();
11713 });
11714
11715 cx.assert_editor_state(indoc! {"
11716 /*
11717 * Multiline comment with leading indentation
11718 *
11719 * ˇ
11720 */
11721 "});
11722
11723 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11724 cx.assert_editor_state(indoc! {"
11725 /*
11726 * Multiline comment with leading indentation
11727 *
11728 *•
11729 */
11730 ˇ"});
11731}
11732
11733#[gpui::test]
11734async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11735 init_test(cx, |_| {});
11736
11737 let mut cx = EditorTestContext::new(cx).await;
11738 cx.update_editor(|editor, _, cx| {
11739 editor.project().unwrap().update(cx, |project, cx| {
11740 project.snippets().update(cx, |snippets, _cx| {
11741 let snippet = project::snippet_provider::Snippet {
11742 prefix: vec!["multi word".to_string()],
11743 body: "this is many words".to_string(),
11744 description: Some("description".to_string()),
11745 name: "multi-word snippet test".to_string(),
11746 };
11747 snippets.add_snippet_for_test(
11748 None,
11749 PathBuf::from("test_snippets.json"),
11750 vec![Arc::new(snippet)],
11751 );
11752 });
11753 })
11754 });
11755
11756 for (input_to_simulate, should_match_snippet) in [
11757 ("m", true),
11758 ("m ", true),
11759 ("m w", true),
11760 ("aa m w", true),
11761 ("aa m g", false),
11762 ] {
11763 cx.set_state("ˇ");
11764 cx.simulate_input(input_to_simulate); // fails correctly
11765
11766 cx.update_editor(|editor, _, _| {
11767 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
11768 else {
11769 assert!(!should_match_snippet); // no completions! don't even show the menu
11770 return;
11771 };
11772 assert!(context_menu.visible());
11773 let completions = context_menu.completions.borrow();
11774
11775 assert_eq!(!completions.is_empty(), should_match_snippet);
11776 });
11777 }
11778}
11779
11780#[gpui::test]
11781async fn test_document_format_during_save(cx: &mut TestAppContext) {
11782 init_test(cx, |_| {});
11783
11784 let fs = FakeFs::new(cx.executor());
11785 fs.insert_file(path!("/file.rs"), Default::default()).await;
11786
11787 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11788
11789 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11790 language_registry.add(rust_lang());
11791 let mut fake_servers = language_registry.register_fake_lsp(
11792 "Rust",
11793 FakeLspAdapter {
11794 capabilities: lsp::ServerCapabilities {
11795 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11796 ..Default::default()
11797 },
11798 ..Default::default()
11799 },
11800 );
11801
11802 let buffer = project
11803 .update(cx, |project, cx| {
11804 project.open_local_buffer(path!("/file.rs"), cx)
11805 })
11806 .await
11807 .unwrap();
11808
11809 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11810 let (editor, cx) = cx.add_window_view(|window, cx| {
11811 build_editor_with_project(project.clone(), buffer, window, cx)
11812 });
11813 editor.update_in(cx, |editor, window, cx| {
11814 editor.set_text("one\ntwo\nthree\n", window, cx)
11815 });
11816 assert!(cx.read(|cx| editor.is_dirty(cx)));
11817
11818 cx.executor().start_waiting();
11819 let fake_server = fake_servers.next().await.unwrap();
11820
11821 {
11822 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11823 move |params, _| async move {
11824 assert_eq!(
11825 params.text_document.uri,
11826 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11827 );
11828 assert_eq!(params.options.tab_size, 4);
11829 Ok(Some(vec![lsp::TextEdit::new(
11830 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11831 ", ".to_string(),
11832 )]))
11833 },
11834 );
11835 let save = editor
11836 .update_in(cx, |editor, window, cx| {
11837 editor.save(
11838 SaveOptions {
11839 format: true,
11840 autosave: false,
11841 },
11842 project.clone(),
11843 window,
11844 cx,
11845 )
11846 })
11847 .unwrap();
11848 cx.executor().start_waiting();
11849 save.await;
11850
11851 assert_eq!(
11852 editor.update(cx, |editor, cx| editor.text(cx)),
11853 "one, two\nthree\n"
11854 );
11855 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11856 }
11857
11858 {
11859 editor.update_in(cx, |editor, window, cx| {
11860 editor.set_text("one\ntwo\nthree\n", window, cx)
11861 });
11862 assert!(cx.read(|cx| editor.is_dirty(cx)));
11863
11864 // Ensure we can still save even if formatting hangs.
11865 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11866 move |params, _| async move {
11867 assert_eq!(
11868 params.text_document.uri,
11869 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11870 );
11871 futures::future::pending::<()>().await;
11872 unreachable!()
11873 },
11874 );
11875 let save = editor
11876 .update_in(cx, |editor, window, cx| {
11877 editor.save(
11878 SaveOptions {
11879 format: true,
11880 autosave: false,
11881 },
11882 project.clone(),
11883 window,
11884 cx,
11885 )
11886 })
11887 .unwrap();
11888 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11889 cx.executor().start_waiting();
11890 save.await;
11891 assert_eq!(
11892 editor.update(cx, |editor, cx| editor.text(cx)),
11893 "one\ntwo\nthree\n"
11894 );
11895 }
11896
11897 // Set rust language override and assert overridden tabsize is sent to language server
11898 update_test_language_settings(cx, |settings| {
11899 settings.languages.0.insert(
11900 "Rust".into(),
11901 LanguageSettingsContent {
11902 tab_size: NonZeroU32::new(8),
11903 ..Default::default()
11904 },
11905 );
11906 });
11907
11908 {
11909 editor.update_in(cx, |editor, window, cx| {
11910 editor.set_text("somehting_new\n", window, cx)
11911 });
11912 assert!(cx.read(|cx| editor.is_dirty(cx)));
11913 let _formatting_request_signal = fake_server
11914 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11915 assert_eq!(
11916 params.text_document.uri,
11917 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11918 );
11919 assert_eq!(params.options.tab_size, 8);
11920 Ok(Some(vec![]))
11921 });
11922 let save = editor
11923 .update_in(cx, |editor, window, cx| {
11924 editor.save(
11925 SaveOptions {
11926 format: true,
11927 autosave: false,
11928 },
11929 project.clone(),
11930 window,
11931 cx,
11932 )
11933 })
11934 .unwrap();
11935 cx.executor().start_waiting();
11936 save.await;
11937 }
11938}
11939
11940#[gpui::test]
11941async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11942 init_test(cx, |settings| {
11943 settings.defaults.ensure_final_newline_on_save = Some(false);
11944 });
11945
11946 let fs = FakeFs::new(cx.executor());
11947 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11948
11949 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11950
11951 let buffer = project
11952 .update(cx, |project, cx| {
11953 project.open_local_buffer(path!("/file.txt"), cx)
11954 })
11955 .await
11956 .unwrap();
11957
11958 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11959 let (editor, cx) = cx.add_window_view(|window, cx| {
11960 build_editor_with_project(project.clone(), buffer, window, cx)
11961 });
11962 editor.update_in(cx, |editor, window, cx| {
11963 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11964 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
11965 });
11966 });
11967 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11968
11969 editor.update_in(cx, |editor, window, cx| {
11970 editor.handle_input("\n", window, cx)
11971 });
11972 cx.run_until_parked();
11973 save(&editor, &project, cx).await;
11974 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11975
11976 editor.update_in(cx, |editor, window, cx| {
11977 editor.undo(&Default::default(), window, cx);
11978 });
11979 save(&editor, &project, cx).await;
11980 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11981
11982 editor.update_in(cx, |editor, window, cx| {
11983 editor.redo(&Default::default(), window, cx);
11984 });
11985 cx.run_until_parked();
11986 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11987
11988 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11989 let save = editor
11990 .update_in(cx, |editor, window, cx| {
11991 editor.save(
11992 SaveOptions {
11993 format: true,
11994 autosave: false,
11995 },
11996 project.clone(),
11997 window,
11998 cx,
11999 )
12000 })
12001 .unwrap();
12002 cx.executor().start_waiting();
12003 save.await;
12004 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12005 }
12006}
12007
12008#[gpui::test]
12009async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
12010 init_test(cx, |_| {});
12011
12012 let cols = 4;
12013 let rows = 10;
12014 let sample_text_1 = sample_text(rows, cols, 'a');
12015 assert_eq!(
12016 sample_text_1,
12017 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12018 );
12019 let sample_text_2 = sample_text(rows, cols, 'l');
12020 assert_eq!(
12021 sample_text_2,
12022 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12023 );
12024 let sample_text_3 = sample_text(rows, cols, 'v');
12025 assert_eq!(
12026 sample_text_3,
12027 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12028 );
12029
12030 let fs = FakeFs::new(cx.executor());
12031 fs.insert_tree(
12032 path!("/a"),
12033 json!({
12034 "main.rs": sample_text_1,
12035 "other.rs": sample_text_2,
12036 "lib.rs": sample_text_3,
12037 }),
12038 )
12039 .await;
12040
12041 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12042 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12043 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12044
12045 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12046 language_registry.add(rust_lang());
12047 let mut fake_servers = language_registry.register_fake_lsp(
12048 "Rust",
12049 FakeLspAdapter {
12050 capabilities: lsp::ServerCapabilities {
12051 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12052 ..Default::default()
12053 },
12054 ..Default::default()
12055 },
12056 );
12057
12058 let worktree = project.update(cx, |project, cx| {
12059 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
12060 assert_eq!(worktrees.len(), 1);
12061 worktrees.pop().unwrap()
12062 });
12063 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12064
12065 let buffer_1 = project
12066 .update(cx, |project, cx| {
12067 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
12068 })
12069 .await
12070 .unwrap();
12071 let buffer_2 = project
12072 .update(cx, |project, cx| {
12073 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
12074 })
12075 .await
12076 .unwrap();
12077 let buffer_3 = project
12078 .update(cx, |project, cx| {
12079 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
12080 })
12081 .await
12082 .unwrap();
12083
12084 let multi_buffer = cx.new(|cx| {
12085 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12086 multi_buffer.push_excerpts(
12087 buffer_1.clone(),
12088 [
12089 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12090 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12091 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12092 ],
12093 cx,
12094 );
12095 multi_buffer.push_excerpts(
12096 buffer_2.clone(),
12097 [
12098 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12099 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12100 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12101 ],
12102 cx,
12103 );
12104 multi_buffer.push_excerpts(
12105 buffer_3.clone(),
12106 [
12107 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12108 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12109 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12110 ],
12111 cx,
12112 );
12113 multi_buffer
12114 });
12115 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12116 Editor::new(
12117 EditorMode::full(),
12118 multi_buffer,
12119 Some(project.clone()),
12120 window,
12121 cx,
12122 )
12123 });
12124
12125 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12126 editor.change_selections(
12127 SelectionEffects::scroll(Autoscroll::Next),
12128 window,
12129 cx,
12130 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
12131 );
12132 editor.insert("|one|two|three|", window, cx);
12133 });
12134 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12135 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12136 editor.change_selections(
12137 SelectionEffects::scroll(Autoscroll::Next),
12138 window,
12139 cx,
12140 |s| s.select_ranges(Some(MultiBufferOffset(60)..MultiBufferOffset(70))),
12141 );
12142 editor.insert("|four|five|six|", window, cx);
12143 });
12144 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12145
12146 // First two buffers should be edited, but not the third one.
12147 assert_eq!(
12148 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12149 "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}",
12150 );
12151 buffer_1.update(cx, |buffer, _| {
12152 assert!(buffer.is_dirty());
12153 assert_eq!(
12154 buffer.text(),
12155 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
12156 )
12157 });
12158 buffer_2.update(cx, |buffer, _| {
12159 assert!(buffer.is_dirty());
12160 assert_eq!(
12161 buffer.text(),
12162 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
12163 )
12164 });
12165 buffer_3.update(cx, |buffer, _| {
12166 assert!(!buffer.is_dirty());
12167 assert_eq!(buffer.text(), sample_text_3,)
12168 });
12169 cx.executor().run_until_parked();
12170
12171 cx.executor().start_waiting();
12172 let save = multi_buffer_editor
12173 .update_in(cx, |editor, window, cx| {
12174 editor.save(
12175 SaveOptions {
12176 format: true,
12177 autosave: false,
12178 },
12179 project.clone(),
12180 window,
12181 cx,
12182 )
12183 })
12184 .unwrap();
12185
12186 let fake_server = fake_servers.next().await.unwrap();
12187 fake_server
12188 .server
12189 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
12190 Ok(Some(vec![lsp::TextEdit::new(
12191 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12192 format!("[{} formatted]", params.text_document.uri),
12193 )]))
12194 })
12195 .detach();
12196 save.await;
12197
12198 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
12199 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
12200 assert_eq!(
12201 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12202 uri!(
12203 "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}"
12204 ),
12205 );
12206 buffer_1.update(cx, |buffer, _| {
12207 assert!(!buffer.is_dirty());
12208 assert_eq!(
12209 buffer.text(),
12210 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
12211 )
12212 });
12213 buffer_2.update(cx, |buffer, _| {
12214 assert!(!buffer.is_dirty());
12215 assert_eq!(
12216 buffer.text(),
12217 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
12218 )
12219 });
12220 buffer_3.update(cx, |buffer, _| {
12221 assert!(!buffer.is_dirty());
12222 assert_eq!(buffer.text(), sample_text_3,)
12223 });
12224}
12225
12226#[gpui::test]
12227async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
12228 init_test(cx, |_| {});
12229
12230 let fs = FakeFs::new(cx.executor());
12231 fs.insert_tree(
12232 path!("/dir"),
12233 json!({
12234 "file1.rs": "fn main() { println!(\"hello\"); }",
12235 "file2.rs": "fn test() { println!(\"test\"); }",
12236 "file3.rs": "fn other() { println!(\"other\"); }\n",
12237 }),
12238 )
12239 .await;
12240
12241 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
12242 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12243 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12244
12245 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12246 language_registry.add(rust_lang());
12247
12248 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
12249 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12250
12251 // Open three buffers
12252 let buffer_1 = project
12253 .update(cx, |project, cx| {
12254 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
12255 })
12256 .await
12257 .unwrap();
12258 let buffer_2 = project
12259 .update(cx, |project, cx| {
12260 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
12261 })
12262 .await
12263 .unwrap();
12264 let buffer_3 = project
12265 .update(cx, |project, cx| {
12266 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
12267 })
12268 .await
12269 .unwrap();
12270
12271 // Create a multi-buffer with all three buffers
12272 let multi_buffer = cx.new(|cx| {
12273 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12274 multi_buffer.push_excerpts(
12275 buffer_1.clone(),
12276 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12277 cx,
12278 );
12279 multi_buffer.push_excerpts(
12280 buffer_2.clone(),
12281 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12282 cx,
12283 );
12284 multi_buffer.push_excerpts(
12285 buffer_3.clone(),
12286 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12287 cx,
12288 );
12289 multi_buffer
12290 });
12291
12292 let editor = cx.new_window_entity(|window, cx| {
12293 Editor::new(
12294 EditorMode::full(),
12295 multi_buffer,
12296 Some(project.clone()),
12297 window,
12298 cx,
12299 )
12300 });
12301
12302 // Edit only the first buffer
12303 editor.update_in(cx, |editor, window, cx| {
12304 editor.change_selections(
12305 SelectionEffects::scroll(Autoscroll::Next),
12306 window,
12307 cx,
12308 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
12309 );
12310 editor.insert("// edited", window, cx);
12311 });
12312
12313 // Verify that only buffer 1 is dirty
12314 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
12315 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12316 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12317
12318 // Get write counts after file creation (files were created with initial content)
12319 // We expect each file to have been written once during creation
12320 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12321 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12322 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12323
12324 // Perform autosave
12325 let save_task = editor.update_in(cx, |editor, window, cx| {
12326 editor.save(
12327 SaveOptions {
12328 format: true,
12329 autosave: true,
12330 },
12331 project.clone(),
12332 window,
12333 cx,
12334 )
12335 });
12336 save_task.await.unwrap();
12337
12338 // Only the dirty buffer should have been saved
12339 assert_eq!(
12340 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12341 1,
12342 "Buffer 1 was dirty, so it should have been written once during autosave"
12343 );
12344 assert_eq!(
12345 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12346 0,
12347 "Buffer 2 was clean, so it should not have been written during autosave"
12348 );
12349 assert_eq!(
12350 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12351 0,
12352 "Buffer 3 was clean, so it should not have been written during autosave"
12353 );
12354
12355 // Verify buffer states after autosave
12356 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12357 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12358 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12359
12360 // Now perform a manual save (format = true)
12361 let save_task = editor.update_in(cx, |editor, window, cx| {
12362 editor.save(
12363 SaveOptions {
12364 format: true,
12365 autosave: false,
12366 },
12367 project.clone(),
12368 window,
12369 cx,
12370 )
12371 });
12372 save_task.await.unwrap();
12373
12374 // During manual save, clean buffers don't get written to disk
12375 // They just get did_save called for language server notifications
12376 assert_eq!(
12377 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12378 1,
12379 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12380 );
12381 assert_eq!(
12382 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12383 0,
12384 "Buffer 2 should not have been written at all"
12385 );
12386 assert_eq!(
12387 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12388 0,
12389 "Buffer 3 should not have been written at all"
12390 );
12391}
12392
12393async fn setup_range_format_test(
12394 cx: &mut TestAppContext,
12395) -> (
12396 Entity<Project>,
12397 Entity<Editor>,
12398 &mut gpui::VisualTestContext,
12399 lsp::FakeLanguageServer,
12400) {
12401 init_test(cx, |_| {});
12402
12403 let fs = FakeFs::new(cx.executor());
12404 fs.insert_file(path!("/file.rs"), Default::default()).await;
12405
12406 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12407
12408 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12409 language_registry.add(rust_lang());
12410 let mut fake_servers = language_registry.register_fake_lsp(
12411 "Rust",
12412 FakeLspAdapter {
12413 capabilities: lsp::ServerCapabilities {
12414 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12415 ..lsp::ServerCapabilities::default()
12416 },
12417 ..FakeLspAdapter::default()
12418 },
12419 );
12420
12421 let buffer = project
12422 .update(cx, |project, cx| {
12423 project.open_local_buffer(path!("/file.rs"), cx)
12424 })
12425 .await
12426 .unwrap();
12427
12428 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12429 let (editor, cx) = cx.add_window_view(|window, cx| {
12430 build_editor_with_project(project.clone(), buffer, window, cx)
12431 });
12432
12433 cx.executor().start_waiting();
12434 let fake_server = fake_servers.next().await.unwrap();
12435
12436 (project, editor, cx, fake_server)
12437}
12438
12439#[gpui::test]
12440async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12441 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12442
12443 editor.update_in(cx, |editor, window, cx| {
12444 editor.set_text("one\ntwo\nthree\n", window, cx)
12445 });
12446 assert!(cx.read(|cx| editor.is_dirty(cx)));
12447
12448 let save = editor
12449 .update_in(cx, |editor, window, cx| {
12450 editor.save(
12451 SaveOptions {
12452 format: true,
12453 autosave: false,
12454 },
12455 project.clone(),
12456 window,
12457 cx,
12458 )
12459 })
12460 .unwrap();
12461 fake_server
12462 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12463 assert_eq!(
12464 params.text_document.uri,
12465 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12466 );
12467 assert_eq!(params.options.tab_size, 4);
12468 Ok(Some(vec![lsp::TextEdit::new(
12469 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12470 ", ".to_string(),
12471 )]))
12472 })
12473 .next()
12474 .await;
12475 cx.executor().start_waiting();
12476 save.await;
12477 assert_eq!(
12478 editor.update(cx, |editor, cx| editor.text(cx)),
12479 "one, two\nthree\n"
12480 );
12481 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12482}
12483
12484#[gpui::test]
12485async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12486 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12487
12488 editor.update_in(cx, |editor, window, cx| {
12489 editor.set_text("one\ntwo\nthree\n", window, cx)
12490 });
12491 assert!(cx.read(|cx| editor.is_dirty(cx)));
12492
12493 // Test that save still works when formatting hangs
12494 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12495 move |params, _| async move {
12496 assert_eq!(
12497 params.text_document.uri,
12498 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12499 );
12500 futures::future::pending::<()>().await;
12501 unreachable!()
12502 },
12503 );
12504 let save = editor
12505 .update_in(cx, |editor, window, cx| {
12506 editor.save(
12507 SaveOptions {
12508 format: true,
12509 autosave: false,
12510 },
12511 project.clone(),
12512 window,
12513 cx,
12514 )
12515 })
12516 .unwrap();
12517 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12518 cx.executor().start_waiting();
12519 save.await;
12520 assert_eq!(
12521 editor.update(cx, |editor, cx| editor.text(cx)),
12522 "one\ntwo\nthree\n"
12523 );
12524 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12525}
12526
12527#[gpui::test]
12528async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12529 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12530
12531 // Buffer starts clean, no formatting should be requested
12532 let save = editor
12533 .update_in(cx, |editor, window, cx| {
12534 editor.save(
12535 SaveOptions {
12536 format: false,
12537 autosave: false,
12538 },
12539 project.clone(),
12540 window,
12541 cx,
12542 )
12543 })
12544 .unwrap();
12545 let _pending_format_request = fake_server
12546 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12547 panic!("Should not be invoked");
12548 })
12549 .next();
12550 cx.executor().start_waiting();
12551 save.await;
12552 cx.run_until_parked();
12553}
12554
12555#[gpui::test]
12556async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12557 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12558
12559 // Set Rust language override and assert overridden tabsize is sent to language server
12560 update_test_language_settings(cx, |settings| {
12561 settings.languages.0.insert(
12562 "Rust".into(),
12563 LanguageSettingsContent {
12564 tab_size: NonZeroU32::new(8),
12565 ..Default::default()
12566 },
12567 );
12568 });
12569
12570 editor.update_in(cx, |editor, window, cx| {
12571 editor.set_text("something_new\n", window, cx)
12572 });
12573 assert!(cx.read(|cx| editor.is_dirty(cx)));
12574 let save = editor
12575 .update_in(cx, |editor, window, cx| {
12576 editor.save(
12577 SaveOptions {
12578 format: true,
12579 autosave: false,
12580 },
12581 project.clone(),
12582 window,
12583 cx,
12584 )
12585 })
12586 .unwrap();
12587 fake_server
12588 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12589 assert_eq!(
12590 params.text_document.uri,
12591 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12592 );
12593 assert_eq!(params.options.tab_size, 8);
12594 Ok(Some(Vec::new()))
12595 })
12596 .next()
12597 .await;
12598 save.await;
12599}
12600
12601#[gpui::test]
12602async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12603 init_test(cx, |settings| {
12604 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12605 settings::LanguageServerFormatterSpecifier::Current,
12606 )))
12607 });
12608
12609 let fs = FakeFs::new(cx.executor());
12610 fs.insert_file(path!("/file.rs"), Default::default()).await;
12611
12612 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12613
12614 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12615 language_registry.add(Arc::new(Language::new(
12616 LanguageConfig {
12617 name: "Rust".into(),
12618 matcher: LanguageMatcher {
12619 path_suffixes: vec!["rs".to_string()],
12620 ..Default::default()
12621 },
12622 ..LanguageConfig::default()
12623 },
12624 Some(tree_sitter_rust::LANGUAGE.into()),
12625 )));
12626 update_test_language_settings(cx, |settings| {
12627 // Enable Prettier formatting for the same buffer, and ensure
12628 // LSP is called instead of Prettier.
12629 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12630 });
12631 let mut fake_servers = language_registry.register_fake_lsp(
12632 "Rust",
12633 FakeLspAdapter {
12634 capabilities: lsp::ServerCapabilities {
12635 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12636 ..Default::default()
12637 },
12638 ..Default::default()
12639 },
12640 );
12641
12642 let buffer = project
12643 .update(cx, |project, cx| {
12644 project.open_local_buffer(path!("/file.rs"), cx)
12645 })
12646 .await
12647 .unwrap();
12648
12649 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12650 let (editor, cx) = cx.add_window_view(|window, cx| {
12651 build_editor_with_project(project.clone(), buffer, window, cx)
12652 });
12653 editor.update_in(cx, |editor, window, cx| {
12654 editor.set_text("one\ntwo\nthree\n", window, cx)
12655 });
12656
12657 cx.executor().start_waiting();
12658 let fake_server = fake_servers.next().await.unwrap();
12659
12660 let format = editor
12661 .update_in(cx, |editor, window, cx| {
12662 editor.perform_format(
12663 project.clone(),
12664 FormatTrigger::Manual,
12665 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12666 window,
12667 cx,
12668 )
12669 })
12670 .unwrap();
12671 fake_server
12672 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12673 assert_eq!(
12674 params.text_document.uri,
12675 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12676 );
12677 assert_eq!(params.options.tab_size, 4);
12678 Ok(Some(vec![lsp::TextEdit::new(
12679 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12680 ", ".to_string(),
12681 )]))
12682 })
12683 .next()
12684 .await;
12685 cx.executor().start_waiting();
12686 format.await;
12687 assert_eq!(
12688 editor.update(cx, |editor, cx| editor.text(cx)),
12689 "one, two\nthree\n"
12690 );
12691
12692 editor.update_in(cx, |editor, window, cx| {
12693 editor.set_text("one\ntwo\nthree\n", window, cx)
12694 });
12695 // Ensure we don't lock if formatting hangs.
12696 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12697 move |params, _| async move {
12698 assert_eq!(
12699 params.text_document.uri,
12700 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12701 );
12702 futures::future::pending::<()>().await;
12703 unreachable!()
12704 },
12705 );
12706 let format = editor
12707 .update_in(cx, |editor, window, cx| {
12708 editor.perform_format(
12709 project,
12710 FormatTrigger::Manual,
12711 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12712 window,
12713 cx,
12714 )
12715 })
12716 .unwrap();
12717 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12718 cx.executor().start_waiting();
12719 format.await;
12720 assert_eq!(
12721 editor.update(cx, |editor, cx| editor.text(cx)),
12722 "one\ntwo\nthree\n"
12723 );
12724}
12725
12726#[gpui::test]
12727async fn test_multiple_formatters(cx: &mut TestAppContext) {
12728 init_test(cx, |settings| {
12729 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12730 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12731 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12732 Formatter::CodeAction("code-action-1".into()),
12733 Formatter::CodeAction("code-action-2".into()),
12734 ]))
12735 });
12736
12737 let fs = FakeFs::new(cx.executor());
12738 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12739 .await;
12740
12741 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12742 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12743 language_registry.add(rust_lang());
12744
12745 let mut fake_servers = language_registry.register_fake_lsp(
12746 "Rust",
12747 FakeLspAdapter {
12748 capabilities: lsp::ServerCapabilities {
12749 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12750 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12751 commands: vec!["the-command-for-code-action-1".into()],
12752 ..Default::default()
12753 }),
12754 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12755 ..Default::default()
12756 },
12757 ..Default::default()
12758 },
12759 );
12760
12761 let buffer = project
12762 .update(cx, |project, cx| {
12763 project.open_local_buffer(path!("/file.rs"), cx)
12764 })
12765 .await
12766 .unwrap();
12767
12768 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12769 let (editor, cx) = cx.add_window_view(|window, cx| {
12770 build_editor_with_project(project.clone(), buffer, window, cx)
12771 });
12772
12773 cx.executor().start_waiting();
12774
12775 let fake_server = fake_servers.next().await.unwrap();
12776 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12777 move |_params, _| async move {
12778 Ok(Some(vec![lsp::TextEdit::new(
12779 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12780 "applied-formatting\n".to_string(),
12781 )]))
12782 },
12783 );
12784 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12785 move |params, _| async move {
12786 let requested_code_actions = params.context.only.expect("Expected code action request");
12787 assert_eq!(requested_code_actions.len(), 1);
12788
12789 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12790 let code_action = match requested_code_actions[0].as_str() {
12791 "code-action-1" => lsp::CodeAction {
12792 kind: Some("code-action-1".into()),
12793 edit: Some(lsp::WorkspaceEdit::new(
12794 [(
12795 uri,
12796 vec![lsp::TextEdit::new(
12797 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12798 "applied-code-action-1-edit\n".to_string(),
12799 )],
12800 )]
12801 .into_iter()
12802 .collect(),
12803 )),
12804 command: Some(lsp::Command {
12805 command: "the-command-for-code-action-1".into(),
12806 ..Default::default()
12807 }),
12808 ..Default::default()
12809 },
12810 "code-action-2" => lsp::CodeAction {
12811 kind: Some("code-action-2".into()),
12812 edit: Some(lsp::WorkspaceEdit::new(
12813 [(
12814 uri,
12815 vec![lsp::TextEdit::new(
12816 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12817 "applied-code-action-2-edit\n".to_string(),
12818 )],
12819 )]
12820 .into_iter()
12821 .collect(),
12822 )),
12823 ..Default::default()
12824 },
12825 req => panic!("Unexpected code action request: {:?}", req),
12826 };
12827 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12828 code_action,
12829 )]))
12830 },
12831 );
12832
12833 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12834 move |params, _| async move { Ok(params) }
12835 });
12836
12837 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12838 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12839 let fake = fake_server.clone();
12840 let lock = command_lock.clone();
12841 move |params, _| {
12842 assert_eq!(params.command, "the-command-for-code-action-1");
12843 let fake = fake.clone();
12844 let lock = lock.clone();
12845 async move {
12846 lock.lock().await;
12847 fake.server
12848 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12849 label: None,
12850 edit: lsp::WorkspaceEdit {
12851 changes: Some(
12852 [(
12853 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12854 vec![lsp::TextEdit {
12855 range: lsp::Range::new(
12856 lsp::Position::new(0, 0),
12857 lsp::Position::new(0, 0),
12858 ),
12859 new_text: "applied-code-action-1-command\n".into(),
12860 }],
12861 )]
12862 .into_iter()
12863 .collect(),
12864 ),
12865 ..Default::default()
12866 },
12867 })
12868 .await
12869 .into_response()
12870 .unwrap();
12871 Ok(Some(json!(null)))
12872 }
12873 }
12874 });
12875
12876 cx.executor().start_waiting();
12877 editor
12878 .update_in(cx, |editor, window, cx| {
12879 editor.perform_format(
12880 project.clone(),
12881 FormatTrigger::Manual,
12882 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12883 window,
12884 cx,
12885 )
12886 })
12887 .unwrap()
12888 .await;
12889 editor.update(cx, |editor, cx| {
12890 assert_eq!(
12891 editor.text(cx),
12892 r#"
12893 applied-code-action-2-edit
12894 applied-code-action-1-command
12895 applied-code-action-1-edit
12896 applied-formatting
12897 one
12898 two
12899 three
12900 "#
12901 .unindent()
12902 );
12903 });
12904
12905 editor.update_in(cx, |editor, window, cx| {
12906 editor.undo(&Default::default(), window, cx);
12907 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12908 });
12909
12910 // Perform a manual edit while waiting for an LSP command
12911 // that's being run as part of a formatting code action.
12912 let lock_guard = command_lock.lock().await;
12913 let format = editor
12914 .update_in(cx, |editor, window, cx| {
12915 editor.perform_format(
12916 project.clone(),
12917 FormatTrigger::Manual,
12918 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12919 window,
12920 cx,
12921 )
12922 })
12923 .unwrap();
12924 cx.run_until_parked();
12925 editor.update(cx, |editor, cx| {
12926 assert_eq!(
12927 editor.text(cx),
12928 r#"
12929 applied-code-action-1-edit
12930 applied-formatting
12931 one
12932 two
12933 three
12934 "#
12935 .unindent()
12936 );
12937
12938 editor.buffer.update(cx, |buffer, cx| {
12939 let ix = buffer.len(cx);
12940 buffer.edit([(ix..ix, "edited\n")], None, cx);
12941 });
12942 });
12943
12944 // Allow the LSP command to proceed. Because the buffer was edited,
12945 // the second code action will not be run.
12946 drop(lock_guard);
12947 format.await;
12948 editor.update_in(cx, |editor, window, cx| {
12949 assert_eq!(
12950 editor.text(cx),
12951 r#"
12952 applied-code-action-1-command
12953 applied-code-action-1-edit
12954 applied-formatting
12955 one
12956 two
12957 three
12958 edited
12959 "#
12960 .unindent()
12961 );
12962
12963 // The manual edit is undone first, because it is the last thing the user did
12964 // (even though the command completed afterwards).
12965 editor.undo(&Default::default(), window, cx);
12966 assert_eq!(
12967 editor.text(cx),
12968 r#"
12969 applied-code-action-1-command
12970 applied-code-action-1-edit
12971 applied-formatting
12972 one
12973 two
12974 three
12975 "#
12976 .unindent()
12977 );
12978
12979 // All the formatting (including the command, which completed after the manual edit)
12980 // is undone together.
12981 editor.undo(&Default::default(), window, cx);
12982 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12983 });
12984}
12985
12986#[gpui::test]
12987async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12988 init_test(cx, |settings| {
12989 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12990 settings::LanguageServerFormatterSpecifier::Current,
12991 )]))
12992 });
12993
12994 let fs = FakeFs::new(cx.executor());
12995 fs.insert_file(path!("/file.ts"), Default::default()).await;
12996
12997 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12998
12999 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13000 language_registry.add(Arc::new(Language::new(
13001 LanguageConfig {
13002 name: "TypeScript".into(),
13003 matcher: LanguageMatcher {
13004 path_suffixes: vec!["ts".to_string()],
13005 ..Default::default()
13006 },
13007 ..LanguageConfig::default()
13008 },
13009 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13010 )));
13011 update_test_language_settings(cx, |settings| {
13012 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
13013 });
13014 let mut fake_servers = language_registry.register_fake_lsp(
13015 "TypeScript",
13016 FakeLspAdapter {
13017 capabilities: lsp::ServerCapabilities {
13018 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
13019 ..Default::default()
13020 },
13021 ..Default::default()
13022 },
13023 );
13024
13025 let buffer = project
13026 .update(cx, |project, cx| {
13027 project.open_local_buffer(path!("/file.ts"), cx)
13028 })
13029 .await
13030 .unwrap();
13031
13032 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13033 let (editor, cx) = cx.add_window_view(|window, cx| {
13034 build_editor_with_project(project.clone(), buffer, window, cx)
13035 });
13036 editor.update_in(cx, |editor, window, cx| {
13037 editor.set_text(
13038 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13039 window,
13040 cx,
13041 )
13042 });
13043
13044 cx.executor().start_waiting();
13045 let fake_server = fake_servers.next().await.unwrap();
13046
13047 let format = editor
13048 .update_in(cx, |editor, window, cx| {
13049 editor.perform_code_action_kind(
13050 project.clone(),
13051 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13052 window,
13053 cx,
13054 )
13055 })
13056 .unwrap();
13057 fake_server
13058 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
13059 assert_eq!(
13060 params.text_document.uri,
13061 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13062 );
13063 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
13064 lsp::CodeAction {
13065 title: "Organize Imports".to_string(),
13066 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
13067 edit: Some(lsp::WorkspaceEdit {
13068 changes: Some(
13069 [(
13070 params.text_document.uri.clone(),
13071 vec![lsp::TextEdit::new(
13072 lsp::Range::new(
13073 lsp::Position::new(1, 0),
13074 lsp::Position::new(2, 0),
13075 ),
13076 "".to_string(),
13077 )],
13078 )]
13079 .into_iter()
13080 .collect(),
13081 ),
13082 ..Default::default()
13083 }),
13084 ..Default::default()
13085 },
13086 )]))
13087 })
13088 .next()
13089 .await;
13090 cx.executor().start_waiting();
13091 format.await;
13092 assert_eq!(
13093 editor.update(cx, |editor, cx| editor.text(cx)),
13094 "import { a } from 'module';\n\nconst x = a;\n"
13095 );
13096
13097 editor.update_in(cx, |editor, window, cx| {
13098 editor.set_text(
13099 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13100 window,
13101 cx,
13102 )
13103 });
13104 // Ensure we don't lock if code action hangs.
13105 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
13106 move |params, _| async move {
13107 assert_eq!(
13108 params.text_document.uri,
13109 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13110 );
13111 futures::future::pending::<()>().await;
13112 unreachable!()
13113 },
13114 );
13115 let format = editor
13116 .update_in(cx, |editor, window, cx| {
13117 editor.perform_code_action_kind(
13118 project,
13119 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13120 window,
13121 cx,
13122 )
13123 })
13124 .unwrap();
13125 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
13126 cx.executor().start_waiting();
13127 format.await;
13128 assert_eq!(
13129 editor.update(cx, |editor, cx| editor.text(cx)),
13130 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
13131 );
13132}
13133
13134#[gpui::test]
13135async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
13136 init_test(cx, |_| {});
13137
13138 let mut cx = EditorLspTestContext::new_rust(
13139 lsp::ServerCapabilities {
13140 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13141 ..Default::default()
13142 },
13143 cx,
13144 )
13145 .await;
13146
13147 cx.set_state(indoc! {"
13148 one.twoˇ
13149 "});
13150
13151 // The format request takes a long time. When it completes, it inserts
13152 // a newline and an indent before the `.`
13153 cx.lsp
13154 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
13155 let executor = cx.background_executor().clone();
13156 async move {
13157 executor.timer(Duration::from_millis(100)).await;
13158 Ok(Some(vec![lsp::TextEdit {
13159 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
13160 new_text: "\n ".into(),
13161 }]))
13162 }
13163 });
13164
13165 // Submit a format request.
13166 let format_1 = cx
13167 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13168 .unwrap();
13169 cx.executor().run_until_parked();
13170
13171 // Submit a second format request.
13172 let format_2 = cx
13173 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13174 .unwrap();
13175 cx.executor().run_until_parked();
13176
13177 // Wait for both format requests to complete
13178 cx.executor().advance_clock(Duration::from_millis(200));
13179 cx.executor().start_waiting();
13180 format_1.await.unwrap();
13181 cx.executor().start_waiting();
13182 format_2.await.unwrap();
13183
13184 // The formatting edits only happens once.
13185 cx.assert_editor_state(indoc! {"
13186 one
13187 .twoˇ
13188 "});
13189}
13190
13191#[gpui::test]
13192async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
13193 init_test(cx, |settings| {
13194 settings.defaults.formatter = Some(FormatterList::default())
13195 });
13196
13197 let mut cx = EditorLspTestContext::new_rust(
13198 lsp::ServerCapabilities {
13199 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13200 ..Default::default()
13201 },
13202 cx,
13203 )
13204 .await;
13205
13206 // Record which buffer changes have been sent to the language server
13207 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
13208 cx.lsp
13209 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
13210 let buffer_changes = buffer_changes.clone();
13211 move |params, _| {
13212 buffer_changes.lock().extend(
13213 params
13214 .content_changes
13215 .into_iter()
13216 .map(|e| (e.range.unwrap(), e.text)),
13217 );
13218 }
13219 });
13220 // Handle formatting requests to the language server.
13221 cx.lsp
13222 .set_request_handler::<lsp::request::Formatting, _, _>({
13223 move |_, _| {
13224 // Insert blank lines between each line of the buffer.
13225 async move {
13226 // TODO: this assertion is not reliably true. Currently nothing guarantees that we deliver
13227 // DidChangedTextDocument to the LSP before sending the formatting request.
13228 // assert_eq!(
13229 // &buffer_changes.lock()[1..],
13230 // &[
13231 // (
13232 // lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13233 // "".into()
13234 // ),
13235 // (
13236 // lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13237 // "".into()
13238 // ),
13239 // (
13240 // lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13241 // "\n".into()
13242 // ),
13243 // ]
13244 // );
13245
13246 Ok(Some(vec![
13247 lsp::TextEdit {
13248 range: lsp::Range::new(
13249 lsp::Position::new(1, 0),
13250 lsp::Position::new(1, 0),
13251 ),
13252 new_text: "\n".into(),
13253 },
13254 lsp::TextEdit {
13255 range: lsp::Range::new(
13256 lsp::Position::new(2, 0),
13257 lsp::Position::new(2, 0),
13258 ),
13259 new_text: "\n".into(),
13260 },
13261 ]))
13262 }
13263 }
13264 });
13265
13266 // Set up a buffer white some trailing whitespace and no trailing newline.
13267 cx.set_state(
13268 &[
13269 "one ", //
13270 "twoˇ", //
13271 "three ", //
13272 "four", //
13273 ]
13274 .join("\n"),
13275 );
13276
13277 // Submit a format request.
13278 let format = cx
13279 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13280 .unwrap();
13281
13282 cx.run_until_parked();
13283 // After formatting the buffer, the trailing whitespace is stripped,
13284 // a newline is appended, and the edits provided by the language server
13285 // have been applied.
13286 format.await.unwrap();
13287
13288 cx.assert_editor_state(
13289 &[
13290 "one", //
13291 "", //
13292 "twoˇ", //
13293 "", //
13294 "three", //
13295 "four", //
13296 "", //
13297 ]
13298 .join("\n"),
13299 );
13300
13301 // Undoing the formatting undoes the trailing whitespace removal, the
13302 // trailing newline, and the LSP edits.
13303 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13304 cx.assert_editor_state(
13305 &[
13306 "one ", //
13307 "twoˇ", //
13308 "three ", //
13309 "four", //
13310 ]
13311 .join("\n"),
13312 );
13313}
13314
13315#[gpui::test]
13316async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13317 cx: &mut TestAppContext,
13318) {
13319 init_test(cx, |_| {});
13320
13321 cx.update(|cx| {
13322 cx.update_global::<SettingsStore, _>(|settings, cx| {
13323 settings.update_user_settings(cx, |settings| {
13324 settings.editor.auto_signature_help = Some(true);
13325 });
13326 });
13327 });
13328
13329 let mut cx = EditorLspTestContext::new_rust(
13330 lsp::ServerCapabilities {
13331 signature_help_provider: Some(lsp::SignatureHelpOptions {
13332 ..Default::default()
13333 }),
13334 ..Default::default()
13335 },
13336 cx,
13337 )
13338 .await;
13339
13340 let language = Language::new(
13341 LanguageConfig {
13342 name: "Rust".into(),
13343 brackets: BracketPairConfig {
13344 pairs: vec![
13345 BracketPair {
13346 start: "{".to_string(),
13347 end: "}".to_string(),
13348 close: true,
13349 surround: true,
13350 newline: true,
13351 },
13352 BracketPair {
13353 start: "(".to_string(),
13354 end: ")".to_string(),
13355 close: true,
13356 surround: true,
13357 newline: true,
13358 },
13359 BracketPair {
13360 start: "/*".to_string(),
13361 end: " */".to_string(),
13362 close: true,
13363 surround: true,
13364 newline: true,
13365 },
13366 BracketPair {
13367 start: "[".to_string(),
13368 end: "]".to_string(),
13369 close: false,
13370 surround: false,
13371 newline: true,
13372 },
13373 BracketPair {
13374 start: "\"".to_string(),
13375 end: "\"".to_string(),
13376 close: true,
13377 surround: true,
13378 newline: false,
13379 },
13380 BracketPair {
13381 start: "<".to_string(),
13382 end: ">".to_string(),
13383 close: false,
13384 surround: true,
13385 newline: true,
13386 },
13387 ],
13388 ..Default::default()
13389 },
13390 autoclose_before: "})]".to_string(),
13391 ..Default::default()
13392 },
13393 Some(tree_sitter_rust::LANGUAGE.into()),
13394 );
13395 let language = Arc::new(language);
13396
13397 cx.language_registry().add(language.clone());
13398 cx.update_buffer(|buffer, cx| {
13399 buffer.set_language(Some(language), cx);
13400 });
13401
13402 cx.set_state(
13403 &r#"
13404 fn main() {
13405 sampleˇ
13406 }
13407 "#
13408 .unindent(),
13409 );
13410
13411 cx.update_editor(|editor, window, cx| {
13412 editor.handle_input("(", window, cx);
13413 });
13414 cx.assert_editor_state(
13415 &"
13416 fn main() {
13417 sample(ˇ)
13418 }
13419 "
13420 .unindent(),
13421 );
13422
13423 let mocked_response = lsp::SignatureHelp {
13424 signatures: vec![lsp::SignatureInformation {
13425 label: "fn sample(param1: u8, param2: u8)".to_string(),
13426 documentation: None,
13427 parameters: Some(vec![
13428 lsp::ParameterInformation {
13429 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13430 documentation: None,
13431 },
13432 lsp::ParameterInformation {
13433 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13434 documentation: None,
13435 },
13436 ]),
13437 active_parameter: None,
13438 }],
13439 active_signature: Some(0),
13440 active_parameter: Some(0),
13441 };
13442 handle_signature_help_request(&mut cx, mocked_response).await;
13443
13444 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13445 .await;
13446
13447 cx.editor(|editor, _, _| {
13448 let signature_help_state = editor.signature_help_state.popover().cloned();
13449 let signature = signature_help_state.unwrap();
13450 assert_eq!(
13451 signature.signatures[signature.current_signature].label,
13452 "fn sample(param1: u8, param2: u8)"
13453 );
13454 });
13455}
13456
13457#[gpui::test]
13458async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13459 init_test(cx, |_| {});
13460
13461 cx.update(|cx| {
13462 cx.update_global::<SettingsStore, _>(|settings, cx| {
13463 settings.update_user_settings(cx, |settings| {
13464 settings.editor.auto_signature_help = Some(false);
13465 settings.editor.show_signature_help_after_edits = Some(false);
13466 });
13467 });
13468 });
13469
13470 let mut cx = EditorLspTestContext::new_rust(
13471 lsp::ServerCapabilities {
13472 signature_help_provider: Some(lsp::SignatureHelpOptions {
13473 ..Default::default()
13474 }),
13475 ..Default::default()
13476 },
13477 cx,
13478 )
13479 .await;
13480
13481 let language = Language::new(
13482 LanguageConfig {
13483 name: "Rust".into(),
13484 brackets: BracketPairConfig {
13485 pairs: vec![
13486 BracketPair {
13487 start: "{".to_string(),
13488 end: "}".to_string(),
13489 close: true,
13490 surround: true,
13491 newline: true,
13492 },
13493 BracketPair {
13494 start: "(".to_string(),
13495 end: ")".to_string(),
13496 close: true,
13497 surround: true,
13498 newline: true,
13499 },
13500 BracketPair {
13501 start: "/*".to_string(),
13502 end: " */".to_string(),
13503 close: true,
13504 surround: true,
13505 newline: true,
13506 },
13507 BracketPair {
13508 start: "[".to_string(),
13509 end: "]".to_string(),
13510 close: false,
13511 surround: false,
13512 newline: true,
13513 },
13514 BracketPair {
13515 start: "\"".to_string(),
13516 end: "\"".to_string(),
13517 close: true,
13518 surround: true,
13519 newline: false,
13520 },
13521 BracketPair {
13522 start: "<".to_string(),
13523 end: ">".to_string(),
13524 close: false,
13525 surround: true,
13526 newline: true,
13527 },
13528 ],
13529 ..Default::default()
13530 },
13531 autoclose_before: "})]".to_string(),
13532 ..Default::default()
13533 },
13534 Some(tree_sitter_rust::LANGUAGE.into()),
13535 );
13536 let language = Arc::new(language);
13537
13538 cx.language_registry().add(language.clone());
13539 cx.update_buffer(|buffer, cx| {
13540 buffer.set_language(Some(language), cx);
13541 });
13542
13543 // Ensure that signature_help is not called when no signature help is enabled.
13544 cx.set_state(
13545 &r#"
13546 fn main() {
13547 sampleˇ
13548 }
13549 "#
13550 .unindent(),
13551 );
13552 cx.update_editor(|editor, window, cx| {
13553 editor.handle_input("(", window, cx);
13554 });
13555 cx.assert_editor_state(
13556 &"
13557 fn main() {
13558 sample(ˇ)
13559 }
13560 "
13561 .unindent(),
13562 );
13563 cx.editor(|editor, _, _| {
13564 assert!(editor.signature_help_state.task().is_none());
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
13587 // Ensure that signature_help is called when enabled afte edits
13588 cx.update(|_, cx| {
13589 cx.update_global::<SettingsStore, _>(|settings, cx| {
13590 settings.update_user_settings(cx, |settings| {
13591 settings.editor.auto_signature_help = Some(false);
13592 settings.editor.show_signature_help_after_edits = Some(true);
13593 });
13594 });
13595 });
13596 cx.set_state(
13597 &r#"
13598 fn main() {
13599 sampleˇ
13600 }
13601 "#
13602 .unindent(),
13603 );
13604 cx.update_editor(|editor, window, cx| {
13605 editor.handle_input("(", window, cx);
13606 });
13607 cx.assert_editor_state(
13608 &"
13609 fn main() {
13610 sample(ˇ)
13611 }
13612 "
13613 .unindent(),
13614 );
13615 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13616 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13617 .await;
13618 cx.update_editor(|editor, _, _| {
13619 let signature_help_state = editor.signature_help_state.popover().cloned();
13620 assert!(signature_help_state.is_some());
13621 let signature = signature_help_state.unwrap();
13622 assert_eq!(
13623 signature.signatures[signature.current_signature].label,
13624 "fn sample(param1: u8, param2: u8)"
13625 );
13626 editor.signature_help_state = SignatureHelpState::default();
13627 });
13628
13629 // Ensure that signature_help is called when auto signature help override is enabled
13630 cx.update(|_, cx| {
13631 cx.update_global::<SettingsStore, _>(|settings, cx| {
13632 settings.update_user_settings(cx, |settings| {
13633 settings.editor.auto_signature_help = Some(true);
13634 settings.editor.show_signature_help_after_edits = Some(false);
13635 });
13636 });
13637 });
13638 cx.set_state(
13639 &r#"
13640 fn main() {
13641 sampleˇ
13642 }
13643 "#
13644 .unindent(),
13645 );
13646 cx.update_editor(|editor, window, cx| {
13647 editor.handle_input("(", window, cx);
13648 });
13649 cx.assert_editor_state(
13650 &"
13651 fn main() {
13652 sample(ˇ)
13653 }
13654 "
13655 .unindent(),
13656 );
13657 handle_signature_help_request(&mut cx, mocked_response).await;
13658 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13659 .await;
13660 cx.editor(|editor, _, _| {
13661 let signature_help_state = editor.signature_help_state.popover().cloned();
13662 assert!(signature_help_state.is_some());
13663 let signature = signature_help_state.unwrap();
13664 assert_eq!(
13665 signature.signatures[signature.current_signature].label,
13666 "fn sample(param1: u8, param2: u8)"
13667 );
13668 });
13669}
13670
13671#[gpui::test]
13672async fn test_signature_help(cx: &mut TestAppContext) {
13673 init_test(cx, |_| {});
13674 cx.update(|cx| {
13675 cx.update_global::<SettingsStore, _>(|settings, cx| {
13676 settings.update_user_settings(cx, |settings| {
13677 settings.editor.auto_signature_help = Some(true);
13678 });
13679 });
13680 });
13681
13682 let mut cx = EditorLspTestContext::new_rust(
13683 lsp::ServerCapabilities {
13684 signature_help_provider: Some(lsp::SignatureHelpOptions {
13685 ..Default::default()
13686 }),
13687 ..Default::default()
13688 },
13689 cx,
13690 )
13691 .await;
13692
13693 // A test that directly calls `show_signature_help`
13694 cx.update_editor(|editor, window, cx| {
13695 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13696 });
13697
13698 let mocked_response = lsp::SignatureHelp {
13699 signatures: vec![lsp::SignatureInformation {
13700 label: "fn sample(param1: u8, param2: u8)".to_string(),
13701 documentation: None,
13702 parameters: Some(vec![
13703 lsp::ParameterInformation {
13704 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13705 documentation: None,
13706 },
13707 lsp::ParameterInformation {
13708 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13709 documentation: None,
13710 },
13711 ]),
13712 active_parameter: None,
13713 }],
13714 active_signature: Some(0),
13715 active_parameter: Some(0),
13716 };
13717 handle_signature_help_request(&mut cx, mocked_response).await;
13718
13719 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13720 .await;
13721
13722 cx.editor(|editor, _, _| {
13723 let signature_help_state = editor.signature_help_state.popover().cloned();
13724 assert!(signature_help_state.is_some());
13725 let signature = signature_help_state.unwrap();
13726 assert_eq!(
13727 signature.signatures[signature.current_signature].label,
13728 "fn sample(param1: u8, param2: u8)"
13729 );
13730 });
13731
13732 // When exiting outside from inside the brackets, `signature_help` is closed.
13733 cx.set_state(indoc! {"
13734 fn main() {
13735 sample(ˇ);
13736 }
13737
13738 fn sample(param1: u8, param2: u8) {}
13739 "});
13740
13741 cx.update_editor(|editor, window, cx| {
13742 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13743 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13744 });
13745 });
13746
13747 let mocked_response = lsp::SignatureHelp {
13748 signatures: Vec::new(),
13749 active_signature: None,
13750 active_parameter: None,
13751 };
13752 handle_signature_help_request(&mut cx, mocked_response).await;
13753
13754 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13755 .await;
13756
13757 cx.editor(|editor, _, _| {
13758 assert!(!editor.signature_help_state.is_shown());
13759 });
13760
13761 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13762 cx.set_state(indoc! {"
13763 fn main() {
13764 sample(ˇ);
13765 }
13766
13767 fn sample(param1: u8, param2: u8) {}
13768 "});
13769
13770 let mocked_response = lsp::SignatureHelp {
13771 signatures: vec![lsp::SignatureInformation {
13772 label: "fn sample(param1: u8, param2: u8)".to_string(),
13773 documentation: None,
13774 parameters: Some(vec![
13775 lsp::ParameterInformation {
13776 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13777 documentation: None,
13778 },
13779 lsp::ParameterInformation {
13780 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13781 documentation: None,
13782 },
13783 ]),
13784 active_parameter: None,
13785 }],
13786 active_signature: Some(0),
13787 active_parameter: Some(0),
13788 };
13789 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13790 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13791 .await;
13792 cx.editor(|editor, _, _| {
13793 assert!(editor.signature_help_state.is_shown());
13794 });
13795
13796 // Restore the popover with more parameter input
13797 cx.set_state(indoc! {"
13798 fn main() {
13799 sample(param1, param2ˇ);
13800 }
13801
13802 fn sample(param1: u8, param2: u8) {}
13803 "});
13804
13805 let mocked_response = lsp::SignatureHelp {
13806 signatures: vec![lsp::SignatureInformation {
13807 label: "fn sample(param1: u8, param2: u8)".to_string(),
13808 documentation: None,
13809 parameters: Some(vec![
13810 lsp::ParameterInformation {
13811 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13812 documentation: None,
13813 },
13814 lsp::ParameterInformation {
13815 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13816 documentation: None,
13817 },
13818 ]),
13819 active_parameter: None,
13820 }],
13821 active_signature: Some(0),
13822 active_parameter: Some(1),
13823 };
13824 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13825 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13826 .await;
13827
13828 // When selecting a range, the popover is gone.
13829 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13830 cx.update_editor(|editor, window, cx| {
13831 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13832 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13833 })
13834 });
13835 cx.assert_editor_state(indoc! {"
13836 fn main() {
13837 sample(param1, «ˇparam2»);
13838 }
13839
13840 fn sample(param1: u8, param2: u8) {}
13841 "});
13842 cx.editor(|editor, _, _| {
13843 assert!(!editor.signature_help_state.is_shown());
13844 });
13845
13846 // When unselecting again, the popover is back if within the brackets.
13847 cx.update_editor(|editor, window, cx| {
13848 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13849 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13850 })
13851 });
13852 cx.assert_editor_state(indoc! {"
13853 fn main() {
13854 sample(param1, ˇparam2);
13855 }
13856
13857 fn sample(param1: u8, param2: u8) {}
13858 "});
13859 handle_signature_help_request(&mut cx, mocked_response).await;
13860 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13861 .await;
13862 cx.editor(|editor, _, _| {
13863 assert!(editor.signature_help_state.is_shown());
13864 });
13865
13866 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13867 cx.update_editor(|editor, window, cx| {
13868 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13869 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13870 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13871 })
13872 });
13873 cx.assert_editor_state(indoc! {"
13874 fn main() {
13875 sample(param1, ˇparam2);
13876 }
13877
13878 fn sample(param1: u8, param2: u8) {}
13879 "});
13880
13881 let mocked_response = lsp::SignatureHelp {
13882 signatures: vec![lsp::SignatureInformation {
13883 label: "fn sample(param1: u8, param2: u8)".to_string(),
13884 documentation: None,
13885 parameters: Some(vec![
13886 lsp::ParameterInformation {
13887 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13888 documentation: None,
13889 },
13890 lsp::ParameterInformation {
13891 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13892 documentation: None,
13893 },
13894 ]),
13895 active_parameter: None,
13896 }],
13897 active_signature: Some(0),
13898 active_parameter: Some(1),
13899 };
13900 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13901 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13902 .await;
13903 cx.update_editor(|editor, _, cx| {
13904 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13905 });
13906 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13907 .await;
13908 cx.update_editor(|editor, window, cx| {
13909 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13910 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13911 })
13912 });
13913 cx.assert_editor_state(indoc! {"
13914 fn main() {
13915 sample(param1, «ˇparam2»);
13916 }
13917
13918 fn sample(param1: u8, param2: u8) {}
13919 "});
13920 cx.update_editor(|editor, window, cx| {
13921 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13922 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13923 })
13924 });
13925 cx.assert_editor_state(indoc! {"
13926 fn main() {
13927 sample(param1, ˇparam2);
13928 }
13929
13930 fn sample(param1: u8, param2: u8) {}
13931 "});
13932 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13933 .await;
13934}
13935
13936#[gpui::test]
13937async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13938 init_test(cx, |_| {});
13939
13940 let mut cx = EditorLspTestContext::new_rust(
13941 lsp::ServerCapabilities {
13942 signature_help_provider: Some(lsp::SignatureHelpOptions {
13943 ..Default::default()
13944 }),
13945 ..Default::default()
13946 },
13947 cx,
13948 )
13949 .await;
13950
13951 cx.set_state(indoc! {"
13952 fn main() {
13953 overloadedˇ
13954 }
13955 "});
13956
13957 cx.update_editor(|editor, window, cx| {
13958 editor.handle_input("(", window, cx);
13959 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13960 });
13961
13962 // Mock response with 3 signatures
13963 let mocked_response = lsp::SignatureHelp {
13964 signatures: vec![
13965 lsp::SignatureInformation {
13966 label: "fn overloaded(x: i32)".to_string(),
13967 documentation: None,
13968 parameters: Some(vec![lsp::ParameterInformation {
13969 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13970 documentation: None,
13971 }]),
13972 active_parameter: None,
13973 },
13974 lsp::SignatureInformation {
13975 label: "fn overloaded(x: i32, y: i32)".to_string(),
13976 documentation: None,
13977 parameters: Some(vec![
13978 lsp::ParameterInformation {
13979 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13980 documentation: None,
13981 },
13982 lsp::ParameterInformation {
13983 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13984 documentation: None,
13985 },
13986 ]),
13987 active_parameter: None,
13988 },
13989 lsp::SignatureInformation {
13990 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13991 documentation: None,
13992 parameters: Some(vec![
13993 lsp::ParameterInformation {
13994 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13995 documentation: None,
13996 },
13997 lsp::ParameterInformation {
13998 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13999 documentation: None,
14000 },
14001 lsp::ParameterInformation {
14002 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
14003 documentation: None,
14004 },
14005 ]),
14006 active_parameter: None,
14007 },
14008 ],
14009 active_signature: Some(1),
14010 active_parameter: Some(0),
14011 };
14012 handle_signature_help_request(&mut cx, mocked_response).await;
14013
14014 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14015 .await;
14016
14017 // Verify we have multiple signatures and the right one is selected
14018 cx.editor(|editor, _, _| {
14019 let popover = editor.signature_help_state.popover().cloned().unwrap();
14020 assert_eq!(popover.signatures.len(), 3);
14021 // active_signature was 1, so that should be the current
14022 assert_eq!(popover.current_signature, 1);
14023 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
14024 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
14025 assert_eq!(
14026 popover.signatures[2].label,
14027 "fn overloaded(x: i32, y: i32, z: i32)"
14028 );
14029 });
14030
14031 // Test navigation functionality
14032 cx.update_editor(|editor, window, cx| {
14033 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14034 });
14035
14036 cx.editor(|editor, _, _| {
14037 let popover = editor.signature_help_state.popover().cloned().unwrap();
14038 assert_eq!(popover.current_signature, 2);
14039 });
14040
14041 // Test wrap around
14042 cx.update_editor(|editor, window, cx| {
14043 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14044 });
14045
14046 cx.editor(|editor, _, _| {
14047 let popover = editor.signature_help_state.popover().cloned().unwrap();
14048 assert_eq!(popover.current_signature, 0);
14049 });
14050
14051 // Test previous navigation
14052 cx.update_editor(|editor, window, cx| {
14053 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
14054 });
14055
14056 cx.editor(|editor, _, _| {
14057 let popover = editor.signature_help_state.popover().cloned().unwrap();
14058 assert_eq!(popover.current_signature, 2);
14059 });
14060}
14061
14062#[gpui::test]
14063async fn test_completion_mode(cx: &mut TestAppContext) {
14064 init_test(cx, |_| {});
14065 let mut cx = EditorLspTestContext::new_rust(
14066 lsp::ServerCapabilities {
14067 completion_provider: Some(lsp::CompletionOptions {
14068 resolve_provider: Some(true),
14069 ..Default::default()
14070 }),
14071 ..Default::default()
14072 },
14073 cx,
14074 )
14075 .await;
14076
14077 struct Run {
14078 run_description: &'static str,
14079 initial_state: String,
14080 buffer_marked_text: String,
14081 completion_label: &'static str,
14082 completion_text: &'static str,
14083 expected_with_insert_mode: String,
14084 expected_with_replace_mode: String,
14085 expected_with_replace_subsequence_mode: String,
14086 expected_with_replace_suffix_mode: String,
14087 }
14088
14089 let runs = [
14090 Run {
14091 run_description: "Start of word matches completion text",
14092 initial_state: "before ediˇ after".into(),
14093 buffer_marked_text: "before <edi|> after".into(),
14094 completion_label: "editor",
14095 completion_text: "editor",
14096 expected_with_insert_mode: "before editorˇ after".into(),
14097 expected_with_replace_mode: "before editorˇ after".into(),
14098 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14099 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14100 },
14101 Run {
14102 run_description: "Accept same text at the middle of the word",
14103 initial_state: "before ediˇtor after".into(),
14104 buffer_marked_text: "before <edi|tor> after".into(),
14105 completion_label: "editor",
14106 completion_text: "editor",
14107 expected_with_insert_mode: "before editorˇtor after".into(),
14108 expected_with_replace_mode: "before editorˇ after".into(),
14109 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14110 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14111 },
14112 Run {
14113 run_description: "End of word matches completion text -- cursor at end",
14114 initial_state: "before torˇ after".into(),
14115 buffer_marked_text: "before <tor|> after".into(),
14116 completion_label: "editor",
14117 completion_text: "editor",
14118 expected_with_insert_mode: "before editorˇ after".into(),
14119 expected_with_replace_mode: "before editorˇ after".into(),
14120 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14121 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14122 },
14123 Run {
14124 run_description: "End of word matches completion text -- cursor at start",
14125 initial_state: "before ˇtor after".into(),
14126 buffer_marked_text: "before <|tor> after".into(),
14127 completion_label: "editor",
14128 completion_text: "editor",
14129 expected_with_insert_mode: "before editorˇtor after".into(),
14130 expected_with_replace_mode: "before editorˇ after".into(),
14131 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14132 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14133 },
14134 Run {
14135 run_description: "Prepend text containing whitespace",
14136 initial_state: "pˇfield: bool".into(),
14137 buffer_marked_text: "<p|field>: bool".into(),
14138 completion_label: "pub ",
14139 completion_text: "pub ",
14140 expected_with_insert_mode: "pub ˇfield: bool".into(),
14141 expected_with_replace_mode: "pub ˇ: bool".into(),
14142 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
14143 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
14144 },
14145 Run {
14146 run_description: "Add element to start of list",
14147 initial_state: "[element_ˇelement_2]".into(),
14148 buffer_marked_text: "[<element_|element_2>]".into(),
14149 completion_label: "element_1",
14150 completion_text: "element_1",
14151 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
14152 expected_with_replace_mode: "[element_1ˇ]".into(),
14153 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
14154 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
14155 },
14156 Run {
14157 run_description: "Add element to start of list -- first and second elements are equal",
14158 initial_state: "[elˇelement]".into(),
14159 buffer_marked_text: "[<el|element>]".into(),
14160 completion_label: "element",
14161 completion_text: "element",
14162 expected_with_insert_mode: "[elementˇelement]".into(),
14163 expected_with_replace_mode: "[elementˇ]".into(),
14164 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
14165 expected_with_replace_suffix_mode: "[elementˇ]".into(),
14166 },
14167 Run {
14168 run_description: "Ends with matching suffix",
14169 initial_state: "SubˇError".into(),
14170 buffer_marked_text: "<Sub|Error>".into(),
14171 completion_label: "SubscriptionError",
14172 completion_text: "SubscriptionError",
14173 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
14174 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14175 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14176 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
14177 },
14178 Run {
14179 run_description: "Suffix is a subsequence -- contiguous",
14180 initial_state: "SubˇErr".into(),
14181 buffer_marked_text: "<Sub|Err>".into(),
14182 completion_label: "SubscriptionError",
14183 completion_text: "SubscriptionError",
14184 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
14185 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14186 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14187 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
14188 },
14189 Run {
14190 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
14191 initial_state: "Suˇscrirr".into(),
14192 buffer_marked_text: "<Su|scrirr>".into(),
14193 completion_label: "SubscriptionError",
14194 completion_text: "SubscriptionError",
14195 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
14196 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14197 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14198 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
14199 },
14200 Run {
14201 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
14202 initial_state: "foo(indˇix)".into(),
14203 buffer_marked_text: "foo(<ind|ix>)".into(),
14204 completion_label: "node_index",
14205 completion_text: "node_index",
14206 expected_with_insert_mode: "foo(node_indexˇix)".into(),
14207 expected_with_replace_mode: "foo(node_indexˇ)".into(),
14208 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
14209 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
14210 },
14211 Run {
14212 run_description: "Replace range ends before cursor - should extend to cursor",
14213 initial_state: "before editˇo after".into(),
14214 buffer_marked_text: "before <{ed}>it|o after".into(),
14215 completion_label: "editor",
14216 completion_text: "editor",
14217 expected_with_insert_mode: "before editorˇo after".into(),
14218 expected_with_replace_mode: "before editorˇo after".into(),
14219 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
14220 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14221 },
14222 Run {
14223 run_description: "Uses label for suffix matching",
14224 initial_state: "before ediˇtor after".into(),
14225 buffer_marked_text: "before <edi|tor> after".into(),
14226 completion_label: "editor",
14227 completion_text: "editor()",
14228 expected_with_insert_mode: "before editor()ˇtor after".into(),
14229 expected_with_replace_mode: "before editor()ˇ after".into(),
14230 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14231 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14232 },
14233 Run {
14234 run_description: "Case insensitive subsequence and suffix matching",
14235 initial_state: "before EDiˇtoR after".into(),
14236 buffer_marked_text: "before <EDi|toR> after".into(),
14237 completion_label: "editor",
14238 completion_text: "editor",
14239 expected_with_insert_mode: "before editorˇtoR after".into(),
14240 expected_with_replace_mode: "before editorˇ after".into(),
14241 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14242 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14243 },
14244 ];
14245
14246 for run in runs {
14247 let run_variations = [
14248 (LspInsertMode::Insert, run.expected_with_insert_mode),
14249 (LspInsertMode::Replace, run.expected_with_replace_mode),
14250 (
14251 LspInsertMode::ReplaceSubsequence,
14252 run.expected_with_replace_subsequence_mode,
14253 ),
14254 (
14255 LspInsertMode::ReplaceSuffix,
14256 run.expected_with_replace_suffix_mode,
14257 ),
14258 ];
14259
14260 for (lsp_insert_mode, expected_text) in run_variations {
14261 eprintln!(
14262 "run = {:?}, mode = {lsp_insert_mode:.?}",
14263 run.run_description,
14264 );
14265
14266 update_test_language_settings(&mut cx, |settings| {
14267 settings.defaults.completions = Some(CompletionSettingsContent {
14268 lsp_insert_mode: Some(lsp_insert_mode),
14269 words: Some(WordsCompletionMode::Disabled),
14270 words_min_length: Some(0),
14271 ..Default::default()
14272 });
14273 });
14274
14275 cx.set_state(&run.initial_state);
14276 cx.update_editor(|editor, window, cx| {
14277 editor.show_completions(&ShowCompletions, window, cx);
14278 });
14279
14280 let counter = Arc::new(AtomicUsize::new(0));
14281 handle_completion_request_with_insert_and_replace(
14282 &mut cx,
14283 &run.buffer_marked_text,
14284 vec![(run.completion_label, run.completion_text)],
14285 counter.clone(),
14286 )
14287 .await;
14288 cx.condition(|editor, _| editor.context_menu_visible())
14289 .await;
14290 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14291
14292 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14293 editor
14294 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14295 .unwrap()
14296 });
14297 cx.assert_editor_state(&expected_text);
14298 handle_resolve_completion_request(&mut cx, None).await;
14299 apply_additional_edits.await.unwrap();
14300 }
14301 }
14302}
14303
14304#[gpui::test]
14305async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14306 init_test(cx, |_| {});
14307 let mut cx = EditorLspTestContext::new_rust(
14308 lsp::ServerCapabilities {
14309 completion_provider: Some(lsp::CompletionOptions {
14310 resolve_provider: Some(true),
14311 ..Default::default()
14312 }),
14313 ..Default::default()
14314 },
14315 cx,
14316 )
14317 .await;
14318
14319 let initial_state = "SubˇError";
14320 let buffer_marked_text = "<Sub|Error>";
14321 let completion_text = "SubscriptionError";
14322 let expected_with_insert_mode = "SubscriptionErrorˇError";
14323 let expected_with_replace_mode = "SubscriptionErrorˇ";
14324
14325 update_test_language_settings(&mut cx, |settings| {
14326 settings.defaults.completions = Some(CompletionSettingsContent {
14327 words: Some(WordsCompletionMode::Disabled),
14328 words_min_length: Some(0),
14329 // set the opposite here to ensure that the action is overriding the default behavior
14330 lsp_insert_mode: Some(LspInsertMode::Insert),
14331 ..Default::default()
14332 });
14333 });
14334
14335 cx.set_state(initial_state);
14336 cx.update_editor(|editor, window, cx| {
14337 editor.show_completions(&ShowCompletions, window, cx);
14338 });
14339
14340 let counter = Arc::new(AtomicUsize::new(0));
14341 handle_completion_request_with_insert_and_replace(
14342 &mut cx,
14343 buffer_marked_text,
14344 vec![(completion_text, completion_text)],
14345 counter.clone(),
14346 )
14347 .await;
14348 cx.condition(|editor, _| editor.context_menu_visible())
14349 .await;
14350 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14351
14352 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14353 editor
14354 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14355 .unwrap()
14356 });
14357 cx.assert_editor_state(expected_with_replace_mode);
14358 handle_resolve_completion_request(&mut cx, None).await;
14359 apply_additional_edits.await.unwrap();
14360
14361 update_test_language_settings(&mut cx, |settings| {
14362 settings.defaults.completions = Some(CompletionSettingsContent {
14363 words: Some(WordsCompletionMode::Disabled),
14364 words_min_length: Some(0),
14365 // set the opposite here to ensure that the action is overriding the default behavior
14366 lsp_insert_mode: Some(LspInsertMode::Replace),
14367 ..Default::default()
14368 });
14369 });
14370
14371 cx.set_state(initial_state);
14372 cx.update_editor(|editor, window, cx| {
14373 editor.show_completions(&ShowCompletions, window, cx);
14374 });
14375 handle_completion_request_with_insert_and_replace(
14376 &mut cx,
14377 buffer_marked_text,
14378 vec![(completion_text, completion_text)],
14379 counter.clone(),
14380 )
14381 .await;
14382 cx.condition(|editor, _| editor.context_menu_visible())
14383 .await;
14384 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14385
14386 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14387 editor
14388 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14389 .unwrap()
14390 });
14391 cx.assert_editor_state(expected_with_insert_mode);
14392 handle_resolve_completion_request(&mut cx, None).await;
14393 apply_additional_edits.await.unwrap();
14394}
14395
14396#[gpui::test]
14397async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14398 init_test(cx, |_| {});
14399 let mut cx = EditorLspTestContext::new_rust(
14400 lsp::ServerCapabilities {
14401 completion_provider: Some(lsp::CompletionOptions {
14402 resolve_provider: Some(true),
14403 ..Default::default()
14404 }),
14405 ..Default::default()
14406 },
14407 cx,
14408 )
14409 .await;
14410
14411 // scenario: surrounding text matches completion text
14412 let completion_text = "to_offset";
14413 let initial_state = indoc! {"
14414 1. buf.to_offˇsuffix
14415 2. buf.to_offˇsuf
14416 3. buf.to_offˇfix
14417 4. buf.to_offˇ
14418 5. into_offˇensive
14419 6. ˇsuffix
14420 7. let ˇ //
14421 8. aaˇzz
14422 9. buf.to_off«zzzzzˇ»suffix
14423 10. buf.«ˇzzzzz»suffix
14424 11. to_off«ˇzzzzz»
14425
14426 buf.to_offˇsuffix // newest cursor
14427 "};
14428 let completion_marked_buffer = indoc! {"
14429 1. buf.to_offsuffix
14430 2. buf.to_offsuf
14431 3. buf.to_offfix
14432 4. buf.to_off
14433 5. into_offensive
14434 6. suffix
14435 7. let //
14436 8. aazz
14437 9. buf.to_offzzzzzsuffix
14438 10. buf.zzzzzsuffix
14439 11. to_offzzzzz
14440
14441 buf.<to_off|suffix> // newest cursor
14442 "};
14443 let expected = indoc! {"
14444 1. buf.to_offsetˇ
14445 2. buf.to_offsetˇsuf
14446 3. buf.to_offsetˇfix
14447 4. buf.to_offsetˇ
14448 5. into_offsetˇensive
14449 6. to_offsetˇsuffix
14450 7. let to_offsetˇ //
14451 8. aato_offsetˇzz
14452 9. buf.to_offsetˇ
14453 10. buf.to_offsetˇsuffix
14454 11. to_offsetˇ
14455
14456 buf.to_offsetˇ // newest cursor
14457 "};
14458 cx.set_state(initial_state);
14459 cx.update_editor(|editor, window, cx| {
14460 editor.show_completions(&ShowCompletions, window, cx);
14461 });
14462 handle_completion_request_with_insert_and_replace(
14463 &mut cx,
14464 completion_marked_buffer,
14465 vec![(completion_text, completion_text)],
14466 Arc::new(AtomicUsize::new(0)),
14467 )
14468 .await;
14469 cx.condition(|editor, _| editor.context_menu_visible())
14470 .await;
14471 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14472 editor
14473 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14474 .unwrap()
14475 });
14476 cx.assert_editor_state(expected);
14477 handle_resolve_completion_request(&mut cx, None).await;
14478 apply_additional_edits.await.unwrap();
14479
14480 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14481 let completion_text = "foo_and_bar";
14482 let initial_state = indoc! {"
14483 1. ooanbˇ
14484 2. zooanbˇ
14485 3. ooanbˇz
14486 4. zooanbˇz
14487 5. ooanˇ
14488 6. oanbˇ
14489
14490 ooanbˇ
14491 "};
14492 let completion_marked_buffer = indoc! {"
14493 1. ooanb
14494 2. zooanb
14495 3. ooanbz
14496 4. zooanbz
14497 5. ooan
14498 6. oanb
14499
14500 <ooanb|>
14501 "};
14502 let expected = indoc! {"
14503 1. foo_and_barˇ
14504 2. zfoo_and_barˇ
14505 3. foo_and_barˇz
14506 4. zfoo_and_barˇz
14507 5. ooanfoo_and_barˇ
14508 6. oanbfoo_and_barˇ
14509
14510 foo_and_barˇ
14511 "};
14512 cx.set_state(initial_state);
14513 cx.update_editor(|editor, window, cx| {
14514 editor.show_completions(&ShowCompletions, window, cx);
14515 });
14516 handle_completion_request_with_insert_and_replace(
14517 &mut cx,
14518 completion_marked_buffer,
14519 vec![(completion_text, completion_text)],
14520 Arc::new(AtomicUsize::new(0)),
14521 )
14522 .await;
14523 cx.condition(|editor, _| editor.context_menu_visible())
14524 .await;
14525 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14526 editor
14527 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14528 .unwrap()
14529 });
14530 cx.assert_editor_state(expected);
14531 handle_resolve_completion_request(&mut cx, None).await;
14532 apply_additional_edits.await.unwrap();
14533
14534 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14535 // (expects the same as if it was inserted at the end)
14536 let completion_text = "foo_and_bar";
14537 let initial_state = indoc! {"
14538 1. ooˇanb
14539 2. zooˇanb
14540 3. ooˇanbz
14541 4. zooˇanbz
14542
14543 ooˇanb
14544 "};
14545 let completion_marked_buffer = indoc! {"
14546 1. ooanb
14547 2. zooanb
14548 3. ooanbz
14549 4. zooanbz
14550
14551 <oo|anb>
14552 "};
14553 let expected = indoc! {"
14554 1. foo_and_barˇ
14555 2. zfoo_and_barˇ
14556 3. foo_and_barˇz
14557 4. zfoo_and_barˇz
14558
14559 foo_and_barˇ
14560 "};
14561 cx.set_state(initial_state);
14562 cx.update_editor(|editor, window, cx| {
14563 editor.show_completions(&ShowCompletions, window, cx);
14564 });
14565 handle_completion_request_with_insert_and_replace(
14566 &mut cx,
14567 completion_marked_buffer,
14568 vec![(completion_text, completion_text)],
14569 Arc::new(AtomicUsize::new(0)),
14570 )
14571 .await;
14572 cx.condition(|editor, _| editor.context_menu_visible())
14573 .await;
14574 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14575 editor
14576 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14577 .unwrap()
14578 });
14579 cx.assert_editor_state(expected);
14580 handle_resolve_completion_request(&mut cx, None).await;
14581 apply_additional_edits.await.unwrap();
14582}
14583
14584// This used to crash
14585#[gpui::test]
14586async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14587 init_test(cx, |_| {});
14588
14589 let buffer_text = indoc! {"
14590 fn main() {
14591 10.satu;
14592
14593 //
14594 // separate cursors so they open in different excerpts (manually reproducible)
14595 //
14596
14597 10.satu20;
14598 }
14599 "};
14600 let multibuffer_text_with_selections = indoc! {"
14601 fn main() {
14602 10.satuˇ;
14603
14604 //
14605
14606 //
14607
14608 10.satuˇ20;
14609 }
14610 "};
14611 let expected_multibuffer = indoc! {"
14612 fn main() {
14613 10.saturating_sub()ˇ;
14614
14615 //
14616
14617 //
14618
14619 10.saturating_sub()ˇ;
14620 }
14621 "};
14622
14623 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14624 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14625
14626 let fs = FakeFs::new(cx.executor());
14627 fs.insert_tree(
14628 path!("/a"),
14629 json!({
14630 "main.rs": buffer_text,
14631 }),
14632 )
14633 .await;
14634
14635 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14636 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14637 language_registry.add(rust_lang());
14638 let mut fake_servers = language_registry.register_fake_lsp(
14639 "Rust",
14640 FakeLspAdapter {
14641 capabilities: lsp::ServerCapabilities {
14642 completion_provider: Some(lsp::CompletionOptions {
14643 resolve_provider: None,
14644 ..lsp::CompletionOptions::default()
14645 }),
14646 ..lsp::ServerCapabilities::default()
14647 },
14648 ..FakeLspAdapter::default()
14649 },
14650 );
14651 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14652 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14653 let buffer = project
14654 .update(cx, |project, cx| {
14655 project.open_local_buffer(path!("/a/main.rs"), cx)
14656 })
14657 .await
14658 .unwrap();
14659
14660 let multi_buffer = cx.new(|cx| {
14661 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14662 multi_buffer.push_excerpts(
14663 buffer.clone(),
14664 [ExcerptRange::new(0..first_excerpt_end)],
14665 cx,
14666 );
14667 multi_buffer.push_excerpts(
14668 buffer.clone(),
14669 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14670 cx,
14671 );
14672 multi_buffer
14673 });
14674
14675 let editor = workspace
14676 .update(cx, |_, window, cx| {
14677 cx.new(|cx| {
14678 Editor::new(
14679 EditorMode::Full {
14680 scale_ui_elements_with_buffer_font_size: false,
14681 show_active_line_background: false,
14682 sizing_behavior: SizingBehavior::Default,
14683 },
14684 multi_buffer.clone(),
14685 Some(project.clone()),
14686 window,
14687 cx,
14688 )
14689 })
14690 })
14691 .unwrap();
14692
14693 let pane = workspace
14694 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14695 .unwrap();
14696 pane.update_in(cx, |pane, window, cx| {
14697 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14698 });
14699
14700 let fake_server = fake_servers.next().await.unwrap();
14701
14702 editor.update_in(cx, |editor, window, cx| {
14703 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14704 s.select_ranges([
14705 Point::new(1, 11)..Point::new(1, 11),
14706 Point::new(7, 11)..Point::new(7, 11),
14707 ])
14708 });
14709
14710 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14711 });
14712
14713 editor.update_in(cx, |editor, window, cx| {
14714 editor.show_completions(&ShowCompletions, window, cx);
14715 });
14716
14717 fake_server
14718 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14719 let completion_item = lsp::CompletionItem {
14720 label: "saturating_sub()".into(),
14721 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14722 lsp::InsertReplaceEdit {
14723 new_text: "saturating_sub()".to_owned(),
14724 insert: lsp::Range::new(
14725 lsp::Position::new(7, 7),
14726 lsp::Position::new(7, 11),
14727 ),
14728 replace: lsp::Range::new(
14729 lsp::Position::new(7, 7),
14730 lsp::Position::new(7, 13),
14731 ),
14732 },
14733 )),
14734 ..lsp::CompletionItem::default()
14735 };
14736
14737 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14738 })
14739 .next()
14740 .await
14741 .unwrap();
14742
14743 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14744 .await;
14745
14746 editor
14747 .update_in(cx, |editor, window, cx| {
14748 editor
14749 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14750 .unwrap()
14751 })
14752 .await
14753 .unwrap();
14754
14755 editor.update(cx, |editor, cx| {
14756 assert_text_with_selections(editor, expected_multibuffer, cx);
14757 })
14758}
14759
14760#[gpui::test]
14761async fn test_completion(cx: &mut TestAppContext) {
14762 init_test(cx, |_| {});
14763
14764 let mut cx = EditorLspTestContext::new_rust(
14765 lsp::ServerCapabilities {
14766 completion_provider: Some(lsp::CompletionOptions {
14767 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14768 resolve_provider: Some(true),
14769 ..Default::default()
14770 }),
14771 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14772 ..Default::default()
14773 },
14774 cx,
14775 )
14776 .await;
14777 let counter = Arc::new(AtomicUsize::new(0));
14778
14779 cx.set_state(indoc! {"
14780 oneˇ
14781 two
14782 three
14783 "});
14784 cx.simulate_keystroke(".");
14785 handle_completion_request(
14786 indoc! {"
14787 one.|<>
14788 two
14789 three
14790 "},
14791 vec!["first_completion", "second_completion"],
14792 true,
14793 counter.clone(),
14794 &mut cx,
14795 )
14796 .await;
14797 cx.condition(|editor, _| editor.context_menu_visible())
14798 .await;
14799 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14800
14801 let _handler = handle_signature_help_request(
14802 &mut cx,
14803 lsp::SignatureHelp {
14804 signatures: vec![lsp::SignatureInformation {
14805 label: "test signature".to_string(),
14806 documentation: None,
14807 parameters: Some(vec![lsp::ParameterInformation {
14808 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14809 documentation: None,
14810 }]),
14811 active_parameter: None,
14812 }],
14813 active_signature: None,
14814 active_parameter: None,
14815 },
14816 );
14817 cx.update_editor(|editor, window, cx| {
14818 assert!(
14819 !editor.signature_help_state.is_shown(),
14820 "No signature help was called for"
14821 );
14822 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14823 });
14824 cx.run_until_parked();
14825 cx.update_editor(|editor, _, _| {
14826 assert!(
14827 !editor.signature_help_state.is_shown(),
14828 "No signature help should be shown when completions menu is open"
14829 );
14830 });
14831
14832 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14833 editor.context_menu_next(&Default::default(), window, cx);
14834 editor
14835 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14836 .unwrap()
14837 });
14838 cx.assert_editor_state(indoc! {"
14839 one.second_completionˇ
14840 two
14841 three
14842 "});
14843
14844 handle_resolve_completion_request(
14845 &mut cx,
14846 Some(vec![
14847 (
14848 //This overlaps with the primary completion edit which is
14849 //misbehavior from the LSP spec, test that we filter it out
14850 indoc! {"
14851 one.second_ˇcompletion
14852 two
14853 threeˇ
14854 "},
14855 "overlapping additional edit",
14856 ),
14857 (
14858 indoc! {"
14859 one.second_completion
14860 two
14861 threeˇ
14862 "},
14863 "\nadditional edit",
14864 ),
14865 ]),
14866 )
14867 .await;
14868 apply_additional_edits.await.unwrap();
14869 cx.assert_editor_state(indoc! {"
14870 one.second_completionˇ
14871 two
14872 three
14873 additional edit
14874 "});
14875
14876 cx.set_state(indoc! {"
14877 one.second_completion
14878 twoˇ
14879 threeˇ
14880 additional edit
14881 "});
14882 cx.simulate_keystroke(" ");
14883 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14884 cx.simulate_keystroke("s");
14885 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14886
14887 cx.assert_editor_state(indoc! {"
14888 one.second_completion
14889 two sˇ
14890 three sˇ
14891 additional edit
14892 "});
14893 handle_completion_request(
14894 indoc! {"
14895 one.second_completion
14896 two s
14897 three <s|>
14898 additional edit
14899 "},
14900 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14901 true,
14902 counter.clone(),
14903 &mut cx,
14904 )
14905 .await;
14906 cx.condition(|editor, _| editor.context_menu_visible())
14907 .await;
14908 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14909
14910 cx.simulate_keystroke("i");
14911
14912 handle_completion_request(
14913 indoc! {"
14914 one.second_completion
14915 two si
14916 three <si|>
14917 additional edit
14918 "},
14919 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14920 true,
14921 counter.clone(),
14922 &mut cx,
14923 )
14924 .await;
14925 cx.condition(|editor, _| editor.context_menu_visible())
14926 .await;
14927 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14928
14929 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14930 editor
14931 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14932 .unwrap()
14933 });
14934 cx.assert_editor_state(indoc! {"
14935 one.second_completion
14936 two sixth_completionˇ
14937 three sixth_completionˇ
14938 additional edit
14939 "});
14940
14941 apply_additional_edits.await.unwrap();
14942
14943 update_test_language_settings(&mut cx, |settings| {
14944 settings.defaults.show_completions_on_input = Some(false);
14945 });
14946 cx.set_state("editorˇ");
14947 cx.simulate_keystroke(".");
14948 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14949 cx.simulate_keystrokes("c l o");
14950 cx.assert_editor_state("editor.cloˇ");
14951 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14952 cx.update_editor(|editor, window, cx| {
14953 editor.show_completions(&ShowCompletions, window, cx);
14954 });
14955 handle_completion_request(
14956 "editor.<clo|>",
14957 vec!["close", "clobber"],
14958 true,
14959 counter.clone(),
14960 &mut cx,
14961 )
14962 .await;
14963 cx.condition(|editor, _| editor.context_menu_visible())
14964 .await;
14965 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14966
14967 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14968 editor
14969 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14970 .unwrap()
14971 });
14972 cx.assert_editor_state("editor.clobberˇ");
14973 handle_resolve_completion_request(&mut cx, None).await;
14974 apply_additional_edits.await.unwrap();
14975}
14976
14977#[gpui::test]
14978async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
14979 init_test(cx, |_| {});
14980
14981 let fs = FakeFs::new(cx.executor());
14982 fs.insert_tree(
14983 path!("/a"),
14984 json!({
14985 "main.rs": "",
14986 }),
14987 )
14988 .await;
14989
14990 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14991 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14992 language_registry.add(rust_lang());
14993 let command_calls = Arc::new(AtomicUsize::new(0));
14994 let registered_command = "_the/command";
14995
14996 let closure_command_calls = command_calls.clone();
14997 let mut fake_servers = language_registry.register_fake_lsp(
14998 "Rust",
14999 FakeLspAdapter {
15000 capabilities: lsp::ServerCapabilities {
15001 completion_provider: Some(lsp::CompletionOptions {
15002 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15003 ..lsp::CompletionOptions::default()
15004 }),
15005 execute_command_provider: Some(lsp::ExecuteCommandOptions {
15006 commands: vec![registered_command.to_owned()],
15007 ..lsp::ExecuteCommandOptions::default()
15008 }),
15009 ..lsp::ServerCapabilities::default()
15010 },
15011 initializer: Some(Box::new(move |fake_server| {
15012 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15013 move |params, _| async move {
15014 Ok(Some(lsp::CompletionResponse::Array(vec![
15015 lsp::CompletionItem {
15016 label: "registered_command".to_owned(),
15017 text_edit: gen_text_edit(¶ms, ""),
15018 command: Some(lsp::Command {
15019 title: registered_command.to_owned(),
15020 command: "_the/command".to_owned(),
15021 arguments: Some(vec![serde_json::Value::Bool(true)]),
15022 }),
15023 ..lsp::CompletionItem::default()
15024 },
15025 lsp::CompletionItem {
15026 label: "unregistered_command".to_owned(),
15027 text_edit: gen_text_edit(¶ms, ""),
15028 command: Some(lsp::Command {
15029 title: "????????????".to_owned(),
15030 command: "????????????".to_owned(),
15031 arguments: Some(vec![serde_json::Value::Null]),
15032 }),
15033 ..lsp::CompletionItem::default()
15034 },
15035 ])))
15036 },
15037 );
15038 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
15039 let command_calls = closure_command_calls.clone();
15040 move |params, _| {
15041 assert_eq!(params.command, registered_command);
15042 let command_calls = command_calls.clone();
15043 async move {
15044 command_calls.fetch_add(1, atomic::Ordering::Release);
15045 Ok(Some(json!(null)))
15046 }
15047 }
15048 });
15049 })),
15050 ..FakeLspAdapter::default()
15051 },
15052 );
15053 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15054 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15055 let editor = workspace
15056 .update(cx, |workspace, window, cx| {
15057 workspace.open_abs_path(
15058 PathBuf::from(path!("/a/main.rs")),
15059 OpenOptions::default(),
15060 window,
15061 cx,
15062 )
15063 })
15064 .unwrap()
15065 .await
15066 .unwrap()
15067 .downcast::<Editor>()
15068 .unwrap();
15069 let _fake_server = fake_servers.next().await.unwrap();
15070
15071 editor.update_in(cx, |editor, window, cx| {
15072 cx.focus_self(window);
15073 editor.move_to_end(&MoveToEnd, window, cx);
15074 editor.handle_input(".", window, cx);
15075 });
15076 cx.run_until_parked();
15077 editor.update(cx, |editor, _| {
15078 assert!(editor.context_menu_visible());
15079 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15080 {
15081 let completion_labels = menu
15082 .completions
15083 .borrow()
15084 .iter()
15085 .map(|c| c.label.text.clone())
15086 .collect::<Vec<_>>();
15087 assert_eq!(
15088 completion_labels,
15089 &["registered_command", "unregistered_command",],
15090 );
15091 } else {
15092 panic!("expected completion menu to be open");
15093 }
15094 });
15095
15096 editor
15097 .update_in(cx, |editor, window, cx| {
15098 editor
15099 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15100 .unwrap()
15101 })
15102 .await
15103 .unwrap();
15104 cx.run_until_parked();
15105 assert_eq!(
15106 command_calls.load(atomic::Ordering::Acquire),
15107 1,
15108 "For completion with a registered command, Zed should send a command execution request",
15109 );
15110
15111 editor.update_in(cx, |editor, window, cx| {
15112 cx.focus_self(window);
15113 editor.handle_input(".", window, cx);
15114 });
15115 cx.run_until_parked();
15116 editor.update(cx, |editor, _| {
15117 assert!(editor.context_menu_visible());
15118 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15119 {
15120 let completion_labels = menu
15121 .completions
15122 .borrow()
15123 .iter()
15124 .map(|c| c.label.text.clone())
15125 .collect::<Vec<_>>();
15126 assert_eq!(
15127 completion_labels,
15128 &["registered_command", "unregistered_command",],
15129 );
15130 } else {
15131 panic!("expected completion menu to be open");
15132 }
15133 });
15134 editor
15135 .update_in(cx, |editor, window, cx| {
15136 editor.context_menu_next(&Default::default(), window, cx);
15137 editor
15138 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15139 .unwrap()
15140 })
15141 .await
15142 .unwrap();
15143 cx.run_until_parked();
15144 assert_eq!(
15145 command_calls.load(atomic::Ordering::Acquire),
15146 1,
15147 "For completion with an unregistered command, Zed should not send a command execution request",
15148 );
15149}
15150
15151#[gpui::test]
15152async fn test_completion_reuse(cx: &mut TestAppContext) {
15153 init_test(cx, |_| {});
15154
15155 let mut cx = EditorLspTestContext::new_rust(
15156 lsp::ServerCapabilities {
15157 completion_provider: Some(lsp::CompletionOptions {
15158 trigger_characters: Some(vec![".".to_string()]),
15159 ..Default::default()
15160 }),
15161 ..Default::default()
15162 },
15163 cx,
15164 )
15165 .await;
15166
15167 let counter = Arc::new(AtomicUsize::new(0));
15168 cx.set_state("objˇ");
15169 cx.simulate_keystroke(".");
15170
15171 // Initial completion request returns complete results
15172 let is_incomplete = false;
15173 handle_completion_request(
15174 "obj.|<>",
15175 vec!["a", "ab", "abc"],
15176 is_incomplete,
15177 counter.clone(),
15178 &mut cx,
15179 )
15180 .await;
15181 cx.run_until_parked();
15182 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15183 cx.assert_editor_state("obj.ˇ");
15184 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15185
15186 // Type "a" - filters existing completions
15187 cx.simulate_keystroke("a");
15188 cx.run_until_parked();
15189 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15190 cx.assert_editor_state("obj.aˇ");
15191 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15192
15193 // Type "b" - filters existing completions
15194 cx.simulate_keystroke("b");
15195 cx.run_until_parked();
15196 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15197 cx.assert_editor_state("obj.abˇ");
15198 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15199
15200 // Type "c" - filters existing completions
15201 cx.simulate_keystroke("c");
15202 cx.run_until_parked();
15203 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15204 cx.assert_editor_state("obj.abcˇ");
15205 check_displayed_completions(vec!["abc"], &mut cx);
15206
15207 // Backspace to delete "c" - filters existing completions
15208 cx.update_editor(|editor, window, cx| {
15209 editor.backspace(&Backspace, window, cx);
15210 });
15211 cx.run_until_parked();
15212 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15213 cx.assert_editor_state("obj.abˇ");
15214 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15215
15216 // Moving cursor to the left dismisses menu.
15217 cx.update_editor(|editor, window, cx| {
15218 editor.move_left(&MoveLeft, window, cx);
15219 });
15220 cx.run_until_parked();
15221 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15222 cx.assert_editor_state("obj.aˇb");
15223 cx.update_editor(|editor, _, _| {
15224 assert_eq!(editor.context_menu_visible(), false);
15225 });
15226
15227 // Type "b" - new request
15228 cx.simulate_keystroke("b");
15229 let is_incomplete = false;
15230 handle_completion_request(
15231 "obj.<ab|>a",
15232 vec!["ab", "abc"],
15233 is_incomplete,
15234 counter.clone(),
15235 &mut cx,
15236 )
15237 .await;
15238 cx.run_until_parked();
15239 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15240 cx.assert_editor_state("obj.abˇb");
15241 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15242
15243 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
15244 cx.update_editor(|editor, window, cx| {
15245 editor.backspace(&Backspace, window, cx);
15246 });
15247 let is_incomplete = false;
15248 handle_completion_request(
15249 "obj.<a|>b",
15250 vec!["a", "ab", "abc"],
15251 is_incomplete,
15252 counter.clone(),
15253 &mut cx,
15254 )
15255 .await;
15256 cx.run_until_parked();
15257 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15258 cx.assert_editor_state("obj.aˇb");
15259 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15260
15261 // Backspace to delete "a" - dismisses menu.
15262 cx.update_editor(|editor, window, cx| {
15263 editor.backspace(&Backspace, window, cx);
15264 });
15265 cx.run_until_parked();
15266 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15267 cx.assert_editor_state("obj.ˇb");
15268 cx.update_editor(|editor, _, _| {
15269 assert_eq!(editor.context_menu_visible(), false);
15270 });
15271}
15272
15273#[gpui::test]
15274async fn test_word_completion(cx: &mut TestAppContext) {
15275 let lsp_fetch_timeout_ms = 10;
15276 init_test(cx, |language_settings| {
15277 language_settings.defaults.completions = Some(CompletionSettingsContent {
15278 words_min_length: Some(0),
15279 lsp_fetch_timeout_ms: Some(10),
15280 lsp_insert_mode: Some(LspInsertMode::Insert),
15281 ..Default::default()
15282 });
15283 });
15284
15285 let mut cx = EditorLspTestContext::new_rust(
15286 lsp::ServerCapabilities {
15287 completion_provider: Some(lsp::CompletionOptions {
15288 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15289 ..lsp::CompletionOptions::default()
15290 }),
15291 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15292 ..lsp::ServerCapabilities::default()
15293 },
15294 cx,
15295 )
15296 .await;
15297
15298 let throttle_completions = Arc::new(AtomicBool::new(false));
15299
15300 let lsp_throttle_completions = throttle_completions.clone();
15301 let _completion_requests_handler =
15302 cx.lsp
15303 .server
15304 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
15305 let lsp_throttle_completions = lsp_throttle_completions.clone();
15306 let cx = cx.clone();
15307 async move {
15308 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
15309 cx.background_executor()
15310 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
15311 .await;
15312 }
15313 Ok(Some(lsp::CompletionResponse::Array(vec![
15314 lsp::CompletionItem {
15315 label: "first".into(),
15316 ..lsp::CompletionItem::default()
15317 },
15318 lsp::CompletionItem {
15319 label: "last".into(),
15320 ..lsp::CompletionItem::default()
15321 },
15322 ])))
15323 }
15324 });
15325
15326 cx.set_state(indoc! {"
15327 oneˇ
15328 two
15329 three
15330 "});
15331 cx.simulate_keystroke(".");
15332 cx.executor().run_until_parked();
15333 cx.condition(|editor, _| editor.context_menu_visible())
15334 .await;
15335 cx.update_editor(|editor, window, cx| {
15336 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15337 {
15338 assert_eq!(
15339 completion_menu_entries(menu),
15340 &["first", "last"],
15341 "When LSP server is fast to reply, no fallback word completions are used"
15342 );
15343 } else {
15344 panic!("expected completion menu to be open");
15345 }
15346 editor.cancel(&Cancel, window, cx);
15347 });
15348 cx.executor().run_until_parked();
15349 cx.condition(|editor, _| !editor.context_menu_visible())
15350 .await;
15351
15352 throttle_completions.store(true, atomic::Ordering::Release);
15353 cx.simulate_keystroke(".");
15354 cx.executor()
15355 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
15356 cx.executor().run_until_parked();
15357 cx.condition(|editor, _| editor.context_menu_visible())
15358 .await;
15359 cx.update_editor(|editor, _, _| {
15360 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15361 {
15362 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
15363 "When LSP server is slow, document words can be shown instead, if configured accordingly");
15364 } else {
15365 panic!("expected completion menu to be open");
15366 }
15367 });
15368}
15369
15370#[gpui::test]
15371async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
15372 init_test(cx, |language_settings| {
15373 language_settings.defaults.completions = Some(CompletionSettingsContent {
15374 words: Some(WordsCompletionMode::Enabled),
15375 words_min_length: Some(0),
15376 lsp_insert_mode: Some(LspInsertMode::Insert),
15377 ..Default::default()
15378 });
15379 });
15380
15381 let mut cx = EditorLspTestContext::new_rust(
15382 lsp::ServerCapabilities {
15383 completion_provider: Some(lsp::CompletionOptions {
15384 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15385 ..lsp::CompletionOptions::default()
15386 }),
15387 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15388 ..lsp::ServerCapabilities::default()
15389 },
15390 cx,
15391 )
15392 .await;
15393
15394 let _completion_requests_handler =
15395 cx.lsp
15396 .server
15397 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15398 Ok(Some(lsp::CompletionResponse::Array(vec![
15399 lsp::CompletionItem {
15400 label: "first".into(),
15401 ..lsp::CompletionItem::default()
15402 },
15403 lsp::CompletionItem {
15404 label: "last".into(),
15405 ..lsp::CompletionItem::default()
15406 },
15407 ])))
15408 });
15409
15410 cx.set_state(indoc! {"ˇ
15411 first
15412 last
15413 second
15414 "});
15415 cx.simulate_keystroke(".");
15416 cx.executor().run_until_parked();
15417 cx.condition(|editor, _| editor.context_menu_visible())
15418 .await;
15419 cx.update_editor(|editor, _, _| {
15420 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15421 {
15422 assert_eq!(
15423 completion_menu_entries(menu),
15424 &["first", "last", "second"],
15425 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15426 );
15427 } else {
15428 panic!("expected completion menu to be open");
15429 }
15430 });
15431}
15432
15433#[gpui::test]
15434async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15435 init_test(cx, |language_settings| {
15436 language_settings.defaults.completions = Some(CompletionSettingsContent {
15437 words: Some(WordsCompletionMode::Disabled),
15438 words_min_length: Some(0),
15439 lsp_insert_mode: Some(LspInsertMode::Insert),
15440 ..Default::default()
15441 });
15442 });
15443
15444 let mut cx = EditorLspTestContext::new_rust(
15445 lsp::ServerCapabilities {
15446 completion_provider: Some(lsp::CompletionOptions {
15447 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15448 ..lsp::CompletionOptions::default()
15449 }),
15450 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15451 ..lsp::ServerCapabilities::default()
15452 },
15453 cx,
15454 )
15455 .await;
15456
15457 let _completion_requests_handler =
15458 cx.lsp
15459 .server
15460 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15461 panic!("LSP completions should not be queried when dealing with word completions")
15462 });
15463
15464 cx.set_state(indoc! {"ˇ
15465 first
15466 last
15467 second
15468 "});
15469 cx.update_editor(|editor, window, cx| {
15470 editor.show_word_completions(&ShowWordCompletions, window, cx);
15471 });
15472 cx.executor().run_until_parked();
15473 cx.condition(|editor, _| editor.context_menu_visible())
15474 .await;
15475 cx.update_editor(|editor, _, _| {
15476 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15477 {
15478 assert_eq!(
15479 completion_menu_entries(menu),
15480 &["first", "last", "second"],
15481 "`ShowWordCompletions` action should show word completions"
15482 );
15483 } else {
15484 panic!("expected completion menu to be open");
15485 }
15486 });
15487
15488 cx.simulate_keystroke("l");
15489 cx.executor().run_until_parked();
15490 cx.condition(|editor, _| editor.context_menu_visible())
15491 .await;
15492 cx.update_editor(|editor, _, _| {
15493 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15494 {
15495 assert_eq!(
15496 completion_menu_entries(menu),
15497 &["last"],
15498 "After showing word completions, further editing should filter them and not query the LSP"
15499 );
15500 } else {
15501 panic!("expected completion menu to be open");
15502 }
15503 });
15504}
15505
15506#[gpui::test]
15507async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15508 init_test(cx, |language_settings| {
15509 language_settings.defaults.completions = Some(CompletionSettingsContent {
15510 words_min_length: Some(0),
15511 lsp: Some(false),
15512 lsp_insert_mode: Some(LspInsertMode::Insert),
15513 ..Default::default()
15514 });
15515 });
15516
15517 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15518
15519 cx.set_state(indoc! {"ˇ
15520 0_usize
15521 let
15522 33
15523 4.5f32
15524 "});
15525 cx.update_editor(|editor, window, cx| {
15526 editor.show_completions(&ShowCompletions, window, cx);
15527 });
15528 cx.executor().run_until_parked();
15529 cx.condition(|editor, _| editor.context_menu_visible())
15530 .await;
15531 cx.update_editor(|editor, window, cx| {
15532 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15533 {
15534 assert_eq!(
15535 completion_menu_entries(menu),
15536 &["let"],
15537 "With no digits in the completion query, no digits should be in the word completions"
15538 );
15539 } else {
15540 panic!("expected completion menu to be open");
15541 }
15542 editor.cancel(&Cancel, window, cx);
15543 });
15544
15545 cx.set_state(indoc! {"3ˇ
15546 0_usize
15547 let
15548 3
15549 33.35f32
15550 "});
15551 cx.update_editor(|editor, window, cx| {
15552 editor.show_completions(&ShowCompletions, window, cx);
15553 });
15554 cx.executor().run_until_parked();
15555 cx.condition(|editor, _| editor.context_menu_visible())
15556 .await;
15557 cx.update_editor(|editor, _, _| {
15558 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15559 {
15560 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15561 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15562 } else {
15563 panic!("expected completion menu to be open");
15564 }
15565 });
15566}
15567
15568#[gpui::test]
15569async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15570 init_test(cx, |language_settings| {
15571 language_settings.defaults.completions = Some(CompletionSettingsContent {
15572 words: Some(WordsCompletionMode::Enabled),
15573 words_min_length: Some(3),
15574 lsp_insert_mode: Some(LspInsertMode::Insert),
15575 ..Default::default()
15576 });
15577 });
15578
15579 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15580 cx.set_state(indoc! {"ˇ
15581 wow
15582 wowen
15583 wowser
15584 "});
15585 cx.simulate_keystroke("w");
15586 cx.executor().run_until_parked();
15587 cx.update_editor(|editor, _, _| {
15588 if editor.context_menu.borrow_mut().is_some() {
15589 panic!(
15590 "expected completion menu to be hidden, as words completion threshold is not met"
15591 );
15592 }
15593 });
15594
15595 cx.update_editor(|editor, window, cx| {
15596 editor.show_word_completions(&ShowWordCompletions, window, cx);
15597 });
15598 cx.executor().run_until_parked();
15599 cx.update_editor(|editor, window, cx| {
15600 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15601 {
15602 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");
15603 } else {
15604 panic!("expected completion menu to be open after the word completions are called with an action");
15605 }
15606
15607 editor.cancel(&Cancel, window, cx);
15608 });
15609 cx.update_editor(|editor, _, _| {
15610 if editor.context_menu.borrow_mut().is_some() {
15611 panic!("expected completion menu to be hidden after canceling");
15612 }
15613 });
15614
15615 cx.simulate_keystroke("o");
15616 cx.executor().run_until_parked();
15617 cx.update_editor(|editor, _, _| {
15618 if editor.context_menu.borrow_mut().is_some() {
15619 panic!(
15620 "expected completion menu to be hidden, as words completion threshold is not met still"
15621 );
15622 }
15623 });
15624
15625 cx.simulate_keystroke("w");
15626 cx.executor().run_until_parked();
15627 cx.update_editor(|editor, _, _| {
15628 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15629 {
15630 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15631 } else {
15632 panic!("expected completion menu to be open after the word completions threshold is met");
15633 }
15634 });
15635}
15636
15637#[gpui::test]
15638async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15639 init_test(cx, |language_settings| {
15640 language_settings.defaults.completions = Some(CompletionSettingsContent {
15641 words: Some(WordsCompletionMode::Enabled),
15642 words_min_length: Some(0),
15643 lsp_insert_mode: Some(LspInsertMode::Insert),
15644 ..Default::default()
15645 });
15646 });
15647
15648 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15649 cx.update_editor(|editor, _, _| {
15650 editor.disable_word_completions();
15651 });
15652 cx.set_state(indoc! {"ˇ
15653 wow
15654 wowen
15655 wowser
15656 "});
15657 cx.simulate_keystroke("w");
15658 cx.executor().run_until_parked();
15659 cx.update_editor(|editor, _, _| {
15660 if editor.context_menu.borrow_mut().is_some() {
15661 panic!(
15662 "expected completion menu to be hidden, as words completion are disabled for this editor"
15663 );
15664 }
15665 });
15666
15667 cx.update_editor(|editor, window, cx| {
15668 editor.show_word_completions(&ShowWordCompletions, window, cx);
15669 });
15670 cx.executor().run_until_parked();
15671 cx.update_editor(|editor, _, _| {
15672 if editor.context_menu.borrow_mut().is_some() {
15673 panic!(
15674 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15675 );
15676 }
15677 });
15678}
15679
15680#[gpui::test]
15681async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15682 init_test(cx, |language_settings| {
15683 language_settings.defaults.completions = Some(CompletionSettingsContent {
15684 words: Some(WordsCompletionMode::Disabled),
15685 words_min_length: Some(0),
15686 lsp_insert_mode: Some(LspInsertMode::Insert),
15687 ..Default::default()
15688 });
15689 });
15690
15691 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15692 cx.update_editor(|editor, _, _| {
15693 editor.set_completion_provider(None);
15694 });
15695 cx.set_state(indoc! {"ˇ
15696 wow
15697 wowen
15698 wowser
15699 "});
15700 cx.simulate_keystroke("w");
15701 cx.executor().run_until_parked();
15702 cx.update_editor(|editor, _, _| {
15703 if editor.context_menu.borrow_mut().is_some() {
15704 panic!("expected completion menu to be hidden, as disabled in settings");
15705 }
15706 });
15707}
15708
15709fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15710 let position = || lsp::Position {
15711 line: params.text_document_position.position.line,
15712 character: params.text_document_position.position.character,
15713 };
15714 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15715 range: lsp::Range {
15716 start: position(),
15717 end: position(),
15718 },
15719 new_text: text.to_string(),
15720 }))
15721}
15722
15723#[gpui::test]
15724async fn test_multiline_completion(cx: &mut TestAppContext) {
15725 init_test(cx, |_| {});
15726
15727 let fs = FakeFs::new(cx.executor());
15728 fs.insert_tree(
15729 path!("/a"),
15730 json!({
15731 "main.ts": "a",
15732 }),
15733 )
15734 .await;
15735
15736 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15737 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15738 let typescript_language = Arc::new(Language::new(
15739 LanguageConfig {
15740 name: "TypeScript".into(),
15741 matcher: LanguageMatcher {
15742 path_suffixes: vec!["ts".to_string()],
15743 ..LanguageMatcher::default()
15744 },
15745 line_comments: vec!["// ".into()],
15746 ..LanguageConfig::default()
15747 },
15748 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15749 ));
15750 language_registry.add(typescript_language.clone());
15751 let mut fake_servers = language_registry.register_fake_lsp(
15752 "TypeScript",
15753 FakeLspAdapter {
15754 capabilities: lsp::ServerCapabilities {
15755 completion_provider: Some(lsp::CompletionOptions {
15756 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15757 ..lsp::CompletionOptions::default()
15758 }),
15759 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15760 ..lsp::ServerCapabilities::default()
15761 },
15762 // Emulate vtsls label generation
15763 label_for_completion: Some(Box::new(|item, _| {
15764 let text = if let Some(description) = item
15765 .label_details
15766 .as_ref()
15767 .and_then(|label_details| label_details.description.as_ref())
15768 {
15769 format!("{} {}", item.label, description)
15770 } else if let Some(detail) = &item.detail {
15771 format!("{} {}", item.label, detail)
15772 } else {
15773 item.label.clone()
15774 };
15775 Some(language::CodeLabel::plain(text, None))
15776 })),
15777 ..FakeLspAdapter::default()
15778 },
15779 );
15780 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15781 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15782 let worktree_id = workspace
15783 .update(cx, |workspace, _window, cx| {
15784 workspace.project().update(cx, |project, cx| {
15785 project.worktrees(cx).next().unwrap().read(cx).id()
15786 })
15787 })
15788 .unwrap();
15789 let _buffer = project
15790 .update(cx, |project, cx| {
15791 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15792 })
15793 .await
15794 .unwrap();
15795 let editor = workspace
15796 .update(cx, |workspace, window, cx| {
15797 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15798 })
15799 .unwrap()
15800 .await
15801 .unwrap()
15802 .downcast::<Editor>()
15803 .unwrap();
15804 let fake_server = fake_servers.next().await.unwrap();
15805
15806 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15807 let multiline_label_2 = "a\nb\nc\n";
15808 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15809 let multiline_description = "d\ne\nf\n";
15810 let multiline_detail_2 = "g\nh\ni\n";
15811
15812 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15813 move |params, _| async move {
15814 Ok(Some(lsp::CompletionResponse::Array(vec![
15815 lsp::CompletionItem {
15816 label: multiline_label.to_string(),
15817 text_edit: gen_text_edit(¶ms, "new_text_1"),
15818 ..lsp::CompletionItem::default()
15819 },
15820 lsp::CompletionItem {
15821 label: "single line label 1".to_string(),
15822 detail: Some(multiline_detail.to_string()),
15823 text_edit: gen_text_edit(¶ms, "new_text_2"),
15824 ..lsp::CompletionItem::default()
15825 },
15826 lsp::CompletionItem {
15827 label: "single line label 2".to_string(),
15828 label_details: Some(lsp::CompletionItemLabelDetails {
15829 description: Some(multiline_description.to_string()),
15830 detail: None,
15831 }),
15832 text_edit: gen_text_edit(¶ms, "new_text_2"),
15833 ..lsp::CompletionItem::default()
15834 },
15835 lsp::CompletionItem {
15836 label: multiline_label_2.to_string(),
15837 detail: Some(multiline_detail_2.to_string()),
15838 text_edit: gen_text_edit(¶ms, "new_text_3"),
15839 ..lsp::CompletionItem::default()
15840 },
15841 lsp::CompletionItem {
15842 label: "Label with many spaces and \t but without newlines".to_string(),
15843 detail: Some(
15844 "Details with many spaces and \t but without newlines".to_string(),
15845 ),
15846 text_edit: gen_text_edit(¶ms, "new_text_4"),
15847 ..lsp::CompletionItem::default()
15848 },
15849 ])))
15850 },
15851 );
15852
15853 editor.update_in(cx, |editor, window, cx| {
15854 cx.focus_self(window);
15855 editor.move_to_end(&MoveToEnd, window, cx);
15856 editor.handle_input(".", window, cx);
15857 });
15858 cx.run_until_parked();
15859 completion_handle.next().await.unwrap();
15860
15861 editor.update(cx, |editor, _| {
15862 assert!(editor.context_menu_visible());
15863 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15864 {
15865 let completion_labels = menu
15866 .completions
15867 .borrow()
15868 .iter()
15869 .map(|c| c.label.text.clone())
15870 .collect::<Vec<_>>();
15871 assert_eq!(
15872 completion_labels,
15873 &[
15874 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15875 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15876 "single line label 2 d e f ",
15877 "a b c g h i ",
15878 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15879 ],
15880 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15881 );
15882
15883 for completion in menu
15884 .completions
15885 .borrow()
15886 .iter() {
15887 assert_eq!(
15888 completion.label.filter_range,
15889 0..completion.label.text.len(),
15890 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15891 );
15892 }
15893 } else {
15894 panic!("expected completion menu to be open");
15895 }
15896 });
15897}
15898
15899#[gpui::test]
15900async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15901 init_test(cx, |_| {});
15902 let mut cx = EditorLspTestContext::new_rust(
15903 lsp::ServerCapabilities {
15904 completion_provider: Some(lsp::CompletionOptions {
15905 trigger_characters: Some(vec![".".to_string()]),
15906 ..Default::default()
15907 }),
15908 ..Default::default()
15909 },
15910 cx,
15911 )
15912 .await;
15913 cx.lsp
15914 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15915 Ok(Some(lsp::CompletionResponse::Array(vec![
15916 lsp::CompletionItem {
15917 label: "first".into(),
15918 ..Default::default()
15919 },
15920 lsp::CompletionItem {
15921 label: "last".into(),
15922 ..Default::default()
15923 },
15924 ])))
15925 });
15926 cx.set_state("variableˇ");
15927 cx.simulate_keystroke(".");
15928 cx.executor().run_until_parked();
15929
15930 cx.update_editor(|editor, _, _| {
15931 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15932 {
15933 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15934 } else {
15935 panic!("expected completion menu to be open");
15936 }
15937 });
15938
15939 cx.update_editor(|editor, window, cx| {
15940 editor.move_page_down(&MovePageDown::default(), window, cx);
15941 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15942 {
15943 assert!(
15944 menu.selected_item == 1,
15945 "expected PageDown to select the last item from the context menu"
15946 );
15947 } else {
15948 panic!("expected completion menu to stay open after PageDown");
15949 }
15950 });
15951
15952 cx.update_editor(|editor, window, cx| {
15953 editor.move_page_up(&MovePageUp::default(), window, cx);
15954 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15955 {
15956 assert!(
15957 menu.selected_item == 0,
15958 "expected PageUp to select the first item from the context menu"
15959 );
15960 } else {
15961 panic!("expected completion menu to stay open after PageUp");
15962 }
15963 });
15964}
15965
15966#[gpui::test]
15967async fn test_as_is_completions(cx: &mut TestAppContext) {
15968 init_test(cx, |_| {});
15969 let mut cx = EditorLspTestContext::new_rust(
15970 lsp::ServerCapabilities {
15971 completion_provider: Some(lsp::CompletionOptions {
15972 ..Default::default()
15973 }),
15974 ..Default::default()
15975 },
15976 cx,
15977 )
15978 .await;
15979 cx.lsp
15980 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15981 Ok(Some(lsp::CompletionResponse::Array(vec![
15982 lsp::CompletionItem {
15983 label: "unsafe".into(),
15984 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15985 range: lsp::Range {
15986 start: lsp::Position {
15987 line: 1,
15988 character: 2,
15989 },
15990 end: lsp::Position {
15991 line: 1,
15992 character: 3,
15993 },
15994 },
15995 new_text: "unsafe".to_string(),
15996 })),
15997 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15998 ..Default::default()
15999 },
16000 ])))
16001 });
16002 cx.set_state("fn a() {}\n nˇ");
16003 cx.executor().run_until_parked();
16004 cx.update_editor(|editor, window, cx| {
16005 editor.trigger_completion_on_input("n", true, window, cx)
16006 });
16007 cx.executor().run_until_parked();
16008
16009 cx.update_editor(|editor, window, cx| {
16010 editor.confirm_completion(&Default::default(), window, cx)
16011 });
16012 cx.executor().run_until_parked();
16013 cx.assert_editor_state("fn a() {}\n unsafeˇ");
16014}
16015
16016#[gpui::test]
16017async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
16018 init_test(cx, |_| {});
16019 let language =
16020 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
16021 let mut cx = EditorLspTestContext::new(
16022 language,
16023 lsp::ServerCapabilities {
16024 completion_provider: Some(lsp::CompletionOptions {
16025 ..lsp::CompletionOptions::default()
16026 }),
16027 ..lsp::ServerCapabilities::default()
16028 },
16029 cx,
16030 )
16031 .await;
16032
16033 cx.set_state(
16034 "#ifndef BAR_H
16035#define BAR_H
16036
16037#include <stdbool.h>
16038
16039int fn_branch(bool do_branch1, bool do_branch2);
16040
16041#endif // BAR_H
16042ˇ",
16043 );
16044 cx.executor().run_until_parked();
16045 cx.update_editor(|editor, window, cx| {
16046 editor.handle_input("#", window, cx);
16047 });
16048 cx.executor().run_until_parked();
16049 cx.update_editor(|editor, window, cx| {
16050 editor.handle_input("i", window, cx);
16051 });
16052 cx.executor().run_until_parked();
16053 cx.update_editor(|editor, window, cx| {
16054 editor.handle_input("n", window, cx);
16055 });
16056 cx.executor().run_until_parked();
16057 cx.assert_editor_state(
16058 "#ifndef BAR_H
16059#define BAR_H
16060
16061#include <stdbool.h>
16062
16063int fn_branch(bool do_branch1, bool do_branch2);
16064
16065#endif // BAR_H
16066#inˇ",
16067 );
16068
16069 cx.lsp
16070 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16071 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16072 is_incomplete: false,
16073 item_defaults: None,
16074 items: vec![lsp::CompletionItem {
16075 kind: Some(lsp::CompletionItemKind::SNIPPET),
16076 label_details: Some(lsp::CompletionItemLabelDetails {
16077 detail: Some("header".to_string()),
16078 description: None,
16079 }),
16080 label: " include".to_string(),
16081 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16082 range: lsp::Range {
16083 start: lsp::Position {
16084 line: 8,
16085 character: 1,
16086 },
16087 end: lsp::Position {
16088 line: 8,
16089 character: 1,
16090 },
16091 },
16092 new_text: "include \"$0\"".to_string(),
16093 })),
16094 sort_text: Some("40b67681include".to_string()),
16095 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16096 filter_text: Some("include".to_string()),
16097 insert_text: Some("include \"$0\"".to_string()),
16098 ..lsp::CompletionItem::default()
16099 }],
16100 })))
16101 });
16102 cx.update_editor(|editor, window, cx| {
16103 editor.show_completions(&ShowCompletions, window, cx);
16104 });
16105 cx.executor().run_until_parked();
16106 cx.update_editor(|editor, window, cx| {
16107 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16108 });
16109 cx.executor().run_until_parked();
16110 cx.assert_editor_state(
16111 "#ifndef BAR_H
16112#define BAR_H
16113
16114#include <stdbool.h>
16115
16116int fn_branch(bool do_branch1, bool do_branch2);
16117
16118#endif // BAR_H
16119#include \"ˇ\"",
16120 );
16121
16122 cx.lsp
16123 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16124 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16125 is_incomplete: true,
16126 item_defaults: None,
16127 items: vec![lsp::CompletionItem {
16128 kind: Some(lsp::CompletionItemKind::FILE),
16129 label: "AGL/".to_string(),
16130 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16131 range: lsp::Range {
16132 start: lsp::Position {
16133 line: 8,
16134 character: 10,
16135 },
16136 end: lsp::Position {
16137 line: 8,
16138 character: 11,
16139 },
16140 },
16141 new_text: "AGL/".to_string(),
16142 })),
16143 sort_text: Some("40b67681AGL/".to_string()),
16144 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16145 filter_text: Some("AGL/".to_string()),
16146 insert_text: Some("AGL/".to_string()),
16147 ..lsp::CompletionItem::default()
16148 }],
16149 })))
16150 });
16151 cx.update_editor(|editor, window, cx| {
16152 editor.show_completions(&ShowCompletions, window, cx);
16153 });
16154 cx.executor().run_until_parked();
16155 cx.update_editor(|editor, window, cx| {
16156 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16157 });
16158 cx.executor().run_until_parked();
16159 cx.assert_editor_state(
16160 r##"#ifndef BAR_H
16161#define BAR_H
16162
16163#include <stdbool.h>
16164
16165int fn_branch(bool do_branch1, bool do_branch2);
16166
16167#endif // BAR_H
16168#include "AGL/ˇ"##,
16169 );
16170
16171 cx.update_editor(|editor, window, cx| {
16172 editor.handle_input("\"", window, cx);
16173 });
16174 cx.executor().run_until_parked();
16175 cx.assert_editor_state(
16176 r##"#ifndef BAR_H
16177#define BAR_H
16178
16179#include <stdbool.h>
16180
16181int fn_branch(bool do_branch1, bool do_branch2);
16182
16183#endif // BAR_H
16184#include "AGL/"ˇ"##,
16185 );
16186}
16187
16188#[gpui::test]
16189async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
16190 init_test(cx, |_| {});
16191
16192 let mut cx = EditorLspTestContext::new_rust(
16193 lsp::ServerCapabilities {
16194 completion_provider: Some(lsp::CompletionOptions {
16195 trigger_characters: Some(vec![".".to_string()]),
16196 resolve_provider: Some(true),
16197 ..Default::default()
16198 }),
16199 ..Default::default()
16200 },
16201 cx,
16202 )
16203 .await;
16204
16205 cx.set_state("fn main() { let a = 2ˇ; }");
16206 cx.simulate_keystroke(".");
16207 let completion_item = lsp::CompletionItem {
16208 label: "Some".into(),
16209 kind: Some(lsp::CompletionItemKind::SNIPPET),
16210 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16211 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16212 kind: lsp::MarkupKind::Markdown,
16213 value: "```rust\nSome(2)\n```".to_string(),
16214 })),
16215 deprecated: Some(false),
16216 sort_text: Some("Some".to_string()),
16217 filter_text: Some("Some".to_string()),
16218 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16219 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16220 range: lsp::Range {
16221 start: lsp::Position {
16222 line: 0,
16223 character: 22,
16224 },
16225 end: lsp::Position {
16226 line: 0,
16227 character: 22,
16228 },
16229 },
16230 new_text: "Some(2)".to_string(),
16231 })),
16232 additional_text_edits: Some(vec![lsp::TextEdit {
16233 range: lsp::Range {
16234 start: lsp::Position {
16235 line: 0,
16236 character: 20,
16237 },
16238 end: lsp::Position {
16239 line: 0,
16240 character: 22,
16241 },
16242 },
16243 new_text: "".to_string(),
16244 }]),
16245 ..Default::default()
16246 };
16247
16248 let closure_completion_item = completion_item.clone();
16249 let counter = Arc::new(AtomicUsize::new(0));
16250 let counter_clone = counter.clone();
16251 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16252 let task_completion_item = closure_completion_item.clone();
16253 counter_clone.fetch_add(1, atomic::Ordering::Release);
16254 async move {
16255 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16256 is_incomplete: true,
16257 item_defaults: None,
16258 items: vec![task_completion_item],
16259 })))
16260 }
16261 });
16262
16263 cx.condition(|editor, _| editor.context_menu_visible())
16264 .await;
16265 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
16266 assert!(request.next().await.is_some());
16267 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16268
16269 cx.simulate_keystrokes("S o m");
16270 cx.condition(|editor, _| editor.context_menu_visible())
16271 .await;
16272 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
16273 assert!(request.next().await.is_some());
16274 assert!(request.next().await.is_some());
16275 assert!(request.next().await.is_some());
16276 request.close();
16277 assert!(request.next().await.is_none());
16278 assert_eq!(
16279 counter.load(atomic::Ordering::Acquire),
16280 4,
16281 "With the completions menu open, only one LSP request should happen per input"
16282 );
16283}
16284
16285#[gpui::test]
16286async fn test_toggle_comment(cx: &mut TestAppContext) {
16287 init_test(cx, |_| {});
16288 let mut cx = EditorTestContext::new(cx).await;
16289 let language = Arc::new(Language::new(
16290 LanguageConfig {
16291 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16292 ..Default::default()
16293 },
16294 Some(tree_sitter_rust::LANGUAGE.into()),
16295 ));
16296 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16297
16298 // If multiple selections intersect a line, the line is only toggled once.
16299 cx.set_state(indoc! {"
16300 fn a() {
16301 «//b();
16302 ˇ»// «c();
16303 //ˇ» d();
16304 }
16305 "});
16306
16307 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16308
16309 cx.assert_editor_state(indoc! {"
16310 fn a() {
16311 «b();
16312 ˇ»«c();
16313 ˇ» d();
16314 }
16315 "});
16316
16317 // The comment prefix is inserted at the same column for every line in a
16318 // selection.
16319 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16320
16321 cx.assert_editor_state(indoc! {"
16322 fn a() {
16323 // «b();
16324 ˇ»// «c();
16325 ˇ» // d();
16326 }
16327 "});
16328
16329 // If a selection ends at the beginning of a line, that line is not toggled.
16330 cx.set_selections_state(indoc! {"
16331 fn a() {
16332 // b();
16333 «// c();
16334 ˇ» // d();
16335 }
16336 "});
16337
16338 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16339
16340 cx.assert_editor_state(indoc! {"
16341 fn a() {
16342 // b();
16343 «c();
16344 ˇ» // d();
16345 }
16346 "});
16347
16348 // If a selection span a single line and is empty, the line is toggled.
16349 cx.set_state(indoc! {"
16350 fn a() {
16351 a();
16352 b();
16353 ˇ
16354 }
16355 "});
16356
16357 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16358
16359 cx.assert_editor_state(indoc! {"
16360 fn a() {
16361 a();
16362 b();
16363 //•ˇ
16364 }
16365 "});
16366
16367 // If a selection span multiple lines, empty lines are not toggled.
16368 cx.set_state(indoc! {"
16369 fn a() {
16370 «a();
16371
16372 c();ˇ»
16373 }
16374 "});
16375
16376 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16377
16378 cx.assert_editor_state(indoc! {"
16379 fn a() {
16380 // «a();
16381
16382 // c();ˇ»
16383 }
16384 "});
16385
16386 // If a selection includes multiple comment prefixes, all lines are uncommented.
16387 cx.set_state(indoc! {"
16388 fn a() {
16389 «// a();
16390 /// b();
16391 //! c();ˇ»
16392 }
16393 "});
16394
16395 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16396
16397 cx.assert_editor_state(indoc! {"
16398 fn a() {
16399 «a();
16400 b();
16401 c();ˇ»
16402 }
16403 "});
16404}
16405
16406#[gpui::test]
16407async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16408 init_test(cx, |_| {});
16409 let mut cx = EditorTestContext::new(cx).await;
16410 let language = Arc::new(Language::new(
16411 LanguageConfig {
16412 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16413 ..Default::default()
16414 },
16415 Some(tree_sitter_rust::LANGUAGE.into()),
16416 ));
16417 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16418
16419 let toggle_comments = &ToggleComments {
16420 advance_downwards: false,
16421 ignore_indent: true,
16422 };
16423
16424 // If multiple selections intersect a line, the line is only toggled once.
16425 cx.set_state(indoc! {"
16426 fn a() {
16427 // «b();
16428 // c();
16429 // ˇ» d();
16430 }
16431 "});
16432
16433 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16434
16435 cx.assert_editor_state(indoc! {"
16436 fn a() {
16437 «b();
16438 c();
16439 ˇ» d();
16440 }
16441 "});
16442
16443 // The comment prefix is inserted at the beginning of each line
16444 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16445
16446 cx.assert_editor_state(indoc! {"
16447 fn a() {
16448 // «b();
16449 // c();
16450 // ˇ» d();
16451 }
16452 "});
16453
16454 // If a selection ends at the beginning of a line, that line is not toggled.
16455 cx.set_selections_state(indoc! {"
16456 fn a() {
16457 // b();
16458 // «c();
16459 ˇ»// d();
16460 }
16461 "});
16462
16463 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16464
16465 cx.assert_editor_state(indoc! {"
16466 fn a() {
16467 // b();
16468 «c();
16469 ˇ»// d();
16470 }
16471 "});
16472
16473 // If a selection span a single line and is empty, the line is toggled.
16474 cx.set_state(indoc! {"
16475 fn a() {
16476 a();
16477 b();
16478 ˇ
16479 }
16480 "});
16481
16482 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16483
16484 cx.assert_editor_state(indoc! {"
16485 fn a() {
16486 a();
16487 b();
16488 //ˇ
16489 }
16490 "});
16491
16492 // If a selection span multiple lines, empty lines are not toggled.
16493 cx.set_state(indoc! {"
16494 fn a() {
16495 «a();
16496
16497 c();ˇ»
16498 }
16499 "});
16500
16501 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16502
16503 cx.assert_editor_state(indoc! {"
16504 fn a() {
16505 // «a();
16506
16507 // c();ˇ»
16508 }
16509 "});
16510
16511 // If a selection includes multiple comment prefixes, all lines are uncommented.
16512 cx.set_state(indoc! {"
16513 fn a() {
16514 // «a();
16515 /// b();
16516 //! c();ˇ»
16517 }
16518 "});
16519
16520 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16521
16522 cx.assert_editor_state(indoc! {"
16523 fn a() {
16524 «a();
16525 b();
16526 c();ˇ»
16527 }
16528 "});
16529}
16530
16531#[gpui::test]
16532async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16533 init_test(cx, |_| {});
16534
16535 let language = Arc::new(Language::new(
16536 LanguageConfig {
16537 line_comments: vec!["// ".into()],
16538 ..Default::default()
16539 },
16540 Some(tree_sitter_rust::LANGUAGE.into()),
16541 ));
16542
16543 let mut cx = EditorTestContext::new(cx).await;
16544
16545 cx.language_registry().add(language.clone());
16546 cx.update_buffer(|buffer, cx| {
16547 buffer.set_language(Some(language), cx);
16548 });
16549
16550 let toggle_comments = &ToggleComments {
16551 advance_downwards: true,
16552 ignore_indent: false,
16553 };
16554
16555 // Single cursor on one line -> advance
16556 // Cursor moves horizontally 3 characters as well on non-blank line
16557 cx.set_state(indoc!(
16558 "fn a() {
16559 ˇdog();
16560 cat();
16561 }"
16562 ));
16563 cx.update_editor(|editor, window, cx| {
16564 editor.toggle_comments(toggle_comments, window, cx);
16565 });
16566 cx.assert_editor_state(indoc!(
16567 "fn a() {
16568 // dog();
16569 catˇ();
16570 }"
16571 ));
16572
16573 // Single selection on one line -> don't advance
16574 cx.set_state(indoc!(
16575 "fn a() {
16576 «dog()ˇ»;
16577 cat();
16578 }"
16579 ));
16580 cx.update_editor(|editor, window, cx| {
16581 editor.toggle_comments(toggle_comments, window, cx);
16582 });
16583 cx.assert_editor_state(indoc!(
16584 "fn a() {
16585 // «dog()ˇ»;
16586 cat();
16587 }"
16588 ));
16589
16590 // Multiple cursors on one line -> advance
16591 cx.set_state(indoc!(
16592 "fn a() {
16593 ˇdˇog();
16594 cat();
16595 }"
16596 ));
16597 cx.update_editor(|editor, window, cx| {
16598 editor.toggle_comments(toggle_comments, window, cx);
16599 });
16600 cx.assert_editor_state(indoc!(
16601 "fn a() {
16602 // dog();
16603 catˇ(ˇ);
16604 }"
16605 ));
16606
16607 // Multiple cursors on one line, with selection -> don't advance
16608 cx.set_state(indoc!(
16609 "fn a() {
16610 ˇdˇog«()ˇ»;
16611 cat();
16612 }"
16613 ));
16614 cx.update_editor(|editor, window, cx| {
16615 editor.toggle_comments(toggle_comments, window, cx);
16616 });
16617 cx.assert_editor_state(indoc!(
16618 "fn a() {
16619 // ˇdˇog«()ˇ»;
16620 cat();
16621 }"
16622 ));
16623
16624 // Single cursor on one line -> advance
16625 // Cursor moves to column 0 on blank line
16626 cx.set_state(indoc!(
16627 "fn a() {
16628 ˇdog();
16629
16630 cat();
16631 }"
16632 ));
16633 cx.update_editor(|editor, window, cx| {
16634 editor.toggle_comments(toggle_comments, window, cx);
16635 });
16636 cx.assert_editor_state(indoc!(
16637 "fn a() {
16638 // dog();
16639 ˇ
16640 cat();
16641 }"
16642 ));
16643
16644 // Single cursor on one line -> advance
16645 // Cursor starts and ends at column 0
16646 cx.set_state(indoc!(
16647 "fn a() {
16648 ˇ dog();
16649 cat();
16650 }"
16651 ));
16652 cx.update_editor(|editor, window, cx| {
16653 editor.toggle_comments(toggle_comments, window, cx);
16654 });
16655 cx.assert_editor_state(indoc!(
16656 "fn a() {
16657 // dog();
16658 ˇ cat();
16659 }"
16660 ));
16661}
16662
16663#[gpui::test]
16664async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16665 init_test(cx, |_| {});
16666
16667 let mut cx = EditorTestContext::new(cx).await;
16668
16669 let html_language = Arc::new(
16670 Language::new(
16671 LanguageConfig {
16672 name: "HTML".into(),
16673 block_comment: Some(BlockCommentConfig {
16674 start: "<!-- ".into(),
16675 prefix: "".into(),
16676 end: " -->".into(),
16677 tab_size: 0,
16678 }),
16679 ..Default::default()
16680 },
16681 Some(tree_sitter_html::LANGUAGE.into()),
16682 )
16683 .with_injection_query(
16684 r#"
16685 (script_element
16686 (raw_text) @injection.content
16687 (#set! injection.language "javascript"))
16688 "#,
16689 )
16690 .unwrap(),
16691 );
16692
16693 let javascript_language = Arc::new(Language::new(
16694 LanguageConfig {
16695 name: "JavaScript".into(),
16696 line_comments: vec!["// ".into()],
16697 ..Default::default()
16698 },
16699 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16700 ));
16701
16702 cx.language_registry().add(html_language.clone());
16703 cx.language_registry().add(javascript_language);
16704 cx.update_buffer(|buffer, cx| {
16705 buffer.set_language(Some(html_language), cx);
16706 });
16707
16708 // Toggle comments for empty selections
16709 cx.set_state(
16710 &r#"
16711 <p>A</p>ˇ
16712 <p>B</p>ˇ
16713 <p>C</p>ˇ
16714 "#
16715 .unindent(),
16716 );
16717 cx.update_editor(|editor, window, cx| {
16718 editor.toggle_comments(&ToggleComments::default(), window, cx)
16719 });
16720 cx.assert_editor_state(
16721 &r#"
16722 <!-- <p>A</p>ˇ -->
16723 <!-- <p>B</p>ˇ -->
16724 <!-- <p>C</p>ˇ -->
16725 "#
16726 .unindent(),
16727 );
16728 cx.update_editor(|editor, window, cx| {
16729 editor.toggle_comments(&ToggleComments::default(), window, cx)
16730 });
16731 cx.assert_editor_state(
16732 &r#"
16733 <p>A</p>ˇ
16734 <p>B</p>ˇ
16735 <p>C</p>ˇ
16736 "#
16737 .unindent(),
16738 );
16739
16740 // Toggle comments for mixture of empty and non-empty selections, where
16741 // multiple selections occupy a given line.
16742 cx.set_state(
16743 &r#"
16744 <p>A«</p>
16745 <p>ˇ»B</p>ˇ
16746 <p>C«</p>
16747 <p>ˇ»D</p>ˇ
16748 "#
16749 .unindent(),
16750 );
16751
16752 cx.update_editor(|editor, window, cx| {
16753 editor.toggle_comments(&ToggleComments::default(), window, cx)
16754 });
16755 cx.assert_editor_state(
16756 &r#"
16757 <!-- <p>A«</p>
16758 <p>ˇ»B</p>ˇ -->
16759 <!-- <p>C«</p>
16760 <p>ˇ»D</p>ˇ -->
16761 "#
16762 .unindent(),
16763 );
16764 cx.update_editor(|editor, window, cx| {
16765 editor.toggle_comments(&ToggleComments::default(), window, cx)
16766 });
16767 cx.assert_editor_state(
16768 &r#"
16769 <p>A«</p>
16770 <p>ˇ»B</p>ˇ
16771 <p>C«</p>
16772 <p>ˇ»D</p>ˇ
16773 "#
16774 .unindent(),
16775 );
16776
16777 // Toggle comments when different languages are active for different
16778 // selections.
16779 cx.set_state(
16780 &r#"
16781 ˇ<script>
16782 ˇvar x = new Y();
16783 ˇ</script>
16784 "#
16785 .unindent(),
16786 );
16787 cx.executor().run_until_parked();
16788 cx.update_editor(|editor, window, cx| {
16789 editor.toggle_comments(&ToggleComments::default(), window, cx)
16790 });
16791 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16792 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16793 cx.assert_editor_state(
16794 &r#"
16795 <!-- ˇ<script> -->
16796 // ˇvar x = new Y();
16797 <!-- ˇ</script> -->
16798 "#
16799 .unindent(),
16800 );
16801}
16802
16803#[gpui::test]
16804fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16805 init_test(cx, |_| {});
16806
16807 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16808 let multibuffer = cx.new(|cx| {
16809 let mut multibuffer = MultiBuffer::new(ReadWrite);
16810 multibuffer.push_excerpts(
16811 buffer.clone(),
16812 [
16813 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16814 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16815 ],
16816 cx,
16817 );
16818 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16819 multibuffer
16820 });
16821
16822 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16823 editor.update_in(cx, |editor, window, cx| {
16824 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16825 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16826 s.select_ranges([
16827 Point::new(0, 0)..Point::new(0, 0),
16828 Point::new(1, 0)..Point::new(1, 0),
16829 ])
16830 });
16831
16832 editor.handle_input("X", window, cx);
16833 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16834 assert_eq!(
16835 editor.selections.ranges(&editor.display_snapshot(cx)),
16836 [
16837 Point::new(0, 1)..Point::new(0, 1),
16838 Point::new(1, 1)..Point::new(1, 1),
16839 ]
16840 );
16841
16842 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16843 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16844 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16845 });
16846 editor.backspace(&Default::default(), window, cx);
16847 assert_eq!(editor.text(cx), "Xa\nbbb");
16848 assert_eq!(
16849 editor.selections.ranges(&editor.display_snapshot(cx)),
16850 [Point::new(1, 0)..Point::new(1, 0)]
16851 );
16852
16853 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16854 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16855 });
16856 editor.backspace(&Default::default(), window, cx);
16857 assert_eq!(editor.text(cx), "X\nbb");
16858 assert_eq!(
16859 editor.selections.ranges(&editor.display_snapshot(cx)),
16860 [Point::new(0, 1)..Point::new(0, 1)]
16861 );
16862 });
16863}
16864
16865#[gpui::test]
16866fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16867 init_test(cx, |_| {});
16868
16869 let markers = vec![('[', ']').into(), ('(', ')').into()];
16870 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16871 indoc! {"
16872 [aaaa
16873 (bbbb]
16874 cccc)",
16875 },
16876 markers.clone(),
16877 );
16878 let excerpt_ranges = markers.into_iter().map(|marker| {
16879 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16880 ExcerptRange::new(context)
16881 });
16882 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16883 let multibuffer = cx.new(|cx| {
16884 let mut multibuffer = MultiBuffer::new(ReadWrite);
16885 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16886 multibuffer
16887 });
16888
16889 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16890 editor.update_in(cx, |editor, window, cx| {
16891 let (expected_text, selection_ranges) = marked_text_ranges(
16892 indoc! {"
16893 aaaa
16894 bˇbbb
16895 bˇbbˇb
16896 cccc"
16897 },
16898 true,
16899 );
16900 assert_eq!(editor.text(cx), expected_text);
16901 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16902 s.select_ranges(
16903 selection_ranges
16904 .iter()
16905 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
16906 )
16907 });
16908
16909 editor.handle_input("X", window, cx);
16910
16911 let (expected_text, expected_selections) = marked_text_ranges(
16912 indoc! {"
16913 aaaa
16914 bXˇbbXb
16915 bXˇbbXˇb
16916 cccc"
16917 },
16918 false,
16919 );
16920 assert_eq!(editor.text(cx), expected_text);
16921 assert_eq!(
16922 editor.selections.ranges(&editor.display_snapshot(cx)),
16923 expected_selections
16924 .iter()
16925 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16926 .collect::<Vec<_>>()
16927 );
16928
16929 editor.newline(&Newline, window, cx);
16930 let (expected_text, expected_selections) = marked_text_ranges(
16931 indoc! {"
16932 aaaa
16933 bX
16934 ˇbbX
16935 b
16936 bX
16937 ˇbbX
16938 ˇb
16939 cccc"
16940 },
16941 false,
16942 );
16943 assert_eq!(editor.text(cx), expected_text);
16944 assert_eq!(
16945 editor.selections.ranges(&editor.display_snapshot(cx)),
16946 expected_selections
16947 .iter()
16948 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16949 .collect::<Vec<_>>()
16950 );
16951 });
16952}
16953
16954#[gpui::test]
16955fn test_refresh_selections(cx: &mut TestAppContext) {
16956 init_test(cx, |_| {});
16957
16958 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16959 let mut excerpt1_id = None;
16960 let multibuffer = cx.new(|cx| {
16961 let mut multibuffer = MultiBuffer::new(ReadWrite);
16962 excerpt1_id = multibuffer
16963 .push_excerpts(
16964 buffer.clone(),
16965 [
16966 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16967 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16968 ],
16969 cx,
16970 )
16971 .into_iter()
16972 .next();
16973 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16974 multibuffer
16975 });
16976
16977 let editor = cx.add_window(|window, cx| {
16978 let mut editor = build_editor(multibuffer.clone(), window, cx);
16979 let snapshot = editor.snapshot(window, cx);
16980 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16981 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16982 });
16983 editor.begin_selection(
16984 Point::new(2, 1).to_display_point(&snapshot),
16985 true,
16986 1,
16987 window,
16988 cx,
16989 );
16990 assert_eq!(
16991 editor.selections.ranges(&editor.display_snapshot(cx)),
16992 [
16993 Point::new(1, 3)..Point::new(1, 3),
16994 Point::new(2, 1)..Point::new(2, 1),
16995 ]
16996 );
16997 editor
16998 });
16999
17000 // Refreshing selections is a no-op when excerpts haven't changed.
17001 _ = editor.update(cx, |editor, window, cx| {
17002 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17003 assert_eq!(
17004 editor.selections.ranges(&editor.display_snapshot(cx)),
17005 [
17006 Point::new(1, 3)..Point::new(1, 3),
17007 Point::new(2, 1)..Point::new(2, 1),
17008 ]
17009 );
17010 });
17011
17012 multibuffer.update(cx, |multibuffer, cx| {
17013 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17014 });
17015 _ = editor.update(cx, |editor, window, cx| {
17016 // Removing an excerpt causes the first selection to become degenerate.
17017 assert_eq!(
17018 editor.selections.ranges(&editor.display_snapshot(cx)),
17019 [
17020 Point::new(0, 0)..Point::new(0, 0),
17021 Point::new(0, 1)..Point::new(0, 1)
17022 ]
17023 );
17024
17025 // Refreshing selections will relocate the first selection to the original buffer
17026 // location.
17027 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17028 assert_eq!(
17029 editor.selections.ranges(&editor.display_snapshot(cx)),
17030 [
17031 Point::new(0, 1)..Point::new(0, 1),
17032 Point::new(0, 3)..Point::new(0, 3)
17033 ]
17034 );
17035 assert!(editor.selections.pending_anchor().is_some());
17036 });
17037}
17038
17039#[gpui::test]
17040fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
17041 init_test(cx, |_| {});
17042
17043 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17044 let mut excerpt1_id = None;
17045 let multibuffer = cx.new(|cx| {
17046 let mut multibuffer = MultiBuffer::new(ReadWrite);
17047 excerpt1_id = multibuffer
17048 .push_excerpts(
17049 buffer.clone(),
17050 [
17051 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17052 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17053 ],
17054 cx,
17055 )
17056 .into_iter()
17057 .next();
17058 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17059 multibuffer
17060 });
17061
17062 let editor = cx.add_window(|window, cx| {
17063 let mut editor = build_editor(multibuffer.clone(), window, cx);
17064 let snapshot = editor.snapshot(window, cx);
17065 editor.begin_selection(
17066 Point::new(1, 3).to_display_point(&snapshot),
17067 false,
17068 1,
17069 window,
17070 cx,
17071 );
17072 assert_eq!(
17073 editor.selections.ranges(&editor.display_snapshot(cx)),
17074 [Point::new(1, 3)..Point::new(1, 3)]
17075 );
17076 editor
17077 });
17078
17079 multibuffer.update(cx, |multibuffer, cx| {
17080 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17081 });
17082 _ = editor.update(cx, |editor, window, cx| {
17083 assert_eq!(
17084 editor.selections.ranges(&editor.display_snapshot(cx)),
17085 [Point::new(0, 0)..Point::new(0, 0)]
17086 );
17087
17088 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
17089 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17090 assert_eq!(
17091 editor.selections.ranges(&editor.display_snapshot(cx)),
17092 [Point::new(0, 3)..Point::new(0, 3)]
17093 );
17094 assert!(editor.selections.pending_anchor().is_some());
17095 });
17096}
17097
17098#[gpui::test]
17099async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
17100 init_test(cx, |_| {});
17101
17102 let language = Arc::new(
17103 Language::new(
17104 LanguageConfig {
17105 brackets: BracketPairConfig {
17106 pairs: vec![
17107 BracketPair {
17108 start: "{".to_string(),
17109 end: "}".to_string(),
17110 close: true,
17111 surround: true,
17112 newline: true,
17113 },
17114 BracketPair {
17115 start: "/* ".to_string(),
17116 end: " */".to_string(),
17117 close: true,
17118 surround: true,
17119 newline: true,
17120 },
17121 ],
17122 ..Default::default()
17123 },
17124 ..Default::default()
17125 },
17126 Some(tree_sitter_rust::LANGUAGE.into()),
17127 )
17128 .with_indents_query("")
17129 .unwrap(),
17130 );
17131
17132 let text = concat!(
17133 "{ }\n", //
17134 " x\n", //
17135 " /* */\n", //
17136 "x\n", //
17137 "{{} }\n", //
17138 );
17139
17140 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17141 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17142 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17143 editor
17144 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
17145 .await;
17146
17147 editor.update_in(cx, |editor, window, cx| {
17148 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17149 s.select_display_ranges([
17150 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
17151 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
17152 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
17153 ])
17154 });
17155 editor.newline(&Newline, window, cx);
17156
17157 assert_eq!(
17158 editor.buffer().read(cx).read(cx).text(),
17159 concat!(
17160 "{ \n", // Suppress rustfmt
17161 "\n", //
17162 "}\n", //
17163 " x\n", //
17164 " /* \n", //
17165 " \n", //
17166 " */\n", //
17167 "x\n", //
17168 "{{} \n", //
17169 "}\n", //
17170 )
17171 );
17172 });
17173}
17174
17175#[gpui::test]
17176fn test_highlighted_ranges(cx: &mut TestAppContext) {
17177 init_test(cx, |_| {});
17178
17179 let editor = cx.add_window(|window, cx| {
17180 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
17181 build_editor(buffer, window, cx)
17182 });
17183
17184 _ = editor.update(cx, |editor, window, cx| {
17185 struct Type1;
17186 struct Type2;
17187
17188 let buffer = editor.buffer.read(cx).snapshot(cx);
17189
17190 let anchor_range =
17191 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
17192
17193 editor.highlight_background::<Type1>(
17194 &[
17195 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
17196 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
17197 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
17198 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
17199 ],
17200 |_, _| Hsla::red(),
17201 cx,
17202 );
17203 editor.highlight_background::<Type2>(
17204 &[
17205 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
17206 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
17207 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
17208 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
17209 ],
17210 |_, _| Hsla::green(),
17211 cx,
17212 );
17213
17214 let snapshot = editor.snapshot(window, cx);
17215 let highlighted_ranges = editor.sorted_background_highlights_in_range(
17216 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
17217 &snapshot,
17218 cx.theme(),
17219 );
17220 assert_eq!(
17221 highlighted_ranges,
17222 &[
17223 (
17224 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
17225 Hsla::green(),
17226 ),
17227 (
17228 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
17229 Hsla::red(),
17230 ),
17231 (
17232 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
17233 Hsla::green(),
17234 ),
17235 (
17236 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17237 Hsla::red(),
17238 ),
17239 ]
17240 );
17241 assert_eq!(
17242 editor.sorted_background_highlights_in_range(
17243 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
17244 &snapshot,
17245 cx.theme(),
17246 ),
17247 &[(
17248 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17249 Hsla::red(),
17250 )]
17251 );
17252 });
17253}
17254
17255#[gpui::test]
17256async fn test_following(cx: &mut TestAppContext) {
17257 init_test(cx, |_| {});
17258
17259 let fs = FakeFs::new(cx.executor());
17260 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17261
17262 let buffer = project.update(cx, |project, cx| {
17263 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
17264 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
17265 });
17266 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17267 let follower = cx.update(|cx| {
17268 cx.open_window(
17269 WindowOptions {
17270 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
17271 gpui::Point::new(px(0.), px(0.)),
17272 gpui::Point::new(px(10.), px(80.)),
17273 ))),
17274 ..Default::default()
17275 },
17276 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
17277 )
17278 .unwrap()
17279 });
17280
17281 let is_still_following = Rc::new(RefCell::new(true));
17282 let follower_edit_event_count = Rc::new(RefCell::new(0));
17283 let pending_update = Rc::new(RefCell::new(None));
17284 let leader_entity = leader.root(cx).unwrap();
17285 let follower_entity = follower.root(cx).unwrap();
17286 _ = follower.update(cx, {
17287 let update = pending_update.clone();
17288 let is_still_following = is_still_following.clone();
17289 let follower_edit_event_count = follower_edit_event_count.clone();
17290 |_, window, cx| {
17291 cx.subscribe_in(
17292 &leader_entity,
17293 window,
17294 move |_, leader, event, window, cx| {
17295 leader.read(cx).add_event_to_update_proto(
17296 event,
17297 &mut update.borrow_mut(),
17298 window,
17299 cx,
17300 );
17301 },
17302 )
17303 .detach();
17304
17305 cx.subscribe_in(
17306 &follower_entity,
17307 window,
17308 move |_, _, event: &EditorEvent, _window, _cx| {
17309 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
17310 *is_still_following.borrow_mut() = false;
17311 }
17312
17313 if let EditorEvent::BufferEdited = event {
17314 *follower_edit_event_count.borrow_mut() += 1;
17315 }
17316 },
17317 )
17318 .detach();
17319 }
17320 });
17321
17322 // Update the selections only
17323 _ = leader.update(cx, |leader, window, cx| {
17324 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17325 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17326 });
17327 });
17328 follower
17329 .update(cx, |follower, window, cx| {
17330 follower.apply_update_proto(
17331 &project,
17332 pending_update.borrow_mut().take().unwrap(),
17333 window,
17334 cx,
17335 )
17336 })
17337 .unwrap()
17338 .await
17339 .unwrap();
17340 _ = follower.update(cx, |follower, _, cx| {
17341 assert_eq!(
17342 follower.selections.ranges(&follower.display_snapshot(cx)),
17343 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
17344 );
17345 });
17346 assert!(*is_still_following.borrow());
17347 assert_eq!(*follower_edit_event_count.borrow(), 0);
17348
17349 // Update the scroll position only
17350 _ = leader.update(cx, |leader, window, cx| {
17351 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17352 });
17353 follower
17354 .update(cx, |follower, window, cx| {
17355 follower.apply_update_proto(
17356 &project,
17357 pending_update.borrow_mut().take().unwrap(),
17358 window,
17359 cx,
17360 )
17361 })
17362 .unwrap()
17363 .await
17364 .unwrap();
17365 assert_eq!(
17366 follower
17367 .update(cx, |follower, _, cx| follower.scroll_position(cx))
17368 .unwrap(),
17369 gpui::Point::new(1.5, 3.5)
17370 );
17371 assert!(*is_still_following.borrow());
17372 assert_eq!(*follower_edit_event_count.borrow(), 0);
17373
17374 // Update the selections and scroll position. The follower's scroll position is updated
17375 // via autoscroll, not via the leader's exact scroll position.
17376 _ = leader.update(cx, |leader, window, cx| {
17377 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17378 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
17379 });
17380 leader.request_autoscroll(Autoscroll::newest(), cx);
17381 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17382 });
17383 follower
17384 .update(cx, |follower, window, cx| {
17385 follower.apply_update_proto(
17386 &project,
17387 pending_update.borrow_mut().take().unwrap(),
17388 window,
17389 cx,
17390 )
17391 })
17392 .unwrap()
17393 .await
17394 .unwrap();
17395 _ = follower.update(cx, |follower, _, cx| {
17396 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17397 assert_eq!(
17398 follower.selections.ranges(&follower.display_snapshot(cx)),
17399 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17400 );
17401 });
17402 assert!(*is_still_following.borrow());
17403
17404 // Creating a pending selection that precedes another selection
17405 _ = leader.update(cx, |leader, window, cx| {
17406 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17407 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17408 });
17409 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17410 });
17411 follower
17412 .update(cx, |follower, window, cx| {
17413 follower.apply_update_proto(
17414 &project,
17415 pending_update.borrow_mut().take().unwrap(),
17416 window,
17417 cx,
17418 )
17419 })
17420 .unwrap()
17421 .await
17422 .unwrap();
17423 _ = follower.update(cx, |follower, _, cx| {
17424 assert_eq!(
17425 follower.selections.ranges(&follower.display_snapshot(cx)),
17426 vec![
17427 MultiBufferOffset(0)..MultiBufferOffset(0),
17428 MultiBufferOffset(1)..MultiBufferOffset(1)
17429 ]
17430 );
17431 });
17432 assert!(*is_still_following.borrow());
17433
17434 // Extend the pending selection so that it surrounds another selection
17435 _ = leader.update(cx, |leader, window, cx| {
17436 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17437 });
17438 follower
17439 .update(cx, |follower, window, cx| {
17440 follower.apply_update_proto(
17441 &project,
17442 pending_update.borrow_mut().take().unwrap(),
17443 window,
17444 cx,
17445 )
17446 })
17447 .unwrap()
17448 .await
17449 .unwrap();
17450 _ = follower.update(cx, |follower, _, cx| {
17451 assert_eq!(
17452 follower.selections.ranges(&follower.display_snapshot(cx)),
17453 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17454 );
17455 });
17456
17457 // Scrolling locally breaks the follow
17458 _ = follower.update(cx, |follower, window, cx| {
17459 let top_anchor = follower
17460 .buffer()
17461 .read(cx)
17462 .read(cx)
17463 .anchor_after(MultiBufferOffset(0));
17464 follower.set_scroll_anchor(
17465 ScrollAnchor {
17466 anchor: top_anchor,
17467 offset: gpui::Point::new(0.0, 0.5),
17468 },
17469 window,
17470 cx,
17471 );
17472 });
17473 assert!(!(*is_still_following.borrow()));
17474}
17475
17476#[gpui::test]
17477async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17478 init_test(cx, |_| {});
17479
17480 let fs = FakeFs::new(cx.executor());
17481 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17482 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17483 let pane = workspace
17484 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17485 .unwrap();
17486
17487 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17488
17489 let leader = pane.update_in(cx, |_, window, cx| {
17490 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17491 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17492 });
17493
17494 // Start following the editor when it has no excerpts.
17495 let mut state_message =
17496 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17497 let workspace_entity = workspace.root(cx).unwrap();
17498 let follower_1 = cx
17499 .update_window(*workspace.deref(), |_, window, cx| {
17500 Editor::from_state_proto(
17501 workspace_entity,
17502 ViewId {
17503 creator: CollaboratorId::PeerId(PeerId::default()),
17504 id: 0,
17505 },
17506 &mut state_message,
17507 window,
17508 cx,
17509 )
17510 })
17511 .unwrap()
17512 .unwrap()
17513 .await
17514 .unwrap();
17515
17516 let update_message = Rc::new(RefCell::new(None));
17517 follower_1.update_in(cx, {
17518 let update = update_message.clone();
17519 |_, window, cx| {
17520 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17521 leader.read(cx).add_event_to_update_proto(
17522 event,
17523 &mut update.borrow_mut(),
17524 window,
17525 cx,
17526 );
17527 })
17528 .detach();
17529 }
17530 });
17531
17532 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17533 (
17534 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17535 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17536 )
17537 });
17538
17539 // Insert some excerpts.
17540 leader.update(cx, |leader, cx| {
17541 leader.buffer.update(cx, |multibuffer, cx| {
17542 multibuffer.set_excerpts_for_path(
17543 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17544 buffer_1.clone(),
17545 vec![
17546 Point::row_range(0..3),
17547 Point::row_range(1..6),
17548 Point::row_range(12..15),
17549 ],
17550 0,
17551 cx,
17552 );
17553 multibuffer.set_excerpts_for_path(
17554 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17555 buffer_2.clone(),
17556 vec![Point::row_range(0..6), Point::row_range(8..12)],
17557 0,
17558 cx,
17559 );
17560 });
17561 });
17562
17563 // Apply the update of adding the excerpts.
17564 follower_1
17565 .update_in(cx, |follower, window, cx| {
17566 follower.apply_update_proto(
17567 &project,
17568 update_message.borrow().clone().unwrap(),
17569 window,
17570 cx,
17571 )
17572 })
17573 .await
17574 .unwrap();
17575 assert_eq!(
17576 follower_1.update(cx, |editor, cx| editor.text(cx)),
17577 leader.update(cx, |editor, cx| editor.text(cx))
17578 );
17579 update_message.borrow_mut().take();
17580
17581 // Start following separately after it already has excerpts.
17582 let mut state_message =
17583 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17584 let workspace_entity = workspace.root(cx).unwrap();
17585 let follower_2 = cx
17586 .update_window(*workspace.deref(), |_, window, cx| {
17587 Editor::from_state_proto(
17588 workspace_entity,
17589 ViewId {
17590 creator: CollaboratorId::PeerId(PeerId::default()),
17591 id: 0,
17592 },
17593 &mut state_message,
17594 window,
17595 cx,
17596 )
17597 })
17598 .unwrap()
17599 .unwrap()
17600 .await
17601 .unwrap();
17602 assert_eq!(
17603 follower_2.update(cx, |editor, cx| editor.text(cx)),
17604 leader.update(cx, |editor, cx| editor.text(cx))
17605 );
17606
17607 // Remove some excerpts.
17608 leader.update(cx, |leader, cx| {
17609 leader.buffer.update(cx, |multibuffer, cx| {
17610 let excerpt_ids = multibuffer.excerpt_ids();
17611 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17612 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17613 });
17614 });
17615
17616 // Apply the update of removing the excerpts.
17617 follower_1
17618 .update_in(cx, |follower, window, cx| {
17619 follower.apply_update_proto(
17620 &project,
17621 update_message.borrow().clone().unwrap(),
17622 window,
17623 cx,
17624 )
17625 })
17626 .await
17627 .unwrap();
17628 follower_2
17629 .update_in(cx, |follower, window, cx| {
17630 follower.apply_update_proto(
17631 &project,
17632 update_message.borrow().clone().unwrap(),
17633 window,
17634 cx,
17635 )
17636 })
17637 .await
17638 .unwrap();
17639 update_message.borrow_mut().take();
17640 assert_eq!(
17641 follower_1.update(cx, |editor, cx| editor.text(cx)),
17642 leader.update(cx, |editor, cx| editor.text(cx))
17643 );
17644}
17645
17646#[gpui::test]
17647async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17648 init_test(cx, |_| {});
17649
17650 let mut cx = EditorTestContext::new(cx).await;
17651 let lsp_store =
17652 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17653
17654 cx.set_state(indoc! {"
17655 ˇfn func(abc def: i32) -> u32 {
17656 }
17657 "});
17658
17659 cx.update(|_, cx| {
17660 lsp_store.update(cx, |lsp_store, cx| {
17661 lsp_store
17662 .update_diagnostics(
17663 LanguageServerId(0),
17664 lsp::PublishDiagnosticsParams {
17665 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17666 version: None,
17667 diagnostics: vec![
17668 lsp::Diagnostic {
17669 range: lsp::Range::new(
17670 lsp::Position::new(0, 11),
17671 lsp::Position::new(0, 12),
17672 ),
17673 severity: Some(lsp::DiagnosticSeverity::ERROR),
17674 ..Default::default()
17675 },
17676 lsp::Diagnostic {
17677 range: lsp::Range::new(
17678 lsp::Position::new(0, 12),
17679 lsp::Position::new(0, 15),
17680 ),
17681 severity: Some(lsp::DiagnosticSeverity::ERROR),
17682 ..Default::default()
17683 },
17684 lsp::Diagnostic {
17685 range: lsp::Range::new(
17686 lsp::Position::new(0, 25),
17687 lsp::Position::new(0, 28),
17688 ),
17689 severity: Some(lsp::DiagnosticSeverity::ERROR),
17690 ..Default::default()
17691 },
17692 ],
17693 },
17694 None,
17695 DiagnosticSourceKind::Pushed,
17696 &[],
17697 cx,
17698 )
17699 .unwrap()
17700 });
17701 });
17702
17703 executor.run_until_parked();
17704
17705 cx.update_editor(|editor, window, cx| {
17706 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17707 });
17708
17709 cx.assert_editor_state(indoc! {"
17710 fn func(abc def: i32) -> ˇu32 {
17711 }
17712 "});
17713
17714 cx.update_editor(|editor, window, cx| {
17715 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17716 });
17717
17718 cx.assert_editor_state(indoc! {"
17719 fn func(abc ˇdef: i32) -> u32 {
17720 }
17721 "});
17722
17723 cx.update_editor(|editor, window, cx| {
17724 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17725 });
17726
17727 cx.assert_editor_state(indoc! {"
17728 fn func(abcˇ def: i32) -> u32 {
17729 }
17730 "});
17731
17732 cx.update_editor(|editor, window, cx| {
17733 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17734 });
17735
17736 cx.assert_editor_state(indoc! {"
17737 fn func(abc def: i32) -> ˇu32 {
17738 }
17739 "});
17740}
17741
17742#[gpui::test]
17743async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17744 init_test(cx, |_| {});
17745
17746 let mut cx = EditorTestContext::new(cx).await;
17747
17748 let diff_base = r#"
17749 use some::mod;
17750
17751 const A: u32 = 42;
17752
17753 fn main() {
17754 println!("hello");
17755
17756 println!("world");
17757 }
17758 "#
17759 .unindent();
17760
17761 // Edits are modified, removed, modified, added
17762 cx.set_state(
17763 &r#"
17764 use some::modified;
17765
17766 ˇ
17767 fn main() {
17768 println!("hello there");
17769
17770 println!("around the");
17771 println!("world");
17772 }
17773 "#
17774 .unindent(),
17775 );
17776
17777 cx.set_head_text(&diff_base);
17778 executor.run_until_parked();
17779
17780 cx.update_editor(|editor, window, cx| {
17781 //Wrap around the bottom of the buffer
17782 for _ in 0..3 {
17783 editor.go_to_next_hunk(&GoToHunk, window, cx);
17784 }
17785 });
17786
17787 cx.assert_editor_state(
17788 &r#"
17789 ˇuse some::modified;
17790
17791
17792 fn main() {
17793 println!("hello there");
17794
17795 println!("around the");
17796 println!("world");
17797 }
17798 "#
17799 .unindent(),
17800 );
17801
17802 cx.update_editor(|editor, window, cx| {
17803 //Wrap around the top of the buffer
17804 for _ in 0..2 {
17805 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17806 }
17807 });
17808
17809 cx.assert_editor_state(
17810 &r#"
17811 use some::modified;
17812
17813
17814 fn main() {
17815 ˇ println!("hello there");
17816
17817 println!("around the");
17818 println!("world");
17819 }
17820 "#
17821 .unindent(),
17822 );
17823
17824 cx.update_editor(|editor, window, cx| {
17825 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17826 });
17827
17828 cx.assert_editor_state(
17829 &r#"
17830 use some::modified;
17831
17832 ˇ
17833 fn main() {
17834 println!("hello there");
17835
17836 println!("around the");
17837 println!("world");
17838 }
17839 "#
17840 .unindent(),
17841 );
17842
17843 cx.update_editor(|editor, window, cx| {
17844 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17845 });
17846
17847 cx.assert_editor_state(
17848 &r#"
17849 ˇuse some::modified;
17850
17851
17852 fn main() {
17853 println!("hello there");
17854
17855 println!("around the");
17856 println!("world");
17857 }
17858 "#
17859 .unindent(),
17860 );
17861
17862 cx.update_editor(|editor, window, cx| {
17863 for _ in 0..2 {
17864 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17865 }
17866 });
17867
17868 cx.assert_editor_state(
17869 &r#"
17870 use some::modified;
17871
17872
17873 fn main() {
17874 ˇ println!("hello there");
17875
17876 println!("around the");
17877 println!("world");
17878 }
17879 "#
17880 .unindent(),
17881 );
17882
17883 cx.update_editor(|editor, window, cx| {
17884 editor.fold(&Fold, window, cx);
17885 });
17886
17887 cx.update_editor(|editor, window, cx| {
17888 editor.go_to_next_hunk(&GoToHunk, window, cx);
17889 });
17890
17891 cx.assert_editor_state(
17892 &r#"
17893 ˇuse some::modified;
17894
17895
17896 fn main() {
17897 println!("hello there");
17898
17899 println!("around the");
17900 println!("world");
17901 }
17902 "#
17903 .unindent(),
17904 );
17905}
17906
17907#[test]
17908fn test_split_words() {
17909 fn split(text: &str) -> Vec<&str> {
17910 split_words(text).collect()
17911 }
17912
17913 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17914 assert_eq!(split("hello_world"), &["hello_", "world"]);
17915 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17916 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17917 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17918 assert_eq!(split("helloworld"), &["helloworld"]);
17919
17920 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17921}
17922
17923#[test]
17924fn test_split_words_for_snippet_prefix() {
17925 fn split(text: &str) -> Vec<&str> {
17926 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
17927 }
17928
17929 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
17930 assert_eq!(split("hello_world"), &["hello_world"]);
17931 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
17932 assert_eq!(split("Hello_World"), &["Hello_World"]);
17933 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
17934 assert_eq!(split("helloworld"), &["helloworld"]);
17935 assert_eq!(
17936 split("this@is!@#$^many . symbols"),
17937 &[
17938 "symbols",
17939 " symbols",
17940 ". symbols",
17941 " . symbols",
17942 " . symbols",
17943 " . symbols",
17944 "many . symbols",
17945 "^many . symbols",
17946 "$^many . symbols",
17947 "#$^many . symbols",
17948 "@#$^many . symbols",
17949 "!@#$^many . symbols",
17950 "is!@#$^many . symbols",
17951 "@is!@#$^many . symbols",
17952 "this@is!@#$^many . symbols",
17953 ],
17954 );
17955 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
17956}
17957
17958#[gpui::test]
17959async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17960 init_test(cx, |_| {});
17961
17962 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17963
17964 #[track_caller]
17965 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
17966 let _state_context = cx.set_state(before);
17967 cx.run_until_parked();
17968 cx.update_editor(|editor, window, cx| {
17969 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17970 });
17971 cx.run_until_parked();
17972 cx.assert_editor_state(after);
17973 }
17974
17975 // Outside bracket jumps to outside of matching bracket
17976 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
17977 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
17978
17979 // Inside bracket jumps to inside of matching bracket
17980 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
17981 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
17982
17983 // When outside a bracket and inside, favor jumping to the inside bracket
17984 assert(
17985 "console.log('foo', [1, 2, 3]ˇ);",
17986 "console.log('foo', ˇ[1, 2, 3]);",
17987 &mut cx,
17988 );
17989 assert(
17990 "console.log(ˇ'foo', [1, 2, 3]);",
17991 "console.log('foo'ˇ, [1, 2, 3]);",
17992 &mut cx,
17993 );
17994
17995 // Bias forward if two options are equally likely
17996 assert(
17997 "let result = curried_fun()ˇ();",
17998 "let result = curried_fun()()ˇ;",
17999 &mut cx,
18000 );
18001
18002 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
18003 assert(
18004 indoc! {"
18005 function test() {
18006 console.log('test')ˇ
18007 }"},
18008 indoc! {"
18009 function test() {
18010 console.logˇ('test')
18011 }"},
18012 &mut cx,
18013 );
18014}
18015
18016#[gpui::test]
18017async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
18018 init_test(cx, |_| {});
18019 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
18020 language_registry.add(markdown_lang());
18021 language_registry.add(rust_lang());
18022 let buffer = cx.new(|cx| {
18023 let mut buffer = language::Buffer::local(
18024 indoc! {"
18025 ```rs
18026 impl Worktree {
18027 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18028 }
18029 }
18030 ```
18031 "},
18032 cx,
18033 );
18034 buffer.set_language_registry(language_registry.clone());
18035 buffer.set_language(Some(markdown_lang()), cx);
18036 buffer
18037 });
18038 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18039 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
18040 cx.executor().run_until_parked();
18041 _ = editor.update(cx, |editor, window, cx| {
18042 // Case 1: Test outer enclosing brackets
18043 select_ranges(
18044 editor,
18045 &indoc! {"
18046 ```rs
18047 impl Worktree {
18048 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18049 }
18050 }ˇ
18051 ```
18052 "},
18053 window,
18054 cx,
18055 );
18056 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18057 assert_text_with_selections(
18058 editor,
18059 &indoc! {"
18060 ```rs
18061 impl Worktree ˇ{
18062 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18063 }
18064 }
18065 ```
18066 "},
18067 cx,
18068 );
18069 // Case 2: Test inner enclosing brackets
18070 select_ranges(
18071 editor,
18072 &indoc! {"
18073 ```rs
18074 impl Worktree {
18075 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18076 }ˇ
18077 }
18078 ```
18079 "},
18080 window,
18081 cx,
18082 );
18083 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18084 assert_text_with_selections(
18085 editor,
18086 &indoc! {"
18087 ```rs
18088 impl Worktree {
18089 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
18090 }
18091 }
18092 ```
18093 "},
18094 cx,
18095 );
18096 });
18097}
18098
18099#[gpui::test]
18100async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
18101 init_test(cx, |_| {});
18102
18103 let fs = FakeFs::new(cx.executor());
18104 fs.insert_tree(
18105 path!("/a"),
18106 json!({
18107 "main.rs": "fn main() { let a = 5; }",
18108 "other.rs": "// Test file",
18109 }),
18110 )
18111 .await;
18112 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18113
18114 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18115 language_registry.add(Arc::new(Language::new(
18116 LanguageConfig {
18117 name: "Rust".into(),
18118 matcher: LanguageMatcher {
18119 path_suffixes: vec!["rs".to_string()],
18120 ..Default::default()
18121 },
18122 brackets: BracketPairConfig {
18123 pairs: vec![BracketPair {
18124 start: "{".to_string(),
18125 end: "}".to_string(),
18126 close: true,
18127 surround: true,
18128 newline: true,
18129 }],
18130 disabled_scopes_by_bracket_ix: Vec::new(),
18131 },
18132 ..Default::default()
18133 },
18134 Some(tree_sitter_rust::LANGUAGE.into()),
18135 )));
18136 let mut fake_servers = language_registry.register_fake_lsp(
18137 "Rust",
18138 FakeLspAdapter {
18139 capabilities: lsp::ServerCapabilities {
18140 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18141 first_trigger_character: "{".to_string(),
18142 more_trigger_character: None,
18143 }),
18144 ..Default::default()
18145 },
18146 ..Default::default()
18147 },
18148 );
18149
18150 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18151
18152 let cx = &mut VisualTestContext::from_window(*workspace, cx);
18153
18154 let worktree_id = workspace
18155 .update(cx, |workspace, _, cx| {
18156 workspace.project().update(cx, |project, cx| {
18157 project.worktrees(cx).next().unwrap().read(cx).id()
18158 })
18159 })
18160 .unwrap();
18161
18162 let buffer = project
18163 .update(cx, |project, cx| {
18164 project.open_local_buffer(path!("/a/main.rs"), cx)
18165 })
18166 .await
18167 .unwrap();
18168 let editor_handle = workspace
18169 .update(cx, |workspace, window, cx| {
18170 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
18171 })
18172 .unwrap()
18173 .await
18174 .unwrap()
18175 .downcast::<Editor>()
18176 .unwrap();
18177
18178 cx.executor().start_waiting();
18179 let fake_server = fake_servers.next().await.unwrap();
18180
18181 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
18182 |params, _| async move {
18183 assert_eq!(
18184 params.text_document_position.text_document.uri,
18185 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
18186 );
18187 assert_eq!(
18188 params.text_document_position.position,
18189 lsp::Position::new(0, 21),
18190 );
18191
18192 Ok(Some(vec![lsp::TextEdit {
18193 new_text: "]".to_string(),
18194 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18195 }]))
18196 },
18197 );
18198
18199 editor_handle.update_in(cx, |editor, window, cx| {
18200 window.focus(&editor.focus_handle(cx), cx);
18201 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18202 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
18203 });
18204 editor.handle_input("{", window, cx);
18205 });
18206
18207 cx.executor().run_until_parked();
18208
18209 buffer.update(cx, |buffer, _| {
18210 assert_eq!(
18211 buffer.text(),
18212 "fn main() { let a = {5}; }",
18213 "No extra braces from on type formatting should appear in the buffer"
18214 )
18215 });
18216}
18217
18218#[gpui::test(iterations = 20, seeds(31))]
18219async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
18220 init_test(cx, |_| {});
18221
18222 let mut cx = EditorLspTestContext::new_rust(
18223 lsp::ServerCapabilities {
18224 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18225 first_trigger_character: ".".to_string(),
18226 more_trigger_character: None,
18227 }),
18228 ..Default::default()
18229 },
18230 cx,
18231 )
18232 .await;
18233
18234 cx.update_buffer(|buffer, _| {
18235 // This causes autoindent to be async.
18236 buffer.set_sync_parse_timeout(None)
18237 });
18238
18239 cx.set_state("fn c() {\n d()ˇ\n}\n");
18240 cx.simulate_keystroke("\n");
18241 cx.run_until_parked();
18242
18243 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
18244 let mut request =
18245 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
18246 let buffer_cloned = buffer_cloned.clone();
18247 async move {
18248 buffer_cloned.update(&mut cx, |buffer, _| {
18249 assert_eq!(
18250 buffer.text(),
18251 "fn c() {\n d()\n .\n}\n",
18252 "OnTypeFormatting should triggered after autoindent applied"
18253 )
18254 })?;
18255
18256 Ok(Some(vec![]))
18257 }
18258 });
18259
18260 cx.simulate_keystroke(".");
18261 cx.run_until_parked();
18262
18263 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
18264 assert!(request.next().await.is_some());
18265 request.close();
18266 assert!(request.next().await.is_none());
18267}
18268
18269#[gpui::test]
18270async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
18271 init_test(cx, |_| {});
18272
18273 let fs = FakeFs::new(cx.executor());
18274 fs.insert_tree(
18275 path!("/a"),
18276 json!({
18277 "main.rs": "fn main() { let a = 5; }",
18278 "other.rs": "// Test file",
18279 }),
18280 )
18281 .await;
18282
18283 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18284
18285 let server_restarts = Arc::new(AtomicUsize::new(0));
18286 let closure_restarts = Arc::clone(&server_restarts);
18287 let language_server_name = "test language server";
18288 let language_name: LanguageName = "Rust".into();
18289
18290 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18291 language_registry.add(Arc::new(Language::new(
18292 LanguageConfig {
18293 name: language_name.clone(),
18294 matcher: LanguageMatcher {
18295 path_suffixes: vec!["rs".to_string()],
18296 ..Default::default()
18297 },
18298 ..Default::default()
18299 },
18300 Some(tree_sitter_rust::LANGUAGE.into()),
18301 )));
18302 let mut fake_servers = language_registry.register_fake_lsp(
18303 "Rust",
18304 FakeLspAdapter {
18305 name: language_server_name,
18306 initialization_options: Some(json!({
18307 "testOptionValue": true
18308 })),
18309 initializer: Some(Box::new(move |fake_server| {
18310 let task_restarts = Arc::clone(&closure_restarts);
18311 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
18312 task_restarts.fetch_add(1, atomic::Ordering::Release);
18313 futures::future::ready(Ok(()))
18314 });
18315 })),
18316 ..Default::default()
18317 },
18318 );
18319
18320 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18321 let _buffer = project
18322 .update(cx, |project, cx| {
18323 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
18324 })
18325 .await
18326 .unwrap();
18327 let _fake_server = fake_servers.next().await.unwrap();
18328 update_test_language_settings(cx, |language_settings| {
18329 language_settings.languages.0.insert(
18330 language_name.clone().0,
18331 LanguageSettingsContent {
18332 tab_size: NonZeroU32::new(8),
18333 ..Default::default()
18334 },
18335 );
18336 });
18337 cx.executor().run_until_parked();
18338 assert_eq!(
18339 server_restarts.load(atomic::Ordering::Acquire),
18340 0,
18341 "Should not restart LSP server on an unrelated change"
18342 );
18343
18344 update_test_project_settings(cx, |project_settings| {
18345 project_settings.lsp.0.insert(
18346 "Some other server name".into(),
18347 LspSettings {
18348 binary: None,
18349 settings: None,
18350 initialization_options: Some(json!({
18351 "some other init value": false
18352 })),
18353 enable_lsp_tasks: false,
18354 fetch: None,
18355 },
18356 );
18357 });
18358 cx.executor().run_until_parked();
18359 assert_eq!(
18360 server_restarts.load(atomic::Ordering::Acquire),
18361 0,
18362 "Should not restart LSP server on an unrelated LSP settings change"
18363 );
18364
18365 update_test_project_settings(cx, |project_settings| {
18366 project_settings.lsp.0.insert(
18367 language_server_name.into(),
18368 LspSettings {
18369 binary: None,
18370 settings: None,
18371 initialization_options: Some(json!({
18372 "anotherInitValue": false
18373 })),
18374 enable_lsp_tasks: false,
18375 fetch: None,
18376 },
18377 );
18378 });
18379 cx.executor().run_until_parked();
18380 assert_eq!(
18381 server_restarts.load(atomic::Ordering::Acquire),
18382 1,
18383 "Should restart LSP server on a related LSP settings change"
18384 );
18385
18386 update_test_project_settings(cx, |project_settings| {
18387 project_settings.lsp.0.insert(
18388 language_server_name.into(),
18389 LspSettings {
18390 binary: None,
18391 settings: None,
18392 initialization_options: Some(json!({
18393 "anotherInitValue": false
18394 })),
18395 enable_lsp_tasks: false,
18396 fetch: None,
18397 },
18398 );
18399 });
18400 cx.executor().run_until_parked();
18401 assert_eq!(
18402 server_restarts.load(atomic::Ordering::Acquire),
18403 1,
18404 "Should not restart LSP server on a related LSP settings change that is the same"
18405 );
18406
18407 update_test_project_settings(cx, |project_settings| {
18408 project_settings.lsp.0.insert(
18409 language_server_name.into(),
18410 LspSettings {
18411 binary: None,
18412 settings: None,
18413 initialization_options: None,
18414 enable_lsp_tasks: false,
18415 fetch: None,
18416 },
18417 );
18418 });
18419 cx.executor().run_until_parked();
18420 assert_eq!(
18421 server_restarts.load(atomic::Ordering::Acquire),
18422 2,
18423 "Should restart LSP server on another related LSP settings change"
18424 );
18425}
18426
18427#[gpui::test]
18428async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18429 init_test(cx, |_| {});
18430
18431 let mut cx = EditorLspTestContext::new_rust(
18432 lsp::ServerCapabilities {
18433 completion_provider: Some(lsp::CompletionOptions {
18434 trigger_characters: Some(vec![".".to_string()]),
18435 resolve_provider: Some(true),
18436 ..Default::default()
18437 }),
18438 ..Default::default()
18439 },
18440 cx,
18441 )
18442 .await;
18443
18444 cx.set_state("fn main() { let a = 2ˇ; }");
18445 cx.simulate_keystroke(".");
18446 let completion_item = lsp::CompletionItem {
18447 label: "some".into(),
18448 kind: Some(lsp::CompletionItemKind::SNIPPET),
18449 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18450 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18451 kind: lsp::MarkupKind::Markdown,
18452 value: "```rust\nSome(2)\n```".to_string(),
18453 })),
18454 deprecated: Some(false),
18455 sort_text: Some("fffffff2".to_string()),
18456 filter_text: Some("some".to_string()),
18457 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18458 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18459 range: lsp::Range {
18460 start: lsp::Position {
18461 line: 0,
18462 character: 22,
18463 },
18464 end: lsp::Position {
18465 line: 0,
18466 character: 22,
18467 },
18468 },
18469 new_text: "Some(2)".to_string(),
18470 })),
18471 additional_text_edits: Some(vec![lsp::TextEdit {
18472 range: lsp::Range {
18473 start: lsp::Position {
18474 line: 0,
18475 character: 20,
18476 },
18477 end: lsp::Position {
18478 line: 0,
18479 character: 22,
18480 },
18481 },
18482 new_text: "".to_string(),
18483 }]),
18484 ..Default::default()
18485 };
18486
18487 let closure_completion_item = completion_item.clone();
18488 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18489 let task_completion_item = closure_completion_item.clone();
18490 async move {
18491 Ok(Some(lsp::CompletionResponse::Array(vec![
18492 task_completion_item,
18493 ])))
18494 }
18495 });
18496
18497 request.next().await;
18498
18499 cx.condition(|editor, _| editor.context_menu_visible())
18500 .await;
18501 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18502 editor
18503 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18504 .unwrap()
18505 });
18506 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18507
18508 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18509 let task_completion_item = completion_item.clone();
18510 async move { Ok(task_completion_item) }
18511 })
18512 .next()
18513 .await
18514 .unwrap();
18515 apply_additional_edits.await.unwrap();
18516 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18517}
18518
18519#[gpui::test]
18520async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18521 init_test(cx, |_| {});
18522
18523 let mut cx = EditorLspTestContext::new_rust(
18524 lsp::ServerCapabilities {
18525 completion_provider: Some(lsp::CompletionOptions {
18526 trigger_characters: Some(vec![".".to_string()]),
18527 resolve_provider: Some(true),
18528 ..Default::default()
18529 }),
18530 ..Default::default()
18531 },
18532 cx,
18533 )
18534 .await;
18535
18536 cx.set_state("fn main() { let a = 2ˇ; }");
18537 cx.simulate_keystroke(".");
18538
18539 let item1 = lsp::CompletionItem {
18540 label: "method id()".to_string(),
18541 filter_text: Some("id".to_string()),
18542 detail: None,
18543 documentation: None,
18544 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18545 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18546 new_text: ".id".to_string(),
18547 })),
18548 ..lsp::CompletionItem::default()
18549 };
18550
18551 let item2 = lsp::CompletionItem {
18552 label: "other".to_string(),
18553 filter_text: Some("other".to_string()),
18554 detail: None,
18555 documentation: None,
18556 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18557 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18558 new_text: ".other".to_string(),
18559 })),
18560 ..lsp::CompletionItem::default()
18561 };
18562
18563 let item1 = item1.clone();
18564 cx.set_request_handler::<lsp::request::Completion, _, _>({
18565 let item1 = item1.clone();
18566 move |_, _, _| {
18567 let item1 = item1.clone();
18568 let item2 = item2.clone();
18569 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
18570 }
18571 })
18572 .next()
18573 .await;
18574
18575 cx.condition(|editor, _| editor.context_menu_visible())
18576 .await;
18577 cx.update_editor(|editor, _, _| {
18578 let context_menu = editor.context_menu.borrow_mut();
18579 let context_menu = context_menu
18580 .as_ref()
18581 .expect("Should have the context menu deployed");
18582 match context_menu {
18583 CodeContextMenu::Completions(completions_menu) => {
18584 let completions = completions_menu.completions.borrow_mut();
18585 assert_eq!(
18586 completions
18587 .iter()
18588 .map(|completion| &completion.label.text)
18589 .collect::<Vec<_>>(),
18590 vec!["method id()", "other"]
18591 )
18592 }
18593 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18594 }
18595 });
18596
18597 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
18598 let item1 = item1.clone();
18599 move |_, item_to_resolve, _| {
18600 let item1 = item1.clone();
18601 async move {
18602 if item1 == item_to_resolve {
18603 Ok(lsp::CompletionItem {
18604 label: "method id()".to_string(),
18605 filter_text: Some("id".to_string()),
18606 detail: Some("Now resolved!".to_string()),
18607 documentation: Some(lsp::Documentation::String("Docs".to_string())),
18608 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18609 range: lsp::Range::new(
18610 lsp::Position::new(0, 22),
18611 lsp::Position::new(0, 22),
18612 ),
18613 new_text: ".id".to_string(),
18614 })),
18615 ..lsp::CompletionItem::default()
18616 })
18617 } else {
18618 Ok(item_to_resolve)
18619 }
18620 }
18621 }
18622 })
18623 .next()
18624 .await
18625 .unwrap();
18626 cx.run_until_parked();
18627
18628 cx.update_editor(|editor, window, cx| {
18629 editor.context_menu_next(&Default::default(), window, cx);
18630 });
18631
18632 cx.update_editor(|editor, _, _| {
18633 let context_menu = editor.context_menu.borrow_mut();
18634 let context_menu = context_menu
18635 .as_ref()
18636 .expect("Should have the context menu deployed");
18637 match context_menu {
18638 CodeContextMenu::Completions(completions_menu) => {
18639 let completions = completions_menu.completions.borrow_mut();
18640 assert_eq!(
18641 completions
18642 .iter()
18643 .map(|completion| &completion.label.text)
18644 .collect::<Vec<_>>(),
18645 vec!["method id() Now resolved!", "other"],
18646 "Should update first completion label, but not second as the filter text did not match."
18647 );
18648 }
18649 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18650 }
18651 });
18652}
18653
18654#[gpui::test]
18655async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
18656 init_test(cx, |_| {});
18657 let mut cx = EditorLspTestContext::new_rust(
18658 lsp::ServerCapabilities {
18659 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
18660 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
18661 completion_provider: Some(lsp::CompletionOptions {
18662 resolve_provider: Some(true),
18663 ..Default::default()
18664 }),
18665 ..Default::default()
18666 },
18667 cx,
18668 )
18669 .await;
18670 cx.set_state(indoc! {"
18671 struct TestStruct {
18672 field: i32
18673 }
18674
18675 fn mainˇ() {
18676 let unused_var = 42;
18677 let test_struct = TestStruct { field: 42 };
18678 }
18679 "});
18680 let symbol_range = cx.lsp_range(indoc! {"
18681 struct TestStruct {
18682 field: i32
18683 }
18684
18685 «fn main»() {
18686 let unused_var = 42;
18687 let test_struct = TestStruct { field: 42 };
18688 }
18689 "});
18690 let mut hover_requests =
18691 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18692 Ok(Some(lsp::Hover {
18693 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18694 kind: lsp::MarkupKind::Markdown,
18695 value: "Function documentation".to_string(),
18696 }),
18697 range: Some(symbol_range),
18698 }))
18699 });
18700
18701 // Case 1: Test that code action menu hide hover popover
18702 cx.dispatch_action(Hover);
18703 hover_requests.next().await;
18704 cx.condition(|editor, _| editor.hover_state.visible()).await;
18705 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18706 move |_, _, _| async move {
18707 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18708 lsp::CodeAction {
18709 title: "Remove unused variable".to_string(),
18710 kind: Some(CodeActionKind::QUICKFIX),
18711 edit: Some(lsp::WorkspaceEdit {
18712 changes: Some(
18713 [(
18714 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18715 vec![lsp::TextEdit {
18716 range: lsp::Range::new(
18717 lsp::Position::new(5, 4),
18718 lsp::Position::new(5, 27),
18719 ),
18720 new_text: "".to_string(),
18721 }],
18722 )]
18723 .into_iter()
18724 .collect(),
18725 ),
18726 ..Default::default()
18727 }),
18728 ..Default::default()
18729 },
18730 )]))
18731 },
18732 );
18733 cx.update_editor(|editor, window, cx| {
18734 editor.toggle_code_actions(
18735 &ToggleCodeActions {
18736 deployed_from: None,
18737 quick_launch: false,
18738 },
18739 window,
18740 cx,
18741 );
18742 });
18743 code_action_requests.next().await;
18744 cx.run_until_parked();
18745 cx.condition(|editor, _| editor.context_menu_visible())
18746 .await;
18747 cx.update_editor(|editor, _, _| {
18748 assert!(
18749 !editor.hover_state.visible(),
18750 "Hover popover should be hidden when code action menu is shown"
18751 );
18752 // Hide code actions
18753 editor.context_menu.take();
18754 });
18755
18756 // Case 2: Test that code completions hide hover popover
18757 cx.dispatch_action(Hover);
18758 hover_requests.next().await;
18759 cx.condition(|editor, _| editor.hover_state.visible()).await;
18760 let counter = Arc::new(AtomicUsize::new(0));
18761 let mut completion_requests =
18762 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18763 let counter = counter.clone();
18764 async move {
18765 counter.fetch_add(1, atomic::Ordering::Release);
18766 Ok(Some(lsp::CompletionResponse::Array(vec![
18767 lsp::CompletionItem {
18768 label: "main".into(),
18769 kind: Some(lsp::CompletionItemKind::FUNCTION),
18770 detail: Some("() -> ()".to_string()),
18771 ..Default::default()
18772 },
18773 lsp::CompletionItem {
18774 label: "TestStruct".into(),
18775 kind: Some(lsp::CompletionItemKind::STRUCT),
18776 detail: Some("struct TestStruct".to_string()),
18777 ..Default::default()
18778 },
18779 ])))
18780 }
18781 });
18782 cx.update_editor(|editor, window, cx| {
18783 editor.show_completions(&ShowCompletions, window, cx);
18784 });
18785 completion_requests.next().await;
18786 cx.condition(|editor, _| editor.context_menu_visible())
18787 .await;
18788 cx.update_editor(|editor, _, _| {
18789 assert!(
18790 !editor.hover_state.visible(),
18791 "Hover popover should be hidden when completion menu is shown"
18792 );
18793 });
18794}
18795
18796#[gpui::test]
18797async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18798 init_test(cx, |_| {});
18799
18800 let mut cx = EditorLspTestContext::new_rust(
18801 lsp::ServerCapabilities {
18802 completion_provider: Some(lsp::CompletionOptions {
18803 trigger_characters: Some(vec![".".to_string()]),
18804 resolve_provider: Some(true),
18805 ..Default::default()
18806 }),
18807 ..Default::default()
18808 },
18809 cx,
18810 )
18811 .await;
18812
18813 cx.set_state("fn main() { let a = 2ˇ; }");
18814 cx.simulate_keystroke(".");
18815
18816 let unresolved_item_1 = lsp::CompletionItem {
18817 label: "id".to_string(),
18818 filter_text: Some("id".to_string()),
18819 detail: None,
18820 documentation: None,
18821 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18822 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18823 new_text: ".id".to_string(),
18824 })),
18825 ..lsp::CompletionItem::default()
18826 };
18827 let resolved_item_1 = lsp::CompletionItem {
18828 additional_text_edits: Some(vec![lsp::TextEdit {
18829 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18830 new_text: "!!".to_string(),
18831 }]),
18832 ..unresolved_item_1.clone()
18833 };
18834 let unresolved_item_2 = lsp::CompletionItem {
18835 label: "other".to_string(),
18836 filter_text: Some("other".to_string()),
18837 detail: None,
18838 documentation: None,
18839 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18840 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18841 new_text: ".other".to_string(),
18842 })),
18843 ..lsp::CompletionItem::default()
18844 };
18845 let resolved_item_2 = lsp::CompletionItem {
18846 additional_text_edits: Some(vec![lsp::TextEdit {
18847 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18848 new_text: "??".to_string(),
18849 }]),
18850 ..unresolved_item_2.clone()
18851 };
18852
18853 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18854 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18855 cx.lsp
18856 .server
18857 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18858 let unresolved_item_1 = unresolved_item_1.clone();
18859 let resolved_item_1 = resolved_item_1.clone();
18860 let unresolved_item_2 = unresolved_item_2.clone();
18861 let resolved_item_2 = resolved_item_2.clone();
18862 let resolve_requests_1 = resolve_requests_1.clone();
18863 let resolve_requests_2 = resolve_requests_2.clone();
18864 move |unresolved_request, _| {
18865 let unresolved_item_1 = unresolved_item_1.clone();
18866 let resolved_item_1 = resolved_item_1.clone();
18867 let unresolved_item_2 = unresolved_item_2.clone();
18868 let resolved_item_2 = resolved_item_2.clone();
18869 let resolve_requests_1 = resolve_requests_1.clone();
18870 let resolve_requests_2 = resolve_requests_2.clone();
18871 async move {
18872 if unresolved_request == unresolved_item_1 {
18873 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18874 Ok(resolved_item_1.clone())
18875 } else if unresolved_request == unresolved_item_2 {
18876 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18877 Ok(resolved_item_2.clone())
18878 } else {
18879 panic!("Unexpected completion item {unresolved_request:?}")
18880 }
18881 }
18882 }
18883 })
18884 .detach();
18885
18886 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18887 let unresolved_item_1 = unresolved_item_1.clone();
18888 let unresolved_item_2 = unresolved_item_2.clone();
18889 async move {
18890 Ok(Some(lsp::CompletionResponse::Array(vec![
18891 unresolved_item_1,
18892 unresolved_item_2,
18893 ])))
18894 }
18895 })
18896 .next()
18897 .await;
18898
18899 cx.condition(|editor, _| editor.context_menu_visible())
18900 .await;
18901 cx.update_editor(|editor, _, _| {
18902 let context_menu = editor.context_menu.borrow_mut();
18903 let context_menu = context_menu
18904 .as_ref()
18905 .expect("Should have the context menu deployed");
18906 match context_menu {
18907 CodeContextMenu::Completions(completions_menu) => {
18908 let completions = completions_menu.completions.borrow_mut();
18909 assert_eq!(
18910 completions
18911 .iter()
18912 .map(|completion| &completion.label.text)
18913 .collect::<Vec<_>>(),
18914 vec!["id", "other"]
18915 )
18916 }
18917 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18918 }
18919 });
18920 cx.run_until_parked();
18921
18922 cx.update_editor(|editor, window, cx| {
18923 editor.context_menu_next(&ContextMenuNext, window, cx);
18924 });
18925 cx.run_until_parked();
18926 cx.update_editor(|editor, window, cx| {
18927 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18928 });
18929 cx.run_until_parked();
18930 cx.update_editor(|editor, window, cx| {
18931 editor.context_menu_next(&ContextMenuNext, window, cx);
18932 });
18933 cx.run_until_parked();
18934 cx.update_editor(|editor, window, cx| {
18935 editor
18936 .compose_completion(&ComposeCompletion::default(), window, cx)
18937 .expect("No task returned")
18938 })
18939 .await
18940 .expect("Completion failed");
18941 cx.run_until_parked();
18942
18943 cx.update_editor(|editor, _, cx| {
18944 assert_eq!(
18945 resolve_requests_1.load(atomic::Ordering::Acquire),
18946 1,
18947 "Should always resolve once despite multiple selections"
18948 );
18949 assert_eq!(
18950 resolve_requests_2.load(atomic::Ordering::Acquire),
18951 1,
18952 "Should always resolve once after multiple selections and applying the completion"
18953 );
18954 assert_eq!(
18955 editor.text(cx),
18956 "fn main() { let a = ??.other; }",
18957 "Should use resolved data when applying the completion"
18958 );
18959 });
18960}
18961
18962#[gpui::test]
18963async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18964 init_test(cx, |_| {});
18965
18966 let item_0 = lsp::CompletionItem {
18967 label: "abs".into(),
18968 insert_text: Some("abs".into()),
18969 data: Some(json!({ "very": "special"})),
18970 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18971 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18972 lsp::InsertReplaceEdit {
18973 new_text: "abs".to_string(),
18974 insert: lsp::Range::default(),
18975 replace: lsp::Range::default(),
18976 },
18977 )),
18978 ..lsp::CompletionItem::default()
18979 };
18980 let items = iter::once(item_0.clone())
18981 .chain((11..51).map(|i| lsp::CompletionItem {
18982 label: format!("item_{}", i),
18983 insert_text: Some(format!("item_{}", i)),
18984 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18985 ..lsp::CompletionItem::default()
18986 }))
18987 .collect::<Vec<_>>();
18988
18989 let default_commit_characters = vec!["?".to_string()];
18990 let default_data = json!({ "default": "data"});
18991 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18992 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18993 let default_edit_range = lsp::Range {
18994 start: lsp::Position {
18995 line: 0,
18996 character: 5,
18997 },
18998 end: lsp::Position {
18999 line: 0,
19000 character: 5,
19001 },
19002 };
19003
19004 let mut cx = EditorLspTestContext::new_rust(
19005 lsp::ServerCapabilities {
19006 completion_provider: Some(lsp::CompletionOptions {
19007 trigger_characters: Some(vec![".".to_string()]),
19008 resolve_provider: Some(true),
19009 ..Default::default()
19010 }),
19011 ..Default::default()
19012 },
19013 cx,
19014 )
19015 .await;
19016
19017 cx.set_state("fn main() { let a = 2ˇ; }");
19018 cx.simulate_keystroke(".");
19019
19020 let completion_data = default_data.clone();
19021 let completion_characters = default_commit_characters.clone();
19022 let completion_items = items.clone();
19023 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19024 let default_data = completion_data.clone();
19025 let default_commit_characters = completion_characters.clone();
19026 let items = completion_items.clone();
19027 async move {
19028 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
19029 items,
19030 item_defaults: Some(lsp::CompletionListItemDefaults {
19031 data: Some(default_data.clone()),
19032 commit_characters: Some(default_commit_characters.clone()),
19033 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
19034 default_edit_range,
19035 )),
19036 insert_text_format: Some(default_insert_text_format),
19037 insert_text_mode: Some(default_insert_text_mode),
19038 }),
19039 ..lsp::CompletionList::default()
19040 })))
19041 }
19042 })
19043 .next()
19044 .await;
19045
19046 let resolved_items = Arc::new(Mutex::new(Vec::new()));
19047 cx.lsp
19048 .server
19049 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19050 let closure_resolved_items = resolved_items.clone();
19051 move |item_to_resolve, _| {
19052 let closure_resolved_items = closure_resolved_items.clone();
19053 async move {
19054 closure_resolved_items.lock().push(item_to_resolve.clone());
19055 Ok(item_to_resolve)
19056 }
19057 }
19058 })
19059 .detach();
19060
19061 cx.condition(|editor, _| editor.context_menu_visible())
19062 .await;
19063 cx.run_until_parked();
19064 cx.update_editor(|editor, _, _| {
19065 let menu = editor.context_menu.borrow_mut();
19066 match menu.as_ref().expect("should have the completions menu") {
19067 CodeContextMenu::Completions(completions_menu) => {
19068 assert_eq!(
19069 completions_menu
19070 .entries
19071 .borrow()
19072 .iter()
19073 .map(|mat| mat.string.clone())
19074 .collect::<Vec<String>>(),
19075 items
19076 .iter()
19077 .map(|completion| completion.label.clone())
19078 .collect::<Vec<String>>()
19079 );
19080 }
19081 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
19082 }
19083 });
19084 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
19085 // with 4 from the end.
19086 assert_eq!(
19087 *resolved_items.lock(),
19088 [&items[0..16], &items[items.len() - 4..items.len()]]
19089 .concat()
19090 .iter()
19091 .cloned()
19092 .map(|mut item| {
19093 if item.data.is_none() {
19094 item.data = Some(default_data.clone());
19095 }
19096 item
19097 })
19098 .collect::<Vec<lsp::CompletionItem>>(),
19099 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
19100 );
19101 resolved_items.lock().clear();
19102
19103 cx.update_editor(|editor, window, cx| {
19104 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19105 });
19106 cx.run_until_parked();
19107 // Completions that have already been resolved are skipped.
19108 assert_eq!(
19109 *resolved_items.lock(),
19110 items[items.len() - 17..items.len() - 4]
19111 .iter()
19112 .cloned()
19113 .map(|mut item| {
19114 if item.data.is_none() {
19115 item.data = Some(default_data.clone());
19116 }
19117 item
19118 })
19119 .collect::<Vec<lsp::CompletionItem>>()
19120 );
19121 resolved_items.lock().clear();
19122}
19123
19124#[gpui::test]
19125async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
19126 init_test(cx, |_| {});
19127
19128 let mut cx = EditorLspTestContext::new(
19129 Language::new(
19130 LanguageConfig {
19131 matcher: LanguageMatcher {
19132 path_suffixes: vec!["jsx".into()],
19133 ..Default::default()
19134 },
19135 overrides: [(
19136 "element".into(),
19137 LanguageConfigOverride {
19138 completion_query_characters: Override::Set(['-'].into_iter().collect()),
19139 ..Default::default()
19140 },
19141 )]
19142 .into_iter()
19143 .collect(),
19144 ..Default::default()
19145 },
19146 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
19147 )
19148 .with_override_query("(jsx_self_closing_element) @element")
19149 .unwrap(),
19150 lsp::ServerCapabilities {
19151 completion_provider: Some(lsp::CompletionOptions {
19152 trigger_characters: Some(vec![":".to_string()]),
19153 ..Default::default()
19154 }),
19155 ..Default::default()
19156 },
19157 cx,
19158 )
19159 .await;
19160
19161 cx.lsp
19162 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19163 Ok(Some(lsp::CompletionResponse::Array(vec![
19164 lsp::CompletionItem {
19165 label: "bg-blue".into(),
19166 ..Default::default()
19167 },
19168 lsp::CompletionItem {
19169 label: "bg-red".into(),
19170 ..Default::default()
19171 },
19172 lsp::CompletionItem {
19173 label: "bg-yellow".into(),
19174 ..Default::default()
19175 },
19176 ])))
19177 });
19178
19179 cx.set_state(r#"<p class="bgˇ" />"#);
19180
19181 // Trigger completion when typing a dash, because the dash is an extra
19182 // word character in the 'element' scope, which contains the cursor.
19183 cx.simulate_keystroke("-");
19184 cx.executor().run_until_parked();
19185 cx.update_editor(|editor, _, _| {
19186 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19187 {
19188 assert_eq!(
19189 completion_menu_entries(menu),
19190 &["bg-blue", "bg-red", "bg-yellow"]
19191 );
19192 } else {
19193 panic!("expected completion menu to be open");
19194 }
19195 });
19196
19197 cx.simulate_keystroke("l");
19198 cx.executor().run_until_parked();
19199 cx.update_editor(|editor, _, _| {
19200 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19201 {
19202 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
19203 } else {
19204 panic!("expected completion menu to be open");
19205 }
19206 });
19207
19208 // When filtering completions, consider the character after the '-' to
19209 // be the start of a subword.
19210 cx.set_state(r#"<p class="yelˇ" />"#);
19211 cx.simulate_keystroke("l");
19212 cx.executor().run_until_parked();
19213 cx.update_editor(|editor, _, _| {
19214 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19215 {
19216 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
19217 } else {
19218 panic!("expected completion menu to be open");
19219 }
19220 });
19221}
19222
19223fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
19224 let entries = menu.entries.borrow();
19225 entries.iter().map(|mat| mat.string.clone()).collect()
19226}
19227
19228#[gpui::test]
19229async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
19230 init_test(cx, |settings| {
19231 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19232 });
19233
19234 let fs = FakeFs::new(cx.executor());
19235 fs.insert_file(path!("/file.ts"), Default::default()).await;
19236
19237 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
19238 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19239
19240 language_registry.add(Arc::new(Language::new(
19241 LanguageConfig {
19242 name: "TypeScript".into(),
19243 matcher: LanguageMatcher {
19244 path_suffixes: vec!["ts".to_string()],
19245 ..Default::default()
19246 },
19247 ..Default::default()
19248 },
19249 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19250 )));
19251 update_test_language_settings(cx, |settings| {
19252 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19253 });
19254
19255 let test_plugin = "test_plugin";
19256 let _ = language_registry.register_fake_lsp(
19257 "TypeScript",
19258 FakeLspAdapter {
19259 prettier_plugins: vec![test_plugin],
19260 ..Default::default()
19261 },
19262 );
19263
19264 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19265 let buffer = project
19266 .update(cx, |project, cx| {
19267 project.open_local_buffer(path!("/file.ts"), cx)
19268 })
19269 .await
19270 .unwrap();
19271
19272 let buffer_text = "one\ntwo\nthree\n";
19273 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19274 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19275 editor.update_in(cx, |editor, window, cx| {
19276 editor.set_text(buffer_text, window, cx)
19277 });
19278
19279 editor
19280 .update_in(cx, |editor, window, cx| {
19281 editor.perform_format(
19282 project.clone(),
19283 FormatTrigger::Manual,
19284 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19285 window,
19286 cx,
19287 )
19288 })
19289 .unwrap()
19290 .await;
19291 assert_eq!(
19292 editor.update(cx, |editor, cx| editor.text(cx)),
19293 buffer_text.to_string() + prettier_format_suffix,
19294 "Test prettier formatting was not applied to the original buffer text",
19295 );
19296
19297 update_test_language_settings(cx, |settings| {
19298 settings.defaults.formatter = Some(FormatterList::default())
19299 });
19300 let format = editor.update_in(cx, |editor, window, cx| {
19301 editor.perform_format(
19302 project.clone(),
19303 FormatTrigger::Manual,
19304 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19305 window,
19306 cx,
19307 )
19308 });
19309 format.await.unwrap();
19310 assert_eq!(
19311 editor.update(cx, |editor, cx| editor.text(cx)),
19312 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
19313 "Autoformatting (via test prettier) was not applied to the original buffer text",
19314 );
19315}
19316
19317#[gpui::test]
19318async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
19319 init_test(cx, |settings| {
19320 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19321 });
19322
19323 let fs = FakeFs::new(cx.executor());
19324 fs.insert_file(path!("/file.settings"), Default::default())
19325 .await;
19326
19327 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
19328 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19329
19330 let ts_lang = Arc::new(Language::new(
19331 LanguageConfig {
19332 name: "TypeScript".into(),
19333 matcher: LanguageMatcher {
19334 path_suffixes: vec!["ts".to_string()],
19335 ..LanguageMatcher::default()
19336 },
19337 prettier_parser_name: Some("typescript".to_string()),
19338 ..LanguageConfig::default()
19339 },
19340 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19341 ));
19342
19343 language_registry.add(ts_lang.clone());
19344
19345 update_test_language_settings(cx, |settings| {
19346 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19347 });
19348
19349 let test_plugin = "test_plugin";
19350 let _ = language_registry.register_fake_lsp(
19351 "TypeScript",
19352 FakeLspAdapter {
19353 prettier_plugins: vec![test_plugin],
19354 ..Default::default()
19355 },
19356 );
19357
19358 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19359 let buffer = project
19360 .update(cx, |project, cx| {
19361 project.open_local_buffer(path!("/file.settings"), cx)
19362 })
19363 .await
19364 .unwrap();
19365
19366 project.update(cx, |project, cx| {
19367 project.set_language_for_buffer(&buffer, ts_lang, cx)
19368 });
19369
19370 let buffer_text = "one\ntwo\nthree\n";
19371 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19372 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19373 editor.update_in(cx, |editor, window, cx| {
19374 editor.set_text(buffer_text, window, cx)
19375 });
19376
19377 editor
19378 .update_in(cx, |editor, window, cx| {
19379 editor.perform_format(
19380 project.clone(),
19381 FormatTrigger::Manual,
19382 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19383 window,
19384 cx,
19385 )
19386 })
19387 .unwrap()
19388 .await;
19389 assert_eq!(
19390 editor.update(cx, |editor, cx| editor.text(cx)),
19391 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
19392 "Test prettier formatting was not applied to the original buffer text",
19393 );
19394
19395 update_test_language_settings(cx, |settings| {
19396 settings.defaults.formatter = Some(FormatterList::default())
19397 });
19398 let format = editor.update_in(cx, |editor, window, cx| {
19399 editor.perform_format(
19400 project.clone(),
19401 FormatTrigger::Manual,
19402 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19403 window,
19404 cx,
19405 )
19406 });
19407 format.await.unwrap();
19408
19409 assert_eq!(
19410 editor.update(cx, |editor, cx| editor.text(cx)),
19411 buffer_text.to_string()
19412 + prettier_format_suffix
19413 + "\ntypescript\n"
19414 + prettier_format_suffix
19415 + "\ntypescript",
19416 "Autoformatting (via test prettier) was not applied to the original buffer text",
19417 );
19418}
19419
19420#[gpui::test]
19421async fn test_addition_reverts(cx: &mut TestAppContext) {
19422 init_test(cx, |_| {});
19423 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19424 let base_text = indoc! {r#"
19425 struct Row;
19426 struct Row1;
19427 struct Row2;
19428
19429 struct Row4;
19430 struct Row5;
19431 struct Row6;
19432
19433 struct Row8;
19434 struct Row9;
19435 struct Row10;"#};
19436
19437 // When addition hunks are not adjacent to carets, no hunk revert is performed
19438 assert_hunk_revert(
19439 indoc! {r#"struct Row;
19440 struct Row1;
19441 struct Row1.1;
19442 struct Row1.2;
19443 struct Row2;ˇ
19444
19445 struct Row4;
19446 struct Row5;
19447 struct Row6;
19448
19449 struct Row8;
19450 ˇstruct Row9;
19451 struct Row9.1;
19452 struct Row9.2;
19453 struct Row9.3;
19454 struct Row10;"#},
19455 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19456 indoc! {r#"struct Row;
19457 struct Row1;
19458 struct Row1.1;
19459 struct Row1.2;
19460 struct Row2;ˇ
19461
19462 struct Row4;
19463 struct Row5;
19464 struct Row6;
19465
19466 struct Row8;
19467 ˇstruct Row9;
19468 struct Row9.1;
19469 struct Row9.2;
19470 struct Row9.3;
19471 struct Row10;"#},
19472 base_text,
19473 &mut cx,
19474 );
19475 // Same for selections
19476 assert_hunk_revert(
19477 indoc! {r#"struct Row;
19478 struct Row1;
19479 struct Row2;
19480 struct Row2.1;
19481 struct Row2.2;
19482 «ˇ
19483 struct Row4;
19484 struct» Row5;
19485 «struct Row6;
19486 ˇ»
19487 struct Row9.1;
19488 struct Row9.2;
19489 struct Row9.3;
19490 struct Row8;
19491 struct Row9;
19492 struct Row10;"#},
19493 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19494 indoc! {r#"struct Row;
19495 struct Row1;
19496 struct Row2;
19497 struct Row2.1;
19498 struct Row2.2;
19499 «ˇ
19500 struct Row4;
19501 struct» Row5;
19502 «struct Row6;
19503 ˇ»
19504 struct Row9.1;
19505 struct Row9.2;
19506 struct Row9.3;
19507 struct Row8;
19508 struct Row9;
19509 struct Row10;"#},
19510 base_text,
19511 &mut cx,
19512 );
19513
19514 // When carets and selections intersect the addition hunks, those are reverted.
19515 // Adjacent carets got merged.
19516 assert_hunk_revert(
19517 indoc! {r#"struct Row;
19518 ˇ// something on the top
19519 struct Row1;
19520 struct Row2;
19521 struct Roˇw3.1;
19522 struct Row2.2;
19523 struct Row2.3;ˇ
19524
19525 struct Row4;
19526 struct ˇRow5.1;
19527 struct Row5.2;
19528 struct «Rowˇ»5.3;
19529 struct Row5;
19530 struct Row6;
19531 ˇ
19532 struct Row9.1;
19533 struct «Rowˇ»9.2;
19534 struct «ˇRow»9.3;
19535 struct Row8;
19536 struct Row9;
19537 «ˇ// something on bottom»
19538 struct Row10;"#},
19539 vec![
19540 DiffHunkStatusKind::Added,
19541 DiffHunkStatusKind::Added,
19542 DiffHunkStatusKind::Added,
19543 DiffHunkStatusKind::Added,
19544 DiffHunkStatusKind::Added,
19545 ],
19546 indoc! {r#"struct Row;
19547 ˇstruct Row1;
19548 struct Row2;
19549 ˇ
19550 struct Row4;
19551 ˇstruct Row5;
19552 struct Row6;
19553 ˇ
19554 ˇstruct Row8;
19555 struct Row9;
19556 ˇstruct Row10;"#},
19557 base_text,
19558 &mut cx,
19559 );
19560}
19561
19562#[gpui::test]
19563async fn test_modification_reverts(cx: &mut TestAppContext) {
19564 init_test(cx, |_| {});
19565 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19566 let base_text = indoc! {r#"
19567 struct Row;
19568 struct Row1;
19569 struct Row2;
19570
19571 struct Row4;
19572 struct Row5;
19573 struct Row6;
19574
19575 struct Row8;
19576 struct Row9;
19577 struct Row10;"#};
19578
19579 // Modification hunks behave the same as the addition ones.
19580 assert_hunk_revert(
19581 indoc! {r#"struct Row;
19582 struct Row1;
19583 struct Row33;
19584 ˇ
19585 struct Row4;
19586 struct Row5;
19587 struct Row6;
19588 ˇ
19589 struct Row99;
19590 struct Row9;
19591 struct Row10;"#},
19592 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19593 indoc! {r#"struct Row;
19594 struct Row1;
19595 struct Row33;
19596 ˇ
19597 struct Row4;
19598 struct Row5;
19599 struct Row6;
19600 ˇ
19601 struct Row99;
19602 struct Row9;
19603 struct Row10;"#},
19604 base_text,
19605 &mut cx,
19606 );
19607 assert_hunk_revert(
19608 indoc! {r#"struct Row;
19609 struct Row1;
19610 struct Row33;
19611 «ˇ
19612 struct Row4;
19613 struct» Row5;
19614 «struct Row6;
19615 ˇ»
19616 struct Row99;
19617 struct Row9;
19618 struct Row10;"#},
19619 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19620 indoc! {r#"struct Row;
19621 struct Row1;
19622 struct Row33;
19623 «ˇ
19624 struct Row4;
19625 struct» Row5;
19626 «struct Row6;
19627 ˇ»
19628 struct Row99;
19629 struct Row9;
19630 struct Row10;"#},
19631 base_text,
19632 &mut cx,
19633 );
19634
19635 assert_hunk_revert(
19636 indoc! {r#"ˇstruct Row1.1;
19637 struct Row1;
19638 «ˇstr»uct Row22;
19639
19640 struct ˇRow44;
19641 struct Row5;
19642 struct «Rˇ»ow66;ˇ
19643
19644 «struˇ»ct Row88;
19645 struct Row9;
19646 struct Row1011;ˇ"#},
19647 vec![
19648 DiffHunkStatusKind::Modified,
19649 DiffHunkStatusKind::Modified,
19650 DiffHunkStatusKind::Modified,
19651 DiffHunkStatusKind::Modified,
19652 DiffHunkStatusKind::Modified,
19653 DiffHunkStatusKind::Modified,
19654 ],
19655 indoc! {r#"struct Row;
19656 ˇstruct Row1;
19657 struct Row2;
19658 ˇ
19659 struct Row4;
19660 ˇstruct Row5;
19661 struct Row6;
19662 ˇ
19663 struct Row8;
19664 ˇstruct Row9;
19665 struct Row10;ˇ"#},
19666 base_text,
19667 &mut cx,
19668 );
19669}
19670
19671#[gpui::test]
19672async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
19673 init_test(cx, |_| {});
19674 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19675 let base_text = indoc! {r#"
19676 one
19677
19678 two
19679 three
19680 "#};
19681
19682 cx.set_head_text(base_text);
19683 cx.set_state("\nˇ\n");
19684 cx.executor().run_until_parked();
19685 cx.update_editor(|editor, _window, cx| {
19686 editor.expand_selected_diff_hunks(cx);
19687 });
19688 cx.executor().run_until_parked();
19689 cx.update_editor(|editor, window, cx| {
19690 editor.backspace(&Default::default(), window, cx);
19691 });
19692 cx.run_until_parked();
19693 cx.assert_state_with_diff(
19694 indoc! {r#"
19695
19696 - two
19697 - threeˇ
19698 +
19699 "#}
19700 .to_string(),
19701 );
19702}
19703
19704#[gpui::test]
19705async fn test_deletion_reverts(cx: &mut TestAppContext) {
19706 init_test(cx, |_| {});
19707 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19708 let base_text = indoc! {r#"struct Row;
19709struct Row1;
19710struct Row2;
19711
19712struct Row4;
19713struct Row5;
19714struct Row6;
19715
19716struct Row8;
19717struct Row9;
19718struct Row10;"#};
19719
19720 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
19721 assert_hunk_revert(
19722 indoc! {r#"struct Row;
19723 struct Row2;
19724
19725 ˇstruct Row4;
19726 struct Row5;
19727 struct Row6;
19728 ˇ
19729 struct Row8;
19730 struct Row10;"#},
19731 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19732 indoc! {r#"struct Row;
19733 struct Row2;
19734
19735 ˇstruct Row4;
19736 struct Row5;
19737 struct Row6;
19738 ˇ
19739 struct Row8;
19740 struct Row10;"#},
19741 base_text,
19742 &mut cx,
19743 );
19744 assert_hunk_revert(
19745 indoc! {r#"struct Row;
19746 struct Row2;
19747
19748 «ˇstruct Row4;
19749 struct» Row5;
19750 «struct Row6;
19751 ˇ»
19752 struct Row8;
19753 struct Row10;"#},
19754 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19755 indoc! {r#"struct Row;
19756 struct Row2;
19757
19758 «ˇstruct Row4;
19759 struct» Row5;
19760 «struct Row6;
19761 ˇ»
19762 struct Row8;
19763 struct Row10;"#},
19764 base_text,
19765 &mut cx,
19766 );
19767
19768 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
19769 assert_hunk_revert(
19770 indoc! {r#"struct Row;
19771 ˇstruct Row2;
19772
19773 struct Row4;
19774 struct Row5;
19775 struct Row6;
19776
19777 struct Row8;ˇ
19778 struct Row10;"#},
19779 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19780 indoc! {r#"struct Row;
19781 struct Row1;
19782 ˇstruct Row2;
19783
19784 struct Row4;
19785 struct Row5;
19786 struct Row6;
19787
19788 struct Row8;ˇ
19789 struct Row9;
19790 struct Row10;"#},
19791 base_text,
19792 &mut cx,
19793 );
19794 assert_hunk_revert(
19795 indoc! {r#"struct Row;
19796 struct Row2«ˇ;
19797 struct Row4;
19798 struct» Row5;
19799 «struct Row6;
19800
19801 struct Row8;ˇ»
19802 struct Row10;"#},
19803 vec![
19804 DiffHunkStatusKind::Deleted,
19805 DiffHunkStatusKind::Deleted,
19806 DiffHunkStatusKind::Deleted,
19807 ],
19808 indoc! {r#"struct Row;
19809 struct Row1;
19810 struct Row2«ˇ;
19811
19812 struct Row4;
19813 struct» Row5;
19814 «struct Row6;
19815
19816 struct Row8;ˇ»
19817 struct Row9;
19818 struct Row10;"#},
19819 base_text,
19820 &mut cx,
19821 );
19822}
19823
19824#[gpui::test]
19825async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
19826 init_test(cx, |_| {});
19827
19828 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
19829 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
19830 let base_text_3 =
19831 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
19832
19833 let text_1 = edit_first_char_of_every_line(base_text_1);
19834 let text_2 = edit_first_char_of_every_line(base_text_2);
19835 let text_3 = edit_first_char_of_every_line(base_text_3);
19836
19837 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19838 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19839 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19840
19841 let multibuffer = cx.new(|cx| {
19842 let mut multibuffer = MultiBuffer::new(ReadWrite);
19843 multibuffer.push_excerpts(
19844 buffer_1.clone(),
19845 [
19846 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19847 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19848 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19849 ],
19850 cx,
19851 );
19852 multibuffer.push_excerpts(
19853 buffer_2.clone(),
19854 [
19855 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19856 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19857 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19858 ],
19859 cx,
19860 );
19861 multibuffer.push_excerpts(
19862 buffer_3.clone(),
19863 [
19864 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19865 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19866 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19867 ],
19868 cx,
19869 );
19870 multibuffer
19871 });
19872
19873 let fs = FakeFs::new(cx.executor());
19874 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19875 let (editor, cx) = cx
19876 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19877 editor.update_in(cx, |editor, _window, cx| {
19878 for (buffer, diff_base) in [
19879 (buffer_1.clone(), base_text_1),
19880 (buffer_2.clone(), base_text_2),
19881 (buffer_3.clone(), base_text_3),
19882 ] {
19883 let diff = cx.new(|cx| {
19884 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
19885 });
19886 editor
19887 .buffer
19888 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19889 }
19890 });
19891 cx.executor().run_until_parked();
19892
19893 editor.update_in(cx, |editor, window, cx| {
19894 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}");
19895 editor.select_all(&SelectAll, window, cx);
19896 editor.git_restore(&Default::default(), window, cx);
19897 });
19898 cx.executor().run_until_parked();
19899
19900 // When all ranges are selected, all buffer hunks are reverted.
19901 editor.update(cx, |editor, cx| {
19902 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");
19903 });
19904 buffer_1.update(cx, |buffer, _| {
19905 assert_eq!(buffer.text(), base_text_1);
19906 });
19907 buffer_2.update(cx, |buffer, _| {
19908 assert_eq!(buffer.text(), base_text_2);
19909 });
19910 buffer_3.update(cx, |buffer, _| {
19911 assert_eq!(buffer.text(), base_text_3);
19912 });
19913
19914 editor.update_in(cx, |editor, window, cx| {
19915 editor.undo(&Default::default(), window, cx);
19916 });
19917
19918 editor.update_in(cx, |editor, window, cx| {
19919 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19920 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19921 });
19922 editor.git_restore(&Default::default(), window, cx);
19923 });
19924
19925 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19926 // but not affect buffer_2 and its related excerpts.
19927 editor.update(cx, |editor, cx| {
19928 assert_eq!(
19929 editor.text(cx),
19930 "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}"
19931 );
19932 });
19933 buffer_1.update(cx, |buffer, _| {
19934 assert_eq!(buffer.text(), base_text_1);
19935 });
19936 buffer_2.update(cx, |buffer, _| {
19937 assert_eq!(
19938 buffer.text(),
19939 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19940 );
19941 });
19942 buffer_3.update(cx, |buffer, _| {
19943 assert_eq!(
19944 buffer.text(),
19945 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19946 );
19947 });
19948
19949 fn edit_first_char_of_every_line(text: &str) -> String {
19950 text.split('\n')
19951 .map(|line| format!("X{}", &line[1..]))
19952 .collect::<Vec<_>>()
19953 .join("\n")
19954 }
19955}
19956
19957#[gpui::test]
19958async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19959 init_test(cx, |_| {});
19960
19961 let cols = 4;
19962 let rows = 10;
19963 let sample_text_1 = sample_text(rows, cols, 'a');
19964 assert_eq!(
19965 sample_text_1,
19966 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19967 );
19968 let sample_text_2 = sample_text(rows, cols, 'l');
19969 assert_eq!(
19970 sample_text_2,
19971 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19972 );
19973 let sample_text_3 = sample_text(rows, cols, 'v');
19974 assert_eq!(
19975 sample_text_3,
19976 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19977 );
19978
19979 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19980 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19981 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19982
19983 let multi_buffer = cx.new(|cx| {
19984 let mut multibuffer = MultiBuffer::new(ReadWrite);
19985 multibuffer.push_excerpts(
19986 buffer_1.clone(),
19987 [
19988 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19989 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19990 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19991 ],
19992 cx,
19993 );
19994 multibuffer.push_excerpts(
19995 buffer_2.clone(),
19996 [
19997 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19998 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19999 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20000 ],
20001 cx,
20002 );
20003 multibuffer.push_excerpts(
20004 buffer_3.clone(),
20005 [
20006 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20007 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20008 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20009 ],
20010 cx,
20011 );
20012 multibuffer
20013 });
20014
20015 let fs = FakeFs::new(cx.executor());
20016 fs.insert_tree(
20017 "/a",
20018 json!({
20019 "main.rs": sample_text_1,
20020 "other.rs": sample_text_2,
20021 "lib.rs": sample_text_3,
20022 }),
20023 )
20024 .await;
20025 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20026 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20027 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20028 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20029 Editor::new(
20030 EditorMode::full(),
20031 multi_buffer,
20032 Some(project.clone()),
20033 window,
20034 cx,
20035 )
20036 });
20037 let multibuffer_item_id = workspace
20038 .update(cx, |workspace, window, cx| {
20039 assert!(
20040 workspace.active_item(cx).is_none(),
20041 "active item should be None before the first item is added"
20042 );
20043 workspace.add_item_to_active_pane(
20044 Box::new(multi_buffer_editor.clone()),
20045 None,
20046 true,
20047 window,
20048 cx,
20049 );
20050 let active_item = workspace
20051 .active_item(cx)
20052 .expect("should have an active item after adding the multi buffer");
20053 assert_eq!(
20054 active_item.buffer_kind(cx),
20055 ItemBufferKind::Multibuffer,
20056 "A multi buffer was expected to active after adding"
20057 );
20058 active_item.item_id()
20059 })
20060 .unwrap();
20061 cx.executor().run_until_parked();
20062
20063 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20064 editor.change_selections(
20065 SelectionEffects::scroll(Autoscroll::Next),
20066 window,
20067 cx,
20068 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
20069 );
20070 editor.open_excerpts(&OpenExcerpts, window, cx);
20071 });
20072 cx.executor().run_until_parked();
20073 let first_item_id = workspace
20074 .update(cx, |workspace, window, cx| {
20075 let active_item = workspace
20076 .active_item(cx)
20077 .expect("should have an active item after navigating into the 1st buffer");
20078 let first_item_id = active_item.item_id();
20079 assert_ne!(
20080 first_item_id, multibuffer_item_id,
20081 "Should navigate into the 1st buffer and activate it"
20082 );
20083 assert_eq!(
20084 active_item.buffer_kind(cx),
20085 ItemBufferKind::Singleton,
20086 "New active item should be a singleton buffer"
20087 );
20088 assert_eq!(
20089 active_item
20090 .act_as::<Editor>(cx)
20091 .expect("should have navigated into an editor for the 1st buffer")
20092 .read(cx)
20093 .text(cx),
20094 sample_text_1
20095 );
20096
20097 workspace
20098 .go_back(workspace.active_pane().downgrade(), window, cx)
20099 .detach_and_log_err(cx);
20100
20101 first_item_id
20102 })
20103 .unwrap();
20104 cx.executor().run_until_parked();
20105 workspace
20106 .update(cx, |workspace, _, cx| {
20107 let active_item = workspace
20108 .active_item(cx)
20109 .expect("should have an active item after navigating back");
20110 assert_eq!(
20111 active_item.item_id(),
20112 multibuffer_item_id,
20113 "Should navigate back to the multi buffer"
20114 );
20115 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20116 })
20117 .unwrap();
20118
20119 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20120 editor.change_selections(
20121 SelectionEffects::scroll(Autoscroll::Next),
20122 window,
20123 cx,
20124 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
20125 );
20126 editor.open_excerpts(&OpenExcerpts, window, cx);
20127 });
20128 cx.executor().run_until_parked();
20129 let second_item_id = workspace
20130 .update(cx, |workspace, window, cx| {
20131 let active_item = workspace
20132 .active_item(cx)
20133 .expect("should have an active item after navigating into the 2nd buffer");
20134 let second_item_id = active_item.item_id();
20135 assert_ne!(
20136 second_item_id, multibuffer_item_id,
20137 "Should navigate away from the multibuffer"
20138 );
20139 assert_ne!(
20140 second_item_id, first_item_id,
20141 "Should navigate into the 2nd buffer and activate it"
20142 );
20143 assert_eq!(
20144 active_item.buffer_kind(cx),
20145 ItemBufferKind::Singleton,
20146 "New active item should be a singleton buffer"
20147 );
20148 assert_eq!(
20149 active_item
20150 .act_as::<Editor>(cx)
20151 .expect("should have navigated into an editor")
20152 .read(cx)
20153 .text(cx),
20154 sample_text_2
20155 );
20156
20157 workspace
20158 .go_back(workspace.active_pane().downgrade(), window, cx)
20159 .detach_and_log_err(cx);
20160
20161 second_item_id
20162 })
20163 .unwrap();
20164 cx.executor().run_until_parked();
20165 workspace
20166 .update(cx, |workspace, _, cx| {
20167 let active_item = workspace
20168 .active_item(cx)
20169 .expect("should have an active item after navigating back from the 2nd buffer");
20170 assert_eq!(
20171 active_item.item_id(),
20172 multibuffer_item_id,
20173 "Should navigate back from the 2nd buffer to the multi buffer"
20174 );
20175 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20176 })
20177 .unwrap();
20178
20179 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20180 editor.change_selections(
20181 SelectionEffects::scroll(Autoscroll::Next),
20182 window,
20183 cx,
20184 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
20185 );
20186 editor.open_excerpts(&OpenExcerpts, window, cx);
20187 });
20188 cx.executor().run_until_parked();
20189 workspace
20190 .update(cx, |workspace, window, cx| {
20191 let active_item = workspace
20192 .active_item(cx)
20193 .expect("should have an active item after navigating into the 3rd buffer");
20194 let third_item_id = active_item.item_id();
20195 assert_ne!(
20196 third_item_id, multibuffer_item_id,
20197 "Should navigate into the 3rd buffer and activate it"
20198 );
20199 assert_ne!(third_item_id, first_item_id);
20200 assert_ne!(third_item_id, second_item_id);
20201 assert_eq!(
20202 active_item.buffer_kind(cx),
20203 ItemBufferKind::Singleton,
20204 "New active item should be a singleton buffer"
20205 );
20206 assert_eq!(
20207 active_item
20208 .act_as::<Editor>(cx)
20209 .expect("should have navigated into an editor")
20210 .read(cx)
20211 .text(cx),
20212 sample_text_3
20213 );
20214
20215 workspace
20216 .go_back(workspace.active_pane().downgrade(), window, cx)
20217 .detach_and_log_err(cx);
20218 })
20219 .unwrap();
20220 cx.executor().run_until_parked();
20221 workspace
20222 .update(cx, |workspace, _, cx| {
20223 let active_item = workspace
20224 .active_item(cx)
20225 .expect("should have an active item after navigating back from the 3rd buffer");
20226 assert_eq!(
20227 active_item.item_id(),
20228 multibuffer_item_id,
20229 "Should navigate back from the 3rd buffer to the multi buffer"
20230 );
20231 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20232 })
20233 .unwrap();
20234}
20235
20236#[gpui::test]
20237async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20238 init_test(cx, |_| {});
20239
20240 let mut cx = EditorTestContext::new(cx).await;
20241
20242 let diff_base = r#"
20243 use some::mod;
20244
20245 const A: u32 = 42;
20246
20247 fn main() {
20248 println!("hello");
20249
20250 println!("world");
20251 }
20252 "#
20253 .unindent();
20254
20255 cx.set_state(
20256 &r#"
20257 use some::modified;
20258
20259 ˇ
20260 fn main() {
20261 println!("hello there");
20262
20263 println!("around the");
20264 println!("world");
20265 }
20266 "#
20267 .unindent(),
20268 );
20269
20270 cx.set_head_text(&diff_base);
20271 executor.run_until_parked();
20272
20273 cx.update_editor(|editor, window, cx| {
20274 editor.go_to_next_hunk(&GoToHunk, window, cx);
20275 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20276 });
20277 executor.run_until_parked();
20278 cx.assert_state_with_diff(
20279 r#"
20280 use some::modified;
20281
20282
20283 fn main() {
20284 - println!("hello");
20285 + ˇ println!("hello there");
20286
20287 println!("around the");
20288 println!("world");
20289 }
20290 "#
20291 .unindent(),
20292 );
20293
20294 cx.update_editor(|editor, window, cx| {
20295 for _ in 0..2 {
20296 editor.go_to_next_hunk(&GoToHunk, window, cx);
20297 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20298 }
20299 });
20300 executor.run_until_parked();
20301 cx.assert_state_with_diff(
20302 r#"
20303 - use some::mod;
20304 + ˇuse some::modified;
20305
20306
20307 fn main() {
20308 - println!("hello");
20309 + println!("hello there");
20310
20311 + println!("around the");
20312 println!("world");
20313 }
20314 "#
20315 .unindent(),
20316 );
20317
20318 cx.update_editor(|editor, window, cx| {
20319 editor.go_to_next_hunk(&GoToHunk, window, cx);
20320 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20321 });
20322 executor.run_until_parked();
20323 cx.assert_state_with_diff(
20324 r#"
20325 - use some::mod;
20326 + use some::modified;
20327
20328 - const A: u32 = 42;
20329 ˇ
20330 fn main() {
20331 - println!("hello");
20332 + println!("hello there");
20333
20334 + println!("around the");
20335 println!("world");
20336 }
20337 "#
20338 .unindent(),
20339 );
20340
20341 cx.update_editor(|editor, window, cx| {
20342 editor.cancel(&Cancel, window, cx);
20343 });
20344
20345 cx.assert_state_with_diff(
20346 r#"
20347 use some::modified;
20348
20349 ˇ
20350 fn main() {
20351 println!("hello there");
20352
20353 println!("around the");
20354 println!("world");
20355 }
20356 "#
20357 .unindent(),
20358 );
20359}
20360
20361#[gpui::test]
20362async fn test_diff_base_change_with_expanded_diff_hunks(
20363 executor: BackgroundExecutor,
20364 cx: &mut TestAppContext,
20365) {
20366 init_test(cx, |_| {});
20367
20368 let mut cx = EditorTestContext::new(cx).await;
20369
20370 let diff_base = r#"
20371 use some::mod1;
20372 use some::mod2;
20373
20374 const A: u32 = 42;
20375 const B: u32 = 42;
20376 const C: u32 = 42;
20377
20378 fn main() {
20379 println!("hello");
20380
20381 println!("world");
20382 }
20383 "#
20384 .unindent();
20385
20386 cx.set_state(
20387 &r#"
20388 use some::mod2;
20389
20390 const A: u32 = 42;
20391 const C: u32 = 42;
20392
20393 fn main(ˇ) {
20394 //println!("hello");
20395
20396 println!("world");
20397 //
20398 //
20399 }
20400 "#
20401 .unindent(),
20402 );
20403
20404 cx.set_head_text(&diff_base);
20405 executor.run_until_parked();
20406
20407 cx.update_editor(|editor, window, cx| {
20408 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20409 });
20410 executor.run_until_parked();
20411 cx.assert_state_with_diff(
20412 r#"
20413 - use some::mod1;
20414 use some::mod2;
20415
20416 const A: u32 = 42;
20417 - const B: u32 = 42;
20418 const C: u32 = 42;
20419
20420 fn main(ˇ) {
20421 - println!("hello");
20422 + //println!("hello");
20423
20424 println!("world");
20425 + //
20426 + //
20427 }
20428 "#
20429 .unindent(),
20430 );
20431
20432 cx.set_head_text("new diff base!");
20433 executor.run_until_parked();
20434 cx.assert_state_with_diff(
20435 r#"
20436 - new diff base!
20437 + use some::mod2;
20438 +
20439 + const A: u32 = 42;
20440 + const C: u32 = 42;
20441 +
20442 + fn main(ˇ) {
20443 + //println!("hello");
20444 +
20445 + println!("world");
20446 + //
20447 + //
20448 + }
20449 "#
20450 .unindent(),
20451 );
20452}
20453
20454#[gpui::test]
20455async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
20456 init_test(cx, |_| {});
20457
20458 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20459 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20460 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20461 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20462 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
20463 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
20464
20465 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
20466 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
20467 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
20468
20469 let multi_buffer = cx.new(|cx| {
20470 let mut multibuffer = MultiBuffer::new(ReadWrite);
20471 multibuffer.push_excerpts(
20472 buffer_1.clone(),
20473 [
20474 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20475 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20476 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20477 ],
20478 cx,
20479 );
20480 multibuffer.push_excerpts(
20481 buffer_2.clone(),
20482 [
20483 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20484 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20485 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20486 ],
20487 cx,
20488 );
20489 multibuffer.push_excerpts(
20490 buffer_3.clone(),
20491 [
20492 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20493 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20494 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20495 ],
20496 cx,
20497 );
20498 multibuffer
20499 });
20500
20501 let editor =
20502 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20503 editor
20504 .update(cx, |editor, _window, cx| {
20505 for (buffer, diff_base) in [
20506 (buffer_1.clone(), file_1_old),
20507 (buffer_2.clone(), file_2_old),
20508 (buffer_3.clone(), file_3_old),
20509 ] {
20510 let diff = cx.new(|cx| {
20511 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
20512 });
20513 editor
20514 .buffer
20515 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20516 }
20517 })
20518 .unwrap();
20519
20520 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20521 cx.run_until_parked();
20522
20523 cx.assert_editor_state(
20524 &"
20525 ˇaaa
20526 ccc
20527 ddd
20528
20529 ggg
20530 hhh
20531
20532
20533 lll
20534 mmm
20535 NNN
20536
20537 qqq
20538 rrr
20539
20540 uuu
20541 111
20542 222
20543 333
20544
20545 666
20546 777
20547
20548 000
20549 !!!"
20550 .unindent(),
20551 );
20552
20553 cx.update_editor(|editor, window, cx| {
20554 editor.select_all(&SelectAll, window, cx);
20555 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20556 });
20557 cx.executor().run_until_parked();
20558
20559 cx.assert_state_with_diff(
20560 "
20561 «aaa
20562 - bbb
20563 ccc
20564 ddd
20565
20566 ggg
20567 hhh
20568
20569
20570 lll
20571 mmm
20572 - nnn
20573 + NNN
20574
20575 qqq
20576 rrr
20577
20578 uuu
20579 111
20580 222
20581 333
20582
20583 + 666
20584 777
20585
20586 000
20587 !!!ˇ»"
20588 .unindent(),
20589 );
20590}
20591
20592#[gpui::test]
20593async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
20594 init_test(cx, |_| {});
20595
20596 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
20597 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
20598
20599 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
20600 let multi_buffer = cx.new(|cx| {
20601 let mut multibuffer = MultiBuffer::new(ReadWrite);
20602 multibuffer.push_excerpts(
20603 buffer.clone(),
20604 [
20605 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
20606 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
20607 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
20608 ],
20609 cx,
20610 );
20611 multibuffer
20612 });
20613
20614 let editor =
20615 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20616 editor
20617 .update(cx, |editor, _window, cx| {
20618 let diff = cx.new(|cx| {
20619 BufferDiff::new_with_base_text(base, &buffer.read(cx).text_snapshot(), cx)
20620 });
20621 editor
20622 .buffer
20623 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
20624 })
20625 .unwrap();
20626
20627 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20628 cx.run_until_parked();
20629
20630 cx.update_editor(|editor, window, cx| {
20631 editor.expand_all_diff_hunks(&Default::default(), window, cx)
20632 });
20633 cx.executor().run_until_parked();
20634
20635 // When the start of a hunk coincides with the start of its excerpt,
20636 // the hunk is expanded. When the start of a hunk is earlier than
20637 // the start of its excerpt, the hunk is not expanded.
20638 cx.assert_state_with_diff(
20639 "
20640 ˇaaa
20641 - bbb
20642 + BBB
20643
20644 - ddd
20645 - eee
20646 + DDD
20647 + EEE
20648 fff
20649
20650 iii
20651 "
20652 .unindent(),
20653 );
20654}
20655
20656#[gpui::test]
20657async fn test_edits_around_expanded_insertion_hunks(
20658 executor: BackgroundExecutor,
20659 cx: &mut TestAppContext,
20660) {
20661 init_test(cx, |_| {});
20662
20663 let mut cx = EditorTestContext::new(cx).await;
20664
20665 let diff_base = r#"
20666 use some::mod1;
20667 use some::mod2;
20668
20669 const A: u32 = 42;
20670
20671 fn main() {
20672 println!("hello");
20673
20674 println!("world");
20675 }
20676 "#
20677 .unindent();
20678 executor.run_until_parked();
20679 cx.set_state(
20680 &r#"
20681 use some::mod1;
20682 use some::mod2;
20683
20684 const A: u32 = 42;
20685 const B: u32 = 42;
20686 const C: u32 = 42;
20687 ˇ
20688
20689 fn main() {
20690 println!("hello");
20691
20692 println!("world");
20693 }
20694 "#
20695 .unindent(),
20696 );
20697
20698 cx.set_head_text(&diff_base);
20699 executor.run_until_parked();
20700
20701 cx.update_editor(|editor, window, cx| {
20702 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20703 });
20704 executor.run_until_parked();
20705
20706 cx.assert_state_with_diff(
20707 r#"
20708 use some::mod1;
20709 use some::mod2;
20710
20711 const A: u32 = 42;
20712 + const B: u32 = 42;
20713 + const C: u32 = 42;
20714 + ˇ
20715
20716 fn main() {
20717 println!("hello");
20718
20719 println!("world");
20720 }
20721 "#
20722 .unindent(),
20723 );
20724
20725 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
20726 executor.run_until_parked();
20727
20728 cx.assert_state_with_diff(
20729 r#"
20730 use some::mod1;
20731 use some::mod2;
20732
20733 const A: u32 = 42;
20734 + const B: u32 = 42;
20735 + const C: u32 = 42;
20736 + const D: u32 = 42;
20737 + ˇ
20738
20739 fn main() {
20740 println!("hello");
20741
20742 println!("world");
20743 }
20744 "#
20745 .unindent(),
20746 );
20747
20748 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
20749 executor.run_until_parked();
20750
20751 cx.assert_state_with_diff(
20752 r#"
20753 use some::mod1;
20754 use some::mod2;
20755
20756 const A: u32 = 42;
20757 + const B: u32 = 42;
20758 + const C: u32 = 42;
20759 + const D: u32 = 42;
20760 + const E: u32 = 42;
20761 + ˇ
20762
20763 fn main() {
20764 println!("hello");
20765
20766 println!("world");
20767 }
20768 "#
20769 .unindent(),
20770 );
20771
20772 cx.update_editor(|editor, window, cx| {
20773 editor.delete_line(&DeleteLine, window, cx);
20774 });
20775 executor.run_until_parked();
20776
20777 cx.assert_state_with_diff(
20778 r#"
20779 use some::mod1;
20780 use some::mod2;
20781
20782 const A: u32 = 42;
20783 + const B: u32 = 42;
20784 + const C: u32 = 42;
20785 + const D: u32 = 42;
20786 + const E: u32 = 42;
20787 ˇ
20788 fn main() {
20789 println!("hello");
20790
20791 println!("world");
20792 }
20793 "#
20794 .unindent(),
20795 );
20796
20797 cx.update_editor(|editor, window, cx| {
20798 editor.move_up(&MoveUp, window, cx);
20799 editor.delete_line(&DeleteLine, window, cx);
20800 editor.move_up(&MoveUp, window, cx);
20801 editor.delete_line(&DeleteLine, window, cx);
20802 editor.move_up(&MoveUp, window, cx);
20803 editor.delete_line(&DeleteLine, window, cx);
20804 });
20805 executor.run_until_parked();
20806 cx.assert_state_with_diff(
20807 r#"
20808 use some::mod1;
20809 use some::mod2;
20810
20811 const A: u32 = 42;
20812 + const B: u32 = 42;
20813 ˇ
20814 fn main() {
20815 println!("hello");
20816
20817 println!("world");
20818 }
20819 "#
20820 .unindent(),
20821 );
20822
20823 cx.update_editor(|editor, window, cx| {
20824 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
20825 editor.delete_line(&DeleteLine, window, cx);
20826 });
20827 executor.run_until_parked();
20828 cx.assert_state_with_diff(
20829 r#"
20830 ˇ
20831 fn main() {
20832 println!("hello");
20833
20834 println!("world");
20835 }
20836 "#
20837 .unindent(),
20838 );
20839}
20840
20841#[gpui::test]
20842async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
20843 init_test(cx, |_| {});
20844
20845 let mut cx = EditorTestContext::new(cx).await;
20846 cx.set_head_text(indoc! { "
20847 one
20848 two
20849 three
20850 four
20851 five
20852 "
20853 });
20854 cx.set_state(indoc! { "
20855 one
20856 ˇthree
20857 five
20858 "});
20859 cx.run_until_parked();
20860 cx.update_editor(|editor, window, cx| {
20861 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20862 });
20863 cx.assert_state_with_diff(
20864 indoc! { "
20865 one
20866 - two
20867 ˇthree
20868 - four
20869 five
20870 "}
20871 .to_string(),
20872 );
20873 cx.update_editor(|editor, window, cx| {
20874 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20875 });
20876
20877 cx.assert_state_with_diff(
20878 indoc! { "
20879 one
20880 ˇthree
20881 five
20882 "}
20883 .to_string(),
20884 );
20885
20886 cx.update_editor(|editor, window, cx| {
20887 editor.move_up(&MoveUp, window, cx);
20888 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20889 });
20890 cx.assert_state_with_diff(
20891 indoc! { "
20892 ˇone
20893 - two
20894 three
20895 five
20896 "}
20897 .to_string(),
20898 );
20899
20900 cx.update_editor(|editor, window, cx| {
20901 editor.move_down(&MoveDown, window, cx);
20902 editor.move_down(&MoveDown, window, cx);
20903 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20904 });
20905 cx.assert_state_with_diff(
20906 indoc! { "
20907 one
20908 - two
20909 ˇthree
20910 - four
20911 five
20912 "}
20913 .to_string(),
20914 );
20915
20916 cx.set_state(indoc! { "
20917 one
20918 ˇTWO
20919 three
20920 four
20921 five
20922 "});
20923 cx.run_until_parked();
20924 cx.update_editor(|editor, window, cx| {
20925 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20926 });
20927
20928 cx.assert_state_with_diff(
20929 indoc! { "
20930 one
20931 - two
20932 + ˇTWO
20933 three
20934 four
20935 five
20936 "}
20937 .to_string(),
20938 );
20939 cx.update_editor(|editor, window, cx| {
20940 editor.move_up(&Default::default(), window, cx);
20941 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20942 });
20943 cx.assert_state_with_diff(
20944 indoc! { "
20945 one
20946 ˇTWO
20947 three
20948 four
20949 five
20950 "}
20951 .to_string(),
20952 );
20953}
20954
20955#[gpui::test]
20956async fn test_toggling_adjacent_diff_hunks_2(
20957 executor: BackgroundExecutor,
20958 cx: &mut TestAppContext,
20959) {
20960 init_test(cx, |_| {});
20961
20962 let mut cx = EditorTestContext::new(cx).await;
20963
20964 let diff_base = r#"
20965 lineA
20966 lineB
20967 lineC
20968 lineD
20969 "#
20970 .unindent();
20971
20972 cx.set_state(
20973 &r#"
20974 ˇlineA1
20975 lineB
20976 lineD
20977 "#
20978 .unindent(),
20979 );
20980 cx.set_head_text(&diff_base);
20981 executor.run_until_parked();
20982
20983 cx.update_editor(|editor, window, cx| {
20984 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20985 });
20986 executor.run_until_parked();
20987 cx.assert_state_with_diff(
20988 r#"
20989 - lineA
20990 + ˇlineA1
20991 lineB
20992 lineD
20993 "#
20994 .unindent(),
20995 );
20996
20997 cx.update_editor(|editor, window, cx| {
20998 editor.move_down(&MoveDown, window, cx);
20999 editor.move_right(&MoveRight, window, cx);
21000 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21001 });
21002 executor.run_until_parked();
21003 cx.assert_state_with_diff(
21004 r#"
21005 - lineA
21006 + lineA1
21007 lˇineB
21008 - lineC
21009 lineD
21010 "#
21011 .unindent(),
21012 );
21013}
21014
21015#[gpui::test]
21016async fn test_edits_around_expanded_deletion_hunks(
21017 executor: BackgroundExecutor,
21018 cx: &mut TestAppContext,
21019) {
21020 init_test(cx, |_| {});
21021
21022 let mut cx = EditorTestContext::new(cx).await;
21023
21024 let diff_base = r#"
21025 use some::mod1;
21026 use some::mod2;
21027
21028 const A: u32 = 42;
21029 const B: u32 = 42;
21030 const C: u32 = 42;
21031
21032
21033 fn main() {
21034 println!("hello");
21035
21036 println!("world");
21037 }
21038 "#
21039 .unindent();
21040 executor.run_until_parked();
21041 cx.set_state(
21042 &r#"
21043 use some::mod1;
21044 use some::mod2;
21045
21046 ˇconst B: u32 = 42;
21047 const C: u32 = 42;
21048
21049
21050 fn main() {
21051 println!("hello");
21052
21053 println!("world");
21054 }
21055 "#
21056 .unindent(),
21057 );
21058
21059 cx.set_head_text(&diff_base);
21060 executor.run_until_parked();
21061
21062 cx.update_editor(|editor, window, cx| {
21063 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21064 });
21065 executor.run_until_parked();
21066
21067 cx.assert_state_with_diff(
21068 r#"
21069 use some::mod1;
21070 use some::mod2;
21071
21072 - const A: u32 = 42;
21073 ˇconst B: u32 = 42;
21074 const C: u32 = 42;
21075
21076
21077 fn main() {
21078 println!("hello");
21079
21080 println!("world");
21081 }
21082 "#
21083 .unindent(),
21084 );
21085
21086 cx.update_editor(|editor, window, cx| {
21087 editor.delete_line(&DeleteLine, window, cx);
21088 });
21089 executor.run_until_parked();
21090 cx.assert_state_with_diff(
21091 r#"
21092 use some::mod1;
21093 use some::mod2;
21094
21095 - const A: u32 = 42;
21096 - const B: u32 = 42;
21097 ˇconst C: u32 = 42;
21098
21099
21100 fn main() {
21101 println!("hello");
21102
21103 println!("world");
21104 }
21105 "#
21106 .unindent(),
21107 );
21108
21109 cx.update_editor(|editor, window, cx| {
21110 editor.delete_line(&DeleteLine, window, cx);
21111 });
21112 executor.run_until_parked();
21113 cx.assert_state_with_diff(
21114 r#"
21115 use some::mod1;
21116 use some::mod2;
21117
21118 - const A: u32 = 42;
21119 - const B: u32 = 42;
21120 - const C: u32 = 42;
21121 ˇ
21122
21123 fn main() {
21124 println!("hello");
21125
21126 println!("world");
21127 }
21128 "#
21129 .unindent(),
21130 );
21131
21132 cx.update_editor(|editor, window, cx| {
21133 editor.handle_input("replacement", window, cx);
21134 });
21135 executor.run_until_parked();
21136 cx.assert_state_with_diff(
21137 r#"
21138 use some::mod1;
21139 use some::mod2;
21140
21141 - const A: u32 = 42;
21142 - const B: u32 = 42;
21143 - const C: u32 = 42;
21144 -
21145 + replacementˇ
21146
21147 fn main() {
21148 println!("hello");
21149
21150 println!("world");
21151 }
21152 "#
21153 .unindent(),
21154 );
21155}
21156
21157#[gpui::test]
21158async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21159 init_test(cx, |_| {});
21160
21161 let mut cx = EditorTestContext::new(cx).await;
21162
21163 let base_text = r#"
21164 one
21165 two
21166 three
21167 four
21168 five
21169 "#
21170 .unindent();
21171 executor.run_until_parked();
21172 cx.set_state(
21173 &r#"
21174 one
21175 two
21176 fˇour
21177 five
21178 "#
21179 .unindent(),
21180 );
21181
21182 cx.set_head_text(&base_text);
21183 executor.run_until_parked();
21184
21185 cx.update_editor(|editor, window, cx| {
21186 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21187 });
21188 executor.run_until_parked();
21189
21190 cx.assert_state_with_diff(
21191 r#"
21192 one
21193 two
21194 - three
21195 fˇour
21196 five
21197 "#
21198 .unindent(),
21199 );
21200
21201 cx.update_editor(|editor, window, cx| {
21202 editor.backspace(&Backspace, window, cx);
21203 editor.backspace(&Backspace, window, cx);
21204 });
21205 executor.run_until_parked();
21206 cx.assert_state_with_diff(
21207 r#"
21208 one
21209 two
21210 - threeˇ
21211 - four
21212 + our
21213 five
21214 "#
21215 .unindent(),
21216 );
21217}
21218
21219#[gpui::test]
21220async fn test_edit_after_expanded_modification_hunk(
21221 executor: BackgroundExecutor,
21222 cx: &mut TestAppContext,
21223) {
21224 init_test(cx, |_| {});
21225
21226 let mut cx = EditorTestContext::new(cx).await;
21227
21228 let diff_base = r#"
21229 use some::mod1;
21230 use some::mod2;
21231
21232 const A: u32 = 42;
21233 const B: u32 = 42;
21234 const C: u32 = 42;
21235 const D: u32 = 42;
21236
21237
21238 fn main() {
21239 println!("hello");
21240
21241 println!("world");
21242 }"#
21243 .unindent();
21244
21245 cx.set_state(
21246 &r#"
21247 use some::mod1;
21248 use some::mod2;
21249
21250 const A: u32 = 42;
21251 const B: u32 = 42;
21252 const C: u32 = 43ˇ
21253 const D: u32 = 42;
21254
21255
21256 fn main() {
21257 println!("hello");
21258
21259 println!("world");
21260 }"#
21261 .unindent(),
21262 );
21263
21264 cx.set_head_text(&diff_base);
21265 executor.run_until_parked();
21266 cx.update_editor(|editor, window, cx| {
21267 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21268 });
21269 executor.run_until_parked();
21270
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 + const C: u32 = 43ˇ
21280 const D: u32 = 42;
21281
21282
21283 fn main() {
21284 println!("hello");
21285
21286 println!("world");
21287 }"#
21288 .unindent(),
21289 );
21290
21291 cx.update_editor(|editor, window, cx| {
21292 editor.handle_input("\nnew_line\n", window, cx);
21293 });
21294 executor.run_until_parked();
21295
21296 cx.assert_state_with_diff(
21297 r#"
21298 use some::mod1;
21299 use some::mod2;
21300
21301 const A: u32 = 42;
21302 const B: u32 = 42;
21303 - const C: u32 = 42;
21304 + const C: u32 = 43
21305 + new_line
21306 + ˇ
21307 const D: u32 = 42;
21308
21309
21310 fn main() {
21311 println!("hello");
21312
21313 println!("world");
21314 }"#
21315 .unindent(),
21316 );
21317}
21318
21319#[gpui::test]
21320async fn test_stage_and_unstage_added_file_hunk(
21321 executor: BackgroundExecutor,
21322 cx: &mut TestAppContext,
21323) {
21324 init_test(cx, |_| {});
21325
21326 let mut cx = EditorTestContext::new(cx).await;
21327 cx.update_editor(|editor, _, cx| {
21328 editor.set_expand_all_diff_hunks(cx);
21329 });
21330
21331 let working_copy = r#"
21332 ˇfn main() {
21333 println!("hello, world!");
21334 }
21335 "#
21336 .unindent();
21337
21338 cx.set_state(&working_copy);
21339 executor.run_until_parked();
21340
21341 cx.assert_state_with_diff(
21342 r#"
21343 + ˇfn main() {
21344 + println!("hello, world!");
21345 + }
21346 "#
21347 .unindent(),
21348 );
21349 cx.assert_index_text(None);
21350
21351 cx.update_editor(|editor, window, cx| {
21352 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21353 });
21354 executor.run_until_parked();
21355 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
21356 cx.assert_state_with_diff(
21357 r#"
21358 + ˇfn main() {
21359 + println!("hello, world!");
21360 + }
21361 "#
21362 .unindent(),
21363 );
21364
21365 cx.update_editor(|editor, window, cx| {
21366 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21367 });
21368 executor.run_until_parked();
21369 cx.assert_index_text(None);
21370}
21371
21372async fn setup_indent_guides_editor(
21373 text: &str,
21374 cx: &mut TestAppContext,
21375) -> (BufferId, EditorTestContext) {
21376 init_test(cx, |_| {});
21377
21378 let mut cx = EditorTestContext::new(cx).await;
21379
21380 let buffer_id = cx.update_editor(|editor, window, cx| {
21381 editor.set_text(text, window, cx);
21382 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
21383
21384 buffer_ids[0]
21385 });
21386
21387 (buffer_id, cx)
21388}
21389
21390fn assert_indent_guides(
21391 range: Range<u32>,
21392 expected: Vec<IndentGuide>,
21393 active_indices: Option<Vec<usize>>,
21394 cx: &mut EditorTestContext,
21395) {
21396 let indent_guides = cx.update_editor(|editor, window, cx| {
21397 let snapshot = editor.snapshot(window, cx).display_snapshot;
21398 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
21399 editor,
21400 MultiBufferRow(range.start)..MultiBufferRow(range.end),
21401 true,
21402 &snapshot,
21403 cx,
21404 );
21405
21406 indent_guides.sort_by(|a, b| {
21407 a.depth.cmp(&b.depth).then(
21408 a.start_row
21409 .cmp(&b.start_row)
21410 .then(a.end_row.cmp(&b.end_row)),
21411 )
21412 });
21413 indent_guides
21414 });
21415
21416 if let Some(expected) = active_indices {
21417 let active_indices = cx.update_editor(|editor, window, cx| {
21418 let snapshot = editor.snapshot(window, cx).display_snapshot;
21419 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
21420 });
21421
21422 assert_eq!(
21423 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
21424 expected,
21425 "Active indent guide indices do not match"
21426 );
21427 }
21428
21429 assert_eq!(indent_guides, expected, "Indent guides do not match");
21430}
21431
21432fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
21433 IndentGuide {
21434 buffer_id,
21435 start_row: MultiBufferRow(start_row),
21436 end_row: MultiBufferRow(end_row),
21437 depth,
21438 tab_size: 4,
21439 settings: IndentGuideSettings {
21440 enabled: true,
21441 line_width: 1,
21442 active_line_width: 1,
21443 coloring: IndentGuideColoring::default(),
21444 background_coloring: IndentGuideBackgroundColoring::default(),
21445 },
21446 }
21447}
21448
21449#[gpui::test]
21450async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
21451 let (buffer_id, mut cx) = setup_indent_guides_editor(
21452 &"
21453 fn main() {
21454 let a = 1;
21455 }"
21456 .unindent(),
21457 cx,
21458 )
21459 .await;
21460
21461 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21462}
21463
21464#[gpui::test]
21465async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
21466 let (buffer_id, mut cx) = setup_indent_guides_editor(
21467 &"
21468 fn main() {
21469 let a = 1;
21470 let b = 2;
21471 }"
21472 .unindent(),
21473 cx,
21474 )
21475 .await;
21476
21477 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
21478}
21479
21480#[gpui::test]
21481async fn test_indent_guide_nested(cx: &mut TestAppContext) {
21482 let (buffer_id, mut cx) = setup_indent_guides_editor(
21483 &"
21484 fn main() {
21485 let a = 1;
21486 if a == 3 {
21487 let b = 2;
21488 } else {
21489 let c = 3;
21490 }
21491 }"
21492 .unindent(),
21493 cx,
21494 )
21495 .await;
21496
21497 assert_indent_guides(
21498 0..8,
21499 vec![
21500 indent_guide(buffer_id, 1, 6, 0),
21501 indent_guide(buffer_id, 3, 3, 1),
21502 indent_guide(buffer_id, 5, 5, 1),
21503 ],
21504 None,
21505 &mut cx,
21506 );
21507}
21508
21509#[gpui::test]
21510async fn test_indent_guide_tab(cx: &mut TestAppContext) {
21511 let (buffer_id, mut cx) = setup_indent_guides_editor(
21512 &"
21513 fn main() {
21514 let a = 1;
21515 let b = 2;
21516 let c = 3;
21517 }"
21518 .unindent(),
21519 cx,
21520 )
21521 .await;
21522
21523 assert_indent_guides(
21524 0..5,
21525 vec![
21526 indent_guide(buffer_id, 1, 3, 0),
21527 indent_guide(buffer_id, 2, 2, 1),
21528 ],
21529 None,
21530 &mut cx,
21531 );
21532}
21533
21534#[gpui::test]
21535async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
21536 let (buffer_id, mut cx) = setup_indent_guides_editor(
21537 &"
21538 fn main() {
21539 let a = 1;
21540
21541 let c = 3;
21542 }"
21543 .unindent(),
21544 cx,
21545 )
21546 .await;
21547
21548 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
21549}
21550
21551#[gpui::test]
21552async fn test_indent_guide_complex(cx: &mut TestAppContext) {
21553 let (buffer_id, mut cx) = setup_indent_guides_editor(
21554 &"
21555 fn main() {
21556 let a = 1;
21557
21558 let c = 3;
21559
21560 if a == 3 {
21561 let b = 2;
21562 } else {
21563 let c = 3;
21564 }
21565 }"
21566 .unindent(),
21567 cx,
21568 )
21569 .await;
21570
21571 assert_indent_guides(
21572 0..11,
21573 vec![
21574 indent_guide(buffer_id, 1, 9, 0),
21575 indent_guide(buffer_id, 6, 6, 1),
21576 indent_guide(buffer_id, 8, 8, 1),
21577 ],
21578 None,
21579 &mut cx,
21580 );
21581}
21582
21583#[gpui::test]
21584async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
21585 let (buffer_id, mut cx) = setup_indent_guides_editor(
21586 &"
21587 fn main() {
21588 let a = 1;
21589
21590 let c = 3;
21591
21592 if a == 3 {
21593 let b = 2;
21594 } else {
21595 let c = 3;
21596 }
21597 }"
21598 .unindent(),
21599 cx,
21600 )
21601 .await;
21602
21603 assert_indent_guides(
21604 1..11,
21605 vec![
21606 indent_guide(buffer_id, 1, 9, 0),
21607 indent_guide(buffer_id, 6, 6, 1),
21608 indent_guide(buffer_id, 8, 8, 1),
21609 ],
21610 None,
21611 &mut cx,
21612 );
21613}
21614
21615#[gpui::test]
21616async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
21617 let (buffer_id, mut cx) = setup_indent_guides_editor(
21618 &"
21619 fn main() {
21620 let a = 1;
21621
21622 let c = 3;
21623
21624 if a == 3 {
21625 let b = 2;
21626 } else {
21627 let c = 3;
21628 }
21629 }"
21630 .unindent(),
21631 cx,
21632 )
21633 .await;
21634
21635 assert_indent_guides(
21636 1..10,
21637 vec![
21638 indent_guide(buffer_id, 1, 9, 0),
21639 indent_guide(buffer_id, 6, 6, 1),
21640 indent_guide(buffer_id, 8, 8, 1),
21641 ],
21642 None,
21643 &mut cx,
21644 );
21645}
21646
21647#[gpui::test]
21648async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
21649 let (buffer_id, mut cx) = setup_indent_guides_editor(
21650 &"
21651 fn main() {
21652 if a {
21653 b(
21654 c,
21655 d,
21656 )
21657 } else {
21658 e(
21659 f
21660 )
21661 }
21662 }"
21663 .unindent(),
21664 cx,
21665 )
21666 .await;
21667
21668 assert_indent_guides(
21669 0..11,
21670 vec![
21671 indent_guide(buffer_id, 1, 10, 0),
21672 indent_guide(buffer_id, 2, 5, 1),
21673 indent_guide(buffer_id, 7, 9, 1),
21674 indent_guide(buffer_id, 3, 4, 2),
21675 indent_guide(buffer_id, 8, 8, 2),
21676 ],
21677 None,
21678 &mut cx,
21679 );
21680
21681 cx.update_editor(|editor, window, cx| {
21682 editor.fold_at(MultiBufferRow(2), window, cx);
21683 assert_eq!(
21684 editor.display_text(cx),
21685 "
21686 fn main() {
21687 if a {
21688 b(⋯
21689 )
21690 } else {
21691 e(
21692 f
21693 )
21694 }
21695 }"
21696 .unindent()
21697 );
21698 });
21699
21700 assert_indent_guides(
21701 0..11,
21702 vec![
21703 indent_guide(buffer_id, 1, 10, 0),
21704 indent_guide(buffer_id, 2, 5, 1),
21705 indent_guide(buffer_id, 7, 9, 1),
21706 indent_guide(buffer_id, 8, 8, 2),
21707 ],
21708 None,
21709 &mut cx,
21710 );
21711}
21712
21713#[gpui::test]
21714async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
21715 let (buffer_id, mut cx) = setup_indent_guides_editor(
21716 &"
21717 block1
21718 block2
21719 block3
21720 block4
21721 block2
21722 block1
21723 block1"
21724 .unindent(),
21725 cx,
21726 )
21727 .await;
21728
21729 assert_indent_guides(
21730 1..10,
21731 vec![
21732 indent_guide(buffer_id, 1, 4, 0),
21733 indent_guide(buffer_id, 2, 3, 1),
21734 indent_guide(buffer_id, 3, 3, 2),
21735 ],
21736 None,
21737 &mut cx,
21738 );
21739}
21740
21741#[gpui::test]
21742async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
21743 let (buffer_id, mut cx) = setup_indent_guides_editor(
21744 &"
21745 block1
21746 block2
21747 block3
21748
21749 block1
21750 block1"
21751 .unindent(),
21752 cx,
21753 )
21754 .await;
21755
21756 assert_indent_guides(
21757 0..6,
21758 vec![
21759 indent_guide(buffer_id, 1, 2, 0),
21760 indent_guide(buffer_id, 2, 2, 1),
21761 ],
21762 None,
21763 &mut cx,
21764 );
21765}
21766
21767#[gpui::test]
21768async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
21769 let (buffer_id, mut cx) = setup_indent_guides_editor(
21770 &"
21771 function component() {
21772 \treturn (
21773 \t\t\t
21774 \t\t<div>
21775 \t\t\t<abc></abc>
21776 \t\t</div>
21777 \t)
21778 }"
21779 .unindent(),
21780 cx,
21781 )
21782 .await;
21783
21784 assert_indent_guides(
21785 0..8,
21786 vec![
21787 indent_guide(buffer_id, 1, 6, 0),
21788 indent_guide(buffer_id, 2, 5, 1),
21789 indent_guide(buffer_id, 4, 4, 2),
21790 ],
21791 None,
21792 &mut cx,
21793 );
21794}
21795
21796#[gpui::test]
21797async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
21798 let (buffer_id, mut cx) = setup_indent_guides_editor(
21799 &"
21800 function component() {
21801 \treturn (
21802 \t
21803 \t\t<div>
21804 \t\t\t<abc></abc>
21805 \t\t</div>
21806 \t)
21807 }"
21808 .unindent(),
21809 cx,
21810 )
21811 .await;
21812
21813 assert_indent_guides(
21814 0..8,
21815 vec![
21816 indent_guide(buffer_id, 1, 6, 0),
21817 indent_guide(buffer_id, 2, 5, 1),
21818 indent_guide(buffer_id, 4, 4, 2),
21819 ],
21820 None,
21821 &mut cx,
21822 );
21823}
21824
21825#[gpui::test]
21826async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
21827 let (buffer_id, mut cx) = setup_indent_guides_editor(
21828 &"
21829 block1
21830
21831
21832
21833 block2
21834 "
21835 .unindent(),
21836 cx,
21837 )
21838 .await;
21839
21840 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21841}
21842
21843#[gpui::test]
21844async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
21845 let (buffer_id, mut cx) = setup_indent_guides_editor(
21846 &"
21847 def a:
21848 \tb = 3
21849 \tif True:
21850 \t\tc = 4
21851 \t\td = 5
21852 \tprint(b)
21853 "
21854 .unindent(),
21855 cx,
21856 )
21857 .await;
21858
21859 assert_indent_guides(
21860 0..6,
21861 vec![
21862 indent_guide(buffer_id, 1, 5, 0),
21863 indent_guide(buffer_id, 3, 4, 1),
21864 ],
21865 None,
21866 &mut cx,
21867 );
21868}
21869
21870#[gpui::test]
21871async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
21872 let (buffer_id, mut cx) = setup_indent_guides_editor(
21873 &"
21874 fn main() {
21875 let a = 1;
21876 }"
21877 .unindent(),
21878 cx,
21879 )
21880 .await;
21881
21882 cx.update_editor(|editor, window, cx| {
21883 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21884 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21885 });
21886 });
21887
21888 assert_indent_guides(
21889 0..3,
21890 vec![indent_guide(buffer_id, 1, 1, 0)],
21891 Some(vec![0]),
21892 &mut cx,
21893 );
21894}
21895
21896#[gpui::test]
21897async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
21898 let (buffer_id, mut cx) = setup_indent_guides_editor(
21899 &"
21900 fn main() {
21901 if 1 == 2 {
21902 let a = 1;
21903 }
21904 }"
21905 .unindent(),
21906 cx,
21907 )
21908 .await;
21909
21910 cx.update_editor(|editor, window, cx| {
21911 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21912 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21913 });
21914 });
21915
21916 assert_indent_guides(
21917 0..4,
21918 vec![
21919 indent_guide(buffer_id, 1, 3, 0),
21920 indent_guide(buffer_id, 2, 2, 1),
21921 ],
21922 Some(vec![1]),
21923 &mut cx,
21924 );
21925
21926 cx.update_editor(|editor, window, cx| {
21927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21928 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21929 });
21930 });
21931
21932 assert_indent_guides(
21933 0..4,
21934 vec![
21935 indent_guide(buffer_id, 1, 3, 0),
21936 indent_guide(buffer_id, 2, 2, 1),
21937 ],
21938 Some(vec![1]),
21939 &mut cx,
21940 );
21941
21942 cx.update_editor(|editor, window, cx| {
21943 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21944 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21945 });
21946 });
21947
21948 assert_indent_guides(
21949 0..4,
21950 vec![
21951 indent_guide(buffer_id, 1, 3, 0),
21952 indent_guide(buffer_id, 2, 2, 1),
21953 ],
21954 Some(vec![0]),
21955 &mut cx,
21956 );
21957}
21958
21959#[gpui::test]
21960async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21961 let (buffer_id, mut cx) = setup_indent_guides_editor(
21962 &"
21963 fn main() {
21964 let a = 1;
21965
21966 let b = 2;
21967 }"
21968 .unindent(),
21969 cx,
21970 )
21971 .await;
21972
21973 cx.update_editor(|editor, window, cx| {
21974 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21975 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21976 });
21977 });
21978
21979 assert_indent_guides(
21980 0..5,
21981 vec![indent_guide(buffer_id, 1, 3, 0)],
21982 Some(vec![0]),
21983 &mut cx,
21984 );
21985}
21986
21987#[gpui::test]
21988async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21989 let (buffer_id, mut cx) = setup_indent_guides_editor(
21990 &"
21991 def m:
21992 a = 1
21993 pass"
21994 .unindent(),
21995 cx,
21996 )
21997 .await;
21998
21999 cx.update_editor(|editor, window, cx| {
22000 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22001 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22002 });
22003 });
22004
22005 assert_indent_guides(
22006 0..3,
22007 vec![indent_guide(buffer_id, 1, 2, 0)],
22008 Some(vec![0]),
22009 &mut cx,
22010 );
22011}
22012
22013#[gpui::test]
22014async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
22015 init_test(cx, |_| {});
22016 let mut cx = EditorTestContext::new(cx).await;
22017 let text = indoc! {
22018 "
22019 impl A {
22020 fn b() {
22021 0;
22022 3;
22023 5;
22024 6;
22025 7;
22026 }
22027 }
22028 "
22029 };
22030 let base_text = indoc! {
22031 "
22032 impl A {
22033 fn b() {
22034 0;
22035 1;
22036 2;
22037 3;
22038 4;
22039 }
22040 fn c() {
22041 5;
22042 6;
22043 7;
22044 }
22045 }
22046 "
22047 };
22048
22049 cx.update_editor(|editor, window, cx| {
22050 editor.set_text(text, window, cx);
22051
22052 editor.buffer().update(cx, |multibuffer, cx| {
22053 let buffer = multibuffer.as_singleton().unwrap();
22054 let diff = cx.new(|cx| {
22055 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
22056 });
22057
22058 multibuffer.set_all_diff_hunks_expanded(cx);
22059 multibuffer.add_diff(diff, cx);
22060
22061 buffer.read(cx).remote_id()
22062 })
22063 });
22064 cx.run_until_parked();
22065
22066 cx.assert_state_with_diff(
22067 indoc! { "
22068 impl A {
22069 fn b() {
22070 0;
22071 - 1;
22072 - 2;
22073 3;
22074 - 4;
22075 - }
22076 - fn c() {
22077 5;
22078 6;
22079 7;
22080 }
22081 }
22082 ˇ"
22083 }
22084 .to_string(),
22085 );
22086
22087 let mut actual_guides = cx.update_editor(|editor, window, cx| {
22088 editor
22089 .snapshot(window, cx)
22090 .buffer_snapshot()
22091 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
22092 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
22093 .collect::<Vec<_>>()
22094 });
22095 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
22096 assert_eq!(
22097 actual_guides,
22098 vec![
22099 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
22100 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
22101 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
22102 ]
22103 );
22104}
22105
22106#[gpui::test]
22107async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22108 init_test(cx, |_| {});
22109 let mut cx = EditorTestContext::new(cx).await;
22110
22111 let diff_base = r#"
22112 a
22113 b
22114 c
22115 "#
22116 .unindent();
22117
22118 cx.set_state(
22119 &r#"
22120 ˇA
22121 b
22122 C
22123 "#
22124 .unindent(),
22125 );
22126 cx.set_head_text(&diff_base);
22127 cx.update_editor(|editor, window, cx| {
22128 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22129 });
22130 executor.run_until_parked();
22131
22132 let both_hunks_expanded = r#"
22133 - a
22134 + ˇA
22135 b
22136 - c
22137 + C
22138 "#
22139 .unindent();
22140
22141 cx.assert_state_with_diff(both_hunks_expanded.clone());
22142
22143 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22144 let snapshot = editor.snapshot(window, cx);
22145 let hunks = editor
22146 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22147 .collect::<Vec<_>>();
22148 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22149 hunks
22150 .into_iter()
22151 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22152 .collect::<Vec<_>>()
22153 });
22154 assert_eq!(hunk_ranges.len(), 2);
22155
22156 cx.update_editor(|editor, _, cx| {
22157 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22158 });
22159 executor.run_until_parked();
22160
22161 let second_hunk_expanded = r#"
22162 ˇA
22163 b
22164 - c
22165 + C
22166 "#
22167 .unindent();
22168
22169 cx.assert_state_with_diff(second_hunk_expanded);
22170
22171 cx.update_editor(|editor, _, cx| {
22172 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22173 });
22174 executor.run_until_parked();
22175
22176 cx.assert_state_with_diff(both_hunks_expanded.clone());
22177
22178 cx.update_editor(|editor, _, cx| {
22179 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22180 });
22181 executor.run_until_parked();
22182
22183 let first_hunk_expanded = r#"
22184 - a
22185 + ˇA
22186 b
22187 C
22188 "#
22189 .unindent();
22190
22191 cx.assert_state_with_diff(first_hunk_expanded);
22192
22193 cx.update_editor(|editor, _, cx| {
22194 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22195 });
22196 executor.run_until_parked();
22197
22198 cx.assert_state_with_diff(both_hunks_expanded);
22199
22200 cx.set_state(
22201 &r#"
22202 ˇA
22203 b
22204 "#
22205 .unindent(),
22206 );
22207 cx.run_until_parked();
22208
22209 // TODO this cursor position seems bad
22210 cx.assert_state_with_diff(
22211 r#"
22212 - ˇa
22213 + A
22214 b
22215 "#
22216 .unindent(),
22217 );
22218
22219 cx.update_editor(|editor, window, cx| {
22220 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22221 });
22222
22223 cx.assert_state_with_diff(
22224 r#"
22225 - ˇa
22226 + A
22227 b
22228 - c
22229 "#
22230 .unindent(),
22231 );
22232
22233 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22234 let snapshot = editor.snapshot(window, cx);
22235 let hunks = editor
22236 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22237 .collect::<Vec<_>>();
22238 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22239 hunks
22240 .into_iter()
22241 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22242 .collect::<Vec<_>>()
22243 });
22244 assert_eq!(hunk_ranges.len(), 2);
22245
22246 cx.update_editor(|editor, _, cx| {
22247 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22248 });
22249 executor.run_until_parked();
22250
22251 cx.assert_state_with_diff(
22252 r#"
22253 - ˇa
22254 + A
22255 b
22256 "#
22257 .unindent(),
22258 );
22259}
22260
22261#[gpui::test]
22262async fn test_toggle_deletion_hunk_at_start_of_file(
22263 executor: BackgroundExecutor,
22264 cx: &mut TestAppContext,
22265) {
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 ˇb
22279 c
22280 "#
22281 .unindent(),
22282 );
22283 cx.set_head_text(&diff_base);
22284 cx.update_editor(|editor, window, cx| {
22285 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22286 });
22287 executor.run_until_parked();
22288
22289 let hunk_expanded = r#"
22290 - a
22291 ˇb
22292 c
22293 "#
22294 .unindent();
22295
22296 cx.assert_state_with_diff(hunk_expanded.clone());
22297
22298 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22299 let snapshot = editor.snapshot(window, cx);
22300 let hunks = editor
22301 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22302 .collect::<Vec<_>>();
22303 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22304 hunks
22305 .into_iter()
22306 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22307 .collect::<Vec<_>>()
22308 });
22309 assert_eq!(hunk_ranges.len(), 1);
22310
22311 cx.update_editor(|editor, _, cx| {
22312 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22313 });
22314 executor.run_until_parked();
22315
22316 let hunk_collapsed = r#"
22317 ˇb
22318 c
22319 "#
22320 .unindent();
22321
22322 cx.assert_state_with_diff(hunk_collapsed);
22323
22324 cx.update_editor(|editor, _, cx| {
22325 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22326 });
22327 executor.run_until_parked();
22328
22329 cx.assert_state_with_diff(hunk_expanded);
22330}
22331
22332#[gpui::test]
22333async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
22334 executor: BackgroundExecutor,
22335 cx: &mut TestAppContext,
22336) {
22337 init_test(cx, |_| {});
22338 let mut cx = EditorTestContext::new(cx).await;
22339
22340 cx.set_state("ˇnew\nsecond\nthird\n");
22341 cx.set_head_text("old\nsecond\nthird\n");
22342 cx.update_editor(|editor, window, cx| {
22343 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
22344 });
22345 executor.run_until_parked();
22346 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22347
22348 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
22349 cx.update_editor(|editor, window, cx| {
22350 let snapshot = editor.snapshot(window, cx);
22351 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22352 let hunks = editor
22353 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22354 .collect::<Vec<_>>();
22355 assert_eq!(hunks.len(), 1);
22356 let hunk_range = Anchor::range_in_buffer(excerpt_id, hunks[0].buffer_range.clone());
22357 editor.toggle_single_diff_hunk(hunk_range, cx)
22358 });
22359 executor.run_until_parked();
22360 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
22361
22362 // Keep the editor scrolled to the top so the full hunk remains visible.
22363 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22364}
22365
22366#[gpui::test]
22367async fn test_display_diff_hunks(cx: &mut TestAppContext) {
22368 init_test(cx, |_| {});
22369
22370 let fs = FakeFs::new(cx.executor());
22371 fs.insert_tree(
22372 path!("/test"),
22373 json!({
22374 ".git": {},
22375 "file-1": "ONE\n",
22376 "file-2": "TWO\n",
22377 "file-3": "THREE\n",
22378 }),
22379 )
22380 .await;
22381
22382 fs.set_head_for_repo(
22383 path!("/test/.git").as_ref(),
22384 &[
22385 ("file-1", "one\n".into()),
22386 ("file-2", "two\n".into()),
22387 ("file-3", "three\n".into()),
22388 ],
22389 "deadbeef",
22390 );
22391
22392 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
22393 let mut buffers = vec![];
22394 for i in 1..=3 {
22395 let buffer = project
22396 .update(cx, |project, cx| {
22397 let path = format!(path!("/test/file-{}"), i);
22398 project.open_local_buffer(path, cx)
22399 })
22400 .await
22401 .unwrap();
22402 buffers.push(buffer);
22403 }
22404
22405 let multibuffer = cx.new(|cx| {
22406 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
22407 multibuffer.set_all_diff_hunks_expanded(cx);
22408 for buffer in &buffers {
22409 let snapshot = buffer.read(cx).snapshot();
22410 multibuffer.set_excerpts_for_path(
22411 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
22412 buffer.clone(),
22413 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
22414 2,
22415 cx,
22416 );
22417 }
22418 multibuffer
22419 });
22420
22421 let editor = cx.add_window(|window, cx| {
22422 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
22423 });
22424 cx.run_until_parked();
22425
22426 let snapshot = editor
22427 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22428 .unwrap();
22429 let hunks = snapshot
22430 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
22431 .map(|hunk| match hunk {
22432 DisplayDiffHunk::Unfolded {
22433 display_row_range, ..
22434 } => display_row_range,
22435 DisplayDiffHunk::Folded { .. } => unreachable!(),
22436 })
22437 .collect::<Vec<_>>();
22438 assert_eq!(
22439 hunks,
22440 [
22441 DisplayRow(2)..DisplayRow(4),
22442 DisplayRow(7)..DisplayRow(9),
22443 DisplayRow(12)..DisplayRow(14),
22444 ]
22445 );
22446}
22447
22448#[gpui::test]
22449async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
22450 init_test(cx, |_| {});
22451
22452 let mut cx = EditorTestContext::new(cx).await;
22453 cx.set_head_text(indoc! { "
22454 one
22455 two
22456 three
22457 four
22458 five
22459 "
22460 });
22461 cx.set_index_text(indoc! { "
22462 one
22463 two
22464 three
22465 four
22466 five
22467 "
22468 });
22469 cx.set_state(indoc! {"
22470 one
22471 TWO
22472 ˇTHREE
22473 FOUR
22474 five
22475 "});
22476 cx.run_until_parked();
22477 cx.update_editor(|editor, window, cx| {
22478 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22479 });
22480 cx.run_until_parked();
22481 cx.assert_index_text(Some(indoc! {"
22482 one
22483 TWO
22484 THREE
22485 FOUR
22486 five
22487 "}));
22488 cx.set_state(indoc! { "
22489 one
22490 TWO
22491 ˇTHREE-HUNDRED
22492 FOUR
22493 five
22494 "});
22495 cx.run_until_parked();
22496 cx.update_editor(|editor, window, cx| {
22497 let snapshot = editor.snapshot(window, cx);
22498 let hunks = editor
22499 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22500 .collect::<Vec<_>>();
22501 assert_eq!(hunks.len(), 1);
22502 assert_eq!(
22503 hunks[0].status(),
22504 DiffHunkStatus {
22505 kind: DiffHunkStatusKind::Modified,
22506 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
22507 }
22508 );
22509
22510 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22511 });
22512 cx.run_until_parked();
22513 cx.assert_index_text(Some(indoc! {"
22514 one
22515 TWO
22516 THREE-HUNDRED
22517 FOUR
22518 five
22519 "}));
22520}
22521
22522#[gpui::test]
22523fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
22524 init_test(cx, |_| {});
22525
22526 let editor = cx.add_window(|window, cx| {
22527 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
22528 build_editor(buffer, window, cx)
22529 });
22530
22531 let render_args = Arc::new(Mutex::new(None));
22532 let snapshot = editor
22533 .update(cx, |editor, window, cx| {
22534 let snapshot = editor.buffer().read(cx).snapshot(cx);
22535 let range =
22536 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
22537
22538 struct RenderArgs {
22539 row: MultiBufferRow,
22540 folded: bool,
22541 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
22542 }
22543
22544 let crease = Crease::inline(
22545 range,
22546 FoldPlaceholder::test(),
22547 {
22548 let toggle_callback = render_args.clone();
22549 move |row, folded, callback, _window, _cx| {
22550 *toggle_callback.lock() = Some(RenderArgs {
22551 row,
22552 folded,
22553 callback,
22554 });
22555 div()
22556 }
22557 },
22558 |_row, _folded, _window, _cx| div(),
22559 );
22560
22561 editor.insert_creases(Some(crease), cx);
22562 let snapshot = editor.snapshot(window, cx);
22563 let _div =
22564 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
22565 snapshot
22566 })
22567 .unwrap();
22568
22569 let render_args = render_args.lock().take().unwrap();
22570 assert_eq!(render_args.row, MultiBufferRow(1));
22571 assert!(!render_args.folded);
22572 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22573
22574 cx.update_window(*editor, |_, window, cx| {
22575 (render_args.callback)(true, window, cx)
22576 })
22577 .unwrap();
22578 let snapshot = editor
22579 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22580 .unwrap();
22581 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
22582
22583 cx.update_window(*editor, |_, window, cx| {
22584 (render_args.callback)(false, window, cx)
22585 })
22586 .unwrap();
22587 let snapshot = editor
22588 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22589 .unwrap();
22590 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22591}
22592
22593#[gpui::test]
22594async fn test_input_text(cx: &mut TestAppContext) {
22595 init_test(cx, |_| {});
22596 let mut cx = EditorTestContext::new(cx).await;
22597
22598 cx.set_state(
22599 &r#"ˇone
22600 two
22601
22602 three
22603 fourˇ
22604 five
22605
22606 siˇx"#
22607 .unindent(),
22608 );
22609
22610 cx.dispatch_action(HandleInput(String::new()));
22611 cx.assert_editor_state(
22612 &r#"ˇone
22613 two
22614
22615 three
22616 fourˇ
22617 five
22618
22619 siˇx"#
22620 .unindent(),
22621 );
22622
22623 cx.dispatch_action(HandleInput("AAAA".to_string()));
22624 cx.assert_editor_state(
22625 &r#"AAAAˇone
22626 two
22627
22628 three
22629 fourAAAAˇ
22630 five
22631
22632 siAAAAˇx"#
22633 .unindent(),
22634 );
22635}
22636
22637#[gpui::test]
22638async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22639 init_test(cx, |_| {});
22640
22641 let mut cx = EditorTestContext::new(cx).await;
22642 cx.set_state(
22643 r#"let foo = 1;
22644let foo = 2;
22645let foo = 3;
22646let fooˇ = 4;
22647let foo = 5;
22648let foo = 6;
22649let foo = 7;
22650let foo = 8;
22651let foo = 9;
22652let foo = 10;
22653let foo = 11;
22654let foo = 12;
22655let foo = 13;
22656let foo = 14;
22657let foo = 15;"#,
22658 );
22659
22660 cx.update_editor(|e, window, cx| {
22661 assert_eq!(
22662 e.next_scroll_position,
22663 NextScrollCursorCenterTopBottom::Center,
22664 "Default next scroll direction is center",
22665 );
22666
22667 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22668 assert_eq!(
22669 e.next_scroll_position,
22670 NextScrollCursorCenterTopBottom::Top,
22671 "After center, next scroll direction should be top",
22672 );
22673
22674 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22675 assert_eq!(
22676 e.next_scroll_position,
22677 NextScrollCursorCenterTopBottom::Bottom,
22678 "After top, next scroll direction should be bottom",
22679 );
22680
22681 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22682 assert_eq!(
22683 e.next_scroll_position,
22684 NextScrollCursorCenterTopBottom::Center,
22685 "After bottom, scrolling should start over",
22686 );
22687
22688 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22689 assert_eq!(
22690 e.next_scroll_position,
22691 NextScrollCursorCenterTopBottom::Top,
22692 "Scrolling continues if retriggered fast enough"
22693 );
22694 });
22695
22696 cx.executor()
22697 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22698 cx.executor().run_until_parked();
22699 cx.update_editor(|e, _, _| {
22700 assert_eq!(
22701 e.next_scroll_position,
22702 NextScrollCursorCenterTopBottom::Center,
22703 "If scrolling is not triggered fast enough, it should reset"
22704 );
22705 });
22706}
22707
22708#[gpui::test]
22709async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22710 init_test(cx, |_| {});
22711 let mut cx = EditorLspTestContext::new_rust(
22712 lsp::ServerCapabilities {
22713 definition_provider: Some(lsp::OneOf::Left(true)),
22714 references_provider: Some(lsp::OneOf::Left(true)),
22715 ..lsp::ServerCapabilities::default()
22716 },
22717 cx,
22718 )
22719 .await;
22720
22721 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22722 let go_to_definition = cx
22723 .lsp
22724 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22725 move |params, _| async move {
22726 if empty_go_to_definition {
22727 Ok(None)
22728 } else {
22729 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22730 uri: params.text_document_position_params.text_document.uri,
22731 range: lsp::Range::new(
22732 lsp::Position::new(4, 3),
22733 lsp::Position::new(4, 6),
22734 ),
22735 })))
22736 }
22737 },
22738 );
22739 let references = cx
22740 .lsp
22741 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22742 Ok(Some(vec![lsp::Location {
22743 uri: params.text_document_position.text_document.uri,
22744 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22745 }]))
22746 });
22747 (go_to_definition, references)
22748 };
22749
22750 cx.set_state(
22751 &r#"fn one() {
22752 let mut a = ˇtwo();
22753 }
22754
22755 fn two() {}"#
22756 .unindent(),
22757 );
22758 set_up_lsp_handlers(false, &mut cx);
22759 let navigated = cx
22760 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22761 .await
22762 .expect("Failed to navigate to definition");
22763 assert_eq!(
22764 navigated,
22765 Navigated::Yes,
22766 "Should have navigated to definition from the GetDefinition response"
22767 );
22768 cx.assert_editor_state(
22769 &r#"fn one() {
22770 let mut a = two();
22771 }
22772
22773 fn «twoˇ»() {}"#
22774 .unindent(),
22775 );
22776
22777 let editors = cx.update_workspace(|workspace, _, cx| {
22778 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22779 });
22780 cx.update_editor(|_, _, test_editor_cx| {
22781 assert_eq!(
22782 editors.len(),
22783 1,
22784 "Initially, only one, test, editor should be open in the workspace"
22785 );
22786 assert_eq!(
22787 test_editor_cx.entity(),
22788 editors.last().expect("Asserted len is 1").clone()
22789 );
22790 });
22791
22792 set_up_lsp_handlers(true, &mut cx);
22793 let navigated = cx
22794 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22795 .await
22796 .expect("Failed to navigate to lookup references");
22797 assert_eq!(
22798 navigated,
22799 Navigated::Yes,
22800 "Should have navigated to references as a fallback after empty GoToDefinition response"
22801 );
22802 // We should not change the selections in the existing file,
22803 // if opening another milti buffer with the references
22804 cx.assert_editor_state(
22805 &r#"fn one() {
22806 let mut a = two();
22807 }
22808
22809 fn «twoˇ»() {}"#
22810 .unindent(),
22811 );
22812 let editors = cx.update_workspace(|workspace, _, cx| {
22813 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22814 });
22815 cx.update_editor(|_, _, test_editor_cx| {
22816 assert_eq!(
22817 editors.len(),
22818 2,
22819 "After falling back to references search, we open a new editor with the results"
22820 );
22821 let references_fallback_text = editors
22822 .into_iter()
22823 .find(|new_editor| *new_editor != test_editor_cx.entity())
22824 .expect("Should have one non-test editor now")
22825 .read(test_editor_cx)
22826 .text(test_editor_cx);
22827 assert_eq!(
22828 references_fallback_text, "fn one() {\n let mut a = two();\n}",
22829 "Should use the range from the references response and not the GoToDefinition one"
22830 );
22831 });
22832}
22833
22834#[gpui::test]
22835async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
22836 init_test(cx, |_| {});
22837 cx.update(|cx| {
22838 let mut editor_settings = EditorSettings::get_global(cx).clone();
22839 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
22840 EditorSettings::override_global(editor_settings, cx);
22841 });
22842 let mut cx = EditorLspTestContext::new_rust(
22843 lsp::ServerCapabilities {
22844 definition_provider: Some(lsp::OneOf::Left(true)),
22845 references_provider: Some(lsp::OneOf::Left(true)),
22846 ..lsp::ServerCapabilities::default()
22847 },
22848 cx,
22849 )
22850 .await;
22851 let original_state = r#"fn one() {
22852 let mut a = ˇtwo();
22853 }
22854
22855 fn two() {}"#
22856 .unindent();
22857 cx.set_state(&original_state);
22858
22859 let mut go_to_definition = cx
22860 .lsp
22861 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22862 move |_, _| async move { Ok(None) },
22863 );
22864 let _references = cx
22865 .lsp
22866 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
22867 panic!("Should not call for references with no go to definition fallback")
22868 });
22869
22870 let navigated = cx
22871 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22872 .await
22873 .expect("Failed to navigate to lookup references");
22874 go_to_definition
22875 .next()
22876 .await
22877 .expect("Should have called the go_to_definition handler");
22878
22879 assert_eq!(
22880 navigated,
22881 Navigated::No,
22882 "Should have navigated to references as a fallback after empty GoToDefinition response"
22883 );
22884 cx.assert_editor_state(&original_state);
22885 let editors = cx.update_workspace(|workspace, _, cx| {
22886 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22887 });
22888 cx.update_editor(|_, _, _| {
22889 assert_eq!(
22890 editors.len(),
22891 1,
22892 "After unsuccessful fallback, no other editor should have been opened"
22893 );
22894 });
22895}
22896
22897#[gpui::test]
22898async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
22899 init_test(cx, |_| {});
22900 let mut cx = EditorLspTestContext::new_rust(
22901 lsp::ServerCapabilities {
22902 references_provider: Some(lsp::OneOf::Left(true)),
22903 ..lsp::ServerCapabilities::default()
22904 },
22905 cx,
22906 )
22907 .await;
22908
22909 cx.set_state(
22910 &r#"
22911 fn one() {
22912 let mut a = two();
22913 }
22914
22915 fn ˇtwo() {}"#
22916 .unindent(),
22917 );
22918 cx.lsp
22919 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22920 Ok(Some(vec![
22921 lsp::Location {
22922 uri: params.text_document_position.text_document.uri.clone(),
22923 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22924 },
22925 lsp::Location {
22926 uri: params.text_document_position.text_document.uri,
22927 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
22928 },
22929 ]))
22930 });
22931 let navigated = cx
22932 .update_editor(|editor, window, cx| {
22933 editor.find_all_references(&FindAllReferences::default(), window, cx)
22934 })
22935 .unwrap()
22936 .await
22937 .expect("Failed to navigate to references");
22938 assert_eq!(
22939 navigated,
22940 Navigated::Yes,
22941 "Should have navigated to references from the FindAllReferences response"
22942 );
22943 cx.assert_editor_state(
22944 &r#"fn one() {
22945 let mut a = two();
22946 }
22947
22948 fn ˇtwo() {}"#
22949 .unindent(),
22950 );
22951
22952 let editors = cx.update_workspace(|workspace, _, cx| {
22953 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22954 });
22955 cx.update_editor(|_, _, _| {
22956 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
22957 });
22958
22959 cx.set_state(
22960 &r#"fn one() {
22961 let mut a = ˇtwo();
22962 }
22963
22964 fn two() {}"#
22965 .unindent(),
22966 );
22967 let navigated = cx
22968 .update_editor(|editor, window, cx| {
22969 editor.find_all_references(&FindAllReferences::default(), window, cx)
22970 })
22971 .unwrap()
22972 .await
22973 .expect("Failed to navigate to references");
22974 assert_eq!(
22975 navigated,
22976 Navigated::Yes,
22977 "Should have navigated to references from the FindAllReferences response"
22978 );
22979 cx.assert_editor_state(
22980 &r#"fn one() {
22981 let mut a = ˇtwo();
22982 }
22983
22984 fn two() {}"#
22985 .unindent(),
22986 );
22987 let editors = cx.update_workspace(|workspace, _, cx| {
22988 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22989 });
22990 cx.update_editor(|_, _, _| {
22991 assert_eq!(
22992 editors.len(),
22993 2,
22994 "should have re-used the previous multibuffer"
22995 );
22996 });
22997
22998 cx.set_state(
22999 &r#"fn one() {
23000 let mut a = ˇtwo();
23001 }
23002 fn three() {}
23003 fn two() {}"#
23004 .unindent(),
23005 );
23006 cx.lsp
23007 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23008 Ok(Some(vec![
23009 lsp::Location {
23010 uri: params.text_document_position.text_document.uri.clone(),
23011 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
23012 },
23013 lsp::Location {
23014 uri: params.text_document_position.text_document.uri,
23015 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
23016 },
23017 ]))
23018 });
23019 let navigated = cx
23020 .update_editor(|editor, window, cx| {
23021 editor.find_all_references(&FindAllReferences::default(), window, cx)
23022 })
23023 .unwrap()
23024 .await
23025 .expect("Failed to navigate to references");
23026 assert_eq!(
23027 navigated,
23028 Navigated::Yes,
23029 "Should have navigated to references from the FindAllReferences response"
23030 );
23031 cx.assert_editor_state(
23032 &r#"fn one() {
23033 let mut a = ˇtwo();
23034 }
23035 fn three() {}
23036 fn two() {}"#
23037 .unindent(),
23038 );
23039 let editors = cx.update_workspace(|workspace, _, cx| {
23040 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23041 });
23042 cx.update_editor(|_, _, _| {
23043 assert_eq!(
23044 editors.len(),
23045 3,
23046 "should have used a new multibuffer as offsets changed"
23047 );
23048 });
23049}
23050#[gpui::test]
23051async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
23052 init_test(cx, |_| {});
23053
23054 let language = Arc::new(Language::new(
23055 LanguageConfig::default(),
23056 Some(tree_sitter_rust::LANGUAGE.into()),
23057 ));
23058
23059 let text = r#"
23060 #[cfg(test)]
23061 mod tests() {
23062 #[test]
23063 fn runnable_1() {
23064 let a = 1;
23065 }
23066
23067 #[test]
23068 fn runnable_2() {
23069 let a = 1;
23070 let b = 2;
23071 }
23072 }
23073 "#
23074 .unindent();
23075
23076 let fs = FakeFs::new(cx.executor());
23077 fs.insert_file("/file.rs", Default::default()).await;
23078
23079 let project = Project::test(fs, ["/a".as_ref()], cx).await;
23080 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23081 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23082 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
23083 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
23084
23085 let editor = cx.new_window_entity(|window, cx| {
23086 Editor::new(
23087 EditorMode::full(),
23088 multi_buffer,
23089 Some(project.clone()),
23090 window,
23091 cx,
23092 )
23093 });
23094
23095 editor.update_in(cx, |editor, window, cx| {
23096 let snapshot = editor.buffer().read(cx).snapshot(cx);
23097 editor.tasks.insert(
23098 (buffer.read(cx).remote_id(), 3),
23099 RunnableTasks {
23100 templates: vec![],
23101 offset: snapshot.anchor_before(MultiBufferOffset(43)),
23102 column: 0,
23103 extra_variables: HashMap::default(),
23104 context_range: BufferOffset(43)..BufferOffset(85),
23105 },
23106 );
23107 editor.tasks.insert(
23108 (buffer.read(cx).remote_id(), 8),
23109 RunnableTasks {
23110 templates: vec![],
23111 offset: snapshot.anchor_before(MultiBufferOffset(86)),
23112 column: 0,
23113 extra_variables: HashMap::default(),
23114 context_range: BufferOffset(86)..BufferOffset(191),
23115 },
23116 );
23117
23118 // Test finding task when cursor is inside function body
23119 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23120 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
23121 });
23122 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23123 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
23124
23125 // Test finding task when cursor is on function name
23126 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23127 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
23128 });
23129 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23130 assert_eq!(row, 8, "Should find task when cursor is on function name");
23131 });
23132}
23133
23134#[gpui::test]
23135async fn test_folding_buffers(cx: &mut TestAppContext) {
23136 init_test(cx, |_| {});
23137
23138 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23139 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
23140 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
23141
23142 let fs = FakeFs::new(cx.executor());
23143 fs.insert_tree(
23144 path!("/a"),
23145 json!({
23146 "first.rs": sample_text_1,
23147 "second.rs": sample_text_2,
23148 "third.rs": sample_text_3,
23149 }),
23150 )
23151 .await;
23152 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23153 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23154 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23155 let worktree = project.update(cx, |project, cx| {
23156 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23157 assert_eq!(worktrees.len(), 1);
23158 worktrees.pop().unwrap()
23159 });
23160 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23161
23162 let buffer_1 = project
23163 .update(cx, |project, cx| {
23164 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23165 })
23166 .await
23167 .unwrap();
23168 let buffer_2 = project
23169 .update(cx, |project, cx| {
23170 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23171 })
23172 .await
23173 .unwrap();
23174 let buffer_3 = project
23175 .update(cx, |project, cx| {
23176 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23177 })
23178 .await
23179 .unwrap();
23180
23181 let multi_buffer = cx.new(|cx| {
23182 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23183 multi_buffer.push_excerpts(
23184 buffer_1.clone(),
23185 [
23186 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23187 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23188 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23189 ],
23190 cx,
23191 );
23192 multi_buffer.push_excerpts(
23193 buffer_2.clone(),
23194 [
23195 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23196 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23197 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23198 ],
23199 cx,
23200 );
23201 multi_buffer.push_excerpts(
23202 buffer_3.clone(),
23203 [
23204 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23205 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23206 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23207 ],
23208 cx,
23209 );
23210 multi_buffer
23211 });
23212 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23213 Editor::new(
23214 EditorMode::full(),
23215 multi_buffer.clone(),
23216 Some(project.clone()),
23217 window,
23218 cx,
23219 )
23220 });
23221
23222 assert_eq!(
23223 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23224 "\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",
23225 );
23226
23227 multi_buffer_editor.update(cx, |editor, cx| {
23228 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23229 });
23230 assert_eq!(
23231 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23232 "\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",
23233 "After folding the first buffer, its text should not be displayed"
23234 );
23235
23236 multi_buffer_editor.update(cx, |editor, cx| {
23237 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23238 });
23239 assert_eq!(
23240 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23241 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
23242 "After folding the second buffer, its text should not be displayed"
23243 );
23244
23245 multi_buffer_editor.update(cx, |editor, cx| {
23246 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23247 });
23248 assert_eq!(
23249 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23250 "\n\n\n\n\n",
23251 "After folding the third buffer, its text should not be displayed"
23252 );
23253
23254 // Emulate selection inside the fold logic, that should work
23255 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23256 editor
23257 .snapshot(window, cx)
23258 .next_line_boundary(Point::new(0, 4));
23259 });
23260
23261 multi_buffer_editor.update(cx, |editor, cx| {
23262 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23263 });
23264 assert_eq!(
23265 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23266 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
23267 "After unfolding the second buffer, its text should be displayed"
23268 );
23269
23270 // Typing inside of buffer 1 causes that buffer to be unfolded.
23271 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23272 assert_eq!(
23273 multi_buffer
23274 .read(cx)
23275 .snapshot(cx)
23276 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
23277 .collect::<String>(),
23278 "bbbb"
23279 );
23280 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23281 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
23282 });
23283 editor.handle_input("B", window, cx);
23284 });
23285
23286 assert_eq!(
23287 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23288 "\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",
23289 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
23290 );
23291
23292 multi_buffer_editor.update(cx, |editor, cx| {
23293 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23294 });
23295 assert_eq!(
23296 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23297 "\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",
23298 "After unfolding the all buffers, all original text should be displayed"
23299 );
23300}
23301
23302#[gpui::test]
23303async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
23304 init_test(cx, |_| {});
23305
23306 let sample_text_1 = "1111\n2222\n3333".to_string();
23307 let sample_text_2 = "4444\n5555\n6666".to_string();
23308 let sample_text_3 = "7777\n8888\n9999".to_string();
23309
23310 let fs = FakeFs::new(cx.executor());
23311 fs.insert_tree(
23312 path!("/a"),
23313 json!({
23314 "first.rs": sample_text_1,
23315 "second.rs": sample_text_2,
23316 "third.rs": sample_text_3,
23317 }),
23318 )
23319 .await;
23320 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23321 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23322 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23323 let worktree = project.update(cx, |project, cx| {
23324 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23325 assert_eq!(worktrees.len(), 1);
23326 worktrees.pop().unwrap()
23327 });
23328 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23329
23330 let buffer_1 = project
23331 .update(cx, |project, cx| {
23332 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23333 })
23334 .await
23335 .unwrap();
23336 let buffer_2 = project
23337 .update(cx, |project, cx| {
23338 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23339 })
23340 .await
23341 .unwrap();
23342 let buffer_3 = project
23343 .update(cx, |project, cx| {
23344 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23345 })
23346 .await
23347 .unwrap();
23348
23349 let multi_buffer = cx.new(|cx| {
23350 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23351 multi_buffer.push_excerpts(
23352 buffer_1.clone(),
23353 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23354 cx,
23355 );
23356 multi_buffer.push_excerpts(
23357 buffer_2.clone(),
23358 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23359 cx,
23360 );
23361 multi_buffer.push_excerpts(
23362 buffer_3.clone(),
23363 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23364 cx,
23365 );
23366 multi_buffer
23367 });
23368
23369 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23370 Editor::new(
23371 EditorMode::full(),
23372 multi_buffer,
23373 Some(project.clone()),
23374 window,
23375 cx,
23376 )
23377 });
23378
23379 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
23380 assert_eq!(
23381 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23382 full_text,
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\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
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
23398 assert_eq!(
23399 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23400 "\n\n\n\n\n\n7777\n8888\n9999",
23401 "After folding the second buffer, its text should not be displayed"
23402 );
23403
23404 multi_buffer_editor.update(cx, |editor, cx| {
23405 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23406 });
23407 assert_eq!(
23408 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23409 "\n\n\n\n\n",
23410 "After folding the third buffer, its text should not be displayed"
23411 );
23412
23413 multi_buffer_editor.update(cx, |editor, cx| {
23414 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23415 });
23416 assert_eq!(
23417 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23418 "\n\n\n\n4444\n5555\n6666\n\n",
23419 "After unfolding the second buffer, its text should be displayed"
23420 );
23421
23422 multi_buffer_editor.update(cx, |editor, cx| {
23423 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
23424 });
23425 assert_eq!(
23426 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23427 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
23428 "After unfolding the first buffer, its text should be displayed"
23429 );
23430
23431 multi_buffer_editor.update(cx, |editor, cx| {
23432 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23433 });
23434 assert_eq!(
23435 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23436 full_text,
23437 "After unfolding all buffers, all original text should be displayed"
23438 );
23439}
23440
23441#[gpui::test]
23442async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
23443 init_test(cx, |_| {});
23444
23445 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23446
23447 let fs = FakeFs::new(cx.executor());
23448 fs.insert_tree(
23449 path!("/a"),
23450 json!({
23451 "main.rs": sample_text,
23452 }),
23453 )
23454 .await;
23455 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23456 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23457 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23458 let worktree = project.update(cx, |project, cx| {
23459 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23460 assert_eq!(worktrees.len(), 1);
23461 worktrees.pop().unwrap()
23462 });
23463 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23464
23465 let buffer_1 = project
23466 .update(cx, |project, cx| {
23467 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23468 })
23469 .await
23470 .unwrap();
23471
23472 let multi_buffer = cx.new(|cx| {
23473 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23474 multi_buffer.push_excerpts(
23475 buffer_1.clone(),
23476 [ExcerptRange::new(
23477 Point::new(0, 0)
23478 ..Point::new(
23479 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
23480 0,
23481 ),
23482 )],
23483 cx,
23484 );
23485 multi_buffer
23486 });
23487 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23488 Editor::new(
23489 EditorMode::full(),
23490 multi_buffer,
23491 Some(project.clone()),
23492 window,
23493 cx,
23494 )
23495 });
23496
23497 let selection_range = Point::new(1, 0)..Point::new(2, 0);
23498 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23499 enum TestHighlight {}
23500 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
23501 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
23502 editor.highlight_text::<TestHighlight>(
23503 vec![highlight_range.clone()],
23504 HighlightStyle::color(Hsla::green()),
23505 cx,
23506 );
23507 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23508 s.select_ranges(Some(highlight_range))
23509 });
23510 });
23511
23512 let full_text = format!("\n\n{sample_text}");
23513 assert_eq!(
23514 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23515 full_text,
23516 );
23517}
23518
23519#[gpui::test]
23520async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
23521 init_test(cx, |_| {});
23522 cx.update(|cx| {
23523 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
23524 "keymaps/default-linux.json",
23525 cx,
23526 )
23527 .unwrap();
23528 cx.bind_keys(default_key_bindings);
23529 });
23530
23531 let (editor, cx) = cx.add_window_view(|window, cx| {
23532 let multi_buffer = MultiBuffer::build_multi(
23533 [
23534 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
23535 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
23536 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
23537 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
23538 ],
23539 cx,
23540 );
23541 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
23542
23543 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
23544 // fold all but the second buffer, so that we test navigating between two
23545 // adjacent folded buffers, as well as folded buffers at the start and
23546 // end the multibuffer
23547 editor.fold_buffer(buffer_ids[0], cx);
23548 editor.fold_buffer(buffer_ids[2], cx);
23549 editor.fold_buffer(buffer_ids[3], cx);
23550
23551 editor
23552 });
23553 cx.simulate_resize(size(px(1000.), px(1000.)));
23554
23555 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
23556 cx.assert_excerpts_with_selections(indoc! {"
23557 [EXCERPT]
23558 ˇ[FOLDED]
23559 [EXCERPT]
23560 a1
23561 b1
23562 [EXCERPT]
23563 [FOLDED]
23564 [EXCERPT]
23565 [FOLDED]
23566 "
23567 });
23568 cx.simulate_keystroke("down");
23569 cx.assert_excerpts_with_selections(indoc! {"
23570 [EXCERPT]
23571 [FOLDED]
23572 [EXCERPT]
23573 ˇa1
23574 b1
23575 [EXCERPT]
23576 [FOLDED]
23577 [EXCERPT]
23578 [FOLDED]
23579 "
23580 });
23581 cx.simulate_keystroke("down");
23582 cx.assert_excerpts_with_selections(indoc! {"
23583 [EXCERPT]
23584 [FOLDED]
23585 [EXCERPT]
23586 a1
23587 ˇb1
23588 [EXCERPT]
23589 [FOLDED]
23590 [EXCERPT]
23591 [FOLDED]
23592 "
23593 });
23594 cx.simulate_keystroke("down");
23595 cx.assert_excerpts_with_selections(indoc! {"
23596 [EXCERPT]
23597 [FOLDED]
23598 [EXCERPT]
23599 a1
23600 b1
23601 ˇ[EXCERPT]
23602 [FOLDED]
23603 [EXCERPT]
23604 [FOLDED]
23605 "
23606 });
23607 cx.simulate_keystroke("down");
23608 cx.assert_excerpts_with_selections(indoc! {"
23609 [EXCERPT]
23610 [FOLDED]
23611 [EXCERPT]
23612 a1
23613 b1
23614 [EXCERPT]
23615 ˇ[FOLDED]
23616 [EXCERPT]
23617 [FOLDED]
23618 "
23619 });
23620 for _ in 0..5 {
23621 cx.simulate_keystroke("down");
23622 cx.assert_excerpts_with_selections(indoc! {"
23623 [EXCERPT]
23624 [FOLDED]
23625 [EXCERPT]
23626 a1
23627 b1
23628 [EXCERPT]
23629 [FOLDED]
23630 [EXCERPT]
23631 ˇ[FOLDED]
23632 "
23633 });
23634 }
23635
23636 cx.simulate_keystroke("up");
23637 cx.assert_excerpts_with_selections(indoc! {"
23638 [EXCERPT]
23639 [FOLDED]
23640 [EXCERPT]
23641 a1
23642 b1
23643 [EXCERPT]
23644 ˇ[FOLDED]
23645 [EXCERPT]
23646 [FOLDED]
23647 "
23648 });
23649 cx.simulate_keystroke("up");
23650 cx.assert_excerpts_with_selections(indoc! {"
23651 [EXCERPT]
23652 [FOLDED]
23653 [EXCERPT]
23654 a1
23655 b1
23656 ˇ[EXCERPT]
23657 [FOLDED]
23658 [EXCERPT]
23659 [FOLDED]
23660 "
23661 });
23662 cx.simulate_keystroke("up");
23663 cx.assert_excerpts_with_selections(indoc! {"
23664 [EXCERPT]
23665 [FOLDED]
23666 [EXCERPT]
23667 a1
23668 ˇb1
23669 [EXCERPT]
23670 [FOLDED]
23671 [EXCERPT]
23672 [FOLDED]
23673 "
23674 });
23675 cx.simulate_keystroke("up");
23676 cx.assert_excerpts_with_selections(indoc! {"
23677 [EXCERPT]
23678 [FOLDED]
23679 [EXCERPT]
23680 ˇa1
23681 b1
23682 [EXCERPT]
23683 [FOLDED]
23684 [EXCERPT]
23685 [FOLDED]
23686 "
23687 });
23688 for _ in 0..5 {
23689 cx.simulate_keystroke("up");
23690 cx.assert_excerpts_with_selections(indoc! {"
23691 [EXCERPT]
23692 ˇ[FOLDED]
23693 [EXCERPT]
23694 a1
23695 b1
23696 [EXCERPT]
23697 [FOLDED]
23698 [EXCERPT]
23699 [FOLDED]
23700 "
23701 });
23702 }
23703}
23704
23705#[gpui::test]
23706async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23707 init_test(cx, |_| {});
23708
23709 // Simple insertion
23710 assert_highlighted_edits(
23711 "Hello, world!",
23712 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23713 true,
23714 cx,
23715 |highlighted_edits, cx| {
23716 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23717 assert_eq!(highlighted_edits.highlights.len(), 1);
23718 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23719 assert_eq!(
23720 highlighted_edits.highlights[0].1.background_color,
23721 Some(cx.theme().status().created_background)
23722 );
23723 },
23724 )
23725 .await;
23726
23727 // Replacement
23728 assert_highlighted_edits(
23729 "This is a test.",
23730 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23731 false,
23732 cx,
23733 |highlighted_edits, cx| {
23734 assert_eq!(highlighted_edits.text, "That is a test.");
23735 assert_eq!(highlighted_edits.highlights.len(), 1);
23736 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23737 assert_eq!(
23738 highlighted_edits.highlights[0].1.background_color,
23739 Some(cx.theme().status().created_background)
23740 );
23741 },
23742 )
23743 .await;
23744
23745 // Multiple edits
23746 assert_highlighted_edits(
23747 "Hello, world!",
23748 vec![
23749 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23750 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23751 ],
23752 false,
23753 cx,
23754 |highlighted_edits, cx| {
23755 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23756 assert_eq!(highlighted_edits.highlights.len(), 2);
23757 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23758 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23759 assert_eq!(
23760 highlighted_edits.highlights[0].1.background_color,
23761 Some(cx.theme().status().created_background)
23762 );
23763 assert_eq!(
23764 highlighted_edits.highlights[1].1.background_color,
23765 Some(cx.theme().status().created_background)
23766 );
23767 },
23768 )
23769 .await;
23770
23771 // Multiple lines with edits
23772 assert_highlighted_edits(
23773 "First line\nSecond line\nThird line\nFourth line",
23774 vec![
23775 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
23776 (
23777 Point::new(2, 0)..Point::new(2, 10),
23778 "New third line".to_string(),
23779 ),
23780 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
23781 ],
23782 false,
23783 cx,
23784 |highlighted_edits, cx| {
23785 assert_eq!(
23786 highlighted_edits.text,
23787 "Second modified\nNew third line\nFourth updated line"
23788 );
23789 assert_eq!(highlighted_edits.highlights.len(), 3);
23790 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
23791 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
23792 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
23793 for highlight in &highlighted_edits.highlights {
23794 assert_eq!(
23795 highlight.1.background_color,
23796 Some(cx.theme().status().created_background)
23797 );
23798 }
23799 },
23800 )
23801 .await;
23802}
23803
23804#[gpui::test]
23805async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
23806 init_test(cx, |_| {});
23807
23808 // Deletion
23809 assert_highlighted_edits(
23810 "Hello, world!",
23811 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
23812 true,
23813 cx,
23814 |highlighted_edits, cx| {
23815 assert_eq!(highlighted_edits.text, "Hello, world!");
23816 assert_eq!(highlighted_edits.highlights.len(), 1);
23817 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
23818 assert_eq!(
23819 highlighted_edits.highlights[0].1.background_color,
23820 Some(cx.theme().status().deleted_background)
23821 );
23822 },
23823 )
23824 .await;
23825
23826 // Insertion
23827 assert_highlighted_edits(
23828 "Hello, world!",
23829 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
23830 true,
23831 cx,
23832 |highlighted_edits, cx| {
23833 assert_eq!(highlighted_edits.highlights.len(), 1);
23834 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
23835 assert_eq!(
23836 highlighted_edits.highlights[0].1.background_color,
23837 Some(cx.theme().status().created_background)
23838 );
23839 },
23840 )
23841 .await;
23842}
23843
23844async fn assert_highlighted_edits(
23845 text: &str,
23846 edits: Vec<(Range<Point>, String)>,
23847 include_deletions: bool,
23848 cx: &mut TestAppContext,
23849 assertion_fn: impl Fn(HighlightedText, &App),
23850) {
23851 let window = cx.add_window(|window, cx| {
23852 let buffer = MultiBuffer::build_simple(text, cx);
23853 Editor::new(EditorMode::full(), buffer, None, window, cx)
23854 });
23855 let cx = &mut VisualTestContext::from_window(*window, cx);
23856
23857 let (buffer, snapshot) = window
23858 .update(cx, |editor, _window, cx| {
23859 (
23860 editor.buffer().clone(),
23861 editor.buffer().read(cx).snapshot(cx),
23862 )
23863 })
23864 .unwrap();
23865
23866 let edits = edits
23867 .into_iter()
23868 .map(|(range, edit)| {
23869 (
23870 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
23871 edit,
23872 )
23873 })
23874 .collect::<Vec<_>>();
23875
23876 let text_anchor_edits = edits
23877 .clone()
23878 .into_iter()
23879 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
23880 .collect::<Vec<_>>();
23881
23882 let edit_preview = window
23883 .update(cx, |_, _window, cx| {
23884 buffer
23885 .read(cx)
23886 .as_singleton()
23887 .unwrap()
23888 .read(cx)
23889 .preview_edits(text_anchor_edits.into(), cx)
23890 })
23891 .unwrap()
23892 .await;
23893
23894 cx.update(|_window, cx| {
23895 let highlighted_edits = edit_prediction_edit_text(
23896 snapshot.as_singleton().unwrap().2,
23897 &edits,
23898 &edit_preview,
23899 include_deletions,
23900 cx,
23901 );
23902 assertion_fn(highlighted_edits, cx)
23903 });
23904}
23905
23906#[track_caller]
23907fn assert_breakpoint(
23908 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
23909 path: &Arc<Path>,
23910 expected: Vec<(u32, Breakpoint)>,
23911) {
23912 if expected.is_empty() {
23913 assert!(!breakpoints.contains_key(path), "{}", path.display());
23914 } else {
23915 let mut breakpoint = breakpoints
23916 .get(path)
23917 .unwrap()
23918 .iter()
23919 .map(|breakpoint| {
23920 (
23921 breakpoint.row,
23922 Breakpoint {
23923 message: breakpoint.message.clone(),
23924 state: breakpoint.state,
23925 condition: breakpoint.condition.clone(),
23926 hit_condition: breakpoint.hit_condition.clone(),
23927 },
23928 )
23929 })
23930 .collect::<Vec<_>>();
23931
23932 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
23933
23934 assert_eq!(expected, breakpoint);
23935 }
23936}
23937
23938fn add_log_breakpoint_at_cursor(
23939 editor: &mut Editor,
23940 log_message: &str,
23941 window: &mut Window,
23942 cx: &mut Context<Editor>,
23943) {
23944 let (anchor, bp) = editor
23945 .breakpoints_at_cursors(window, cx)
23946 .first()
23947 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
23948 .unwrap_or_else(|| {
23949 let snapshot = editor.snapshot(window, cx);
23950 let cursor_position: Point =
23951 editor.selections.newest(&snapshot.display_snapshot).head();
23952
23953 let breakpoint_position = snapshot
23954 .buffer_snapshot()
23955 .anchor_before(Point::new(cursor_position.row, 0));
23956
23957 (breakpoint_position, Breakpoint::new_log(log_message))
23958 });
23959
23960 editor.edit_breakpoint_at_anchor(
23961 anchor,
23962 bp,
23963 BreakpointEditAction::EditLogMessage(log_message.into()),
23964 cx,
23965 );
23966}
23967
23968#[gpui::test]
23969async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23970 init_test(cx, |_| {});
23971
23972 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23973 let fs = FakeFs::new(cx.executor());
23974 fs.insert_tree(
23975 path!("/a"),
23976 json!({
23977 "main.rs": sample_text,
23978 }),
23979 )
23980 .await;
23981 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23982 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23983 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23984
23985 let fs = FakeFs::new(cx.executor());
23986 fs.insert_tree(
23987 path!("/a"),
23988 json!({
23989 "main.rs": sample_text,
23990 }),
23991 )
23992 .await;
23993 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23994 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23995 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23996 let worktree_id = workspace
23997 .update(cx, |workspace, _window, cx| {
23998 workspace.project().update(cx, |project, cx| {
23999 project.worktrees(cx).next().unwrap().read(cx).id()
24000 })
24001 })
24002 .unwrap();
24003
24004 let buffer = project
24005 .update(cx, |project, cx| {
24006 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24007 })
24008 .await
24009 .unwrap();
24010
24011 let (editor, cx) = cx.add_window_view(|window, cx| {
24012 Editor::new(
24013 EditorMode::full(),
24014 MultiBuffer::build_from_buffer(buffer, cx),
24015 Some(project.clone()),
24016 window,
24017 cx,
24018 )
24019 });
24020
24021 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24022 let abs_path = project.read_with(cx, |project, cx| {
24023 project
24024 .absolute_path(&project_path, cx)
24025 .map(Arc::from)
24026 .unwrap()
24027 });
24028
24029 // assert we can add breakpoint on the first line
24030 editor.update_in(cx, |editor, window, cx| {
24031 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24032 editor.move_to_end(&MoveToEnd, window, cx);
24033 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24034 });
24035
24036 let breakpoints = editor.update(cx, |editor, cx| {
24037 editor
24038 .breakpoint_store()
24039 .as_ref()
24040 .unwrap()
24041 .read(cx)
24042 .all_source_breakpoints(cx)
24043 });
24044
24045 assert_eq!(1, breakpoints.len());
24046 assert_breakpoint(
24047 &breakpoints,
24048 &abs_path,
24049 vec![
24050 (0, Breakpoint::new_standard()),
24051 (3, Breakpoint::new_standard()),
24052 ],
24053 );
24054
24055 editor.update_in(cx, |editor, window, cx| {
24056 editor.move_to_beginning(&MoveToBeginning, window, cx);
24057 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24058 });
24059
24060 let breakpoints = editor.update(cx, |editor, cx| {
24061 editor
24062 .breakpoint_store()
24063 .as_ref()
24064 .unwrap()
24065 .read(cx)
24066 .all_source_breakpoints(cx)
24067 });
24068
24069 assert_eq!(1, breakpoints.len());
24070 assert_breakpoint(
24071 &breakpoints,
24072 &abs_path,
24073 vec![(3, Breakpoint::new_standard())],
24074 );
24075
24076 editor.update_in(cx, |editor, window, cx| {
24077 editor.move_to_end(&MoveToEnd, window, cx);
24078 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24079 });
24080
24081 let breakpoints = editor.update(cx, |editor, cx| {
24082 editor
24083 .breakpoint_store()
24084 .as_ref()
24085 .unwrap()
24086 .read(cx)
24087 .all_source_breakpoints(cx)
24088 });
24089
24090 assert_eq!(0, breakpoints.len());
24091 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24092}
24093
24094#[gpui::test]
24095async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
24096 init_test(cx, |_| {});
24097
24098 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24099
24100 let fs = FakeFs::new(cx.executor());
24101 fs.insert_tree(
24102 path!("/a"),
24103 json!({
24104 "main.rs": sample_text,
24105 }),
24106 )
24107 .await;
24108 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24109 let (workspace, cx) =
24110 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24111
24112 let worktree_id = workspace.update(cx, |workspace, cx| {
24113 workspace.project().update(cx, |project, cx| {
24114 project.worktrees(cx).next().unwrap().read(cx).id()
24115 })
24116 });
24117
24118 let buffer = project
24119 .update(cx, |project, cx| {
24120 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24121 })
24122 .await
24123 .unwrap();
24124
24125 let (editor, cx) = cx.add_window_view(|window, cx| {
24126 Editor::new(
24127 EditorMode::full(),
24128 MultiBuffer::build_from_buffer(buffer, cx),
24129 Some(project.clone()),
24130 window,
24131 cx,
24132 )
24133 });
24134
24135 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24136 let abs_path = project.read_with(cx, |project, cx| {
24137 project
24138 .absolute_path(&project_path, cx)
24139 .map(Arc::from)
24140 .unwrap()
24141 });
24142
24143 editor.update_in(cx, |editor, window, cx| {
24144 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24145 });
24146
24147 let breakpoints = editor.update(cx, |editor, cx| {
24148 editor
24149 .breakpoint_store()
24150 .as_ref()
24151 .unwrap()
24152 .read(cx)
24153 .all_source_breakpoints(cx)
24154 });
24155
24156 assert_breakpoint(
24157 &breakpoints,
24158 &abs_path,
24159 vec![(0, Breakpoint::new_log("hello world"))],
24160 );
24161
24162 // Removing a log message from a log breakpoint should remove it
24163 editor.update_in(cx, |editor, window, cx| {
24164 add_log_breakpoint_at_cursor(editor, "", window, cx);
24165 });
24166
24167 let breakpoints = editor.update(cx, |editor, cx| {
24168 editor
24169 .breakpoint_store()
24170 .as_ref()
24171 .unwrap()
24172 .read(cx)
24173 .all_source_breakpoints(cx)
24174 });
24175
24176 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24177
24178 editor.update_in(cx, |editor, window, cx| {
24179 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24180 editor.move_to_end(&MoveToEnd, window, cx);
24181 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24182 // Not adding a log message to a standard breakpoint shouldn't remove it
24183 add_log_breakpoint_at_cursor(editor, "", window, cx);
24184 });
24185
24186 let breakpoints = editor.update(cx, |editor, cx| {
24187 editor
24188 .breakpoint_store()
24189 .as_ref()
24190 .unwrap()
24191 .read(cx)
24192 .all_source_breakpoints(cx)
24193 });
24194
24195 assert_breakpoint(
24196 &breakpoints,
24197 &abs_path,
24198 vec![
24199 (0, Breakpoint::new_standard()),
24200 (3, Breakpoint::new_standard()),
24201 ],
24202 );
24203
24204 editor.update_in(cx, |editor, window, cx| {
24205 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24206 });
24207
24208 let breakpoints = editor.update(cx, |editor, cx| {
24209 editor
24210 .breakpoint_store()
24211 .as_ref()
24212 .unwrap()
24213 .read(cx)
24214 .all_source_breakpoints(cx)
24215 });
24216
24217 assert_breakpoint(
24218 &breakpoints,
24219 &abs_path,
24220 vec![
24221 (0, Breakpoint::new_standard()),
24222 (3, Breakpoint::new_log("hello world")),
24223 ],
24224 );
24225
24226 editor.update_in(cx, |editor, window, cx| {
24227 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
24228 });
24229
24230 let breakpoints = editor.update(cx, |editor, cx| {
24231 editor
24232 .breakpoint_store()
24233 .as_ref()
24234 .unwrap()
24235 .read(cx)
24236 .all_source_breakpoints(cx)
24237 });
24238
24239 assert_breakpoint(
24240 &breakpoints,
24241 &abs_path,
24242 vec![
24243 (0, Breakpoint::new_standard()),
24244 (3, Breakpoint::new_log("hello Earth!!")),
24245 ],
24246 );
24247}
24248
24249/// This also tests that Editor::breakpoint_at_cursor_head is working properly
24250/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
24251/// or when breakpoints were placed out of order. This tests for a regression too
24252#[gpui::test]
24253async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
24254 init_test(cx, |_| {});
24255
24256 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24257 let fs = FakeFs::new(cx.executor());
24258 fs.insert_tree(
24259 path!("/a"),
24260 json!({
24261 "main.rs": sample_text,
24262 }),
24263 )
24264 .await;
24265 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24266 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24267 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24268
24269 let fs = FakeFs::new(cx.executor());
24270 fs.insert_tree(
24271 path!("/a"),
24272 json!({
24273 "main.rs": sample_text,
24274 }),
24275 )
24276 .await;
24277 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24278 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24279 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24280 let worktree_id = workspace
24281 .update(cx, |workspace, _window, cx| {
24282 workspace.project().update(cx, |project, cx| {
24283 project.worktrees(cx).next().unwrap().read(cx).id()
24284 })
24285 })
24286 .unwrap();
24287
24288 let buffer = project
24289 .update(cx, |project, cx| {
24290 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24291 })
24292 .await
24293 .unwrap();
24294
24295 let (editor, cx) = cx.add_window_view(|window, cx| {
24296 Editor::new(
24297 EditorMode::full(),
24298 MultiBuffer::build_from_buffer(buffer, cx),
24299 Some(project.clone()),
24300 window,
24301 cx,
24302 )
24303 });
24304
24305 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24306 let abs_path = project.read_with(cx, |project, cx| {
24307 project
24308 .absolute_path(&project_path, cx)
24309 .map(Arc::from)
24310 .unwrap()
24311 });
24312
24313 // assert we can add breakpoint on the first line
24314 editor.update_in(cx, |editor, window, cx| {
24315 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24316 editor.move_to_end(&MoveToEnd, window, cx);
24317 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24318 editor.move_up(&MoveUp, window, cx);
24319 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24320 });
24321
24322 let breakpoints = editor.update(cx, |editor, cx| {
24323 editor
24324 .breakpoint_store()
24325 .as_ref()
24326 .unwrap()
24327 .read(cx)
24328 .all_source_breakpoints(cx)
24329 });
24330
24331 assert_eq!(1, breakpoints.len());
24332 assert_breakpoint(
24333 &breakpoints,
24334 &abs_path,
24335 vec![
24336 (0, Breakpoint::new_standard()),
24337 (2, Breakpoint::new_standard()),
24338 (3, Breakpoint::new_standard()),
24339 ],
24340 );
24341
24342 editor.update_in(cx, |editor, window, cx| {
24343 editor.move_to_beginning(&MoveToBeginning, window, cx);
24344 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24345 editor.move_to_end(&MoveToEnd, window, cx);
24346 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24347 // Disabling a breakpoint that doesn't exist should do nothing
24348 editor.move_up(&MoveUp, window, cx);
24349 editor.move_up(&MoveUp, window, cx);
24350 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24351 });
24352
24353 let breakpoints = editor.update(cx, |editor, cx| {
24354 editor
24355 .breakpoint_store()
24356 .as_ref()
24357 .unwrap()
24358 .read(cx)
24359 .all_source_breakpoints(cx)
24360 });
24361
24362 let disable_breakpoint = {
24363 let mut bp = Breakpoint::new_standard();
24364 bp.state = BreakpointState::Disabled;
24365 bp
24366 };
24367
24368 assert_eq!(1, breakpoints.len());
24369 assert_breakpoint(
24370 &breakpoints,
24371 &abs_path,
24372 vec![
24373 (0, disable_breakpoint.clone()),
24374 (2, Breakpoint::new_standard()),
24375 (3, disable_breakpoint.clone()),
24376 ],
24377 );
24378
24379 editor.update_in(cx, |editor, window, cx| {
24380 editor.move_to_beginning(&MoveToBeginning, window, cx);
24381 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24382 editor.move_to_end(&MoveToEnd, window, cx);
24383 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24384 editor.move_up(&MoveUp, window, cx);
24385 editor.disable_breakpoint(&actions::DisableBreakpoint, 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_eq!(1, breakpoints.len());
24398 assert_breakpoint(
24399 &breakpoints,
24400 &abs_path,
24401 vec![
24402 (0, Breakpoint::new_standard()),
24403 (2, disable_breakpoint),
24404 (3, Breakpoint::new_standard()),
24405 ],
24406 );
24407}
24408
24409#[gpui::test]
24410async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
24411 init_test(cx, |_| {});
24412 let capabilities = lsp::ServerCapabilities {
24413 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
24414 prepare_provider: Some(true),
24415 work_done_progress_options: Default::default(),
24416 })),
24417 ..Default::default()
24418 };
24419 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24420
24421 cx.set_state(indoc! {"
24422 struct Fˇoo {}
24423 "});
24424
24425 cx.update_editor(|editor, _, cx| {
24426 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24427 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24428 editor.highlight_background::<DocumentHighlightRead>(
24429 &[highlight_range],
24430 |_, theme| theme.colors().editor_document_highlight_read_background,
24431 cx,
24432 );
24433 });
24434
24435 let mut prepare_rename_handler = cx
24436 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
24437 move |_, _, _| async move {
24438 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
24439 start: lsp::Position {
24440 line: 0,
24441 character: 7,
24442 },
24443 end: lsp::Position {
24444 line: 0,
24445 character: 10,
24446 },
24447 })))
24448 },
24449 );
24450 let prepare_rename_task = cx
24451 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24452 .expect("Prepare rename was not started");
24453 prepare_rename_handler.next().await.unwrap();
24454 prepare_rename_task.await.expect("Prepare rename failed");
24455
24456 let mut rename_handler =
24457 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24458 let edit = lsp::TextEdit {
24459 range: lsp::Range {
24460 start: lsp::Position {
24461 line: 0,
24462 character: 7,
24463 },
24464 end: lsp::Position {
24465 line: 0,
24466 character: 10,
24467 },
24468 },
24469 new_text: "FooRenamed".to_string(),
24470 };
24471 Ok(Some(lsp::WorkspaceEdit::new(
24472 // Specify the same edit twice
24473 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
24474 )))
24475 });
24476 let rename_task = cx
24477 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24478 .expect("Confirm rename was not started");
24479 rename_handler.next().await.unwrap();
24480 rename_task.await.expect("Confirm rename failed");
24481 cx.run_until_parked();
24482
24483 // Despite two edits, only one is actually applied as those are identical
24484 cx.assert_editor_state(indoc! {"
24485 struct FooRenamedˇ {}
24486 "});
24487}
24488
24489#[gpui::test]
24490async fn test_rename_without_prepare(cx: &mut TestAppContext) {
24491 init_test(cx, |_| {});
24492 // These capabilities indicate that the server does not support prepare rename.
24493 let capabilities = lsp::ServerCapabilities {
24494 rename_provider: Some(lsp::OneOf::Left(true)),
24495 ..Default::default()
24496 };
24497 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24498
24499 cx.set_state(indoc! {"
24500 struct Fˇoo {}
24501 "});
24502
24503 cx.update_editor(|editor, _window, cx| {
24504 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24505 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24506 editor.highlight_background::<DocumentHighlightRead>(
24507 &[highlight_range],
24508 |_, theme| theme.colors().editor_document_highlight_read_background,
24509 cx,
24510 );
24511 });
24512
24513 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24514 .expect("Prepare rename was not started")
24515 .await
24516 .expect("Prepare rename failed");
24517
24518 let mut rename_handler =
24519 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24520 let edit = lsp::TextEdit {
24521 range: lsp::Range {
24522 start: lsp::Position {
24523 line: 0,
24524 character: 7,
24525 },
24526 end: lsp::Position {
24527 line: 0,
24528 character: 10,
24529 },
24530 },
24531 new_text: "FooRenamed".to_string(),
24532 };
24533 Ok(Some(lsp::WorkspaceEdit::new(
24534 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
24535 )))
24536 });
24537 let rename_task = cx
24538 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24539 .expect("Confirm rename was not started");
24540 rename_handler.next().await.unwrap();
24541 rename_task.await.expect("Confirm rename failed");
24542 cx.run_until_parked();
24543
24544 // Correct range is renamed, as `surrounding_word` is used to find it.
24545 cx.assert_editor_state(indoc! {"
24546 struct FooRenamedˇ {}
24547 "});
24548}
24549
24550#[gpui::test]
24551async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
24552 init_test(cx, |_| {});
24553 let mut cx = EditorTestContext::new(cx).await;
24554
24555 let language = Arc::new(
24556 Language::new(
24557 LanguageConfig::default(),
24558 Some(tree_sitter_html::LANGUAGE.into()),
24559 )
24560 .with_brackets_query(
24561 r#"
24562 ("<" @open "/>" @close)
24563 ("</" @open ">" @close)
24564 ("<" @open ">" @close)
24565 ("\"" @open "\"" @close)
24566 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
24567 "#,
24568 )
24569 .unwrap(),
24570 );
24571 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24572
24573 cx.set_state(indoc! {"
24574 <span>ˇ</span>
24575 "});
24576 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24577 cx.assert_editor_state(indoc! {"
24578 <span>
24579 ˇ
24580 </span>
24581 "});
24582
24583 cx.set_state(indoc! {"
24584 <span><span></span>ˇ</span>
24585 "});
24586 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24587 cx.assert_editor_state(indoc! {"
24588 <span><span></span>
24589 ˇ</span>
24590 "});
24591
24592 cx.set_state(indoc! {"
24593 <span>ˇ
24594 </span>
24595 "});
24596 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24597 cx.assert_editor_state(indoc! {"
24598 <span>
24599 ˇ
24600 </span>
24601 "});
24602}
24603
24604#[gpui::test(iterations = 10)]
24605async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
24606 init_test(cx, |_| {});
24607
24608 let fs = FakeFs::new(cx.executor());
24609 fs.insert_tree(
24610 path!("/dir"),
24611 json!({
24612 "a.ts": "a",
24613 }),
24614 )
24615 .await;
24616
24617 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
24618 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24619 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24620
24621 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24622 language_registry.add(Arc::new(Language::new(
24623 LanguageConfig {
24624 name: "TypeScript".into(),
24625 matcher: LanguageMatcher {
24626 path_suffixes: vec!["ts".to_string()],
24627 ..Default::default()
24628 },
24629 ..Default::default()
24630 },
24631 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24632 )));
24633 let mut fake_language_servers = language_registry.register_fake_lsp(
24634 "TypeScript",
24635 FakeLspAdapter {
24636 capabilities: lsp::ServerCapabilities {
24637 code_lens_provider: Some(lsp::CodeLensOptions {
24638 resolve_provider: Some(true),
24639 }),
24640 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24641 commands: vec!["_the/command".to_string()],
24642 ..lsp::ExecuteCommandOptions::default()
24643 }),
24644 ..lsp::ServerCapabilities::default()
24645 },
24646 ..FakeLspAdapter::default()
24647 },
24648 );
24649
24650 let editor = workspace
24651 .update(cx, |workspace, window, cx| {
24652 workspace.open_abs_path(
24653 PathBuf::from(path!("/dir/a.ts")),
24654 OpenOptions::default(),
24655 window,
24656 cx,
24657 )
24658 })
24659 .unwrap()
24660 .await
24661 .unwrap()
24662 .downcast::<Editor>()
24663 .unwrap();
24664 cx.executor().run_until_parked();
24665
24666 let fake_server = fake_language_servers.next().await.unwrap();
24667
24668 let buffer = editor.update(cx, |editor, cx| {
24669 editor
24670 .buffer()
24671 .read(cx)
24672 .as_singleton()
24673 .expect("have opened a single file by path")
24674 });
24675
24676 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24677 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24678 drop(buffer_snapshot);
24679 let actions = cx
24680 .update_window(*workspace, |_, window, cx| {
24681 project.code_actions(&buffer, anchor..anchor, window, cx)
24682 })
24683 .unwrap();
24684
24685 fake_server
24686 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24687 Ok(Some(vec![
24688 lsp::CodeLens {
24689 range: lsp::Range::default(),
24690 command: Some(lsp::Command {
24691 title: "Code lens command".to_owned(),
24692 command: "_the/command".to_owned(),
24693 arguments: None,
24694 }),
24695 data: None,
24696 },
24697 lsp::CodeLens {
24698 range: lsp::Range::default(),
24699 command: Some(lsp::Command {
24700 title: "Command not in capabilities".to_owned(),
24701 command: "not in capabilities".to_owned(),
24702 arguments: None,
24703 }),
24704 data: None,
24705 },
24706 lsp::CodeLens {
24707 range: lsp::Range {
24708 start: lsp::Position {
24709 line: 1,
24710 character: 1,
24711 },
24712 end: lsp::Position {
24713 line: 1,
24714 character: 1,
24715 },
24716 },
24717 command: Some(lsp::Command {
24718 title: "Command not in range".to_owned(),
24719 command: "_the/command".to_owned(),
24720 arguments: None,
24721 }),
24722 data: None,
24723 },
24724 ]))
24725 })
24726 .next()
24727 .await;
24728
24729 let actions = actions.await.unwrap();
24730 assert_eq!(
24731 actions.len(),
24732 1,
24733 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24734 );
24735 let action = actions[0].clone();
24736 let apply = project.update(cx, |project, cx| {
24737 project.apply_code_action(buffer.clone(), action, true, cx)
24738 });
24739
24740 // Resolving the code action does not populate its edits. In absence of
24741 // edits, we must execute the given command.
24742 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24743 |mut lens, _| async move {
24744 let lens_command = lens.command.as_mut().expect("should have a command");
24745 assert_eq!(lens_command.title, "Code lens command");
24746 lens_command.arguments = Some(vec![json!("the-argument")]);
24747 Ok(lens)
24748 },
24749 );
24750
24751 // While executing the command, the language server sends the editor
24752 // a `workspaceEdit` request.
24753 fake_server
24754 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24755 let fake = fake_server.clone();
24756 move |params, _| {
24757 assert_eq!(params.command, "_the/command");
24758 let fake = fake.clone();
24759 async move {
24760 fake.server
24761 .request::<lsp::request::ApplyWorkspaceEdit>(
24762 lsp::ApplyWorkspaceEditParams {
24763 label: None,
24764 edit: lsp::WorkspaceEdit {
24765 changes: Some(
24766 [(
24767 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
24768 vec![lsp::TextEdit {
24769 range: lsp::Range::new(
24770 lsp::Position::new(0, 0),
24771 lsp::Position::new(0, 0),
24772 ),
24773 new_text: "X".into(),
24774 }],
24775 )]
24776 .into_iter()
24777 .collect(),
24778 ),
24779 ..lsp::WorkspaceEdit::default()
24780 },
24781 },
24782 )
24783 .await
24784 .into_response()
24785 .unwrap();
24786 Ok(Some(json!(null)))
24787 }
24788 }
24789 })
24790 .next()
24791 .await;
24792
24793 // Applying the code lens command returns a project transaction containing the edits
24794 // sent by the language server in its `workspaceEdit` request.
24795 let transaction = apply.await.unwrap();
24796 assert!(transaction.0.contains_key(&buffer));
24797 buffer.update(cx, |buffer, cx| {
24798 assert_eq!(buffer.text(), "Xa");
24799 buffer.undo(cx);
24800 assert_eq!(buffer.text(), "a");
24801 });
24802
24803 let actions_after_edits = cx
24804 .update_window(*workspace, |_, window, cx| {
24805 project.code_actions(&buffer, anchor..anchor, window, cx)
24806 })
24807 .unwrap()
24808 .await
24809 .unwrap();
24810 assert_eq!(
24811 actions, actions_after_edits,
24812 "For the same selection, same code lens actions should be returned"
24813 );
24814
24815 let _responses =
24816 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24817 panic!("No more code lens requests are expected");
24818 });
24819 editor.update_in(cx, |editor, window, cx| {
24820 editor.select_all(&SelectAll, window, cx);
24821 });
24822 cx.executor().run_until_parked();
24823 let new_actions = cx
24824 .update_window(*workspace, |_, window, cx| {
24825 project.code_actions(&buffer, anchor..anchor, window, cx)
24826 })
24827 .unwrap()
24828 .await
24829 .unwrap();
24830 assert_eq!(
24831 actions, new_actions,
24832 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
24833 );
24834}
24835
24836#[gpui::test]
24837async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
24838 init_test(cx, |_| {});
24839
24840 let fs = FakeFs::new(cx.executor());
24841 let main_text = r#"fn main() {
24842println!("1");
24843println!("2");
24844println!("3");
24845println!("4");
24846println!("5");
24847}"#;
24848 let lib_text = "mod foo {}";
24849 fs.insert_tree(
24850 path!("/a"),
24851 json!({
24852 "lib.rs": lib_text,
24853 "main.rs": main_text,
24854 }),
24855 )
24856 .await;
24857
24858 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24859 let (workspace, cx) =
24860 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24861 let worktree_id = workspace.update(cx, |workspace, cx| {
24862 workspace.project().update(cx, |project, cx| {
24863 project.worktrees(cx).next().unwrap().read(cx).id()
24864 })
24865 });
24866
24867 let expected_ranges = vec![
24868 Point::new(0, 0)..Point::new(0, 0),
24869 Point::new(1, 0)..Point::new(1, 1),
24870 Point::new(2, 0)..Point::new(2, 2),
24871 Point::new(3, 0)..Point::new(3, 3),
24872 ];
24873
24874 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24875 let editor_1 = workspace
24876 .update_in(cx, |workspace, window, cx| {
24877 workspace.open_path(
24878 (worktree_id, rel_path("main.rs")),
24879 Some(pane_1.downgrade()),
24880 true,
24881 window,
24882 cx,
24883 )
24884 })
24885 .unwrap()
24886 .await
24887 .downcast::<Editor>()
24888 .unwrap();
24889 pane_1.update(cx, |pane, cx| {
24890 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24891 open_editor.update(cx, |editor, cx| {
24892 assert_eq!(
24893 editor.display_text(cx),
24894 main_text,
24895 "Original main.rs text on initial open",
24896 );
24897 assert_eq!(
24898 editor
24899 .selections
24900 .all::<Point>(&editor.display_snapshot(cx))
24901 .into_iter()
24902 .map(|s| s.range())
24903 .collect::<Vec<_>>(),
24904 vec![Point::zero()..Point::zero()],
24905 "Default selections on initial open",
24906 );
24907 })
24908 });
24909 editor_1.update_in(cx, |editor, window, cx| {
24910 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24911 s.select_ranges(expected_ranges.clone());
24912 });
24913 });
24914
24915 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
24916 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
24917 });
24918 let editor_2 = workspace
24919 .update_in(cx, |workspace, window, cx| {
24920 workspace.open_path(
24921 (worktree_id, rel_path("main.rs")),
24922 Some(pane_2.downgrade()),
24923 true,
24924 window,
24925 cx,
24926 )
24927 })
24928 .unwrap()
24929 .await
24930 .downcast::<Editor>()
24931 .unwrap();
24932 pane_2.update(cx, |pane, cx| {
24933 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24934 open_editor.update(cx, |editor, cx| {
24935 assert_eq!(
24936 editor.display_text(cx),
24937 main_text,
24938 "Original main.rs text on initial open in another panel",
24939 );
24940 assert_eq!(
24941 editor
24942 .selections
24943 .all::<Point>(&editor.display_snapshot(cx))
24944 .into_iter()
24945 .map(|s| s.range())
24946 .collect::<Vec<_>>(),
24947 vec![Point::zero()..Point::zero()],
24948 "Default selections on initial open in another panel",
24949 );
24950 })
24951 });
24952
24953 editor_2.update_in(cx, |editor, window, cx| {
24954 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
24955 });
24956
24957 let _other_editor_1 = workspace
24958 .update_in(cx, |workspace, window, cx| {
24959 workspace.open_path(
24960 (worktree_id, rel_path("lib.rs")),
24961 Some(pane_1.downgrade()),
24962 true,
24963 window,
24964 cx,
24965 )
24966 })
24967 .unwrap()
24968 .await
24969 .downcast::<Editor>()
24970 .unwrap();
24971 pane_1
24972 .update_in(cx, |pane, window, cx| {
24973 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24974 })
24975 .await
24976 .unwrap();
24977 drop(editor_1);
24978 pane_1.update(cx, |pane, cx| {
24979 pane.active_item()
24980 .unwrap()
24981 .downcast::<Editor>()
24982 .unwrap()
24983 .update(cx, |editor, cx| {
24984 assert_eq!(
24985 editor.display_text(cx),
24986 lib_text,
24987 "Other file should be open and active",
24988 );
24989 });
24990 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24991 });
24992
24993 let _other_editor_2 = workspace
24994 .update_in(cx, |workspace, window, cx| {
24995 workspace.open_path(
24996 (worktree_id, rel_path("lib.rs")),
24997 Some(pane_2.downgrade()),
24998 true,
24999 window,
25000 cx,
25001 )
25002 })
25003 .unwrap()
25004 .await
25005 .downcast::<Editor>()
25006 .unwrap();
25007 pane_2
25008 .update_in(cx, |pane, window, cx| {
25009 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
25010 })
25011 .await
25012 .unwrap();
25013 drop(editor_2);
25014 pane_2.update(cx, |pane, cx| {
25015 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25016 open_editor.update(cx, |editor, cx| {
25017 assert_eq!(
25018 editor.display_text(cx),
25019 lib_text,
25020 "Other file should be open and active in another panel too",
25021 );
25022 });
25023 assert_eq!(
25024 pane.items().count(),
25025 1,
25026 "No other editors should be open in another pane",
25027 );
25028 });
25029
25030 let _editor_1_reopened = workspace
25031 .update_in(cx, |workspace, window, cx| {
25032 workspace.open_path(
25033 (worktree_id, rel_path("main.rs")),
25034 Some(pane_1.downgrade()),
25035 true,
25036 window,
25037 cx,
25038 )
25039 })
25040 .unwrap()
25041 .await
25042 .downcast::<Editor>()
25043 .unwrap();
25044 let _editor_2_reopened = workspace
25045 .update_in(cx, |workspace, window, cx| {
25046 workspace.open_path(
25047 (worktree_id, rel_path("main.rs")),
25048 Some(pane_2.downgrade()),
25049 true,
25050 window,
25051 cx,
25052 )
25053 })
25054 .unwrap()
25055 .await
25056 .downcast::<Editor>()
25057 .unwrap();
25058 pane_1.update(cx, |pane, cx| {
25059 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25060 open_editor.update(cx, |editor, cx| {
25061 assert_eq!(
25062 editor.display_text(cx),
25063 main_text,
25064 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
25065 );
25066 assert_eq!(
25067 editor
25068 .selections
25069 .all::<Point>(&editor.display_snapshot(cx))
25070 .into_iter()
25071 .map(|s| s.range())
25072 .collect::<Vec<_>>(),
25073 expected_ranges,
25074 "Previous editor in the 1st panel had selections and should get them restored on reopen",
25075 );
25076 })
25077 });
25078 pane_2.update(cx, |pane, cx| {
25079 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25080 open_editor.update(cx, |editor, cx| {
25081 assert_eq!(
25082 editor.display_text(cx),
25083 r#"fn main() {
25084⋯rintln!("1");
25085⋯intln!("2");
25086⋯ntln!("3");
25087println!("4");
25088println!("5");
25089}"#,
25090 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
25091 );
25092 assert_eq!(
25093 editor
25094 .selections
25095 .all::<Point>(&editor.display_snapshot(cx))
25096 .into_iter()
25097 .map(|s| s.range())
25098 .collect::<Vec<_>>(),
25099 vec![Point::zero()..Point::zero()],
25100 "Previous editor in the 2nd pane had no selections changed hence should restore none",
25101 );
25102 })
25103 });
25104}
25105
25106#[gpui::test]
25107async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
25108 init_test(cx, |_| {});
25109
25110 let fs = FakeFs::new(cx.executor());
25111 let main_text = r#"fn main() {
25112println!("1");
25113println!("2");
25114println!("3");
25115println!("4");
25116println!("5");
25117}"#;
25118 let lib_text = "mod foo {}";
25119 fs.insert_tree(
25120 path!("/a"),
25121 json!({
25122 "lib.rs": lib_text,
25123 "main.rs": main_text,
25124 }),
25125 )
25126 .await;
25127
25128 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25129 let (workspace, cx) =
25130 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25131 let worktree_id = workspace.update(cx, |workspace, cx| {
25132 workspace.project().update(cx, |project, cx| {
25133 project.worktrees(cx).next().unwrap().read(cx).id()
25134 })
25135 });
25136
25137 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25138 let editor = workspace
25139 .update_in(cx, |workspace, window, cx| {
25140 workspace.open_path(
25141 (worktree_id, rel_path("main.rs")),
25142 Some(pane.downgrade()),
25143 true,
25144 window,
25145 cx,
25146 )
25147 })
25148 .unwrap()
25149 .await
25150 .downcast::<Editor>()
25151 .unwrap();
25152 pane.update(cx, |pane, cx| {
25153 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25154 open_editor.update(cx, |editor, cx| {
25155 assert_eq!(
25156 editor.display_text(cx),
25157 main_text,
25158 "Original main.rs text on initial open",
25159 );
25160 })
25161 });
25162 editor.update_in(cx, |editor, window, cx| {
25163 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
25164 });
25165
25166 cx.update_global(|store: &mut SettingsStore, cx| {
25167 store.update_user_settings(cx, |s| {
25168 s.workspace.restore_on_file_reopen = Some(false);
25169 });
25170 });
25171 editor.update_in(cx, |editor, window, cx| {
25172 editor.fold_ranges(
25173 vec![
25174 Point::new(1, 0)..Point::new(1, 1),
25175 Point::new(2, 0)..Point::new(2, 2),
25176 Point::new(3, 0)..Point::new(3, 3),
25177 ],
25178 false,
25179 window,
25180 cx,
25181 );
25182 });
25183 pane.update_in(cx, |pane, window, cx| {
25184 pane.close_all_items(&CloseAllItems::default(), window, cx)
25185 })
25186 .await
25187 .unwrap();
25188 pane.update(cx, |pane, _| {
25189 assert!(pane.active_item().is_none());
25190 });
25191 cx.update_global(|store: &mut SettingsStore, cx| {
25192 store.update_user_settings(cx, |s| {
25193 s.workspace.restore_on_file_reopen = Some(true);
25194 });
25195 });
25196
25197 let _editor_reopened = workspace
25198 .update_in(cx, |workspace, window, cx| {
25199 workspace.open_path(
25200 (worktree_id, rel_path("main.rs")),
25201 Some(pane.downgrade()),
25202 true,
25203 window,
25204 cx,
25205 )
25206 })
25207 .unwrap()
25208 .await
25209 .downcast::<Editor>()
25210 .unwrap();
25211 pane.update(cx, |pane, cx| {
25212 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25213 open_editor.update(cx, |editor, cx| {
25214 assert_eq!(
25215 editor.display_text(cx),
25216 main_text,
25217 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
25218 );
25219 })
25220 });
25221}
25222
25223#[gpui::test]
25224async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
25225 struct EmptyModalView {
25226 focus_handle: gpui::FocusHandle,
25227 }
25228 impl EventEmitter<DismissEvent> for EmptyModalView {}
25229 impl Render for EmptyModalView {
25230 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
25231 div()
25232 }
25233 }
25234 impl Focusable for EmptyModalView {
25235 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
25236 self.focus_handle.clone()
25237 }
25238 }
25239 impl workspace::ModalView for EmptyModalView {}
25240 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
25241 EmptyModalView {
25242 focus_handle: cx.focus_handle(),
25243 }
25244 }
25245
25246 init_test(cx, |_| {});
25247
25248 let fs = FakeFs::new(cx.executor());
25249 let project = Project::test(fs, [], cx).await;
25250 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25251 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
25252 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
25253 let editor = cx.new_window_entity(|window, cx| {
25254 Editor::new(
25255 EditorMode::full(),
25256 buffer,
25257 Some(project.clone()),
25258 window,
25259 cx,
25260 )
25261 });
25262 workspace
25263 .update(cx, |workspace, window, cx| {
25264 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
25265 })
25266 .unwrap();
25267 editor.update_in(cx, |editor, window, cx| {
25268 editor.open_context_menu(&OpenContextMenu, window, cx);
25269 assert!(editor.mouse_context_menu.is_some());
25270 });
25271 workspace
25272 .update(cx, |workspace, window, cx| {
25273 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
25274 })
25275 .unwrap();
25276 cx.read(|cx| {
25277 assert!(editor.read(cx).mouse_context_menu.is_none());
25278 });
25279}
25280
25281fn set_linked_edit_ranges(
25282 opening: (Point, Point),
25283 closing: (Point, Point),
25284 editor: &mut Editor,
25285 cx: &mut Context<Editor>,
25286) {
25287 let Some((buffer, _)) = editor
25288 .buffer
25289 .read(cx)
25290 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
25291 else {
25292 panic!("Failed to get buffer for selection position");
25293 };
25294 let buffer = buffer.read(cx);
25295 let buffer_id = buffer.remote_id();
25296 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
25297 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
25298 let mut linked_ranges = HashMap::default();
25299 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
25300 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
25301}
25302
25303#[gpui::test]
25304async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
25305 init_test(cx, |_| {});
25306
25307 let fs = FakeFs::new(cx.executor());
25308 fs.insert_file(path!("/file.html"), Default::default())
25309 .await;
25310
25311 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
25312
25313 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25314 let html_language = Arc::new(Language::new(
25315 LanguageConfig {
25316 name: "HTML".into(),
25317 matcher: LanguageMatcher {
25318 path_suffixes: vec!["html".to_string()],
25319 ..LanguageMatcher::default()
25320 },
25321 brackets: BracketPairConfig {
25322 pairs: vec![BracketPair {
25323 start: "<".into(),
25324 end: ">".into(),
25325 close: true,
25326 ..Default::default()
25327 }],
25328 ..Default::default()
25329 },
25330 ..Default::default()
25331 },
25332 Some(tree_sitter_html::LANGUAGE.into()),
25333 ));
25334 language_registry.add(html_language);
25335 let mut fake_servers = language_registry.register_fake_lsp(
25336 "HTML",
25337 FakeLspAdapter {
25338 capabilities: lsp::ServerCapabilities {
25339 completion_provider: Some(lsp::CompletionOptions {
25340 resolve_provider: Some(true),
25341 ..Default::default()
25342 }),
25343 ..Default::default()
25344 },
25345 ..Default::default()
25346 },
25347 );
25348
25349 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25350 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25351
25352 let worktree_id = workspace
25353 .update(cx, |workspace, _window, cx| {
25354 workspace.project().update(cx, |project, cx| {
25355 project.worktrees(cx).next().unwrap().read(cx).id()
25356 })
25357 })
25358 .unwrap();
25359 project
25360 .update(cx, |project, cx| {
25361 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
25362 })
25363 .await
25364 .unwrap();
25365 let editor = workspace
25366 .update(cx, |workspace, window, cx| {
25367 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
25368 })
25369 .unwrap()
25370 .await
25371 .unwrap()
25372 .downcast::<Editor>()
25373 .unwrap();
25374
25375 let fake_server = fake_servers.next().await.unwrap();
25376 editor.update_in(cx, |editor, window, cx| {
25377 editor.set_text("<ad></ad>", window, cx);
25378 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
25379 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
25380 });
25381 set_linked_edit_ranges(
25382 (Point::new(0, 1), Point::new(0, 3)),
25383 (Point::new(0, 6), Point::new(0, 8)),
25384 editor,
25385 cx,
25386 );
25387 });
25388 let mut completion_handle =
25389 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25390 Ok(Some(lsp::CompletionResponse::Array(vec![
25391 lsp::CompletionItem {
25392 label: "head".to_string(),
25393 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25394 lsp::InsertReplaceEdit {
25395 new_text: "head".to_string(),
25396 insert: lsp::Range::new(
25397 lsp::Position::new(0, 1),
25398 lsp::Position::new(0, 3),
25399 ),
25400 replace: lsp::Range::new(
25401 lsp::Position::new(0, 1),
25402 lsp::Position::new(0, 3),
25403 ),
25404 },
25405 )),
25406 ..Default::default()
25407 },
25408 ])))
25409 });
25410 editor.update_in(cx, |editor, window, cx| {
25411 editor.show_completions(&ShowCompletions, window, cx);
25412 });
25413 cx.run_until_parked();
25414 completion_handle.next().await.unwrap();
25415 editor.update(cx, |editor, _| {
25416 assert!(
25417 editor.context_menu_visible(),
25418 "Completion menu should be visible"
25419 );
25420 });
25421 editor.update_in(cx, |editor, window, cx| {
25422 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
25423 });
25424 cx.executor().run_until_parked();
25425 editor.update(cx, |editor, cx| {
25426 assert_eq!(editor.text(cx), "<head></head>");
25427 });
25428}
25429
25430#[gpui::test]
25431async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
25432 init_test(cx, |_| {});
25433
25434 let mut cx = EditorTestContext::new(cx).await;
25435 let language = Arc::new(Language::new(
25436 LanguageConfig {
25437 name: "TSX".into(),
25438 matcher: LanguageMatcher {
25439 path_suffixes: vec!["tsx".to_string()],
25440 ..LanguageMatcher::default()
25441 },
25442 brackets: BracketPairConfig {
25443 pairs: vec![BracketPair {
25444 start: "<".into(),
25445 end: ">".into(),
25446 close: true,
25447 ..Default::default()
25448 }],
25449 ..Default::default()
25450 },
25451 linked_edit_characters: HashSet::from_iter(['.']),
25452 ..Default::default()
25453 },
25454 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
25455 ));
25456 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25457
25458 // Test typing > does not extend linked pair
25459 cx.set_state("<divˇ<div></div>");
25460 cx.update_editor(|editor, _, cx| {
25461 set_linked_edit_ranges(
25462 (Point::new(0, 1), Point::new(0, 4)),
25463 (Point::new(0, 11), Point::new(0, 14)),
25464 editor,
25465 cx,
25466 );
25467 });
25468 cx.update_editor(|editor, window, cx| {
25469 editor.handle_input(">", window, cx);
25470 });
25471 cx.assert_editor_state("<div>ˇ<div></div>");
25472
25473 // Test typing . do extend linked pair
25474 cx.set_state("<Animatedˇ></Animated>");
25475 cx.update_editor(|editor, _, cx| {
25476 set_linked_edit_ranges(
25477 (Point::new(0, 1), Point::new(0, 9)),
25478 (Point::new(0, 12), Point::new(0, 20)),
25479 editor,
25480 cx,
25481 );
25482 });
25483 cx.update_editor(|editor, window, cx| {
25484 editor.handle_input(".", window, cx);
25485 });
25486 cx.assert_editor_state("<Animated.ˇ></Animated.>");
25487 cx.update_editor(|editor, _, cx| {
25488 set_linked_edit_ranges(
25489 (Point::new(0, 1), Point::new(0, 10)),
25490 (Point::new(0, 13), Point::new(0, 21)),
25491 editor,
25492 cx,
25493 );
25494 });
25495 cx.update_editor(|editor, window, cx| {
25496 editor.handle_input("V", window, cx);
25497 });
25498 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
25499}
25500
25501#[gpui::test]
25502async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
25503 init_test(cx, |_| {});
25504
25505 let fs = FakeFs::new(cx.executor());
25506 fs.insert_tree(
25507 path!("/root"),
25508 json!({
25509 "a": {
25510 "main.rs": "fn main() {}",
25511 },
25512 "foo": {
25513 "bar": {
25514 "external_file.rs": "pub mod external {}",
25515 }
25516 }
25517 }),
25518 )
25519 .await;
25520
25521 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
25522 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25523 language_registry.add(rust_lang());
25524 let _fake_servers = language_registry.register_fake_lsp(
25525 "Rust",
25526 FakeLspAdapter {
25527 ..FakeLspAdapter::default()
25528 },
25529 );
25530 let (workspace, cx) =
25531 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25532 let worktree_id = workspace.update(cx, |workspace, cx| {
25533 workspace.project().update(cx, |project, cx| {
25534 project.worktrees(cx).next().unwrap().read(cx).id()
25535 })
25536 });
25537
25538 let assert_language_servers_count =
25539 |expected: usize, context: &str, cx: &mut VisualTestContext| {
25540 project.update(cx, |project, cx| {
25541 let current = project
25542 .lsp_store()
25543 .read(cx)
25544 .as_local()
25545 .unwrap()
25546 .language_servers
25547 .len();
25548 assert_eq!(expected, current, "{context}");
25549 });
25550 };
25551
25552 assert_language_servers_count(
25553 0,
25554 "No servers should be running before any file is open",
25555 cx,
25556 );
25557 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25558 let main_editor = workspace
25559 .update_in(cx, |workspace, window, cx| {
25560 workspace.open_path(
25561 (worktree_id, rel_path("main.rs")),
25562 Some(pane.downgrade()),
25563 true,
25564 window,
25565 cx,
25566 )
25567 })
25568 .unwrap()
25569 .await
25570 .downcast::<Editor>()
25571 .unwrap();
25572 pane.update(cx, |pane, cx| {
25573 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25574 open_editor.update(cx, |editor, cx| {
25575 assert_eq!(
25576 editor.display_text(cx),
25577 "fn main() {}",
25578 "Original main.rs text on initial open",
25579 );
25580 });
25581 assert_eq!(open_editor, main_editor);
25582 });
25583 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
25584
25585 let external_editor = workspace
25586 .update_in(cx, |workspace, window, cx| {
25587 workspace.open_abs_path(
25588 PathBuf::from("/root/foo/bar/external_file.rs"),
25589 OpenOptions::default(),
25590 window,
25591 cx,
25592 )
25593 })
25594 .await
25595 .expect("opening external file")
25596 .downcast::<Editor>()
25597 .expect("downcasted external file's open element to editor");
25598 pane.update(cx, |pane, cx| {
25599 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25600 open_editor.update(cx, |editor, cx| {
25601 assert_eq!(
25602 editor.display_text(cx),
25603 "pub mod external {}",
25604 "External file is open now",
25605 );
25606 });
25607 assert_eq!(open_editor, external_editor);
25608 });
25609 assert_language_servers_count(
25610 1,
25611 "Second, external, *.rs file should join the existing server",
25612 cx,
25613 );
25614
25615 pane.update_in(cx, |pane, window, cx| {
25616 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25617 })
25618 .await
25619 .unwrap();
25620 pane.update_in(cx, |pane, window, cx| {
25621 pane.navigate_backward(&Default::default(), window, cx);
25622 });
25623 cx.run_until_parked();
25624 pane.update(cx, |pane, cx| {
25625 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25626 open_editor.update(cx, |editor, cx| {
25627 assert_eq!(
25628 editor.display_text(cx),
25629 "pub mod external {}",
25630 "External file is open now",
25631 );
25632 });
25633 });
25634 assert_language_servers_count(
25635 1,
25636 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25637 cx,
25638 );
25639
25640 cx.update(|_, cx| {
25641 workspace::reload(cx);
25642 });
25643 assert_language_servers_count(
25644 1,
25645 "After reloading the worktree with local and external files opened, only one project should be started",
25646 cx,
25647 );
25648}
25649
25650#[gpui::test]
25651async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25652 init_test(cx, |_| {});
25653
25654 let mut cx = EditorTestContext::new(cx).await;
25655 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25656 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25657
25658 // test cursor move to start of each line on tab
25659 // for `if`, `elif`, `else`, `while`, `with` and `for`
25660 cx.set_state(indoc! {"
25661 def main():
25662 ˇ for item in items:
25663 ˇ while item.active:
25664 ˇ if item.value > 10:
25665 ˇ continue
25666 ˇ elif item.value < 0:
25667 ˇ break
25668 ˇ else:
25669 ˇ with item.context() as ctx:
25670 ˇ yield count
25671 ˇ else:
25672 ˇ log('while else')
25673 ˇ else:
25674 ˇ log('for else')
25675 "});
25676 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25677 cx.wait_for_autoindent_applied().await;
25678 cx.assert_editor_state(indoc! {"
25679 def main():
25680 ˇfor item in items:
25681 ˇwhile item.active:
25682 ˇif item.value > 10:
25683 ˇcontinue
25684 ˇelif item.value < 0:
25685 ˇbreak
25686 ˇelse:
25687 ˇwith item.context() as ctx:
25688 ˇyield count
25689 ˇelse:
25690 ˇlog('while else')
25691 ˇelse:
25692 ˇlog('for else')
25693 "});
25694 // test relative indent is preserved when tab
25695 // for `if`, `elif`, `else`, `while`, `with` and `for`
25696 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25697 cx.wait_for_autoindent_applied().await;
25698 cx.assert_editor_state(indoc! {"
25699 def main():
25700 ˇfor item in items:
25701 ˇwhile item.active:
25702 ˇif item.value > 10:
25703 ˇcontinue
25704 ˇelif item.value < 0:
25705 ˇbreak
25706 ˇelse:
25707 ˇwith item.context() as ctx:
25708 ˇyield count
25709 ˇelse:
25710 ˇlog('while else')
25711 ˇelse:
25712 ˇlog('for else')
25713 "});
25714
25715 // test cursor move to start of each line on tab
25716 // for `try`, `except`, `else`, `finally`, `match` and `def`
25717 cx.set_state(indoc! {"
25718 def main():
25719 ˇ try:
25720 ˇ fetch()
25721 ˇ except ValueError:
25722 ˇ handle_error()
25723 ˇ else:
25724 ˇ match value:
25725 ˇ case _:
25726 ˇ finally:
25727 ˇ def status():
25728 ˇ return 0
25729 "});
25730 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25731 cx.wait_for_autoindent_applied().await;
25732 cx.assert_editor_state(indoc! {"
25733 def main():
25734 ˇtry:
25735 ˇfetch()
25736 ˇexcept ValueError:
25737 ˇhandle_error()
25738 ˇelse:
25739 ˇmatch value:
25740 ˇcase _:
25741 ˇfinally:
25742 ˇdef status():
25743 ˇreturn 0
25744 "});
25745 // test relative indent is preserved when tab
25746 // for `try`, `except`, `else`, `finally`, `match` and `def`
25747 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25748 cx.wait_for_autoindent_applied().await;
25749 cx.assert_editor_state(indoc! {"
25750 def main():
25751 ˇtry:
25752 ˇfetch()
25753 ˇexcept ValueError:
25754 ˇhandle_error()
25755 ˇelse:
25756 ˇmatch value:
25757 ˇcase _:
25758 ˇfinally:
25759 ˇdef status():
25760 ˇreturn 0
25761 "});
25762}
25763
25764#[gpui::test]
25765async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
25766 init_test(cx, |_| {});
25767
25768 let mut cx = EditorTestContext::new(cx).await;
25769 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25770 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25771
25772 // test `else` auto outdents when typed inside `if` block
25773 cx.set_state(indoc! {"
25774 def main():
25775 if i == 2:
25776 return
25777 ˇ
25778 "});
25779 cx.update_editor(|editor, window, cx| {
25780 editor.handle_input("else:", window, cx);
25781 });
25782 cx.wait_for_autoindent_applied().await;
25783 cx.assert_editor_state(indoc! {"
25784 def main():
25785 if i == 2:
25786 return
25787 else:ˇ
25788 "});
25789
25790 // test `except` auto outdents when typed inside `try` block
25791 cx.set_state(indoc! {"
25792 def main():
25793 try:
25794 i = 2
25795 ˇ
25796 "});
25797 cx.update_editor(|editor, window, cx| {
25798 editor.handle_input("except:", window, cx);
25799 });
25800 cx.wait_for_autoindent_applied().await;
25801 cx.assert_editor_state(indoc! {"
25802 def main():
25803 try:
25804 i = 2
25805 except:ˇ
25806 "});
25807
25808 // test `else` auto outdents when typed inside `except` block
25809 cx.set_state(indoc! {"
25810 def main():
25811 try:
25812 i = 2
25813 except:
25814 j = 2
25815 ˇ
25816 "});
25817 cx.update_editor(|editor, window, cx| {
25818 editor.handle_input("else:", window, cx);
25819 });
25820 cx.wait_for_autoindent_applied().await;
25821 cx.assert_editor_state(indoc! {"
25822 def main():
25823 try:
25824 i = 2
25825 except:
25826 j = 2
25827 else:ˇ
25828 "});
25829
25830 // test `finally` auto outdents when typed inside `else` block
25831 cx.set_state(indoc! {"
25832 def main():
25833 try:
25834 i = 2
25835 except:
25836 j = 2
25837 else:
25838 k = 2
25839 ˇ
25840 "});
25841 cx.update_editor(|editor, window, cx| {
25842 editor.handle_input("finally:", window, cx);
25843 });
25844 cx.wait_for_autoindent_applied().await;
25845 cx.assert_editor_state(indoc! {"
25846 def main():
25847 try:
25848 i = 2
25849 except:
25850 j = 2
25851 else:
25852 k = 2
25853 finally:ˇ
25854 "});
25855
25856 // test `else` does not outdents when typed inside `except` block right after for block
25857 cx.set_state(indoc! {"
25858 def main():
25859 try:
25860 i = 2
25861 except:
25862 for i in range(n):
25863 pass
25864 ˇ
25865 "});
25866 cx.update_editor(|editor, window, cx| {
25867 editor.handle_input("else:", window, cx);
25868 });
25869 cx.wait_for_autoindent_applied().await;
25870 cx.assert_editor_state(indoc! {"
25871 def main():
25872 try:
25873 i = 2
25874 except:
25875 for i in range(n):
25876 pass
25877 else:ˇ
25878 "});
25879
25880 // test `finally` auto outdents when typed inside `else` block right after for block
25881 cx.set_state(indoc! {"
25882 def main():
25883 try:
25884 i = 2
25885 except:
25886 j = 2
25887 else:
25888 for i in range(n):
25889 pass
25890 ˇ
25891 "});
25892 cx.update_editor(|editor, window, cx| {
25893 editor.handle_input("finally:", window, cx);
25894 });
25895 cx.wait_for_autoindent_applied().await;
25896 cx.assert_editor_state(indoc! {"
25897 def main():
25898 try:
25899 i = 2
25900 except:
25901 j = 2
25902 else:
25903 for i in range(n):
25904 pass
25905 finally:ˇ
25906 "});
25907
25908 // test `except` outdents to inner "try" block
25909 cx.set_state(indoc! {"
25910 def main():
25911 try:
25912 i = 2
25913 if i == 2:
25914 try:
25915 i = 3
25916 ˇ
25917 "});
25918 cx.update_editor(|editor, window, cx| {
25919 editor.handle_input("except:", window, cx);
25920 });
25921 cx.wait_for_autoindent_applied().await;
25922 cx.assert_editor_state(indoc! {"
25923 def main():
25924 try:
25925 i = 2
25926 if i == 2:
25927 try:
25928 i = 3
25929 except:ˇ
25930 "});
25931
25932 // test `except` outdents to outer "try" block
25933 cx.set_state(indoc! {"
25934 def main():
25935 try:
25936 i = 2
25937 if i == 2:
25938 try:
25939 i = 3
25940 ˇ
25941 "});
25942 cx.update_editor(|editor, window, cx| {
25943 editor.handle_input("except:", window, cx);
25944 });
25945 cx.wait_for_autoindent_applied().await;
25946 cx.assert_editor_state(indoc! {"
25947 def main():
25948 try:
25949 i = 2
25950 if i == 2:
25951 try:
25952 i = 3
25953 except:ˇ
25954 "});
25955
25956 // test `else` stays at correct indent when typed after `for` block
25957 cx.set_state(indoc! {"
25958 def main():
25959 for i in range(10):
25960 if i == 3:
25961 break
25962 ˇ
25963 "});
25964 cx.update_editor(|editor, window, cx| {
25965 editor.handle_input("else:", window, cx);
25966 });
25967 cx.wait_for_autoindent_applied().await;
25968 cx.assert_editor_state(indoc! {"
25969 def main():
25970 for i in range(10):
25971 if i == 3:
25972 break
25973 else:ˇ
25974 "});
25975
25976 // test does not outdent on typing after line with square brackets
25977 cx.set_state(indoc! {"
25978 def f() -> list[str]:
25979 ˇ
25980 "});
25981 cx.update_editor(|editor, window, cx| {
25982 editor.handle_input("a", window, cx);
25983 });
25984 cx.wait_for_autoindent_applied().await;
25985 cx.assert_editor_state(indoc! {"
25986 def f() -> list[str]:
25987 aˇ
25988 "});
25989
25990 // test does not outdent on typing : after case keyword
25991 cx.set_state(indoc! {"
25992 match 1:
25993 caseˇ
25994 "});
25995 cx.update_editor(|editor, window, cx| {
25996 editor.handle_input(":", window, cx);
25997 });
25998 cx.wait_for_autoindent_applied().await;
25999 cx.assert_editor_state(indoc! {"
26000 match 1:
26001 case:ˇ
26002 "});
26003}
26004
26005#[gpui::test]
26006async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
26007 init_test(cx, |_| {});
26008 update_test_language_settings(cx, |settings| {
26009 settings.defaults.extend_comment_on_newline = Some(false);
26010 });
26011 let mut cx = EditorTestContext::new(cx).await;
26012 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26013 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26014
26015 // test correct indent after newline on comment
26016 cx.set_state(indoc! {"
26017 # COMMENT:ˇ
26018 "});
26019 cx.update_editor(|editor, window, cx| {
26020 editor.newline(&Newline, window, cx);
26021 });
26022 cx.wait_for_autoindent_applied().await;
26023 cx.assert_editor_state(indoc! {"
26024 # COMMENT:
26025 ˇ
26026 "});
26027
26028 // test correct indent after newline in brackets
26029 cx.set_state(indoc! {"
26030 {ˇ}
26031 "});
26032 cx.update_editor(|editor, window, cx| {
26033 editor.newline(&Newline, window, cx);
26034 });
26035 cx.wait_for_autoindent_applied().await;
26036 cx.assert_editor_state(indoc! {"
26037 {
26038 ˇ
26039 }
26040 "});
26041
26042 cx.set_state(indoc! {"
26043 (ˇ)
26044 "});
26045 cx.update_editor(|editor, window, cx| {
26046 editor.newline(&Newline, window, cx);
26047 });
26048 cx.run_until_parked();
26049 cx.assert_editor_state(indoc! {"
26050 (
26051 ˇ
26052 )
26053 "});
26054
26055 // do not indent after empty lists or dictionaries
26056 cx.set_state(indoc! {"
26057 a = []ˇ
26058 "});
26059 cx.update_editor(|editor, window, cx| {
26060 editor.newline(&Newline, window, cx);
26061 });
26062 cx.run_until_parked();
26063 cx.assert_editor_state(indoc! {"
26064 a = []
26065 ˇ
26066 "});
26067}
26068
26069#[gpui::test]
26070async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
26071 init_test(cx, |_| {});
26072
26073 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
26074 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
26075 language_registry.add(markdown_lang());
26076 language_registry.add(python_lang);
26077
26078 let mut cx = EditorTestContext::new(cx).await;
26079 cx.update_buffer(|buffer, cx| {
26080 buffer.set_language_registry(language_registry);
26081 buffer.set_language(Some(markdown_lang()), cx);
26082 });
26083
26084 // Test that `else:` correctly outdents to match `if:` inside the Python code block
26085 cx.set_state(indoc! {"
26086 # Heading
26087
26088 ```python
26089 def main():
26090 if condition:
26091 pass
26092 ˇ
26093 ```
26094 "});
26095 cx.update_editor(|editor, window, cx| {
26096 editor.handle_input("else:", window, cx);
26097 });
26098 cx.run_until_parked();
26099 cx.assert_editor_state(indoc! {"
26100 # Heading
26101
26102 ```python
26103 def main():
26104 if condition:
26105 pass
26106 else:ˇ
26107 ```
26108 "});
26109}
26110
26111#[gpui::test]
26112async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
26113 init_test(cx, |_| {});
26114
26115 let mut cx = EditorTestContext::new(cx).await;
26116 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26117 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26118
26119 // test cursor move to start of each line on tab
26120 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
26121 cx.set_state(indoc! {"
26122 function main() {
26123 ˇ for item in $items; do
26124 ˇ while [ -n \"$item\" ]; do
26125 ˇ if [ \"$value\" -gt 10 ]; then
26126 ˇ continue
26127 ˇ elif [ \"$value\" -lt 0 ]; then
26128 ˇ break
26129 ˇ else
26130 ˇ echo \"$item\"
26131 ˇ fi
26132 ˇ done
26133 ˇ done
26134 ˇ}
26135 "});
26136 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26137 cx.wait_for_autoindent_applied().await;
26138 cx.assert_editor_state(indoc! {"
26139 function main() {
26140 ˇfor item in $items; do
26141 ˇwhile [ -n \"$item\" ]; do
26142 ˇif [ \"$value\" -gt 10 ]; then
26143 ˇcontinue
26144 ˇelif [ \"$value\" -lt 0 ]; then
26145 ˇbreak
26146 ˇelse
26147 ˇecho \"$item\"
26148 ˇfi
26149 ˇdone
26150 ˇdone
26151 ˇ}
26152 "});
26153 // test relative indent is preserved when tab
26154 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26155 cx.wait_for_autoindent_applied().await;
26156 cx.assert_editor_state(indoc! {"
26157 function main() {
26158 ˇfor item in $items; do
26159 ˇwhile [ -n \"$item\" ]; do
26160 ˇif [ \"$value\" -gt 10 ]; then
26161 ˇcontinue
26162 ˇelif [ \"$value\" -lt 0 ]; then
26163 ˇbreak
26164 ˇelse
26165 ˇecho \"$item\"
26166 ˇfi
26167 ˇdone
26168 ˇdone
26169 ˇ}
26170 "});
26171
26172 // test cursor move to start of each line on tab
26173 // for `case` statement with patterns
26174 cx.set_state(indoc! {"
26175 function handle() {
26176 ˇ case \"$1\" in
26177 ˇ start)
26178 ˇ echo \"a\"
26179 ˇ ;;
26180 ˇ stop)
26181 ˇ echo \"b\"
26182 ˇ ;;
26183 ˇ *)
26184 ˇ echo \"c\"
26185 ˇ ;;
26186 ˇ esac
26187 ˇ}
26188 "});
26189 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26190 cx.wait_for_autoindent_applied().await;
26191 cx.assert_editor_state(indoc! {"
26192 function handle() {
26193 ˇcase \"$1\" in
26194 ˇstart)
26195 ˇecho \"a\"
26196 ˇ;;
26197 ˇstop)
26198 ˇecho \"b\"
26199 ˇ;;
26200 ˇ*)
26201 ˇecho \"c\"
26202 ˇ;;
26203 ˇesac
26204 ˇ}
26205 "});
26206}
26207
26208#[gpui::test]
26209async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
26210 init_test(cx, |_| {});
26211
26212 let mut cx = EditorTestContext::new(cx).await;
26213 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26214 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26215
26216 // test indents on comment insert
26217 cx.set_state(indoc! {"
26218 function main() {
26219 ˇ for item in $items; do
26220 ˇ while [ -n \"$item\" ]; do
26221 ˇ if [ \"$value\" -gt 10 ]; then
26222 ˇ continue
26223 ˇ elif [ \"$value\" -lt 0 ]; then
26224 ˇ break
26225 ˇ else
26226 ˇ echo \"$item\"
26227 ˇ fi
26228 ˇ done
26229 ˇ done
26230 ˇ}
26231 "});
26232 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
26233 cx.wait_for_autoindent_applied().await;
26234 cx.assert_editor_state(indoc! {"
26235 function main() {
26236 #ˇ for item in $items; do
26237 #ˇ while [ -n \"$item\" ]; do
26238 #ˇ if [ \"$value\" -gt 10 ]; then
26239 #ˇ continue
26240 #ˇ elif [ \"$value\" -lt 0 ]; then
26241 #ˇ break
26242 #ˇ else
26243 #ˇ echo \"$item\"
26244 #ˇ fi
26245 #ˇ done
26246 #ˇ done
26247 #ˇ}
26248 "});
26249}
26250
26251#[gpui::test]
26252async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
26253 init_test(cx, |_| {});
26254
26255 let mut cx = EditorTestContext::new(cx).await;
26256 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26257 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26258
26259 // test `else` auto outdents when typed inside `if` block
26260 cx.set_state(indoc! {"
26261 if [ \"$1\" = \"test\" ]; then
26262 echo \"foo bar\"
26263 ˇ
26264 "});
26265 cx.update_editor(|editor, window, cx| {
26266 editor.handle_input("else", window, cx);
26267 });
26268 cx.wait_for_autoindent_applied().await;
26269 cx.assert_editor_state(indoc! {"
26270 if [ \"$1\" = \"test\" ]; then
26271 echo \"foo bar\"
26272 elseˇ
26273 "});
26274
26275 // test `elif` auto outdents when typed inside `if` block
26276 cx.set_state(indoc! {"
26277 if [ \"$1\" = \"test\" ]; then
26278 echo \"foo bar\"
26279 ˇ
26280 "});
26281 cx.update_editor(|editor, window, cx| {
26282 editor.handle_input("elif", window, cx);
26283 });
26284 cx.wait_for_autoindent_applied().await;
26285 cx.assert_editor_state(indoc! {"
26286 if [ \"$1\" = \"test\" ]; then
26287 echo \"foo bar\"
26288 elifˇ
26289 "});
26290
26291 // test `fi` auto outdents when typed inside `else` block
26292 cx.set_state(indoc! {"
26293 if [ \"$1\" = \"test\" ]; then
26294 echo \"foo bar\"
26295 else
26296 echo \"bar baz\"
26297 ˇ
26298 "});
26299 cx.update_editor(|editor, window, cx| {
26300 editor.handle_input("fi", window, cx);
26301 });
26302 cx.wait_for_autoindent_applied().await;
26303 cx.assert_editor_state(indoc! {"
26304 if [ \"$1\" = \"test\" ]; then
26305 echo \"foo bar\"
26306 else
26307 echo \"bar baz\"
26308 fiˇ
26309 "});
26310
26311 // test `done` auto outdents when typed inside `while` block
26312 cx.set_state(indoc! {"
26313 while read line; do
26314 echo \"$line\"
26315 ˇ
26316 "});
26317 cx.update_editor(|editor, window, cx| {
26318 editor.handle_input("done", window, cx);
26319 });
26320 cx.wait_for_autoindent_applied().await;
26321 cx.assert_editor_state(indoc! {"
26322 while read line; do
26323 echo \"$line\"
26324 doneˇ
26325 "});
26326
26327 // test `done` auto outdents when typed inside `for` block
26328 cx.set_state(indoc! {"
26329 for file in *.txt; do
26330 cat \"$file\"
26331 ˇ
26332 "});
26333 cx.update_editor(|editor, window, cx| {
26334 editor.handle_input("done", window, cx);
26335 });
26336 cx.wait_for_autoindent_applied().await;
26337 cx.assert_editor_state(indoc! {"
26338 for file in *.txt; do
26339 cat \"$file\"
26340 doneˇ
26341 "});
26342
26343 // test `esac` auto outdents when typed inside `case` block
26344 cx.set_state(indoc! {"
26345 case \"$1\" in
26346 start)
26347 echo \"foo bar\"
26348 ;;
26349 stop)
26350 echo \"bar baz\"
26351 ;;
26352 ˇ
26353 "});
26354 cx.update_editor(|editor, window, cx| {
26355 editor.handle_input("esac", window, cx);
26356 });
26357 cx.wait_for_autoindent_applied().await;
26358 cx.assert_editor_state(indoc! {"
26359 case \"$1\" in
26360 start)
26361 echo \"foo bar\"
26362 ;;
26363 stop)
26364 echo \"bar baz\"
26365 ;;
26366 esacˇ
26367 "});
26368
26369 // test `*)` auto outdents when typed inside `case` block
26370 cx.set_state(indoc! {"
26371 case \"$1\" in
26372 start)
26373 echo \"foo bar\"
26374 ;;
26375 ˇ
26376 "});
26377 cx.update_editor(|editor, window, cx| {
26378 editor.handle_input("*)", window, cx);
26379 });
26380 cx.wait_for_autoindent_applied().await;
26381 cx.assert_editor_state(indoc! {"
26382 case \"$1\" in
26383 start)
26384 echo \"foo bar\"
26385 ;;
26386 *)ˇ
26387 "});
26388
26389 // test `fi` outdents to correct level with nested if blocks
26390 cx.set_state(indoc! {"
26391 if [ \"$1\" = \"test\" ]; then
26392 echo \"outer if\"
26393 if [ \"$2\" = \"debug\" ]; then
26394 echo \"inner if\"
26395 ˇ
26396 "});
26397 cx.update_editor(|editor, window, cx| {
26398 editor.handle_input("fi", window, cx);
26399 });
26400 cx.wait_for_autoindent_applied().await;
26401 cx.assert_editor_state(indoc! {"
26402 if [ \"$1\" = \"test\" ]; then
26403 echo \"outer if\"
26404 if [ \"$2\" = \"debug\" ]; then
26405 echo \"inner if\"
26406 fiˇ
26407 "});
26408}
26409
26410#[gpui::test]
26411async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
26412 init_test(cx, |_| {});
26413 update_test_language_settings(cx, |settings| {
26414 settings.defaults.extend_comment_on_newline = Some(false);
26415 });
26416 let mut cx = EditorTestContext::new(cx).await;
26417 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26418 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26419
26420 // test correct indent after newline on comment
26421 cx.set_state(indoc! {"
26422 # COMMENT:ˇ
26423 "});
26424 cx.update_editor(|editor, window, cx| {
26425 editor.newline(&Newline, window, cx);
26426 });
26427 cx.wait_for_autoindent_applied().await;
26428 cx.assert_editor_state(indoc! {"
26429 # COMMENT:
26430 ˇ
26431 "});
26432
26433 // test correct indent after newline after `then`
26434 cx.set_state(indoc! {"
26435
26436 if [ \"$1\" = \"test\" ]; thenˇ
26437 "});
26438 cx.update_editor(|editor, window, cx| {
26439 editor.newline(&Newline, window, cx);
26440 });
26441 cx.wait_for_autoindent_applied().await;
26442 cx.assert_editor_state(indoc! {"
26443
26444 if [ \"$1\" = \"test\" ]; then
26445 ˇ
26446 "});
26447
26448 // test correct indent after newline after `else`
26449 cx.set_state(indoc! {"
26450 if [ \"$1\" = \"test\" ]; then
26451 elseˇ
26452 "});
26453 cx.update_editor(|editor, window, cx| {
26454 editor.newline(&Newline, window, cx);
26455 });
26456 cx.wait_for_autoindent_applied().await;
26457 cx.assert_editor_state(indoc! {"
26458 if [ \"$1\" = \"test\" ]; then
26459 else
26460 ˇ
26461 "});
26462
26463 // test correct indent after newline after `elif`
26464 cx.set_state(indoc! {"
26465 if [ \"$1\" = \"test\" ]; then
26466 elifˇ
26467 "});
26468 cx.update_editor(|editor, window, cx| {
26469 editor.newline(&Newline, window, cx);
26470 });
26471 cx.wait_for_autoindent_applied().await;
26472 cx.assert_editor_state(indoc! {"
26473 if [ \"$1\" = \"test\" ]; then
26474 elif
26475 ˇ
26476 "});
26477
26478 // test correct indent after newline after `do`
26479 cx.set_state(indoc! {"
26480 for file in *.txt; doˇ
26481 "});
26482 cx.update_editor(|editor, window, cx| {
26483 editor.newline(&Newline, window, cx);
26484 });
26485 cx.wait_for_autoindent_applied().await;
26486 cx.assert_editor_state(indoc! {"
26487 for file in *.txt; do
26488 ˇ
26489 "});
26490
26491 // test correct indent after newline after case pattern
26492 cx.set_state(indoc! {"
26493 case \"$1\" in
26494 start)ˇ
26495 "});
26496 cx.update_editor(|editor, window, cx| {
26497 editor.newline(&Newline, window, cx);
26498 });
26499 cx.wait_for_autoindent_applied().await;
26500 cx.assert_editor_state(indoc! {"
26501 case \"$1\" in
26502 start)
26503 ˇ
26504 "});
26505
26506 // test correct indent after newline after case pattern
26507 cx.set_state(indoc! {"
26508 case \"$1\" in
26509 start)
26510 ;;
26511 *)ˇ
26512 "});
26513 cx.update_editor(|editor, window, cx| {
26514 editor.newline(&Newline, window, cx);
26515 });
26516 cx.wait_for_autoindent_applied().await;
26517 cx.assert_editor_state(indoc! {"
26518 case \"$1\" in
26519 start)
26520 ;;
26521 *)
26522 ˇ
26523 "});
26524
26525 // test correct indent after newline after function opening brace
26526 cx.set_state(indoc! {"
26527 function test() {ˇ}
26528 "});
26529 cx.update_editor(|editor, window, cx| {
26530 editor.newline(&Newline, window, cx);
26531 });
26532 cx.wait_for_autoindent_applied().await;
26533 cx.assert_editor_state(indoc! {"
26534 function test() {
26535 ˇ
26536 }
26537 "});
26538
26539 // test no extra indent after semicolon on same line
26540 cx.set_state(indoc! {"
26541 echo \"test\";ˇ
26542 "});
26543 cx.update_editor(|editor, window, cx| {
26544 editor.newline(&Newline, window, cx);
26545 });
26546 cx.wait_for_autoindent_applied().await;
26547 cx.assert_editor_state(indoc! {"
26548 echo \"test\";
26549 ˇ
26550 "});
26551}
26552
26553fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
26554 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
26555 point..point
26556}
26557
26558#[track_caller]
26559fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
26560 let (text, ranges) = marked_text_ranges(marked_text, true);
26561 assert_eq!(editor.text(cx), text);
26562 assert_eq!(
26563 editor.selections.ranges(&editor.display_snapshot(cx)),
26564 ranges
26565 .iter()
26566 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
26567 .collect::<Vec<_>>(),
26568 "Assert selections are {}",
26569 marked_text
26570 );
26571}
26572
26573pub fn handle_signature_help_request(
26574 cx: &mut EditorLspTestContext,
26575 mocked_response: lsp::SignatureHelp,
26576) -> impl Future<Output = ()> + use<> {
26577 let mut request =
26578 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
26579 let mocked_response = mocked_response.clone();
26580 async move { Ok(Some(mocked_response)) }
26581 });
26582
26583 async move {
26584 request.next().await;
26585 }
26586}
26587
26588#[track_caller]
26589pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
26590 cx.update_editor(|editor, _, _| {
26591 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
26592 let entries = menu.entries.borrow();
26593 let entries = entries
26594 .iter()
26595 .map(|entry| entry.string.as_str())
26596 .collect::<Vec<_>>();
26597 assert_eq!(entries, expected);
26598 } else {
26599 panic!("Expected completions menu");
26600 }
26601 });
26602}
26603
26604#[gpui::test]
26605async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
26606 init_test(cx, |_| {});
26607 let mut cx = EditorLspTestContext::new_rust(
26608 lsp::ServerCapabilities {
26609 completion_provider: Some(lsp::CompletionOptions {
26610 ..Default::default()
26611 }),
26612 ..Default::default()
26613 },
26614 cx,
26615 )
26616 .await;
26617 cx.lsp
26618 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
26619 Ok(Some(lsp::CompletionResponse::Array(vec![
26620 lsp::CompletionItem {
26621 label: "unsafe".into(),
26622 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26623 range: lsp::Range {
26624 start: lsp::Position {
26625 line: 0,
26626 character: 9,
26627 },
26628 end: lsp::Position {
26629 line: 0,
26630 character: 11,
26631 },
26632 },
26633 new_text: "unsafe".to_string(),
26634 })),
26635 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
26636 ..Default::default()
26637 },
26638 ])))
26639 });
26640
26641 cx.update_editor(|editor, _, cx| {
26642 editor.project().unwrap().update(cx, |project, cx| {
26643 project.snippets().update(cx, |snippets, _cx| {
26644 snippets.add_snippet_for_test(
26645 None,
26646 PathBuf::from("test_snippets.json"),
26647 vec![
26648 Arc::new(project::snippet_provider::Snippet {
26649 prefix: vec![
26650 "unlimited word count".to_string(),
26651 "unlimit word count".to_string(),
26652 "unlimited unknown".to_string(),
26653 ],
26654 body: "this is many words".to_string(),
26655 description: Some("description".to_string()),
26656 name: "multi-word snippet test".to_string(),
26657 }),
26658 Arc::new(project::snippet_provider::Snippet {
26659 prefix: vec!["unsnip".to_string(), "@few".to_string()],
26660 body: "fewer words".to_string(),
26661 description: Some("alt description".to_string()),
26662 name: "other name".to_string(),
26663 }),
26664 Arc::new(project::snippet_provider::Snippet {
26665 prefix: vec!["ab aa".to_string()],
26666 body: "abcd".to_string(),
26667 description: None,
26668 name: "alphabet".to_string(),
26669 }),
26670 ],
26671 );
26672 });
26673 })
26674 });
26675
26676 let get_completions = |cx: &mut EditorLspTestContext| {
26677 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
26678 Some(CodeContextMenu::Completions(context_menu)) => {
26679 let entries = context_menu.entries.borrow();
26680 entries
26681 .iter()
26682 .map(|entry| entry.string.clone())
26683 .collect_vec()
26684 }
26685 _ => vec![],
26686 })
26687 };
26688
26689 // snippets:
26690 // @foo
26691 // foo bar
26692 //
26693 // when typing:
26694 //
26695 // when typing:
26696 // - if I type a symbol "open the completions with snippets only"
26697 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
26698 //
26699 // stuff we need:
26700 // - filtering logic change?
26701 // - remember how far back the completion started.
26702
26703 let test_cases: &[(&str, &[&str])] = &[
26704 (
26705 "un",
26706 &[
26707 "unsafe",
26708 "unlimit word count",
26709 "unlimited unknown",
26710 "unlimited word count",
26711 "unsnip",
26712 ],
26713 ),
26714 (
26715 "u ",
26716 &[
26717 "unlimit word count",
26718 "unlimited unknown",
26719 "unlimited word count",
26720 ],
26721 ),
26722 ("u a", &["ab aa", "unsafe"]), // unsAfe
26723 (
26724 "u u",
26725 &[
26726 "unsafe",
26727 "unlimit word count",
26728 "unlimited unknown", // ranked highest among snippets
26729 "unlimited word count",
26730 "unsnip",
26731 ],
26732 ),
26733 ("uw c", &["unlimit word count", "unlimited word count"]),
26734 (
26735 "u w",
26736 &[
26737 "unlimit word count",
26738 "unlimited word count",
26739 "unlimited unknown",
26740 ],
26741 ),
26742 ("u w ", &["unlimit word count", "unlimited word count"]),
26743 (
26744 "u ",
26745 &[
26746 "unlimit word count",
26747 "unlimited unknown",
26748 "unlimited word count",
26749 ],
26750 ),
26751 ("wor", &[]),
26752 ("uf", &["unsafe"]),
26753 ("af", &["unsafe"]),
26754 ("afu", &[]),
26755 (
26756 "ue",
26757 &["unsafe", "unlimited unknown", "unlimited word count"],
26758 ),
26759 ("@", &["@few"]),
26760 ("@few", &["@few"]),
26761 ("@ ", &[]),
26762 ("a@", &["@few"]),
26763 ("a@f", &["@few", "unsafe"]),
26764 ("a@fw", &["@few"]),
26765 ("a", &["ab aa", "unsafe"]),
26766 ("aa", &["ab aa"]),
26767 ("aaa", &["ab aa"]),
26768 ("ab", &["ab aa"]),
26769 ("ab ", &["ab aa"]),
26770 ("ab a", &["ab aa", "unsafe"]),
26771 ("ab ab", &["ab aa"]),
26772 ("ab ab aa", &["ab aa"]),
26773 ];
26774
26775 for &(input_to_simulate, expected_completions) in test_cases {
26776 cx.set_state("fn a() { ˇ }\n");
26777 for c in input_to_simulate.split("") {
26778 cx.simulate_input(c);
26779 cx.run_until_parked();
26780 }
26781 let expected_completions = expected_completions
26782 .iter()
26783 .map(|s| s.to_string())
26784 .collect_vec();
26785 assert_eq!(
26786 get_completions(&mut cx),
26787 expected_completions,
26788 "< actual / expected >, input = {input_to_simulate:?}",
26789 );
26790 }
26791}
26792
26793/// Handle completion request passing a marked string specifying where the completion
26794/// should be triggered from using '|' character, what range should be replaced, and what completions
26795/// should be returned using '<' and '>' to delimit the range.
26796///
26797/// Also see `handle_completion_request_with_insert_and_replace`.
26798#[track_caller]
26799pub fn handle_completion_request(
26800 marked_string: &str,
26801 completions: Vec<&'static str>,
26802 is_incomplete: bool,
26803 counter: Arc<AtomicUsize>,
26804 cx: &mut EditorLspTestContext,
26805) -> impl Future<Output = ()> {
26806 let complete_from_marker: TextRangeMarker = '|'.into();
26807 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26808 let (_, mut marked_ranges) = marked_text_ranges_by(
26809 marked_string,
26810 vec![complete_from_marker.clone(), replace_range_marker.clone()],
26811 );
26812
26813 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26814 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26815 ));
26816 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26817 let replace_range =
26818 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26819
26820 let mut request =
26821 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26822 let completions = completions.clone();
26823 counter.fetch_add(1, atomic::Ordering::Release);
26824 async move {
26825 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26826 assert_eq!(
26827 params.text_document_position.position,
26828 complete_from_position
26829 );
26830 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
26831 is_incomplete,
26832 item_defaults: None,
26833 items: completions
26834 .iter()
26835 .map(|completion_text| lsp::CompletionItem {
26836 label: completion_text.to_string(),
26837 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26838 range: replace_range,
26839 new_text: completion_text.to_string(),
26840 })),
26841 ..Default::default()
26842 })
26843 .collect(),
26844 })))
26845 }
26846 });
26847
26848 async move {
26849 request.next().await;
26850 }
26851}
26852
26853/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
26854/// given instead, which also contains an `insert` range.
26855///
26856/// This function uses markers to define ranges:
26857/// - `|` marks the cursor position
26858/// - `<>` marks the replace range
26859/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
26860pub fn handle_completion_request_with_insert_and_replace(
26861 cx: &mut EditorLspTestContext,
26862 marked_string: &str,
26863 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
26864 counter: Arc<AtomicUsize>,
26865) -> impl Future<Output = ()> {
26866 let complete_from_marker: TextRangeMarker = '|'.into();
26867 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26868 let insert_range_marker: TextRangeMarker = ('{', '}').into();
26869
26870 let (_, mut marked_ranges) = marked_text_ranges_by(
26871 marked_string,
26872 vec![
26873 complete_from_marker.clone(),
26874 replace_range_marker.clone(),
26875 insert_range_marker.clone(),
26876 ],
26877 );
26878
26879 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26880 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26881 ));
26882 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26883 let replace_range =
26884 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26885
26886 let insert_range = match marked_ranges.remove(&insert_range_marker) {
26887 Some(ranges) if !ranges.is_empty() => {
26888 let range1 = ranges[0].clone();
26889 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
26890 }
26891 _ => lsp::Range {
26892 start: replace_range.start,
26893 end: complete_from_position,
26894 },
26895 };
26896
26897 let mut request =
26898 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26899 let completions = completions.clone();
26900 counter.fetch_add(1, atomic::Ordering::Release);
26901 async move {
26902 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26903 assert_eq!(
26904 params.text_document_position.position, complete_from_position,
26905 "marker `|` position doesn't match",
26906 );
26907 Ok(Some(lsp::CompletionResponse::Array(
26908 completions
26909 .iter()
26910 .map(|(label, new_text)| lsp::CompletionItem {
26911 label: label.to_string(),
26912 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
26913 lsp::InsertReplaceEdit {
26914 insert: insert_range,
26915 replace: replace_range,
26916 new_text: new_text.to_string(),
26917 },
26918 )),
26919 ..Default::default()
26920 })
26921 .collect(),
26922 )))
26923 }
26924 });
26925
26926 async move {
26927 request.next().await;
26928 }
26929}
26930
26931fn handle_resolve_completion_request(
26932 cx: &mut EditorLspTestContext,
26933 edits: Option<Vec<(&'static str, &'static str)>>,
26934) -> impl Future<Output = ()> {
26935 let edits = edits.map(|edits| {
26936 edits
26937 .iter()
26938 .map(|(marked_string, new_text)| {
26939 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
26940 let replace_range = cx.to_lsp_range(
26941 MultiBufferOffset(marked_ranges[0].start)
26942 ..MultiBufferOffset(marked_ranges[0].end),
26943 );
26944 lsp::TextEdit::new(replace_range, new_text.to_string())
26945 })
26946 .collect::<Vec<_>>()
26947 });
26948
26949 let mut request =
26950 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
26951 let edits = edits.clone();
26952 async move {
26953 Ok(lsp::CompletionItem {
26954 additional_text_edits: edits,
26955 ..Default::default()
26956 })
26957 }
26958 });
26959
26960 async move {
26961 request.next().await;
26962 }
26963}
26964
26965pub(crate) fn update_test_language_settings(
26966 cx: &mut TestAppContext,
26967 f: impl Fn(&mut AllLanguageSettingsContent),
26968) {
26969 cx.update(|cx| {
26970 SettingsStore::update_global(cx, |store, cx| {
26971 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
26972 });
26973 });
26974}
26975
26976pub(crate) fn update_test_project_settings(
26977 cx: &mut TestAppContext,
26978 f: impl Fn(&mut ProjectSettingsContent),
26979) {
26980 cx.update(|cx| {
26981 SettingsStore::update_global(cx, |store, cx| {
26982 store.update_user_settings(cx, |settings| f(&mut settings.project));
26983 });
26984 });
26985}
26986
26987pub(crate) fn update_test_editor_settings(
26988 cx: &mut TestAppContext,
26989 f: impl Fn(&mut EditorSettingsContent),
26990) {
26991 cx.update(|cx| {
26992 SettingsStore::update_global(cx, |store, cx| {
26993 store.update_user_settings(cx, |settings| f(&mut settings.editor));
26994 })
26995 })
26996}
26997
26998pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
26999 cx.update(|cx| {
27000 assets::Assets.load_test_fonts(cx);
27001 let store = SettingsStore::test(cx);
27002 cx.set_global(store);
27003 theme::init(theme::LoadThemes::JustBase, cx);
27004 release_channel::init(semver::Version::new(0, 0, 0), cx);
27005 crate::init(cx);
27006 });
27007 zlog::init_test();
27008 update_test_language_settings(cx, f);
27009}
27010
27011#[track_caller]
27012fn assert_hunk_revert(
27013 not_reverted_text_with_selections: &str,
27014 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
27015 expected_reverted_text_with_selections: &str,
27016 base_text: &str,
27017 cx: &mut EditorLspTestContext,
27018) {
27019 cx.set_state(not_reverted_text_with_selections);
27020 cx.set_head_text(base_text);
27021 cx.executor().run_until_parked();
27022
27023 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
27024 let snapshot = editor.snapshot(window, cx);
27025 let reverted_hunk_statuses = snapshot
27026 .buffer_snapshot()
27027 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
27028 .map(|hunk| hunk.status().kind)
27029 .collect::<Vec<_>>();
27030
27031 editor.git_restore(&Default::default(), window, cx);
27032 reverted_hunk_statuses
27033 });
27034 cx.executor().run_until_parked();
27035 cx.assert_editor_state(expected_reverted_text_with_selections);
27036 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
27037}
27038
27039#[gpui::test(iterations = 10)]
27040async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
27041 init_test(cx, |_| {});
27042
27043 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
27044 let counter = diagnostic_requests.clone();
27045
27046 let fs = FakeFs::new(cx.executor());
27047 fs.insert_tree(
27048 path!("/a"),
27049 json!({
27050 "first.rs": "fn main() { let a = 5; }",
27051 "second.rs": "// Test file",
27052 }),
27053 )
27054 .await;
27055
27056 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27057 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27058 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27059
27060 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27061 language_registry.add(rust_lang());
27062 let mut fake_servers = language_registry.register_fake_lsp(
27063 "Rust",
27064 FakeLspAdapter {
27065 capabilities: lsp::ServerCapabilities {
27066 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
27067 lsp::DiagnosticOptions {
27068 identifier: None,
27069 inter_file_dependencies: true,
27070 workspace_diagnostics: true,
27071 work_done_progress_options: Default::default(),
27072 },
27073 )),
27074 ..Default::default()
27075 },
27076 ..Default::default()
27077 },
27078 );
27079
27080 let editor = workspace
27081 .update(cx, |workspace, window, cx| {
27082 workspace.open_abs_path(
27083 PathBuf::from(path!("/a/first.rs")),
27084 OpenOptions::default(),
27085 window,
27086 cx,
27087 )
27088 })
27089 .unwrap()
27090 .await
27091 .unwrap()
27092 .downcast::<Editor>()
27093 .unwrap();
27094 let fake_server = fake_servers.next().await.unwrap();
27095 let server_id = fake_server.server.server_id();
27096 let mut first_request = fake_server
27097 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
27098 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
27099 let result_id = Some(new_result_id.to_string());
27100 assert_eq!(
27101 params.text_document.uri,
27102 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27103 );
27104 async move {
27105 Ok(lsp::DocumentDiagnosticReportResult::Report(
27106 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
27107 related_documents: None,
27108 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
27109 items: Vec::new(),
27110 result_id,
27111 },
27112 }),
27113 ))
27114 }
27115 });
27116
27117 let ensure_result_id = |expected: Option<SharedString>, cx: &mut TestAppContext| {
27118 project.update(cx, |project, cx| {
27119 let buffer_id = editor
27120 .read(cx)
27121 .buffer()
27122 .read(cx)
27123 .as_singleton()
27124 .expect("created a singleton buffer")
27125 .read(cx)
27126 .remote_id();
27127 let buffer_result_id = project
27128 .lsp_store()
27129 .read(cx)
27130 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
27131 assert_eq!(expected, buffer_result_id);
27132 });
27133 };
27134
27135 ensure_result_id(None, cx);
27136 cx.executor().advance_clock(Duration::from_millis(60));
27137 cx.executor().run_until_parked();
27138 assert_eq!(
27139 diagnostic_requests.load(atomic::Ordering::Acquire),
27140 1,
27141 "Opening file should trigger diagnostic request"
27142 );
27143 first_request
27144 .next()
27145 .await
27146 .expect("should have sent the first diagnostics pull request");
27147 ensure_result_id(Some(SharedString::new("1")), cx);
27148
27149 // Editing should trigger diagnostics
27150 editor.update_in(cx, |editor, window, cx| {
27151 editor.handle_input("2", window, cx)
27152 });
27153 cx.executor().advance_clock(Duration::from_millis(60));
27154 cx.executor().run_until_parked();
27155 assert_eq!(
27156 diagnostic_requests.load(atomic::Ordering::Acquire),
27157 2,
27158 "Editing should trigger diagnostic request"
27159 );
27160 ensure_result_id(Some(SharedString::new("2")), cx);
27161
27162 // Moving cursor should not trigger diagnostic request
27163 editor.update_in(cx, |editor, window, cx| {
27164 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27165 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
27166 });
27167 });
27168 cx.executor().advance_clock(Duration::from_millis(60));
27169 cx.executor().run_until_parked();
27170 assert_eq!(
27171 diagnostic_requests.load(atomic::Ordering::Acquire),
27172 2,
27173 "Cursor movement should not trigger diagnostic request"
27174 );
27175 ensure_result_id(Some(SharedString::new("2")), cx);
27176 // Multiple rapid edits should be debounced
27177 for _ in 0..5 {
27178 editor.update_in(cx, |editor, window, cx| {
27179 editor.handle_input("x", window, cx)
27180 });
27181 }
27182 cx.executor().advance_clock(Duration::from_millis(60));
27183 cx.executor().run_until_parked();
27184
27185 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
27186 assert!(
27187 final_requests <= 4,
27188 "Multiple rapid edits should be debounced (got {final_requests} requests)",
27189 );
27190 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
27191}
27192
27193#[gpui::test]
27194async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
27195 // Regression test for issue #11671
27196 // Previously, adding a cursor after moving multiple cursors would reset
27197 // the cursor count instead of adding to the existing cursors.
27198 init_test(cx, |_| {});
27199 let mut cx = EditorTestContext::new(cx).await;
27200
27201 // Create a simple buffer with cursor at start
27202 cx.set_state(indoc! {"
27203 ˇaaaa
27204 bbbb
27205 cccc
27206 dddd
27207 eeee
27208 ffff
27209 gggg
27210 hhhh"});
27211
27212 // Add 2 cursors below (so we have 3 total)
27213 cx.update_editor(|editor, window, cx| {
27214 editor.add_selection_below(&Default::default(), window, cx);
27215 editor.add_selection_below(&Default::default(), window, cx);
27216 });
27217
27218 // Verify we have 3 cursors
27219 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
27220 assert_eq!(
27221 initial_count, 3,
27222 "Should have 3 cursors after adding 2 below"
27223 );
27224
27225 // Move down one line
27226 cx.update_editor(|editor, window, cx| {
27227 editor.move_down(&MoveDown, window, cx);
27228 });
27229
27230 // Add another cursor below
27231 cx.update_editor(|editor, window, cx| {
27232 editor.add_selection_below(&Default::default(), window, cx);
27233 });
27234
27235 // Should now have 4 cursors (3 original + 1 new)
27236 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
27237 assert_eq!(
27238 final_count, 4,
27239 "Should have 4 cursors after moving and adding another"
27240 );
27241}
27242
27243#[gpui::test]
27244async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
27245 init_test(cx, |_| {});
27246
27247 let mut cx = EditorTestContext::new(cx).await;
27248
27249 cx.set_state(indoc!(
27250 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
27251 Second line here"#
27252 ));
27253
27254 cx.update_editor(|editor, window, cx| {
27255 // Enable soft wrapping with a narrow width to force soft wrapping and
27256 // confirm that more than 2 rows are being displayed.
27257 editor.set_wrap_width(Some(100.0.into()), cx);
27258 assert!(editor.display_text(cx).lines().count() > 2);
27259
27260 editor.add_selection_below(
27261 &AddSelectionBelow {
27262 skip_soft_wrap: true,
27263 },
27264 window,
27265 cx,
27266 );
27267
27268 assert_eq!(
27269 display_ranges(editor, cx),
27270 &[
27271 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27272 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
27273 ]
27274 );
27275
27276 editor.add_selection_above(
27277 &AddSelectionAbove {
27278 skip_soft_wrap: true,
27279 },
27280 window,
27281 cx,
27282 );
27283
27284 assert_eq!(
27285 display_ranges(editor, cx),
27286 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27287 );
27288
27289 editor.add_selection_below(
27290 &AddSelectionBelow {
27291 skip_soft_wrap: false,
27292 },
27293 window,
27294 cx,
27295 );
27296
27297 assert_eq!(
27298 display_ranges(editor, cx),
27299 &[
27300 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27301 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
27302 ]
27303 );
27304
27305 editor.add_selection_above(
27306 &AddSelectionAbove {
27307 skip_soft_wrap: false,
27308 },
27309 window,
27310 cx,
27311 );
27312
27313 assert_eq!(
27314 display_ranges(editor, cx),
27315 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27316 );
27317 });
27318
27319 // Set up text where selections are in the middle of a soft-wrapped line.
27320 // When adding selection below with `skip_soft_wrap` set to `true`, the new
27321 // selection should be at the same buffer column, not the same pixel
27322 // position.
27323 cx.set_state(indoc!(
27324 r#"1. Very long line to show «howˇ» a wrapped line would look
27325 2. Very long line to show how a wrapped line would look"#
27326 ));
27327
27328 cx.update_editor(|editor, window, cx| {
27329 // Enable soft wrapping with a narrow width to force soft wrapping and
27330 // confirm that more than 2 rows are being displayed.
27331 editor.set_wrap_width(Some(100.0.into()), cx);
27332 assert!(editor.display_text(cx).lines().count() > 2);
27333
27334 editor.add_selection_below(
27335 &AddSelectionBelow {
27336 skip_soft_wrap: true,
27337 },
27338 window,
27339 cx,
27340 );
27341
27342 // Assert that there's now 2 selections, both selecting the same column
27343 // range in the buffer row.
27344 let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
27345 let selections = editor.selections.all::<Point>(&display_map);
27346 assert_eq!(selections.len(), 2);
27347 assert_eq!(selections[0].start.column, selections[1].start.column);
27348 assert_eq!(selections[0].end.column, selections[1].end.column);
27349 });
27350}
27351
27352#[gpui::test]
27353async fn test_insert_snippet(cx: &mut TestAppContext) {
27354 init_test(cx, |_| {});
27355 let mut cx = EditorTestContext::new(cx).await;
27356
27357 cx.update_editor(|editor, _, cx| {
27358 editor.project().unwrap().update(cx, |project, cx| {
27359 project.snippets().update(cx, |snippets, _cx| {
27360 let snippet = project::snippet_provider::Snippet {
27361 prefix: vec![], // no prefix needed!
27362 body: "an Unspecified".to_string(),
27363 description: Some("shhhh it's a secret".to_string()),
27364 name: "super secret snippet".to_string(),
27365 };
27366 snippets.add_snippet_for_test(
27367 None,
27368 PathBuf::from("test_snippets.json"),
27369 vec![Arc::new(snippet)],
27370 );
27371
27372 let snippet = project::snippet_provider::Snippet {
27373 prefix: vec![], // no prefix needed!
27374 body: " Location".to_string(),
27375 description: Some("the word 'location'".to_string()),
27376 name: "location word".to_string(),
27377 };
27378 snippets.add_snippet_for_test(
27379 Some("Markdown".to_string()),
27380 PathBuf::from("test_snippets.json"),
27381 vec![Arc::new(snippet)],
27382 );
27383 });
27384 })
27385 });
27386
27387 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
27388
27389 cx.update_editor(|editor, window, cx| {
27390 editor.insert_snippet_at_selections(
27391 &InsertSnippet {
27392 language: None,
27393 name: Some("super secret snippet".to_string()),
27394 snippet: None,
27395 },
27396 window,
27397 cx,
27398 );
27399
27400 // Language is specified in the action,
27401 // so the buffer language does not need to match
27402 editor.insert_snippet_at_selections(
27403 &InsertSnippet {
27404 language: Some("Markdown".to_string()),
27405 name: Some("location word".to_string()),
27406 snippet: None,
27407 },
27408 window,
27409 cx,
27410 );
27411
27412 editor.insert_snippet_at_selections(
27413 &InsertSnippet {
27414 language: None,
27415 name: None,
27416 snippet: Some("$0 after".to_string()),
27417 },
27418 window,
27419 cx,
27420 );
27421 });
27422
27423 cx.assert_editor_state(
27424 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
27425 );
27426}
27427
27428#[gpui::test(iterations = 10)]
27429async fn test_document_colors(cx: &mut TestAppContext) {
27430 let expected_color = Rgba {
27431 r: 0.33,
27432 g: 0.33,
27433 b: 0.33,
27434 a: 0.33,
27435 };
27436
27437 init_test(cx, |_| {});
27438
27439 let fs = FakeFs::new(cx.executor());
27440 fs.insert_tree(
27441 path!("/a"),
27442 json!({
27443 "first.rs": "fn main() { let a = 5; }",
27444 }),
27445 )
27446 .await;
27447
27448 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27449 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27450 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27451
27452 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27453 language_registry.add(rust_lang());
27454 let mut fake_servers = language_registry.register_fake_lsp(
27455 "Rust",
27456 FakeLspAdapter {
27457 capabilities: lsp::ServerCapabilities {
27458 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
27459 ..lsp::ServerCapabilities::default()
27460 },
27461 name: "rust-analyzer",
27462 ..FakeLspAdapter::default()
27463 },
27464 );
27465 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
27466 "Rust",
27467 FakeLspAdapter {
27468 capabilities: lsp::ServerCapabilities {
27469 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
27470 ..lsp::ServerCapabilities::default()
27471 },
27472 name: "not-rust-analyzer",
27473 ..FakeLspAdapter::default()
27474 },
27475 );
27476
27477 let editor = workspace
27478 .update(cx, |workspace, window, cx| {
27479 workspace.open_abs_path(
27480 PathBuf::from(path!("/a/first.rs")),
27481 OpenOptions::default(),
27482 window,
27483 cx,
27484 )
27485 })
27486 .unwrap()
27487 .await
27488 .unwrap()
27489 .downcast::<Editor>()
27490 .unwrap();
27491 let fake_language_server = fake_servers.next().await.unwrap();
27492 let fake_language_server_without_capabilities =
27493 fake_servers_without_capabilities.next().await.unwrap();
27494 let requests_made = Arc::new(AtomicUsize::new(0));
27495 let closure_requests_made = Arc::clone(&requests_made);
27496 let mut color_request_handle = fake_language_server
27497 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27498 let requests_made = Arc::clone(&closure_requests_made);
27499 async move {
27500 assert_eq!(
27501 params.text_document.uri,
27502 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27503 );
27504 requests_made.fetch_add(1, atomic::Ordering::Release);
27505 Ok(vec![
27506 lsp::ColorInformation {
27507 range: lsp::Range {
27508 start: lsp::Position {
27509 line: 0,
27510 character: 0,
27511 },
27512 end: lsp::Position {
27513 line: 0,
27514 character: 1,
27515 },
27516 },
27517 color: lsp::Color {
27518 red: 0.33,
27519 green: 0.33,
27520 blue: 0.33,
27521 alpha: 0.33,
27522 },
27523 },
27524 lsp::ColorInformation {
27525 range: lsp::Range {
27526 start: lsp::Position {
27527 line: 0,
27528 character: 0,
27529 },
27530 end: lsp::Position {
27531 line: 0,
27532 character: 1,
27533 },
27534 },
27535 color: lsp::Color {
27536 red: 0.33,
27537 green: 0.33,
27538 blue: 0.33,
27539 alpha: 0.33,
27540 },
27541 },
27542 ])
27543 }
27544 });
27545
27546 let _handle = fake_language_server_without_capabilities
27547 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
27548 panic!("Should not be called");
27549 });
27550 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27551 color_request_handle.next().await.unwrap();
27552 cx.run_until_parked();
27553 assert_eq!(
27554 1,
27555 requests_made.load(atomic::Ordering::Acquire),
27556 "Should query for colors once per editor open"
27557 );
27558 editor.update_in(cx, |editor, _, cx| {
27559 assert_eq!(
27560 vec![expected_color],
27561 extract_color_inlays(editor, cx),
27562 "Should have an initial inlay"
27563 );
27564 });
27565
27566 // opening another file in a split should not influence the LSP query counter
27567 workspace
27568 .update(cx, |workspace, window, cx| {
27569 assert_eq!(
27570 workspace.panes().len(),
27571 1,
27572 "Should have one pane with one editor"
27573 );
27574 workspace.move_item_to_pane_in_direction(
27575 &MoveItemToPaneInDirection {
27576 direction: SplitDirection::Right,
27577 focus: false,
27578 clone: true,
27579 },
27580 window,
27581 cx,
27582 );
27583 })
27584 .unwrap();
27585 cx.run_until_parked();
27586 workspace
27587 .update(cx, |workspace, _, cx| {
27588 let panes = workspace.panes();
27589 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
27590 for pane in panes {
27591 let editor = pane
27592 .read(cx)
27593 .active_item()
27594 .and_then(|item| item.downcast::<Editor>())
27595 .expect("Should have opened an editor in each split");
27596 let editor_file = editor
27597 .read(cx)
27598 .buffer()
27599 .read(cx)
27600 .as_singleton()
27601 .expect("test deals with singleton buffers")
27602 .read(cx)
27603 .file()
27604 .expect("test buffese should have a file")
27605 .path();
27606 assert_eq!(
27607 editor_file.as_ref(),
27608 rel_path("first.rs"),
27609 "Both editors should be opened for the same file"
27610 )
27611 }
27612 })
27613 .unwrap();
27614
27615 cx.executor().advance_clock(Duration::from_millis(500));
27616 let save = editor.update_in(cx, |editor, window, cx| {
27617 editor.move_to_end(&MoveToEnd, window, cx);
27618 editor.handle_input("dirty", window, cx);
27619 editor.save(
27620 SaveOptions {
27621 format: true,
27622 autosave: true,
27623 },
27624 project.clone(),
27625 window,
27626 cx,
27627 )
27628 });
27629 save.await.unwrap();
27630
27631 color_request_handle.next().await.unwrap();
27632 cx.run_until_parked();
27633 assert_eq!(
27634 2,
27635 requests_made.load(atomic::Ordering::Acquire),
27636 "Should query for colors once per save (deduplicated) and once per formatting after save"
27637 );
27638
27639 drop(editor);
27640 let close = workspace
27641 .update(cx, |workspace, window, cx| {
27642 workspace.active_pane().update(cx, |pane, cx| {
27643 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27644 })
27645 })
27646 .unwrap();
27647 close.await.unwrap();
27648 let close = workspace
27649 .update(cx, |workspace, window, cx| {
27650 workspace.active_pane().update(cx, |pane, cx| {
27651 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27652 })
27653 })
27654 .unwrap();
27655 close.await.unwrap();
27656 assert_eq!(
27657 2,
27658 requests_made.load(atomic::Ordering::Acquire),
27659 "After saving and closing all editors, no extra requests should be made"
27660 );
27661 workspace
27662 .update(cx, |workspace, _, cx| {
27663 assert!(
27664 workspace.active_item(cx).is_none(),
27665 "Should close all editors"
27666 )
27667 })
27668 .unwrap();
27669
27670 workspace
27671 .update(cx, |workspace, window, cx| {
27672 workspace.active_pane().update(cx, |pane, cx| {
27673 pane.navigate_backward(&workspace::GoBack, window, cx);
27674 })
27675 })
27676 .unwrap();
27677 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27678 cx.run_until_parked();
27679 let editor = workspace
27680 .update(cx, |workspace, _, cx| {
27681 workspace
27682 .active_item(cx)
27683 .expect("Should have reopened the editor again after navigating back")
27684 .downcast::<Editor>()
27685 .expect("Should be an editor")
27686 })
27687 .unwrap();
27688
27689 assert_eq!(
27690 2,
27691 requests_made.load(atomic::Ordering::Acquire),
27692 "Cache should be reused on buffer close and reopen"
27693 );
27694 editor.update(cx, |editor, cx| {
27695 assert_eq!(
27696 vec![expected_color],
27697 extract_color_inlays(editor, cx),
27698 "Should have an initial inlay"
27699 );
27700 });
27701
27702 drop(color_request_handle);
27703 let closure_requests_made = Arc::clone(&requests_made);
27704 let mut empty_color_request_handle = fake_language_server
27705 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27706 let requests_made = Arc::clone(&closure_requests_made);
27707 async move {
27708 assert_eq!(
27709 params.text_document.uri,
27710 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27711 );
27712 requests_made.fetch_add(1, atomic::Ordering::Release);
27713 Ok(Vec::new())
27714 }
27715 });
27716 let save = editor.update_in(cx, |editor, window, cx| {
27717 editor.move_to_end(&MoveToEnd, window, cx);
27718 editor.handle_input("dirty_again", window, cx);
27719 editor.save(
27720 SaveOptions {
27721 format: false,
27722 autosave: true,
27723 },
27724 project.clone(),
27725 window,
27726 cx,
27727 )
27728 });
27729 save.await.unwrap();
27730
27731 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27732 empty_color_request_handle.next().await.unwrap();
27733 cx.run_until_parked();
27734 assert_eq!(
27735 3,
27736 requests_made.load(atomic::Ordering::Acquire),
27737 "Should query for colors once per save only, as formatting was not requested"
27738 );
27739 editor.update(cx, |editor, cx| {
27740 assert_eq!(
27741 Vec::<Rgba>::new(),
27742 extract_color_inlays(editor, cx),
27743 "Should clear all colors when the server returns an empty response"
27744 );
27745 });
27746}
27747
27748#[gpui::test]
27749async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
27750 init_test(cx, |_| {});
27751 let (editor, cx) = cx.add_window_view(Editor::single_line);
27752 editor.update_in(cx, |editor, window, cx| {
27753 editor.set_text("oops\n\nwow\n", window, cx)
27754 });
27755 cx.run_until_parked();
27756 editor.update(cx, |editor, cx| {
27757 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
27758 });
27759 editor.update(cx, |editor, cx| {
27760 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
27761 });
27762 cx.run_until_parked();
27763 editor.update(cx, |editor, cx| {
27764 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
27765 });
27766}
27767
27768#[gpui::test]
27769async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
27770 init_test(cx, |_| {});
27771
27772 cx.update(|cx| {
27773 register_project_item::<Editor>(cx);
27774 });
27775
27776 let fs = FakeFs::new(cx.executor());
27777 fs.insert_tree("/root1", json!({})).await;
27778 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
27779 .await;
27780
27781 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
27782 let (workspace, cx) =
27783 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
27784
27785 let worktree_id = project.update(cx, |project, cx| {
27786 project.worktrees(cx).next().unwrap().read(cx).id()
27787 });
27788
27789 let handle = workspace
27790 .update_in(cx, |workspace, window, cx| {
27791 let project_path = (worktree_id, rel_path("one.pdf"));
27792 workspace.open_path(project_path, None, true, window, cx)
27793 })
27794 .await
27795 .unwrap();
27796 // The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
27797 // Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
27798 // With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
27799 assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
27800}
27801
27802#[gpui::test]
27803async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
27804 init_test(cx, |_| {});
27805
27806 let language = Arc::new(Language::new(
27807 LanguageConfig::default(),
27808 Some(tree_sitter_rust::LANGUAGE.into()),
27809 ));
27810
27811 // Test hierarchical sibling navigation
27812 let text = r#"
27813 fn outer() {
27814 if condition {
27815 let a = 1;
27816 }
27817 let b = 2;
27818 }
27819
27820 fn another() {
27821 let c = 3;
27822 }
27823 "#;
27824
27825 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
27826 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
27827 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
27828
27829 // Wait for parsing to complete
27830 editor
27831 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
27832 .await;
27833
27834 editor.update_in(cx, |editor, window, cx| {
27835 // Start by selecting "let a = 1;" inside the if block
27836 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27837 s.select_display_ranges([
27838 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
27839 ]);
27840 });
27841
27842 let initial_selection = editor
27843 .selections
27844 .display_ranges(&editor.display_snapshot(cx));
27845 assert_eq!(initial_selection.len(), 1, "Should have one selection");
27846
27847 // Test select next sibling - should move up levels to find the next sibling
27848 // Since "let a = 1;" has no siblings in the if block, it should move up
27849 // to find "let b = 2;" which is a sibling of the if block
27850 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27851 let next_selection = editor
27852 .selections
27853 .display_ranges(&editor.display_snapshot(cx));
27854
27855 // Should have a selection and it should be different from the initial
27856 assert_eq!(
27857 next_selection.len(),
27858 1,
27859 "Should have one selection after next"
27860 );
27861 assert_ne!(
27862 next_selection[0], initial_selection[0],
27863 "Next sibling selection should be different"
27864 );
27865
27866 // Test hierarchical navigation by going to the end of the current function
27867 // and trying to navigate to the next function
27868 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27869 s.select_display_ranges([
27870 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
27871 ]);
27872 });
27873
27874 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27875 let function_next_selection = editor
27876 .selections
27877 .display_ranges(&editor.display_snapshot(cx));
27878
27879 // Should move to the next function
27880 assert_eq!(
27881 function_next_selection.len(),
27882 1,
27883 "Should have one selection after function next"
27884 );
27885
27886 // Test select previous sibling navigation
27887 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
27888 let prev_selection = editor
27889 .selections
27890 .display_ranges(&editor.display_snapshot(cx));
27891
27892 // Should have a selection and it should be different
27893 assert_eq!(
27894 prev_selection.len(),
27895 1,
27896 "Should have one selection after prev"
27897 );
27898 assert_ne!(
27899 prev_selection[0], function_next_selection[0],
27900 "Previous sibling selection should be different from next"
27901 );
27902 });
27903}
27904
27905#[gpui::test]
27906async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
27907 init_test(cx, |_| {});
27908
27909 let mut cx = EditorTestContext::new(cx).await;
27910 cx.set_state(
27911 "let ˇvariable = 42;
27912let another = variable + 1;
27913let result = variable * 2;",
27914 );
27915
27916 // Set up document highlights manually (simulating LSP response)
27917 cx.update_editor(|editor, _window, cx| {
27918 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
27919
27920 // Create highlights for "variable" occurrences
27921 let highlight_ranges = [
27922 Point::new(0, 4)..Point::new(0, 12), // First "variable"
27923 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
27924 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
27925 ];
27926
27927 let anchor_ranges: Vec<_> = highlight_ranges
27928 .iter()
27929 .map(|range| range.clone().to_anchors(&buffer_snapshot))
27930 .collect();
27931
27932 editor.highlight_background::<DocumentHighlightRead>(
27933 &anchor_ranges,
27934 |_, theme| theme.colors().editor_document_highlight_read_background,
27935 cx,
27936 );
27937 });
27938
27939 // Go to next highlight - should move to second "variable"
27940 cx.update_editor(|editor, window, cx| {
27941 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27942 });
27943 cx.assert_editor_state(
27944 "let variable = 42;
27945let another = ˇvariable + 1;
27946let result = variable * 2;",
27947 );
27948
27949 // Go to next highlight - should move to third "variable"
27950 cx.update_editor(|editor, window, cx| {
27951 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27952 });
27953 cx.assert_editor_state(
27954 "let variable = 42;
27955let another = variable + 1;
27956let result = ˇvariable * 2;",
27957 );
27958
27959 // Go to next highlight - should stay at third "variable" (no wrap-around)
27960 cx.update_editor(|editor, window, cx| {
27961 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27962 });
27963 cx.assert_editor_state(
27964 "let variable = 42;
27965let another = variable + 1;
27966let result = ˇvariable * 2;",
27967 );
27968
27969 // Now test going backwards from third position
27970 cx.update_editor(|editor, window, cx| {
27971 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27972 });
27973 cx.assert_editor_state(
27974 "let variable = 42;
27975let another = ˇvariable + 1;
27976let result = variable * 2;",
27977 );
27978
27979 // Go to previous highlight - should move to first "variable"
27980 cx.update_editor(|editor, window, cx| {
27981 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27982 });
27983 cx.assert_editor_state(
27984 "let ˇvariable = 42;
27985let another = variable + 1;
27986let result = variable * 2;",
27987 );
27988
27989 // Go to previous highlight - should stay on first "variable"
27990 cx.update_editor(|editor, window, cx| {
27991 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27992 });
27993 cx.assert_editor_state(
27994 "let ˇvariable = 42;
27995let another = variable + 1;
27996let result = variable * 2;",
27997 );
27998}
27999
28000#[gpui::test]
28001async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
28002 cx: &mut gpui::TestAppContext,
28003) {
28004 init_test(cx, |_| {});
28005
28006 let url = "https://zed.dev";
28007
28008 let markdown_language = Arc::new(Language::new(
28009 LanguageConfig {
28010 name: "Markdown".into(),
28011 ..LanguageConfig::default()
28012 },
28013 None,
28014 ));
28015
28016 let mut cx = EditorTestContext::new(cx).await;
28017 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28018 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
28019
28020 cx.update_editor(|editor, window, cx| {
28021 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28022 editor.paste(&Paste, window, cx);
28023 });
28024
28025 cx.assert_editor_state(&format!(
28026 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
28027 ));
28028}
28029
28030#[gpui::test]
28031async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
28032 init_test(cx, |_| {});
28033
28034 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
28035 let mut cx = EditorTestContext::new(cx).await;
28036
28037 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28038
28039 // Case 1: Test if adding a character with multi cursors preserves nested list indents
28040 cx.set_state(&indoc! {"
28041 - [ ] Item 1
28042 - [ ] Item 1.a
28043 - [ˇ] Item 2
28044 - [ˇ] Item 2.a
28045 - [ˇ] Item 2.b
28046 "
28047 });
28048 cx.update_editor(|editor, window, cx| {
28049 editor.handle_input("x", window, cx);
28050 });
28051 cx.run_until_parked();
28052 cx.assert_editor_state(indoc! {"
28053 - [ ] Item 1
28054 - [ ] Item 1.a
28055 - [xˇ] Item 2
28056 - [xˇ] Item 2.a
28057 - [xˇ] Item 2.b
28058 "
28059 });
28060
28061 // Case 2: Test adding new line after nested list continues the list with unchecked task
28062 cx.set_state(&indoc! {"
28063 - [ ] Item 1
28064 - [ ] Item 1.a
28065 - [x] Item 2
28066 - [x] Item 2.a
28067 - [x] Item 2.bˇ"
28068 });
28069 cx.update_editor(|editor, window, cx| {
28070 editor.newline(&Newline, window, cx);
28071 });
28072 cx.assert_editor_state(indoc! {"
28073 - [ ] Item 1
28074 - [ ] Item 1.a
28075 - [x] Item 2
28076 - [x] Item 2.a
28077 - [x] Item 2.b
28078 - [ ] ˇ"
28079 });
28080
28081 // Case 3: Test adding content to continued list item
28082 cx.update_editor(|editor, window, cx| {
28083 editor.handle_input("Item 2.c", window, cx);
28084 });
28085 cx.run_until_parked();
28086 cx.assert_editor_state(indoc! {"
28087 - [ ] Item 1
28088 - [ ] Item 1.a
28089 - [x] Item 2
28090 - [x] Item 2.a
28091 - [x] Item 2.b
28092 - [ ] Item 2.cˇ"
28093 });
28094
28095 // Case 4: Test adding new line after nested ordered list continues with next number
28096 cx.set_state(indoc! {"
28097 1. Item 1
28098 1. Item 1.a
28099 2. Item 2
28100 1. Item 2.a
28101 2. Item 2.bˇ"
28102 });
28103 cx.update_editor(|editor, window, cx| {
28104 editor.newline(&Newline, window, cx);
28105 });
28106 cx.assert_editor_state(indoc! {"
28107 1. Item 1
28108 1. Item 1.a
28109 2. Item 2
28110 1. Item 2.a
28111 2. Item 2.b
28112 3. ˇ"
28113 });
28114
28115 // Case 5: Adding content to continued ordered list item
28116 cx.update_editor(|editor, window, cx| {
28117 editor.handle_input("Item 2.c", window, cx);
28118 });
28119 cx.run_until_parked();
28120 cx.assert_editor_state(indoc! {"
28121 1. Item 1
28122 1. Item 1.a
28123 2. Item 2
28124 1. Item 2.a
28125 2. Item 2.b
28126 3. Item 2.cˇ"
28127 });
28128
28129 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
28130 cx.set_state(indoc! {"
28131 - Item 1
28132 - Item 1.a
28133 - Item 1.a
28134 ˇ"});
28135 cx.update_editor(|editor, window, cx| {
28136 editor.handle_input("-", window, cx);
28137 });
28138 cx.run_until_parked();
28139 cx.assert_editor_state(indoc! {"
28140 - Item 1
28141 - Item 1.a
28142 - Item 1.a
28143 -ˇ"});
28144
28145 // Case 7: Test blockquote newline preserves something
28146 cx.set_state(indoc! {"
28147 > Item 1ˇ"
28148 });
28149 cx.update_editor(|editor, window, cx| {
28150 editor.newline(&Newline, window, cx);
28151 });
28152 cx.assert_editor_state(indoc! {"
28153 > Item 1
28154 ˇ"
28155 });
28156}
28157
28158#[gpui::test]
28159async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
28160 cx: &mut gpui::TestAppContext,
28161) {
28162 init_test(cx, |_| {});
28163
28164 let url = "https://zed.dev";
28165
28166 let markdown_language = Arc::new(Language::new(
28167 LanguageConfig {
28168 name: "Markdown".into(),
28169 ..LanguageConfig::default()
28170 },
28171 None,
28172 ));
28173
28174 let mut cx = EditorTestContext::new(cx).await;
28175 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28176 cx.set_state(&format!(
28177 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
28178 ));
28179
28180 cx.update_editor(|editor, window, cx| {
28181 editor.copy(&Copy, window, cx);
28182 });
28183
28184 cx.set_state(&format!(
28185 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
28186 ));
28187
28188 cx.update_editor(|editor, window, cx| {
28189 editor.paste(&Paste, window, cx);
28190 });
28191
28192 cx.assert_editor_state(&format!(
28193 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
28194 ));
28195}
28196
28197#[gpui::test]
28198async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
28199 cx: &mut gpui::TestAppContext,
28200) {
28201 init_test(cx, |_| {});
28202
28203 let url = "https://zed.dev";
28204
28205 let markdown_language = Arc::new(Language::new(
28206 LanguageConfig {
28207 name: "Markdown".into(),
28208 ..LanguageConfig::default()
28209 },
28210 None,
28211 ));
28212
28213 let mut cx = EditorTestContext::new(cx).await;
28214 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28215 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
28216
28217 cx.update_editor(|editor, window, cx| {
28218 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28219 editor.paste(&Paste, window, cx);
28220 });
28221
28222 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
28223}
28224
28225#[gpui::test]
28226async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
28227 cx: &mut gpui::TestAppContext,
28228) {
28229 init_test(cx, |_| {});
28230
28231 let text = "Awesome";
28232
28233 let markdown_language = Arc::new(Language::new(
28234 LanguageConfig {
28235 name: "Markdown".into(),
28236 ..LanguageConfig::default()
28237 },
28238 None,
28239 ));
28240
28241 let mut cx = EditorTestContext::new(cx).await;
28242 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28243 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
28244
28245 cx.update_editor(|editor, window, cx| {
28246 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
28247 editor.paste(&Paste, window, cx);
28248 });
28249
28250 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
28251}
28252
28253#[gpui::test]
28254async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
28255 cx: &mut gpui::TestAppContext,
28256) {
28257 init_test(cx, |_| {});
28258
28259 let url = "https://zed.dev";
28260
28261 let markdown_language = Arc::new(Language::new(
28262 LanguageConfig {
28263 name: "Rust".into(),
28264 ..LanguageConfig::default()
28265 },
28266 None,
28267 ));
28268
28269 let mut cx = EditorTestContext::new(cx).await;
28270 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28271 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
28272
28273 cx.update_editor(|editor, window, cx| {
28274 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28275 editor.paste(&Paste, window, cx);
28276 });
28277
28278 cx.assert_editor_state(&format!(
28279 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
28280 ));
28281}
28282
28283#[gpui::test]
28284async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
28285 cx: &mut TestAppContext,
28286) {
28287 init_test(cx, |_| {});
28288
28289 let url = "https://zed.dev";
28290
28291 let markdown_language = Arc::new(Language::new(
28292 LanguageConfig {
28293 name: "Markdown".into(),
28294 ..LanguageConfig::default()
28295 },
28296 None,
28297 ));
28298
28299 let (editor, cx) = cx.add_window_view(|window, cx| {
28300 let multi_buffer = MultiBuffer::build_multi(
28301 [
28302 ("this will embed -> link", vec![Point::row_range(0..1)]),
28303 ("this will replace -> link", vec![Point::row_range(0..1)]),
28304 ],
28305 cx,
28306 );
28307 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
28308 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28309 s.select_ranges(vec![
28310 Point::new(0, 19)..Point::new(0, 23),
28311 Point::new(1, 21)..Point::new(1, 25),
28312 ])
28313 });
28314 let first_buffer_id = multi_buffer
28315 .read(cx)
28316 .excerpt_buffer_ids()
28317 .into_iter()
28318 .next()
28319 .unwrap();
28320 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
28321 first_buffer.update(cx, |buffer, cx| {
28322 buffer.set_language(Some(markdown_language.clone()), cx);
28323 });
28324
28325 editor
28326 });
28327 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28328
28329 cx.update_editor(|editor, window, cx| {
28330 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28331 editor.paste(&Paste, window, cx);
28332 });
28333
28334 cx.assert_editor_state(&format!(
28335 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
28336 ));
28337}
28338
28339#[gpui::test]
28340async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
28341 init_test(cx, |_| {});
28342
28343 let fs = FakeFs::new(cx.executor());
28344 fs.insert_tree(
28345 path!("/project"),
28346 json!({
28347 "first.rs": "# First Document\nSome content here.",
28348 "second.rs": "Plain text content for second file.",
28349 }),
28350 )
28351 .await;
28352
28353 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
28354 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
28355 let cx = &mut VisualTestContext::from_window(*workspace, cx);
28356
28357 let language = rust_lang();
28358 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28359 language_registry.add(language.clone());
28360 let mut fake_servers = language_registry.register_fake_lsp(
28361 "Rust",
28362 FakeLspAdapter {
28363 ..FakeLspAdapter::default()
28364 },
28365 );
28366
28367 let buffer1 = project
28368 .update(cx, |project, cx| {
28369 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
28370 })
28371 .await
28372 .unwrap();
28373 let buffer2 = project
28374 .update(cx, |project, cx| {
28375 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
28376 })
28377 .await
28378 .unwrap();
28379
28380 let multi_buffer = cx.new(|cx| {
28381 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
28382 multi_buffer.set_excerpts_for_path(
28383 PathKey::for_buffer(&buffer1, cx),
28384 buffer1.clone(),
28385 [Point::zero()..buffer1.read(cx).max_point()],
28386 3,
28387 cx,
28388 );
28389 multi_buffer.set_excerpts_for_path(
28390 PathKey::for_buffer(&buffer2, cx),
28391 buffer2.clone(),
28392 [Point::zero()..buffer1.read(cx).max_point()],
28393 3,
28394 cx,
28395 );
28396 multi_buffer
28397 });
28398
28399 let (editor, cx) = cx.add_window_view(|window, cx| {
28400 Editor::new(
28401 EditorMode::full(),
28402 multi_buffer,
28403 Some(project.clone()),
28404 window,
28405 cx,
28406 )
28407 });
28408
28409 let fake_language_server = fake_servers.next().await.unwrap();
28410
28411 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
28412
28413 let save = editor.update_in(cx, |editor, window, cx| {
28414 assert!(editor.is_dirty(cx));
28415
28416 editor.save(
28417 SaveOptions {
28418 format: true,
28419 autosave: true,
28420 },
28421 project,
28422 window,
28423 cx,
28424 )
28425 });
28426 let (start_edit_tx, start_edit_rx) = oneshot::channel();
28427 let (done_edit_tx, done_edit_rx) = oneshot::channel();
28428 let mut done_edit_rx = Some(done_edit_rx);
28429 let mut start_edit_tx = Some(start_edit_tx);
28430
28431 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
28432 start_edit_tx.take().unwrap().send(()).unwrap();
28433 let done_edit_rx = done_edit_rx.take().unwrap();
28434 async move {
28435 done_edit_rx.await.unwrap();
28436 Ok(None)
28437 }
28438 });
28439
28440 start_edit_rx.await.unwrap();
28441 buffer2
28442 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
28443 .unwrap();
28444
28445 done_edit_tx.send(()).unwrap();
28446
28447 save.await.unwrap();
28448 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
28449}
28450
28451#[track_caller]
28452fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
28453 editor
28454 .all_inlays(cx)
28455 .into_iter()
28456 .filter_map(|inlay| inlay.get_color())
28457 .map(Rgba::from)
28458 .collect()
28459}
28460
28461#[gpui::test]
28462fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
28463 init_test(cx, |_| {});
28464
28465 let editor = cx.add_window(|window, cx| {
28466 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
28467 build_editor(buffer, window, cx)
28468 });
28469
28470 editor
28471 .update(cx, |editor, window, cx| {
28472 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28473 s.select_display_ranges([
28474 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
28475 ])
28476 });
28477
28478 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
28479
28480 assert_eq!(
28481 editor.display_text(cx),
28482 "line1\nline2\nline2",
28483 "Duplicating last line upward should create duplicate above, not on same line"
28484 );
28485
28486 assert_eq!(
28487 editor
28488 .selections
28489 .display_ranges(&editor.display_snapshot(cx)),
28490 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
28491 "Selection should move to the duplicated line"
28492 );
28493 })
28494 .unwrap();
28495}
28496
28497#[gpui::test]
28498async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
28499 init_test(cx, |_| {});
28500
28501 let mut cx = EditorTestContext::new(cx).await;
28502
28503 cx.set_state("line1\nline2ˇ");
28504
28505 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28506
28507 let clipboard_text = cx
28508 .read_from_clipboard()
28509 .and_then(|item| item.text().as_deref().map(str::to_string));
28510
28511 assert_eq!(
28512 clipboard_text,
28513 Some("line2\n".to_string()),
28514 "Copying a line without trailing newline should include a newline"
28515 );
28516
28517 cx.set_state("line1\nˇ");
28518
28519 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28520
28521 cx.assert_editor_state("line1\nline2\nˇ");
28522}
28523
28524#[gpui::test]
28525async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28526 init_test(cx, |_| {});
28527
28528 let mut cx = EditorTestContext::new(cx).await;
28529
28530 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28531
28532 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28533
28534 let clipboard_text = cx
28535 .read_from_clipboard()
28536 .and_then(|item| item.text().as_deref().map(str::to_string));
28537
28538 assert_eq!(
28539 clipboard_text,
28540 Some("line1\nline2\nline3\n".to_string()),
28541 "Copying multiple lines should include a single newline between lines"
28542 );
28543
28544 cx.set_state("lineA\nˇ");
28545
28546 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28547
28548 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28549}
28550
28551#[gpui::test]
28552async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28553 init_test(cx, |_| {});
28554
28555 let mut cx = EditorTestContext::new(cx).await;
28556
28557 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28558
28559 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
28560
28561 let clipboard_text = cx
28562 .read_from_clipboard()
28563 .and_then(|item| item.text().as_deref().map(str::to_string));
28564
28565 assert_eq!(
28566 clipboard_text,
28567 Some("line1\nline2\nline3\n".to_string()),
28568 "Copying multiple lines should include a single newline between lines"
28569 );
28570
28571 cx.set_state("lineA\nˇ");
28572
28573 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28574
28575 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28576}
28577
28578#[gpui::test]
28579async fn test_end_of_editor_context(cx: &mut TestAppContext) {
28580 init_test(cx, |_| {});
28581
28582 let mut cx = EditorTestContext::new(cx).await;
28583
28584 cx.set_state("line1\nline2ˇ");
28585 cx.update_editor(|e, window, cx| {
28586 e.set_mode(EditorMode::SingleLine);
28587 assert!(e.key_context(window, cx).contains("end_of_input"));
28588 });
28589 cx.set_state("ˇline1\nline2");
28590 cx.update_editor(|e, window, cx| {
28591 assert!(!e.key_context(window, cx).contains("end_of_input"));
28592 });
28593 cx.set_state("line1ˇ\nline2");
28594 cx.update_editor(|e, window, cx| {
28595 assert!(!e.key_context(window, cx).contains("end_of_input"));
28596 });
28597}
28598
28599#[gpui::test]
28600async fn test_sticky_scroll(cx: &mut TestAppContext) {
28601 init_test(cx, |_| {});
28602 let mut cx = EditorTestContext::new(cx).await;
28603
28604 let buffer = indoc! {"
28605 ˇfn foo() {
28606 let abc = 123;
28607 }
28608 struct Bar;
28609 impl Bar {
28610 fn new() -> Self {
28611 Self
28612 }
28613 }
28614 fn baz() {
28615 }
28616 "};
28617 cx.set_state(&buffer);
28618
28619 cx.update_editor(|e, _, cx| {
28620 e.buffer()
28621 .read(cx)
28622 .as_singleton()
28623 .unwrap()
28624 .update(cx, |buffer, cx| {
28625 buffer.set_language(Some(rust_lang()), cx);
28626 })
28627 });
28628
28629 let mut sticky_headers = |offset: ScrollOffset| {
28630 cx.update_editor(|e, window, cx| {
28631 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
28632 let style = e.style(cx).clone();
28633 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), &style, cx)
28634 .into_iter()
28635 .map(
28636 |StickyHeader {
28637 start_point,
28638 offset,
28639 ..
28640 }| { (start_point, offset) },
28641 )
28642 .collect::<Vec<_>>()
28643 })
28644 };
28645
28646 let fn_foo = Point { row: 0, column: 0 };
28647 let impl_bar = Point { row: 4, column: 0 };
28648 let fn_new = Point { row: 5, column: 4 };
28649
28650 assert_eq!(sticky_headers(0.0), vec![]);
28651 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
28652 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
28653 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
28654 assert_eq!(sticky_headers(2.0), vec![]);
28655 assert_eq!(sticky_headers(2.5), vec![]);
28656 assert_eq!(sticky_headers(3.0), vec![]);
28657 assert_eq!(sticky_headers(3.5), vec![]);
28658 assert_eq!(sticky_headers(4.0), vec![]);
28659 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28660 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28661 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
28662 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
28663 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
28664 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
28665 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
28666 assert_eq!(sticky_headers(8.0), vec![]);
28667 assert_eq!(sticky_headers(8.5), vec![]);
28668 assert_eq!(sticky_headers(9.0), vec![]);
28669 assert_eq!(sticky_headers(9.5), vec![]);
28670 assert_eq!(sticky_headers(10.0), vec![]);
28671}
28672
28673#[gpui::test]
28674fn test_relative_line_numbers(cx: &mut TestAppContext) {
28675 init_test(cx, |_| {});
28676
28677 let buffer_1 = cx.new(|cx| Buffer::local("aaaaaaaaaa\nbbb\n", cx));
28678 let buffer_2 = cx.new(|cx| Buffer::local("cccccccccc\nddd\n", cx));
28679 let buffer_3 = cx.new(|cx| Buffer::local("eee\nffffffffff\n", cx));
28680
28681 let multibuffer = cx.new(|cx| {
28682 let mut multibuffer = MultiBuffer::new(ReadWrite);
28683 multibuffer.push_excerpts(
28684 buffer_1.clone(),
28685 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
28686 cx,
28687 );
28688 multibuffer.push_excerpts(
28689 buffer_2.clone(),
28690 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
28691 cx,
28692 );
28693 multibuffer.push_excerpts(
28694 buffer_3.clone(),
28695 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
28696 cx,
28697 );
28698 multibuffer
28699 });
28700
28701 // wrapped contents of multibuffer:
28702 // aaa
28703 // aaa
28704 // aaa
28705 // a
28706 // bbb
28707 //
28708 // ccc
28709 // ccc
28710 // ccc
28711 // c
28712 // ddd
28713 //
28714 // eee
28715 // fff
28716 // fff
28717 // fff
28718 // f
28719
28720 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
28721 editor.update_in(cx, |editor, window, cx| {
28722 editor.set_wrap_width(Some(30.0.into()), cx); // every 3 characters
28723
28724 // includes trailing newlines.
28725 let expected_line_numbers = [2, 6, 7, 10, 14, 15, 18, 19, 23];
28726 let expected_wrapped_line_numbers = [
28727 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23,
28728 ];
28729
28730 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28731 s.select_ranges([
28732 Point::new(7, 0)..Point::new(7, 1), // second row of `ccc`
28733 ]);
28734 });
28735
28736 let snapshot = editor.snapshot(window, cx);
28737
28738 // these are all 0-indexed
28739 let base_display_row = DisplayRow(11);
28740 let base_row = 3;
28741 let wrapped_base_row = 7;
28742
28743 // test not counting wrapped lines
28744 let expected_relative_numbers = expected_line_numbers
28745 .into_iter()
28746 .enumerate()
28747 .map(|(i, row)| (DisplayRow(row), i.abs_diff(base_row) as u32))
28748 .collect_vec();
28749 let actual_relative_numbers = snapshot
28750 .calculate_relative_line_numbers(
28751 &(DisplayRow(0)..DisplayRow(24)),
28752 base_display_row,
28753 false,
28754 )
28755 .into_iter()
28756 .sorted()
28757 .collect_vec();
28758 assert_eq!(expected_relative_numbers, actual_relative_numbers);
28759 // check `calculate_relative_line_numbers()` against `relative_line_delta()` for each line
28760 for (display_row, relative_number) in expected_relative_numbers {
28761 assert_eq!(
28762 relative_number,
28763 snapshot
28764 .relative_line_delta(display_row, base_display_row, false)
28765 .unsigned_abs() as u32,
28766 );
28767 }
28768
28769 // test counting wrapped lines
28770 let expected_wrapped_relative_numbers = expected_wrapped_line_numbers
28771 .into_iter()
28772 .enumerate()
28773 .map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32))
28774 .filter(|(row, _)| *row != base_display_row)
28775 .collect_vec();
28776 let actual_relative_numbers = snapshot
28777 .calculate_relative_line_numbers(
28778 &(DisplayRow(0)..DisplayRow(24)),
28779 base_display_row,
28780 true,
28781 )
28782 .into_iter()
28783 .sorted()
28784 .collect_vec();
28785 assert_eq!(expected_wrapped_relative_numbers, actual_relative_numbers);
28786 // check `calculate_relative_line_numbers()` against `relative_wrapped_line_delta()` for each line
28787 for (display_row, relative_number) in expected_wrapped_relative_numbers {
28788 assert_eq!(
28789 relative_number,
28790 snapshot
28791 .relative_line_delta(display_row, base_display_row, true)
28792 .unsigned_abs() as u32,
28793 );
28794 }
28795 });
28796}
28797
28798#[gpui::test]
28799async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
28800 init_test(cx, |_| {});
28801 cx.update(|cx| {
28802 SettingsStore::update_global(cx, |store, cx| {
28803 store.update_user_settings(cx, |settings| {
28804 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
28805 enabled: Some(true),
28806 })
28807 });
28808 });
28809 });
28810 let mut cx = EditorTestContext::new(cx).await;
28811
28812 let line_height = cx.update_editor(|editor, window, cx| {
28813 editor
28814 .style(cx)
28815 .text
28816 .line_height_in_pixels(window.rem_size())
28817 });
28818
28819 let buffer = indoc! {"
28820 ˇfn foo() {
28821 let abc = 123;
28822 }
28823 struct Bar;
28824 impl Bar {
28825 fn new() -> Self {
28826 Self
28827 }
28828 }
28829 fn baz() {
28830 }
28831 "};
28832 cx.set_state(&buffer);
28833
28834 cx.update_editor(|e, _, cx| {
28835 e.buffer()
28836 .read(cx)
28837 .as_singleton()
28838 .unwrap()
28839 .update(cx, |buffer, cx| {
28840 buffer.set_language(Some(rust_lang()), cx);
28841 })
28842 });
28843
28844 let fn_foo = || empty_range(0, 0);
28845 let impl_bar = || empty_range(4, 0);
28846 let fn_new = || empty_range(5, 4);
28847
28848 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
28849 cx.update_editor(|e, window, cx| {
28850 e.scroll(
28851 gpui::Point {
28852 x: 0.,
28853 y: scroll_offset,
28854 },
28855 None,
28856 window,
28857 cx,
28858 );
28859 });
28860 cx.simulate_click(
28861 gpui::Point {
28862 x: px(0.),
28863 y: click_offset as f32 * line_height,
28864 },
28865 Modifiers::none(),
28866 );
28867 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
28868 };
28869
28870 assert_eq!(
28871 scroll_and_click(
28872 4.5, // impl Bar is halfway off the screen
28873 0.0 // click top of screen
28874 ),
28875 // scrolled to impl Bar
28876 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28877 );
28878
28879 assert_eq!(
28880 scroll_and_click(
28881 4.5, // impl Bar is halfway off the screen
28882 0.25 // click middle of impl Bar
28883 ),
28884 // scrolled to impl Bar
28885 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28886 );
28887
28888 assert_eq!(
28889 scroll_and_click(
28890 4.5, // impl Bar is halfway off the screen
28891 1.5 // click below impl Bar (e.g. fn new())
28892 ),
28893 // scrolled to fn new() - this is below the impl Bar header which has persisted
28894 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28895 );
28896
28897 assert_eq!(
28898 scroll_and_click(
28899 5.5, // fn new is halfway underneath impl Bar
28900 0.75 // click on the overlap of impl Bar and fn new()
28901 ),
28902 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28903 );
28904
28905 assert_eq!(
28906 scroll_and_click(
28907 5.5, // fn new is halfway underneath impl Bar
28908 1.25 // click on the visible part of fn new()
28909 ),
28910 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28911 );
28912
28913 assert_eq!(
28914 scroll_and_click(
28915 1.5, // fn foo is halfway off the screen
28916 0.0 // click top of screen
28917 ),
28918 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
28919 );
28920
28921 assert_eq!(
28922 scroll_and_click(
28923 1.5, // fn foo is halfway off the screen
28924 0.75 // click visible part of let abc...
28925 )
28926 .0,
28927 // no change in scroll
28928 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
28929 (gpui::Point { x: 0., y: 1.5 })
28930 );
28931}
28932
28933#[gpui::test]
28934async fn test_next_prev_reference(cx: &mut TestAppContext) {
28935 const CYCLE_POSITIONS: &[&'static str] = &[
28936 indoc! {"
28937 fn foo() {
28938 let ˇabc = 123;
28939 let x = abc + 1;
28940 let y = abc + 2;
28941 let z = abc + 2;
28942 }
28943 "},
28944 indoc! {"
28945 fn foo() {
28946 let abc = 123;
28947 let x = ˇabc + 1;
28948 let y = abc + 2;
28949 let z = abc + 2;
28950 }
28951 "},
28952 indoc! {"
28953 fn foo() {
28954 let abc = 123;
28955 let x = abc + 1;
28956 let y = ˇabc + 2;
28957 let z = abc + 2;
28958 }
28959 "},
28960 indoc! {"
28961 fn foo() {
28962 let abc = 123;
28963 let x = abc + 1;
28964 let y = abc + 2;
28965 let z = ˇabc + 2;
28966 }
28967 "},
28968 ];
28969
28970 init_test(cx, |_| {});
28971
28972 let mut cx = EditorLspTestContext::new_rust(
28973 lsp::ServerCapabilities {
28974 references_provider: Some(lsp::OneOf::Left(true)),
28975 ..Default::default()
28976 },
28977 cx,
28978 )
28979 .await;
28980
28981 // importantly, the cursor is in the middle
28982 cx.set_state(indoc! {"
28983 fn foo() {
28984 let aˇbc = 123;
28985 let x = abc + 1;
28986 let y = abc + 2;
28987 let z = abc + 2;
28988 }
28989 "});
28990
28991 let reference_ranges = [
28992 lsp::Position::new(1, 8),
28993 lsp::Position::new(2, 12),
28994 lsp::Position::new(3, 12),
28995 lsp::Position::new(4, 12),
28996 ]
28997 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
28998
28999 cx.lsp
29000 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
29001 Ok(Some(
29002 reference_ranges
29003 .map(|range| lsp::Location {
29004 uri: params.text_document_position.text_document.uri.clone(),
29005 range,
29006 })
29007 .to_vec(),
29008 ))
29009 });
29010
29011 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
29012 cx.update_editor(|editor, window, cx| {
29013 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
29014 })
29015 .unwrap()
29016 .await
29017 .unwrap()
29018 };
29019
29020 _move(Direction::Next, 1, &mut cx).await;
29021 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29022
29023 _move(Direction::Next, 1, &mut cx).await;
29024 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29025
29026 _move(Direction::Next, 1, &mut cx).await;
29027 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29028
29029 // loops back to the start
29030 _move(Direction::Next, 1, &mut cx).await;
29031 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29032
29033 // loops back to the end
29034 _move(Direction::Prev, 1, &mut cx).await;
29035 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29036
29037 _move(Direction::Prev, 1, &mut cx).await;
29038 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29039
29040 _move(Direction::Prev, 1, &mut cx).await;
29041 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29042
29043 _move(Direction::Prev, 1, &mut cx).await;
29044 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29045
29046 _move(Direction::Next, 3, &mut cx).await;
29047 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29048
29049 _move(Direction::Prev, 2, &mut cx).await;
29050 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29051}
29052
29053#[gpui::test]
29054async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
29055 init_test(cx, |_| {});
29056
29057 let (editor, cx) = cx.add_window_view(|window, cx| {
29058 let multi_buffer = MultiBuffer::build_multi(
29059 [
29060 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29061 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29062 ],
29063 cx,
29064 );
29065 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29066 });
29067
29068 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29069 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
29070
29071 cx.assert_excerpts_with_selections(indoc! {"
29072 [EXCERPT]
29073 ˇ1
29074 2
29075 3
29076 [EXCERPT]
29077 1
29078 2
29079 3
29080 "});
29081
29082 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
29083 cx.update_editor(|editor, window, cx| {
29084 editor.change_selections(None.into(), window, cx, |s| {
29085 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29086 });
29087 });
29088 cx.assert_excerpts_with_selections(indoc! {"
29089 [EXCERPT]
29090 1
29091 2ˇ
29092 3
29093 [EXCERPT]
29094 1
29095 2
29096 3
29097 "});
29098
29099 cx.update_editor(|editor, window, cx| {
29100 editor
29101 .select_all_matches(&SelectAllMatches, window, cx)
29102 .unwrap();
29103 });
29104 cx.assert_excerpts_with_selections(indoc! {"
29105 [EXCERPT]
29106 1
29107 2ˇ
29108 3
29109 [EXCERPT]
29110 1
29111 2ˇ
29112 3
29113 "});
29114
29115 cx.update_editor(|editor, window, cx| {
29116 editor.handle_input("X", window, cx);
29117 });
29118 cx.assert_excerpts_with_selections(indoc! {"
29119 [EXCERPT]
29120 1
29121 Xˇ
29122 3
29123 [EXCERPT]
29124 1
29125 Xˇ
29126 3
29127 "});
29128
29129 // Scenario 2: Select "2", then fold second buffer before insertion
29130 cx.update_multibuffer(|mb, cx| {
29131 for buffer_id in buffer_ids.iter() {
29132 let buffer = mb.buffer(*buffer_id).unwrap();
29133 buffer.update(cx, |buffer, cx| {
29134 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29135 });
29136 }
29137 });
29138
29139 // Select "2" and select all matches
29140 cx.update_editor(|editor, window, cx| {
29141 editor.change_selections(None.into(), window, cx, |s| {
29142 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29143 });
29144 editor
29145 .select_all_matches(&SelectAllMatches, window, cx)
29146 .unwrap();
29147 });
29148
29149 // Fold second buffer - should remove selections from folded buffer
29150 cx.update_editor(|editor, _, cx| {
29151 editor.fold_buffer(buffer_ids[1], cx);
29152 });
29153 cx.assert_excerpts_with_selections(indoc! {"
29154 [EXCERPT]
29155 1
29156 2ˇ
29157 3
29158 [EXCERPT]
29159 [FOLDED]
29160 "});
29161
29162 // Insert text - should only affect first buffer
29163 cx.update_editor(|editor, window, cx| {
29164 editor.handle_input("Y", window, cx);
29165 });
29166 cx.update_editor(|editor, _, cx| {
29167 editor.unfold_buffer(buffer_ids[1], cx);
29168 });
29169 cx.assert_excerpts_with_selections(indoc! {"
29170 [EXCERPT]
29171 1
29172 Yˇ
29173 3
29174 [EXCERPT]
29175 1
29176 2
29177 3
29178 "});
29179
29180 // Scenario 3: Select "2", then fold first buffer before insertion
29181 cx.update_multibuffer(|mb, cx| {
29182 for buffer_id in buffer_ids.iter() {
29183 let buffer = mb.buffer(*buffer_id).unwrap();
29184 buffer.update(cx, |buffer, cx| {
29185 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29186 });
29187 }
29188 });
29189
29190 // Select "2" and select all matches
29191 cx.update_editor(|editor, window, cx| {
29192 editor.change_selections(None.into(), window, cx, |s| {
29193 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29194 });
29195 editor
29196 .select_all_matches(&SelectAllMatches, window, cx)
29197 .unwrap();
29198 });
29199
29200 // Fold first buffer - should remove selections from folded buffer
29201 cx.update_editor(|editor, _, cx| {
29202 editor.fold_buffer(buffer_ids[0], cx);
29203 });
29204 cx.assert_excerpts_with_selections(indoc! {"
29205 [EXCERPT]
29206 [FOLDED]
29207 [EXCERPT]
29208 1
29209 2ˇ
29210 3
29211 "});
29212
29213 // Insert text - should only affect second buffer
29214 cx.update_editor(|editor, window, cx| {
29215 editor.handle_input("Z", window, cx);
29216 });
29217 cx.update_editor(|editor, _, cx| {
29218 editor.unfold_buffer(buffer_ids[0], cx);
29219 });
29220 cx.assert_excerpts_with_selections(indoc! {"
29221 [EXCERPT]
29222 1
29223 2
29224 3
29225 [EXCERPT]
29226 1
29227 Zˇ
29228 3
29229 "});
29230
29231 // Test correct folded header is selected upon fold
29232 cx.update_editor(|editor, _, cx| {
29233 editor.fold_buffer(buffer_ids[0], cx);
29234 editor.fold_buffer(buffer_ids[1], cx);
29235 });
29236 cx.assert_excerpts_with_selections(indoc! {"
29237 [EXCERPT]
29238 [FOLDED]
29239 [EXCERPT]
29240 ˇ[FOLDED]
29241 "});
29242
29243 // Test selection inside folded buffer unfolds it on type
29244 cx.update_editor(|editor, window, cx| {
29245 editor.handle_input("W", window, cx);
29246 });
29247 cx.update_editor(|editor, _, cx| {
29248 editor.unfold_buffer(buffer_ids[0], cx);
29249 });
29250 cx.assert_excerpts_with_selections(indoc! {"
29251 [EXCERPT]
29252 1
29253 2
29254 3
29255 [EXCERPT]
29256 Wˇ1
29257 Z
29258 3
29259 "});
29260}
29261
29262#[gpui::test]
29263async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
29264 init_test(cx, |_| {});
29265
29266 let (editor, cx) = cx.add_window_view(|window, cx| {
29267 let multi_buffer = MultiBuffer::build_multi(
29268 [
29269 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29270 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
29271 ],
29272 cx,
29273 );
29274 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29275 });
29276
29277 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29278
29279 cx.assert_excerpts_with_selections(indoc! {"
29280 [EXCERPT]
29281 ˇ1
29282 2
29283 3
29284 [EXCERPT]
29285 1
29286 2
29287 3
29288 4
29289 5
29290 6
29291 7
29292 8
29293 9
29294 "});
29295
29296 cx.update_editor(|editor, window, cx| {
29297 editor.change_selections(None.into(), window, cx, |s| {
29298 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
29299 });
29300 });
29301
29302 cx.assert_excerpts_with_selections(indoc! {"
29303 [EXCERPT]
29304 1
29305 2
29306 3
29307 [EXCERPT]
29308 1
29309 2
29310 3
29311 4
29312 5
29313 6
29314 ˇ7
29315 8
29316 9
29317 "});
29318
29319 cx.update_editor(|editor, _window, cx| {
29320 editor.set_vertical_scroll_margin(0, cx);
29321 });
29322
29323 cx.update_editor(|editor, window, cx| {
29324 assert_eq!(editor.vertical_scroll_margin(), 0);
29325 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29326 assert_eq!(
29327 editor.snapshot(window, cx).scroll_position(),
29328 gpui::Point::new(0., 12.0)
29329 );
29330 });
29331
29332 cx.update_editor(|editor, _window, cx| {
29333 editor.set_vertical_scroll_margin(3, cx);
29334 });
29335
29336 cx.update_editor(|editor, window, cx| {
29337 assert_eq!(editor.vertical_scroll_margin(), 3);
29338 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29339 assert_eq!(
29340 editor.snapshot(window, cx).scroll_position(),
29341 gpui::Point::new(0., 9.0)
29342 );
29343 });
29344}
29345
29346#[gpui::test]
29347async fn test_find_references_single_case(cx: &mut TestAppContext) {
29348 init_test(cx, |_| {});
29349 let mut cx = EditorLspTestContext::new_rust(
29350 lsp::ServerCapabilities {
29351 references_provider: Some(lsp::OneOf::Left(true)),
29352 ..lsp::ServerCapabilities::default()
29353 },
29354 cx,
29355 )
29356 .await;
29357
29358 let before = indoc!(
29359 r#"
29360 fn main() {
29361 let aˇbc = 123;
29362 let xyz = abc;
29363 }
29364 "#
29365 );
29366 let after = indoc!(
29367 r#"
29368 fn main() {
29369 let abc = 123;
29370 let xyz = ˇabc;
29371 }
29372 "#
29373 );
29374
29375 cx.lsp
29376 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
29377 Ok(Some(vec![
29378 lsp::Location {
29379 uri: params.text_document_position.text_document.uri.clone(),
29380 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
29381 },
29382 lsp::Location {
29383 uri: params.text_document_position.text_document.uri,
29384 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
29385 },
29386 ]))
29387 });
29388
29389 cx.set_state(before);
29390
29391 let action = FindAllReferences {
29392 always_open_multibuffer: false,
29393 };
29394
29395 let navigated = cx
29396 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
29397 .expect("should have spawned a task")
29398 .await
29399 .unwrap();
29400
29401 assert_eq!(navigated, Navigated::No);
29402
29403 cx.run_until_parked();
29404
29405 cx.assert_editor_state(after);
29406}
29407
29408#[gpui::test]
29409async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
29410 init_test(cx, |settings| {
29411 settings.defaults.tab_size = Some(2.try_into().unwrap());
29412 });
29413
29414 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29415 let mut cx = EditorTestContext::new(cx).await;
29416 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29417
29418 // Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
29419 cx.set_state(indoc! {"
29420 - [ ] taskˇ
29421 "});
29422 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29423 cx.wait_for_autoindent_applied().await;
29424 cx.assert_editor_state(indoc! {"
29425 - [ ] task
29426 - [ ] ˇ
29427 "});
29428
29429 // Case 2: Works with checked task items too
29430 cx.set_state(indoc! {"
29431 - [x] completed taskˇ
29432 "});
29433 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29434 cx.wait_for_autoindent_applied().await;
29435 cx.assert_editor_state(indoc! {"
29436 - [x] completed task
29437 - [ ] ˇ
29438 "});
29439
29440 // Case 2.1: Works with uppercase checked marker too
29441 cx.set_state(indoc! {"
29442 - [X] completed taskˇ
29443 "});
29444 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29445 cx.wait_for_autoindent_applied().await;
29446 cx.assert_editor_state(indoc! {"
29447 - [X] completed task
29448 - [ ] ˇ
29449 "});
29450
29451 // Case 3: Cursor position doesn't matter - content after marker is what counts
29452 cx.set_state(indoc! {"
29453 - [ ] taˇsk
29454 "});
29455 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29456 cx.wait_for_autoindent_applied().await;
29457 cx.assert_editor_state(indoc! {"
29458 - [ ] ta
29459 - [ ] ˇsk
29460 "});
29461
29462 // Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
29463 cx.set_state(indoc! {"
29464 - [ ] ˇ
29465 "});
29466 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29467 cx.wait_for_autoindent_applied().await;
29468 cx.assert_editor_state(
29469 indoc! {"
29470 - [ ]$$
29471 ˇ
29472 "}
29473 .replace("$", " ")
29474 .as_str(),
29475 );
29476
29477 // Case 5: Adding newline with content adds marker preserving indentation
29478 cx.set_state(indoc! {"
29479 - [ ] task
29480 - [ ] indentedˇ
29481 "});
29482 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29483 cx.wait_for_autoindent_applied().await;
29484 cx.assert_editor_state(indoc! {"
29485 - [ ] task
29486 - [ ] indented
29487 - [ ] ˇ
29488 "});
29489
29490 // Case 6: Adding newline with cursor right after prefix, unindents
29491 cx.set_state(indoc! {"
29492 - [ ] task
29493 - [ ] sub task
29494 - [ ] ˇ
29495 "});
29496 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29497 cx.wait_for_autoindent_applied().await;
29498 cx.assert_editor_state(indoc! {"
29499 - [ ] task
29500 - [ ] sub task
29501 - [ ] ˇ
29502 "});
29503 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29504 cx.wait_for_autoindent_applied().await;
29505
29506 // Case 7: Adding newline with cursor right after prefix, removes marker
29507 cx.assert_editor_state(indoc! {"
29508 - [ ] task
29509 - [ ] sub task
29510 - [ ] ˇ
29511 "});
29512 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29513 cx.wait_for_autoindent_applied().await;
29514 cx.assert_editor_state(indoc! {"
29515 - [ ] task
29516 - [ ] sub task
29517 ˇ
29518 "});
29519
29520 // Case 8: Cursor before or inside prefix does not add marker
29521 cx.set_state(indoc! {"
29522 ˇ- [ ] task
29523 "});
29524 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29525 cx.wait_for_autoindent_applied().await;
29526 cx.assert_editor_state(indoc! {"
29527
29528 ˇ- [ ] task
29529 "});
29530
29531 cx.set_state(indoc! {"
29532 - [ˇ ] task
29533 "});
29534 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29535 cx.wait_for_autoindent_applied().await;
29536 cx.assert_editor_state(indoc! {"
29537 - [
29538 ˇ
29539 ] task
29540 "});
29541}
29542
29543#[gpui::test]
29544async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
29545 init_test(cx, |settings| {
29546 settings.defaults.tab_size = Some(2.try_into().unwrap());
29547 });
29548
29549 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29550 let mut cx = EditorTestContext::new(cx).await;
29551 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29552
29553 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
29554 cx.set_state(indoc! {"
29555 - itemˇ
29556 "});
29557 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29558 cx.wait_for_autoindent_applied().await;
29559 cx.assert_editor_state(indoc! {"
29560 - item
29561 - ˇ
29562 "});
29563
29564 // Case 2: Works with different markers
29565 cx.set_state(indoc! {"
29566 * starred itemˇ
29567 "});
29568 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29569 cx.wait_for_autoindent_applied().await;
29570 cx.assert_editor_state(indoc! {"
29571 * starred item
29572 * ˇ
29573 "});
29574
29575 cx.set_state(indoc! {"
29576 + plus itemˇ
29577 "});
29578 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29579 cx.wait_for_autoindent_applied().await;
29580 cx.assert_editor_state(indoc! {"
29581 + plus item
29582 + ˇ
29583 "});
29584
29585 // Case 3: Cursor position doesn't matter - content after marker is what counts
29586 cx.set_state(indoc! {"
29587 - itˇem
29588 "});
29589 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29590 cx.wait_for_autoindent_applied().await;
29591 cx.assert_editor_state(indoc! {"
29592 - it
29593 - ˇem
29594 "});
29595
29596 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
29597 cx.set_state(indoc! {"
29598 - ˇ
29599 "});
29600 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29601 cx.wait_for_autoindent_applied().await;
29602 cx.assert_editor_state(
29603 indoc! {"
29604 - $
29605 ˇ
29606 "}
29607 .replace("$", " ")
29608 .as_str(),
29609 );
29610
29611 // Case 5: Adding newline with content adds marker preserving indentation
29612 cx.set_state(indoc! {"
29613 - item
29614 - indentedˇ
29615 "});
29616 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29617 cx.wait_for_autoindent_applied().await;
29618 cx.assert_editor_state(indoc! {"
29619 - item
29620 - indented
29621 - ˇ
29622 "});
29623
29624 // Case 6: Adding newline with cursor right after marker, unindents
29625 cx.set_state(indoc! {"
29626 - item
29627 - sub item
29628 - ˇ
29629 "});
29630 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29631 cx.wait_for_autoindent_applied().await;
29632 cx.assert_editor_state(indoc! {"
29633 - item
29634 - sub item
29635 - ˇ
29636 "});
29637 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29638 cx.wait_for_autoindent_applied().await;
29639
29640 // Case 7: Adding newline with cursor right after marker, removes marker
29641 cx.assert_editor_state(indoc! {"
29642 - item
29643 - sub item
29644 - ˇ
29645 "});
29646 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29647 cx.wait_for_autoindent_applied().await;
29648 cx.assert_editor_state(indoc! {"
29649 - item
29650 - sub item
29651 ˇ
29652 "});
29653
29654 // Case 8: Cursor before or inside prefix does not add marker
29655 cx.set_state(indoc! {"
29656 ˇ- item
29657 "});
29658 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29659 cx.wait_for_autoindent_applied().await;
29660 cx.assert_editor_state(indoc! {"
29661
29662 ˇ- item
29663 "});
29664
29665 cx.set_state(indoc! {"
29666 -ˇ item
29667 "});
29668 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29669 cx.wait_for_autoindent_applied().await;
29670 cx.assert_editor_state(indoc! {"
29671 -
29672 ˇitem
29673 "});
29674}
29675
29676#[gpui::test]
29677async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
29678 init_test(cx, |settings| {
29679 settings.defaults.tab_size = Some(2.try_into().unwrap());
29680 });
29681
29682 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29683 let mut cx = EditorTestContext::new(cx).await;
29684 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29685
29686 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
29687 cx.set_state(indoc! {"
29688 1. first itemˇ
29689 "});
29690 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29691 cx.wait_for_autoindent_applied().await;
29692 cx.assert_editor_state(indoc! {"
29693 1. first item
29694 2. ˇ
29695 "});
29696
29697 // Case 2: Works with larger numbers
29698 cx.set_state(indoc! {"
29699 10. tenth itemˇ
29700 "});
29701 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29702 cx.wait_for_autoindent_applied().await;
29703 cx.assert_editor_state(indoc! {"
29704 10. tenth item
29705 11. ˇ
29706 "});
29707
29708 // Case 3: Cursor position doesn't matter - content after marker is what counts
29709 cx.set_state(indoc! {"
29710 1. itˇem
29711 "});
29712 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29713 cx.wait_for_autoindent_applied().await;
29714 cx.assert_editor_state(indoc! {"
29715 1. it
29716 2. ˇem
29717 "});
29718
29719 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
29720 cx.set_state(indoc! {"
29721 1. ˇ
29722 "});
29723 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29724 cx.wait_for_autoindent_applied().await;
29725 cx.assert_editor_state(
29726 indoc! {"
29727 1. $
29728 ˇ
29729 "}
29730 .replace("$", " ")
29731 .as_str(),
29732 );
29733
29734 // Case 5: Adding newline with content adds marker preserving indentation
29735 cx.set_state(indoc! {"
29736 1. item
29737 2. indentedˇ
29738 "});
29739 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29740 cx.wait_for_autoindent_applied().await;
29741 cx.assert_editor_state(indoc! {"
29742 1. item
29743 2. indented
29744 3. ˇ
29745 "});
29746
29747 // Case 6: Adding newline with cursor right after marker, unindents
29748 cx.set_state(indoc! {"
29749 1. item
29750 2. sub item
29751 3. ˇ
29752 "});
29753 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29754 cx.wait_for_autoindent_applied().await;
29755 cx.assert_editor_state(indoc! {"
29756 1. item
29757 2. sub item
29758 1. ˇ
29759 "});
29760 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29761 cx.wait_for_autoindent_applied().await;
29762
29763 // Case 7: Adding newline with cursor right after marker, removes marker
29764 cx.assert_editor_state(indoc! {"
29765 1. item
29766 2. sub item
29767 1. ˇ
29768 "});
29769 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29770 cx.wait_for_autoindent_applied().await;
29771 cx.assert_editor_state(indoc! {"
29772 1. item
29773 2. sub item
29774 ˇ
29775 "});
29776
29777 // Case 8: Cursor before or inside prefix does not add marker
29778 cx.set_state(indoc! {"
29779 ˇ1. item
29780 "});
29781 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29782 cx.wait_for_autoindent_applied().await;
29783 cx.assert_editor_state(indoc! {"
29784
29785 ˇ1. item
29786 "});
29787
29788 cx.set_state(indoc! {"
29789 1ˇ. item
29790 "});
29791 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29792 cx.wait_for_autoindent_applied().await;
29793 cx.assert_editor_state(indoc! {"
29794 1
29795 ˇ. item
29796 "});
29797}
29798
29799#[gpui::test]
29800async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
29801 init_test(cx, |settings| {
29802 settings.defaults.tab_size = Some(2.try_into().unwrap());
29803 });
29804
29805 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29806 let mut cx = EditorTestContext::new(cx).await;
29807 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29808
29809 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
29810 cx.set_state(indoc! {"
29811 1. first item
29812 1. sub first item
29813 2. sub second item
29814 3. ˇ
29815 "});
29816 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29817 cx.wait_for_autoindent_applied().await;
29818 cx.assert_editor_state(indoc! {"
29819 1. first item
29820 1. sub first item
29821 2. sub second item
29822 1. ˇ
29823 "});
29824}
29825
29826#[gpui::test]
29827async fn test_tab_list_indent(cx: &mut TestAppContext) {
29828 init_test(cx, |settings| {
29829 settings.defaults.tab_size = Some(2.try_into().unwrap());
29830 });
29831
29832 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29833 let mut cx = EditorTestContext::new(cx).await;
29834 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29835
29836 // Case 1: Unordered list - cursor after prefix, adds indent before prefix
29837 cx.set_state(indoc! {"
29838 - ˇitem
29839 "});
29840 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
29841 cx.wait_for_autoindent_applied().await;
29842 let expected = indoc! {"
29843 $$- ˇitem
29844 "};
29845 cx.assert_editor_state(expected.replace("$", " ").as_str());
29846
29847 // Case 2: Task list - cursor after prefix
29848 cx.set_state(indoc! {"
29849 - [ ] ˇtask
29850 "});
29851 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
29852 cx.wait_for_autoindent_applied().await;
29853 let expected = indoc! {"
29854 $$- [ ] ˇtask
29855 "};
29856 cx.assert_editor_state(expected.replace("$", " ").as_str());
29857
29858 // Case 3: Ordered list - cursor after prefix
29859 cx.set_state(indoc! {"
29860 1. ˇfirst
29861 "});
29862 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
29863 cx.wait_for_autoindent_applied().await;
29864 let expected = indoc! {"
29865 $$1. ˇfirst
29866 "};
29867 cx.assert_editor_state(expected.replace("$", " ").as_str());
29868
29869 // Case 4: With existing indentation - adds more indent
29870 let initial = indoc! {"
29871 $$- ˇitem
29872 "};
29873 cx.set_state(initial.replace("$", " ").as_str());
29874 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
29875 cx.wait_for_autoindent_applied().await;
29876 let expected = indoc! {"
29877 $$$$- ˇitem
29878 "};
29879 cx.assert_editor_state(expected.replace("$", " ").as_str());
29880
29881 // Case 5: Empty list item
29882 cx.set_state(indoc! {"
29883 - ˇ
29884 "});
29885 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
29886 cx.wait_for_autoindent_applied().await;
29887 let expected = indoc! {"
29888 $$- ˇ
29889 "};
29890 cx.assert_editor_state(expected.replace("$", " ").as_str());
29891
29892 // Case 6: Cursor at end of line with content
29893 cx.set_state(indoc! {"
29894 - itemˇ
29895 "});
29896 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
29897 cx.wait_for_autoindent_applied().await;
29898 let expected = indoc! {"
29899 $$- itemˇ
29900 "};
29901 cx.assert_editor_state(expected.replace("$", " ").as_str());
29902
29903 // Case 7: Cursor at start of list item, indents it
29904 cx.set_state(indoc! {"
29905 - item
29906 ˇ - sub item
29907 "});
29908 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
29909 cx.wait_for_autoindent_applied().await;
29910 let expected = indoc! {"
29911 - item
29912 ˇ - sub item
29913 "};
29914 cx.assert_editor_state(expected);
29915
29916 // Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
29917 cx.update_editor(|_, _, cx| {
29918 SettingsStore::update_global(cx, |store, cx| {
29919 store.update_user_settings(cx, |settings| {
29920 settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
29921 });
29922 });
29923 });
29924 cx.set_state(indoc! {"
29925 - item
29926 ˇ - sub item
29927 "});
29928 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
29929 cx.wait_for_autoindent_applied().await;
29930 let expected = indoc! {"
29931 - item
29932 ˇ- sub item
29933 "};
29934 cx.assert_editor_state(expected);
29935}
29936
29937#[gpui::test]
29938async fn test_local_worktree_trust(cx: &mut TestAppContext) {
29939 init_test(cx, |_| {});
29940 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), None, None, cx));
29941
29942 cx.update(|cx| {
29943 SettingsStore::update_global(cx, |store, cx| {
29944 store.update_user_settings(cx, |settings| {
29945 settings.project.all_languages.defaults.inlay_hints =
29946 Some(InlayHintSettingsContent {
29947 enabled: Some(true),
29948 ..InlayHintSettingsContent::default()
29949 });
29950 });
29951 });
29952 });
29953
29954 let fs = FakeFs::new(cx.executor());
29955 fs.insert_tree(
29956 path!("/project"),
29957 json!({
29958 ".zed": {
29959 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
29960 },
29961 "main.rs": "fn main() {}"
29962 }),
29963 )
29964 .await;
29965
29966 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
29967 let server_name = "override-rust-analyzer";
29968 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
29969
29970 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
29971 language_registry.add(rust_lang());
29972
29973 let capabilities = lsp::ServerCapabilities {
29974 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
29975 ..lsp::ServerCapabilities::default()
29976 };
29977 let mut fake_language_servers = language_registry.register_fake_lsp(
29978 "Rust",
29979 FakeLspAdapter {
29980 name: server_name,
29981 capabilities,
29982 initializer: Some(Box::new({
29983 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
29984 move |fake_server| {
29985 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
29986 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
29987 move |_params, _| {
29988 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
29989 async move {
29990 Ok(Some(vec![lsp::InlayHint {
29991 position: lsp::Position::new(0, 0),
29992 label: lsp::InlayHintLabel::String("hint".to_string()),
29993 kind: None,
29994 text_edits: None,
29995 tooltip: None,
29996 padding_left: None,
29997 padding_right: None,
29998 data: None,
29999 }]))
30000 }
30001 },
30002 );
30003 }
30004 })),
30005 ..FakeLspAdapter::default()
30006 },
30007 );
30008
30009 cx.run_until_parked();
30010
30011 let worktree_id = project.read_with(cx, |project, cx| {
30012 project
30013 .worktrees(cx)
30014 .next()
30015 .map(|wt| wt.read(cx).id())
30016 .expect("should have a worktree")
30017 });
30018 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
30019
30020 let trusted_worktrees =
30021 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
30022
30023 let can_trust = trusted_worktrees.update(cx, |store, cx| {
30024 store.can_trust(&worktree_store, worktree_id, cx)
30025 });
30026 assert!(!can_trust, "worktree should be restricted initially");
30027
30028 let buffer_before_approval = project
30029 .update(cx, |project, cx| {
30030 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
30031 })
30032 .await
30033 .unwrap();
30034
30035 let (editor, cx) = cx.add_window_view(|window, cx| {
30036 Editor::new(
30037 EditorMode::full(),
30038 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
30039 Some(project.clone()),
30040 window,
30041 cx,
30042 )
30043 });
30044 cx.run_until_parked();
30045 let fake_language_server = fake_language_servers.next();
30046
30047 cx.read(|cx| {
30048 let file = buffer_before_approval.read(cx).file();
30049 assert_eq!(
30050 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30051 .language_servers,
30052 ["...".to_string()],
30053 "local .zed/settings.json must not apply before trust approval"
30054 )
30055 });
30056
30057 editor.update_in(cx, |editor, window, cx| {
30058 editor.handle_input("1", window, cx);
30059 });
30060 cx.run_until_parked();
30061 cx.executor()
30062 .advance_clock(std::time::Duration::from_secs(1));
30063 assert_eq!(
30064 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
30065 0,
30066 "inlay hints must not be queried before trust approval"
30067 );
30068
30069 trusted_worktrees.update(cx, |store, cx| {
30070 store.trust(
30071 &worktree_store,
30072 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
30073 cx,
30074 );
30075 });
30076 cx.run_until_parked();
30077
30078 cx.read(|cx| {
30079 let file = buffer_before_approval.read(cx).file();
30080 assert_eq!(
30081 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30082 .language_servers,
30083 ["override-rust-analyzer".to_string()],
30084 "local .zed/settings.json should apply after trust approval"
30085 )
30086 });
30087 let _fake_language_server = fake_language_server.await.unwrap();
30088 editor.update_in(cx, |editor, window, cx| {
30089 editor.handle_input("1", window, cx);
30090 });
30091 cx.run_until_parked();
30092 cx.executor()
30093 .advance_clock(std::time::Duration::from_secs(1));
30094 assert!(
30095 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
30096 "inlay hints should be queried after trust approval"
30097 );
30098
30099 let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
30100 store.can_trust(&worktree_store, worktree_id, cx)
30101 });
30102 assert!(can_trust_after, "worktree should be trusted after trust()");
30103}
30104
30105#[gpui::test]
30106fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
30107 // This test reproduces a bug where drawing an editor at a position above the viewport
30108 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
30109 // causes an infinite loop in blocks_in_range.
30110 //
30111 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
30112 // the content mask intersection produces visible_bounds with origin at the viewport top.
30113 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
30114 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
30115 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
30116 init_test(cx, |_| {});
30117
30118 let window = cx.add_window(|_, _| gpui::Empty);
30119 let mut cx = VisualTestContext::from_window(*window, cx);
30120
30121 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
30122 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
30123
30124 // Simulate a small viewport (500x500 pixels at origin 0,0)
30125 cx.simulate_resize(gpui::size(px(500.), px(500.)));
30126
30127 // Draw the editor at a very negative Y position, simulating an editor that's been
30128 // scrolled way above the visible viewport (like in a List that has scrolled past it).
30129 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
30130 // This should NOT hang - it should just render nothing.
30131 cx.draw(
30132 gpui::point(px(0.), px(-10000.)),
30133 gpui::size(px(500.), px(3000.)),
30134 |_, _| editor.clone(),
30135 );
30136
30137 // If we get here without hanging, the test passes
30138}