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 IndentGuide, MultiBufferFilterMode, 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 let buffer_changes = buffer_changes.clone();
13224 move |_, _| {
13225 let buffer_changes = buffer_changes.clone();
13226 // Insert blank lines between each line of the buffer.
13227 async move {
13228 // When formatting is requested, trailing whitespace has already been stripped,
13229 // and the trailing newline has already been added.
13230 assert_eq!(
13231 &buffer_changes.lock()[1..],
13232 &[
13233 (
13234 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13235 "".into()
13236 ),
13237 (
13238 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13239 "".into()
13240 ),
13241 (
13242 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13243 "\n".into()
13244 ),
13245 ]
13246 );
13247
13248 Ok(Some(vec![
13249 lsp::TextEdit {
13250 range: lsp::Range::new(
13251 lsp::Position::new(1, 0),
13252 lsp::Position::new(1, 0),
13253 ),
13254 new_text: "\n".into(),
13255 },
13256 lsp::TextEdit {
13257 range: lsp::Range::new(
13258 lsp::Position::new(2, 0),
13259 lsp::Position::new(2, 0),
13260 ),
13261 new_text: "\n".into(),
13262 },
13263 ]))
13264 }
13265 }
13266 });
13267
13268 // Set up a buffer white some trailing whitespace and no trailing newline.
13269 cx.set_state(
13270 &[
13271 "one ", //
13272 "twoˇ", //
13273 "three ", //
13274 "four", //
13275 ]
13276 .join("\n"),
13277 );
13278 cx.run_until_parked();
13279
13280 // Submit a format request.
13281 let format = cx
13282 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13283 .unwrap();
13284
13285 cx.run_until_parked();
13286 // After formatting the buffer, the trailing whitespace is stripped,
13287 // a newline is appended, and the edits provided by the language server
13288 // have been applied.
13289 format.await.unwrap();
13290
13291 cx.assert_editor_state(
13292 &[
13293 "one", //
13294 "", //
13295 "twoˇ", //
13296 "", //
13297 "three", //
13298 "four", //
13299 "", //
13300 ]
13301 .join("\n"),
13302 );
13303
13304 // Undoing the formatting undoes the trailing whitespace removal, the
13305 // trailing newline, and the LSP edits.
13306 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13307 cx.assert_editor_state(
13308 &[
13309 "one ", //
13310 "twoˇ", //
13311 "three ", //
13312 "four", //
13313 ]
13314 .join("\n"),
13315 );
13316}
13317
13318#[gpui::test]
13319async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13320 cx: &mut TestAppContext,
13321) {
13322 init_test(cx, |_| {});
13323
13324 cx.update(|cx| {
13325 cx.update_global::<SettingsStore, _>(|settings, cx| {
13326 settings.update_user_settings(cx, |settings| {
13327 settings.editor.auto_signature_help = Some(true);
13328 });
13329 });
13330 });
13331
13332 let mut cx = EditorLspTestContext::new_rust(
13333 lsp::ServerCapabilities {
13334 signature_help_provider: Some(lsp::SignatureHelpOptions {
13335 ..Default::default()
13336 }),
13337 ..Default::default()
13338 },
13339 cx,
13340 )
13341 .await;
13342
13343 let language = Language::new(
13344 LanguageConfig {
13345 name: "Rust".into(),
13346 brackets: BracketPairConfig {
13347 pairs: vec![
13348 BracketPair {
13349 start: "{".to_string(),
13350 end: "}".to_string(),
13351 close: true,
13352 surround: true,
13353 newline: true,
13354 },
13355 BracketPair {
13356 start: "(".to_string(),
13357 end: ")".to_string(),
13358 close: true,
13359 surround: true,
13360 newline: true,
13361 },
13362 BracketPair {
13363 start: "/*".to_string(),
13364 end: " */".to_string(),
13365 close: true,
13366 surround: true,
13367 newline: true,
13368 },
13369 BracketPair {
13370 start: "[".to_string(),
13371 end: "]".to_string(),
13372 close: false,
13373 surround: false,
13374 newline: true,
13375 },
13376 BracketPair {
13377 start: "\"".to_string(),
13378 end: "\"".to_string(),
13379 close: true,
13380 surround: true,
13381 newline: false,
13382 },
13383 BracketPair {
13384 start: "<".to_string(),
13385 end: ">".to_string(),
13386 close: false,
13387 surround: true,
13388 newline: true,
13389 },
13390 ],
13391 ..Default::default()
13392 },
13393 autoclose_before: "})]".to_string(),
13394 ..Default::default()
13395 },
13396 Some(tree_sitter_rust::LANGUAGE.into()),
13397 );
13398 let language = Arc::new(language);
13399
13400 cx.language_registry().add(language.clone());
13401 cx.update_buffer(|buffer, cx| {
13402 buffer.set_language(Some(language), cx);
13403 });
13404
13405 cx.set_state(
13406 &r#"
13407 fn main() {
13408 sampleˇ
13409 }
13410 "#
13411 .unindent(),
13412 );
13413
13414 cx.update_editor(|editor, window, cx| {
13415 editor.handle_input("(", window, cx);
13416 });
13417 cx.assert_editor_state(
13418 &"
13419 fn main() {
13420 sample(ˇ)
13421 }
13422 "
13423 .unindent(),
13424 );
13425
13426 let mocked_response = lsp::SignatureHelp {
13427 signatures: vec![lsp::SignatureInformation {
13428 label: "fn sample(param1: u8, param2: u8)".to_string(),
13429 documentation: None,
13430 parameters: Some(vec![
13431 lsp::ParameterInformation {
13432 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13433 documentation: None,
13434 },
13435 lsp::ParameterInformation {
13436 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13437 documentation: None,
13438 },
13439 ]),
13440 active_parameter: None,
13441 }],
13442 active_signature: Some(0),
13443 active_parameter: Some(0),
13444 };
13445 handle_signature_help_request(&mut cx, mocked_response).await;
13446
13447 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13448 .await;
13449
13450 cx.editor(|editor, _, _| {
13451 let signature_help_state = editor.signature_help_state.popover().cloned();
13452 let signature = signature_help_state.unwrap();
13453 assert_eq!(
13454 signature.signatures[signature.current_signature].label,
13455 "fn sample(param1: u8, param2: u8)"
13456 );
13457 });
13458}
13459
13460#[gpui::test]
13461async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13462 init_test(cx, |_| {});
13463
13464 cx.update(|cx| {
13465 cx.update_global::<SettingsStore, _>(|settings, cx| {
13466 settings.update_user_settings(cx, |settings| {
13467 settings.editor.auto_signature_help = Some(false);
13468 settings.editor.show_signature_help_after_edits = Some(false);
13469 });
13470 });
13471 });
13472
13473 let mut cx = EditorLspTestContext::new_rust(
13474 lsp::ServerCapabilities {
13475 signature_help_provider: Some(lsp::SignatureHelpOptions {
13476 ..Default::default()
13477 }),
13478 ..Default::default()
13479 },
13480 cx,
13481 )
13482 .await;
13483
13484 let language = Language::new(
13485 LanguageConfig {
13486 name: "Rust".into(),
13487 brackets: BracketPairConfig {
13488 pairs: vec![
13489 BracketPair {
13490 start: "{".to_string(),
13491 end: "}".to_string(),
13492 close: true,
13493 surround: true,
13494 newline: true,
13495 },
13496 BracketPair {
13497 start: "(".to_string(),
13498 end: ")".to_string(),
13499 close: true,
13500 surround: true,
13501 newline: true,
13502 },
13503 BracketPair {
13504 start: "/*".to_string(),
13505 end: " */".to_string(),
13506 close: true,
13507 surround: true,
13508 newline: true,
13509 },
13510 BracketPair {
13511 start: "[".to_string(),
13512 end: "]".to_string(),
13513 close: false,
13514 surround: false,
13515 newline: true,
13516 },
13517 BracketPair {
13518 start: "\"".to_string(),
13519 end: "\"".to_string(),
13520 close: true,
13521 surround: true,
13522 newline: false,
13523 },
13524 BracketPair {
13525 start: "<".to_string(),
13526 end: ">".to_string(),
13527 close: false,
13528 surround: true,
13529 newline: true,
13530 },
13531 ],
13532 ..Default::default()
13533 },
13534 autoclose_before: "})]".to_string(),
13535 ..Default::default()
13536 },
13537 Some(tree_sitter_rust::LANGUAGE.into()),
13538 );
13539 let language = Arc::new(language);
13540
13541 cx.language_registry().add(language.clone());
13542 cx.update_buffer(|buffer, cx| {
13543 buffer.set_language(Some(language), cx);
13544 });
13545
13546 // Ensure that signature_help is not called when no signature help is enabled.
13547 cx.set_state(
13548 &r#"
13549 fn main() {
13550 sampleˇ
13551 }
13552 "#
13553 .unindent(),
13554 );
13555 cx.update_editor(|editor, window, cx| {
13556 editor.handle_input("(", window, cx);
13557 });
13558 cx.assert_editor_state(
13559 &"
13560 fn main() {
13561 sample(ˇ)
13562 }
13563 "
13564 .unindent(),
13565 );
13566 cx.editor(|editor, _, _| {
13567 assert!(editor.signature_help_state.task().is_none());
13568 });
13569
13570 let mocked_response = lsp::SignatureHelp {
13571 signatures: vec![lsp::SignatureInformation {
13572 label: "fn sample(param1: u8, param2: u8)".to_string(),
13573 documentation: None,
13574 parameters: Some(vec![
13575 lsp::ParameterInformation {
13576 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13577 documentation: None,
13578 },
13579 lsp::ParameterInformation {
13580 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13581 documentation: None,
13582 },
13583 ]),
13584 active_parameter: None,
13585 }],
13586 active_signature: Some(0),
13587 active_parameter: Some(0),
13588 };
13589
13590 // Ensure that signature_help is called when enabled afte edits
13591 cx.update(|_, cx| {
13592 cx.update_global::<SettingsStore, _>(|settings, cx| {
13593 settings.update_user_settings(cx, |settings| {
13594 settings.editor.auto_signature_help = Some(false);
13595 settings.editor.show_signature_help_after_edits = Some(true);
13596 });
13597 });
13598 });
13599 cx.set_state(
13600 &r#"
13601 fn main() {
13602 sampleˇ
13603 }
13604 "#
13605 .unindent(),
13606 );
13607 cx.update_editor(|editor, window, cx| {
13608 editor.handle_input("(", window, cx);
13609 });
13610 cx.assert_editor_state(
13611 &"
13612 fn main() {
13613 sample(ˇ)
13614 }
13615 "
13616 .unindent(),
13617 );
13618 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13619 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13620 .await;
13621 cx.update_editor(|editor, _, _| {
13622 let signature_help_state = editor.signature_help_state.popover().cloned();
13623 assert!(signature_help_state.is_some());
13624 let signature = signature_help_state.unwrap();
13625 assert_eq!(
13626 signature.signatures[signature.current_signature].label,
13627 "fn sample(param1: u8, param2: u8)"
13628 );
13629 editor.signature_help_state = SignatureHelpState::default();
13630 });
13631
13632 // Ensure that signature_help is called when auto signature help override is enabled
13633 cx.update(|_, cx| {
13634 cx.update_global::<SettingsStore, _>(|settings, cx| {
13635 settings.update_user_settings(cx, |settings| {
13636 settings.editor.auto_signature_help = Some(true);
13637 settings.editor.show_signature_help_after_edits = Some(false);
13638 });
13639 });
13640 });
13641 cx.set_state(
13642 &r#"
13643 fn main() {
13644 sampleˇ
13645 }
13646 "#
13647 .unindent(),
13648 );
13649 cx.update_editor(|editor, window, cx| {
13650 editor.handle_input("(", window, cx);
13651 });
13652 cx.assert_editor_state(
13653 &"
13654 fn main() {
13655 sample(ˇ)
13656 }
13657 "
13658 .unindent(),
13659 );
13660 handle_signature_help_request(&mut cx, mocked_response).await;
13661 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13662 .await;
13663 cx.editor(|editor, _, _| {
13664 let signature_help_state = editor.signature_help_state.popover().cloned();
13665 assert!(signature_help_state.is_some());
13666 let signature = signature_help_state.unwrap();
13667 assert_eq!(
13668 signature.signatures[signature.current_signature].label,
13669 "fn sample(param1: u8, param2: u8)"
13670 );
13671 });
13672}
13673
13674#[gpui::test]
13675async fn test_signature_help(cx: &mut TestAppContext) {
13676 init_test(cx, |_| {});
13677 cx.update(|cx| {
13678 cx.update_global::<SettingsStore, _>(|settings, cx| {
13679 settings.update_user_settings(cx, |settings| {
13680 settings.editor.auto_signature_help = Some(true);
13681 });
13682 });
13683 });
13684
13685 let mut cx = EditorLspTestContext::new_rust(
13686 lsp::ServerCapabilities {
13687 signature_help_provider: Some(lsp::SignatureHelpOptions {
13688 ..Default::default()
13689 }),
13690 ..Default::default()
13691 },
13692 cx,
13693 )
13694 .await;
13695
13696 // A test that directly calls `show_signature_help`
13697 cx.update_editor(|editor, window, cx| {
13698 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13699 });
13700
13701 let mocked_response = lsp::SignatureHelp {
13702 signatures: vec![lsp::SignatureInformation {
13703 label: "fn sample(param1: u8, param2: u8)".to_string(),
13704 documentation: None,
13705 parameters: Some(vec![
13706 lsp::ParameterInformation {
13707 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13708 documentation: None,
13709 },
13710 lsp::ParameterInformation {
13711 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13712 documentation: None,
13713 },
13714 ]),
13715 active_parameter: None,
13716 }],
13717 active_signature: Some(0),
13718 active_parameter: Some(0),
13719 };
13720 handle_signature_help_request(&mut cx, mocked_response).await;
13721
13722 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13723 .await;
13724
13725 cx.editor(|editor, _, _| {
13726 let signature_help_state = editor.signature_help_state.popover().cloned();
13727 assert!(signature_help_state.is_some());
13728 let signature = signature_help_state.unwrap();
13729 assert_eq!(
13730 signature.signatures[signature.current_signature].label,
13731 "fn sample(param1: u8, param2: u8)"
13732 );
13733 });
13734
13735 // When exiting outside from inside the brackets, `signature_help` is closed.
13736 cx.set_state(indoc! {"
13737 fn main() {
13738 sample(ˇ);
13739 }
13740
13741 fn sample(param1: u8, param2: u8) {}
13742 "});
13743
13744 cx.update_editor(|editor, window, cx| {
13745 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13746 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13747 });
13748 });
13749
13750 let mocked_response = lsp::SignatureHelp {
13751 signatures: Vec::new(),
13752 active_signature: None,
13753 active_parameter: None,
13754 };
13755 handle_signature_help_request(&mut cx, mocked_response).await;
13756
13757 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13758 .await;
13759
13760 cx.editor(|editor, _, _| {
13761 assert!(!editor.signature_help_state.is_shown());
13762 });
13763
13764 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13765 cx.set_state(indoc! {"
13766 fn main() {
13767 sample(ˇ);
13768 }
13769
13770 fn sample(param1: u8, param2: u8) {}
13771 "});
13772
13773 let mocked_response = lsp::SignatureHelp {
13774 signatures: vec![lsp::SignatureInformation {
13775 label: "fn sample(param1: u8, param2: u8)".to_string(),
13776 documentation: None,
13777 parameters: Some(vec![
13778 lsp::ParameterInformation {
13779 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13780 documentation: None,
13781 },
13782 lsp::ParameterInformation {
13783 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13784 documentation: None,
13785 },
13786 ]),
13787 active_parameter: None,
13788 }],
13789 active_signature: Some(0),
13790 active_parameter: Some(0),
13791 };
13792 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13793 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13794 .await;
13795 cx.editor(|editor, _, _| {
13796 assert!(editor.signature_help_state.is_shown());
13797 });
13798
13799 // Restore the popover with more parameter input
13800 cx.set_state(indoc! {"
13801 fn main() {
13802 sample(param1, param2ˇ);
13803 }
13804
13805 fn sample(param1: u8, param2: u8) {}
13806 "});
13807
13808 let mocked_response = lsp::SignatureHelp {
13809 signatures: vec![lsp::SignatureInformation {
13810 label: "fn sample(param1: u8, param2: u8)".to_string(),
13811 documentation: None,
13812 parameters: Some(vec![
13813 lsp::ParameterInformation {
13814 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13815 documentation: None,
13816 },
13817 lsp::ParameterInformation {
13818 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13819 documentation: None,
13820 },
13821 ]),
13822 active_parameter: None,
13823 }],
13824 active_signature: Some(0),
13825 active_parameter: Some(1),
13826 };
13827 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13828 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13829 .await;
13830
13831 // When selecting a range, the popover is gone.
13832 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13833 cx.update_editor(|editor, window, cx| {
13834 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13835 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13836 })
13837 });
13838 cx.assert_editor_state(indoc! {"
13839 fn main() {
13840 sample(param1, «ˇparam2»);
13841 }
13842
13843 fn sample(param1: u8, param2: u8) {}
13844 "});
13845 cx.editor(|editor, _, _| {
13846 assert!(!editor.signature_help_state.is_shown());
13847 });
13848
13849 // When unselecting again, the popover is back if within the brackets.
13850 cx.update_editor(|editor, window, cx| {
13851 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13852 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13853 })
13854 });
13855 cx.assert_editor_state(indoc! {"
13856 fn main() {
13857 sample(param1, ˇparam2);
13858 }
13859
13860 fn sample(param1: u8, param2: u8) {}
13861 "});
13862 handle_signature_help_request(&mut cx, mocked_response).await;
13863 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13864 .await;
13865 cx.editor(|editor, _, _| {
13866 assert!(editor.signature_help_state.is_shown());
13867 });
13868
13869 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13870 cx.update_editor(|editor, window, cx| {
13871 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13872 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13873 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13874 })
13875 });
13876 cx.assert_editor_state(indoc! {"
13877 fn main() {
13878 sample(param1, ˇparam2);
13879 }
13880
13881 fn sample(param1: u8, param2: u8) {}
13882 "});
13883
13884 let mocked_response = lsp::SignatureHelp {
13885 signatures: vec![lsp::SignatureInformation {
13886 label: "fn sample(param1: u8, param2: u8)".to_string(),
13887 documentation: None,
13888 parameters: Some(vec![
13889 lsp::ParameterInformation {
13890 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13891 documentation: None,
13892 },
13893 lsp::ParameterInformation {
13894 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13895 documentation: None,
13896 },
13897 ]),
13898 active_parameter: None,
13899 }],
13900 active_signature: Some(0),
13901 active_parameter: Some(1),
13902 };
13903 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13904 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13905 .await;
13906 cx.update_editor(|editor, _, cx| {
13907 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13908 });
13909 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13910 .await;
13911 cx.update_editor(|editor, window, cx| {
13912 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13913 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13914 })
13915 });
13916 cx.assert_editor_state(indoc! {"
13917 fn main() {
13918 sample(param1, «ˇparam2»);
13919 }
13920
13921 fn sample(param1: u8, param2: u8) {}
13922 "});
13923 cx.update_editor(|editor, window, cx| {
13924 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13925 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13926 })
13927 });
13928 cx.assert_editor_state(indoc! {"
13929 fn main() {
13930 sample(param1, ˇparam2);
13931 }
13932
13933 fn sample(param1: u8, param2: u8) {}
13934 "});
13935 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13936 .await;
13937}
13938
13939#[gpui::test]
13940async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13941 init_test(cx, |_| {});
13942
13943 let mut cx = EditorLspTestContext::new_rust(
13944 lsp::ServerCapabilities {
13945 signature_help_provider: Some(lsp::SignatureHelpOptions {
13946 ..Default::default()
13947 }),
13948 ..Default::default()
13949 },
13950 cx,
13951 )
13952 .await;
13953
13954 cx.set_state(indoc! {"
13955 fn main() {
13956 overloadedˇ
13957 }
13958 "});
13959
13960 cx.update_editor(|editor, window, cx| {
13961 editor.handle_input("(", window, cx);
13962 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13963 });
13964
13965 // Mock response with 3 signatures
13966 let mocked_response = lsp::SignatureHelp {
13967 signatures: vec![
13968 lsp::SignatureInformation {
13969 label: "fn overloaded(x: i32)".to_string(),
13970 documentation: None,
13971 parameters: Some(vec![lsp::ParameterInformation {
13972 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13973 documentation: None,
13974 }]),
13975 active_parameter: None,
13976 },
13977 lsp::SignatureInformation {
13978 label: "fn overloaded(x: i32, y: i32)".to_string(),
13979 documentation: None,
13980 parameters: Some(vec![
13981 lsp::ParameterInformation {
13982 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13983 documentation: None,
13984 },
13985 lsp::ParameterInformation {
13986 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13987 documentation: None,
13988 },
13989 ]),
13990 active_parameter: None,
13991 },
13992 lsp::SignatureInformation {
13993 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13994 documentation: None,
13995 parameters: Some(vec![
13996 lsp::ParameterInformation {
13997 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13998 documentation: None,
13999 },
14000 lsp::ParameterInformation {
14001 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14002 documentation: None,
14003 },
14004 lsp::ParameterInformation {
14005 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
14006 documentation: None,
14007 },
14008 ]),
14009 active_parameter: None,
14010 },
14011 ],
14012 active_signature: Some(1),
14013 active_parameter: Some(0),
14014 };
14015 handle_signature_help_request(&mut cx, mocked_response).await;
14016
14017 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14018 .await;
14019
14020 // Verify we have multiple signatures and the right one is selected
14021 cx.editor(|editor, _, _| {
14022 let popover = editor.signature_help_state.popover().cloned().unwrap();
14023 assert_eq!(popover.signatures.len(), 3);
14024 // active_signature was 1, so that should be the current
14025 assert_eq!(popover.current_signature, 1);
14026 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
14027 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
14028 assert_eq!(
14029 popover.signatures[2].label,
14030 "fn overloaded(x: i32, y: i32, z: i32)"
14031 );
14032 });
14033
14034 // Test navigation functionality
14035 cx.update_editor(|editor, window, cx| {
14036 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14037 });
14038
14039 cx.editor(|editor, _, _| {
14040 let popover = editor.signature_help_state.popover().cloned().unwrap();
14041 assert_eq!(popover.current_signature, 2);
14042 });
14043
14044 // Test wrap around
14045 cx.update_editor(|editor, window, cx| {
14046 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14047 });
14048
14049 cx.editor(|editor, _, _| {
14050 let popover = editor.signature_help_state.popover().cloned().unwrap();
14051 assert_eq!(popover.current_signature, 0);
14052 });
14053
14054 // Test previous navigation
14055 cx.update_editor(|editor, window, cx| {
14056 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
14057 });
14058
14059 cx.editor(|editor, _, _| {
14060 let popover = editor.signature_help_state.popover().cloned().unwrap();
14061 assert_eq!(popover.current_signature, 2);
14062 });
14063}
14064
14065#[gpui::test]
14066async fn test_completion_mode(cx: &mut TestAppContext) {
14067 init_test(cx, |_| {});
14068 let mut cx = EditorLspTestContext::new_rust(
14069 lsp::ServerCapabilities {
14070 completion_provider: Some(lsp::CompletionOptions {
14071 resolve_provider: Some(true),
14072 ..Default::default()
14073 }),
14074 ..Default::default()
14075 },
14076 cx,
14077 )
14078 .await;
14079
14080 struct Run {
14081 run_description: &'static str,
14082 initial_state: String,
14083 buffer_marked_text: String,
14084 completion_label: &'static str,
14085 completion_text: &'static str,
14086 expected_with_insert_mode: String,
14087 expected_with_replace_mode: String,
14088 expected_with_replace_subsequence_mode: String,
14089 expected_with_replace_suffix_mode: String,
14090 }
14091
14092 let runs = [
14093 Run {
14094 run_description: "Start of word matches completion text",
14095 initial_state: "before ediˇ after".into(),
14096 buffer_marked_text: "before <edi|> after".into(),
14097 completion_label: "editor",
14098 completion_text: "editor",
14099 expected_with_insert_mode: "before editorˇ after".into(),
14100 expected_with_replace_mode: "before editorˇ after".into(),
14101 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14102 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14103 },
14104 Run {
14105 run_description: "Accept same text at the middle of the word",
14106 initial_state: "before ediˇtor after".into(),
14107 buffer_marked_text: "before <edi|tor> after".into(),
14108 completion_label: "editor",
14109 completion_text: "editor",
14110 expected_with_insert_mode: "before editorˇtor after".into(),
14111 expected_with_replace_mode: "before editorˇ after".into(),
14112 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14113 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14114 },
14115 Run {
14116 run_description: "End of word matches completion text -- cursor at end",
14117 initial_state: "before torˇ after".into(),
14118 buffer_marked_text: "before <tor|> after".into(),
14119 completion_label: "editor",
14120 completion_text: "editor",
14121 expected_with_insert_mode: "before editorˇ after".into(),
14122 expected_with_replace_mode: "before editorˇ after".into(),
14123 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14124 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14125 },
14126 Run {
14127 run_description: "End of word matches completion text -- cursor at start",
14128 initial_state: "before ˇtor after".into(),
14129 buffer_marked_text: "before <|tor> after".into(),
14130 completion_label: "editor",
14131 completion_text: "editor",
14132 expected_with_insert_mode: "before editorˇtor after".into(),
14133 expected_with_replace_mode: "before editorˇ after".into(),
14134 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14135 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14136 },
14137 Run {
14138 run_description: "Prepend text containing whitespace",
14139 initial_state: "pˇfield: bool".into(),
14140 buffer_marked_text: "<p|field>: bool".into(),
14141 completion_label: "pub ",
14142 completion_text: "pub ",
14143 expected_with_insert_mode: "pub ˇfield: bool".into(),
14144 expected_with_replace_mode: "pub ˇ: bool".into(),
14145 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
14146 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
14147 },
14148 Run {
14149 run_description: "Add element to start of list",
14150 initial_state: "[element_ˇelement_2]".into(),
14151 buffer_marked_text: "[<element_|element_2>]".into(),
14152 completion_label: "element_1",
14153 completion_text: "element_1",
14154 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
14155 expected_with_replace_mode: "[element_1ˇ]".into(),
14156 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
14157 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
14158 },
14159 Run {
14160 run_description: "Add element to start of list -- first and second elements are equal",
14161 initial_state: "[elˇelement]".into(),
14162 buffer_marked_text: "[<el|element>]".into(),
14163 completion_label: "element",
14164 completion_text: "element",
14165 expected_with_insert_mode: "[elementˇelement]".into(),
14166 expected_with_replace_mode: "[elementˇ]".into(),
14167 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
14168 expected_with_replace_suffix_mode: "[elementˇ]".into(),
14169 },
14170 Run {
14171 run_description: "Ends with matching suffix",
14172 initial_state: "SubˇError".into(),
14173 buffer_marked_text: "<Sub|Error>".into(),
14174 completion_label: "SubscriptionError",
14175 completion_text: "SubscriptionError",
14176 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
14177 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14178 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14179 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
14180 },
14181 Run {
14182 run_description: "Suffix is a subsequence -- contiguous",
14183 initial_state: "SubˇErr".into(),
14184 buffer_marked_text: "<Sub|Err>".into(),
14185 completion_label: "SubscriptionError",
14186 completion_text: "SubscriptionError",
14187 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
14188 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14189 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14190 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
14191 },
14192 Run {
14193 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
14194 initial_state: "Suˇscrirr".into(),
14195 buffer_marked_text: "<Su|scrirr>".into(),
14196 completion_label: "SubscriptionError",
14197 completion_text: "SubscriptionError",
14198 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
14199 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14200 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14201 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
14202 },
14203 Run {
14204 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
14205 initial_state: "foo(indˇix)".into(),
14206 buffer_marked_text: "foo(<ind|ix>)".into(),
14207 completion_label: "node_index",
14208 completion_text: "node_index",
14209 expected_with_insert_mode: "foo(node_indexˇix)".into(),
14210 expected_with_replace_mode: "foo(node_indexˇ)".into(),
14211 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
14212 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
14213 },
14214 Run {
14215 run_description: "Replace range ends before cursor - should extend to cursor",
14216 initial_state: "before editˇo after".into(),
14217 buffer_marked_text: "before <{ed}>it|o after".into(),
14218 completion_label: "editor",
14219 completion_text: "editor",
14220 expected_with_insert_mode: "before editorˇo after".into(),
14221 expected_with_replace_mode: "before editorˇo after".into(),
14222 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
14223 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14224 },
14225 Run {
14226 run_description: "Uses label for suffix matching",
14227 initial_state: "before ediˇtor after".into(),
14228 buffer_marked_text: "before <edi|tor> after".into(),
14229 completion_label: "editor",
14230 completion_text: "editor()",
14231 expected_with_insert_mode: "before editor()ˇtor after".into(),
14232 expected_with_replace_mode: "before editor()ˇ after".into(),
14233 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14234 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14235 },
14236 Run {
14237 run_description: "Case insensitive subsequence and suffix matching",
14238 initial_state: "before EDiˇtoR after".into(),
14239 buffer_marked_text: "before <EDi|toR> after".into(),
14240 completion_label: "editor",
14241 completion_text: "editor",
14242 expected_with_insert_mode: "before editorˇtoR after".into(),
14243 expected_with_replace_mode: "before editorˇ after".into(),
14244 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14245 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14246 },
14247 ];
14248
14249 for run in runs {
14250 let run_variations = [
14251 (LspInsertMode::Insert, run.expected_with_insert_mode),
14252 (LspInsertMode::Replace, run.expected_with_replace_mode),
14253 (
14254 LspInsertMode::ReplaceSubsequence,
14255 run.expected_with_replace_subsequence_mode,
14256 ),
14257 (
14258 LspInsertMode::ReplaceSuffix,
14259 run.expected_with_replace_suffix_mode,
14260 ),
14261 ];
14262
14263 for (lsp_insert_mode, expected_text) in run_variations {
14264 eprintln!(
14265 "run = {:?}, mode = {lsp_insert_mode:.?}",
14266 run.run_description,
14267 );
14268
14269 update_test_language_settings(&mut cx, |settings| {
14270 settings.defaults.completions = Some(CompletionSettingsContent {
14271 lsp_insert_mode: Some(lsp_insert_mode),
14272 words: Some(WordsCompletionMode::Disabled),
14273 words_min_length: Some(0),
14274 ..Default::default()
14275 });
14276 });
14277
14278 cx.set_state(&run.initial_state);
14279 cx.update_editor(|editor, window, cx| {
14280 editor.show_completions(&ShowCompletions, window, cx);
14281 });
14282
14283 let counter = Arc::new(AtomicUsize::new(0));
14284 handle_completion_request_with_insert_and_replace(
14285 &mut cx,
14286 &run.buffer_marked_text,
14287 vec![(run.completion_label, run.completion_text)],
14288 counter.clone(),
14289 )
14290 .await;
14291 cx.condition(|editor, _| editor.context_menu_visible())
14292 .await;
14293 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14294
14295 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14296 editor
14297 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14298 .unwrap()
14299 });
14300 cx.assert_editor_state(&expected_text);
14301 handle_resolve_completion_request(&mut cx, None).await;
14302 apply_additional_edits.await.unwrap();
14303 }
14304 }
14305}
14306
14307#[gpui::test]
14308async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14309 init_test(cx, |_| {});
14310 let mut cx = EditorLspTestContext::new_rust(
14311 lsp::ServerCapabilities {
14312 completion_provider: Some(lsp::CompletionOptions {
14313 resolve_provider: Some(true),
14314 ..Default::default()
14315 }),
14316 ..Default::default()
14317 },
14318 cx,
14319 )
14320 .await;
14321
14322 let initial_state = "SubˇError";
14323 let buffer_marked_text = "<Sub|Error>";
14324 let completion_text = "SubscriptionError";
14325 let expected_with_insert_mode = "SubscriptionErrorˇError";
14326 let expected_with_replace_mode = "SubscriptionErrorˇ";
14327
14328 update_test_language_settings(&mut cx, |settings| {
14329 settings.defaults.completions = Some(CompletionSettingsContent {
14330 words: Some(WordsCompletionMode::Disabled),
14331 words_min_length: Some(0),
14332 // set the opposite here to ensure that the action is overriding the default behavior
14333 lsp_insert_mode: Some(LspInsertMode::Insert),
14334 ..Default::default()
14335 });
14336 });
14337
14338 cx.set_state(initial_state);
14339 cx.update_editor(|editor, window, cx| {
14340 editor.show_completions(&ShowCompletions, window, cx);
14341 });
14342
14343 let counter = Arc::new(AtomicUsize::new(0));
14344 handle_completion_request_with_insert_and_replace(
14345 &mut cx,
14346 buffer_marked_text,
14347 vec![(completion_text, completion_text)],
14348 counter.clone(),
14349 )
14350 .await;
14351 cx.condition(|editor, _| editor.context_menu_visible())
14352 .await;
14353 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14354
14355 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14356 editor
14357 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14358 .unwrap()
14359 });
14360 cx.assert_editor_state(expected_with_replace_mode);
14361 handle_resolve_completion_request(&mut cx, None).await;
14362 apply_additional_edits.await.unwrap();
14363
14364 update_test_language_settings(&mut cx, |settings| {
14365 settings.defaults.completions = Some(CompletionSettingsContent {
14366 words: Some(WordsCompletionMode::Disabled),
14367 words_min_length: Some(0),
14368 // set the opposite here to ensure that the action is overriding the default behavior
14369 lsp_insert_mode: Some(LspInsertMode::Replace),
14370 ..Default::default()
14371 });
14372 });
14373
14374 cx.set_state(initial_state);
14375 cx.update_editor(|editor, window, cx| {
14376 editor.show_completions(&ShowCompletions, window, cx);
14377 });
14378 handle_completion_request_with_insert_and_replace(
14379 &mut cx,
14380 buffer_marked_text,
14381 vec![(completion_text, completion_text)],
14382 counter.clone(),
14383 )
14384 .await;
14385 cx.condition(|editor, _| editor.context_menu_visible())
14386 .await;
14387 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14388
14389 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14390 editor
14391 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14392 .unwrap()
14393 });
14394 cx.assert_editor_state(expected_with_insert_mode);
14395 handle_resolve_completion_request(&mut cx, None).await;
14396 apply_additional_edits.await.unwrap();
14397}
14398
14399#[gpui::test]
14400async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14401 init_test(cx, |_| {});
14402 let mut cx = EditorLspTestContext::new_rust(
14403 lsp::ServerCapabilities {
14404 completion_provider: Some(lsp::CompletionOptions {
14405 resolve_provider: Some(true),
14406 ..Default::default()
14407 }),
14408 ..Default::default()
14409 },
14410 cx,
14411 )
14412 .await;
14413
14414 // scenario: surrounding text matches completion text
14415 let completion_text = "to_offset";
14416 let initial_state = indoc! {"
14417 1. buf.to_offˇsuffix
14418 2. buf.to_offˇsuf
14419 3. buf.to_offˇfix
14420 4. buf.to_offˇ
14421 5. into_offˇensive
14422 6. ˇsuffix
14423 7. let ˇ //
14424 8. aaˇzz
14425 9. buf.to_off«zzzzzˇ»suffix
14426 10. buf.«ˇzzzzz»suffix
14427 11. to_off«ˇzzzzz»
14428
14429 buf.to_offˇsuffix // newest cursor
14430 "};
14431 let completion_marked_buffer = indoc! {"
14432 1. buf.to_offsuffix
14433 2. buf.to_offsuf
14434 3. buf.to_offfix
14435 4. buf.to_off
14436 5. into_offensive
14437 6. suffix
14438 7. let //
14439 8. aazz
14440 9. buf.to_offzzzzzsuffix
14441 10. buf.zzzzzsuffix
14442 11. to_offzzzzz
14443
14444 buf.<to_off|suffix> // newest cursor
14445 "};
14446 let expected = indoc! {"
14447 1. buf.to_offsetˇ
14448 2. buf.to_offsetˇsuf
14449 3. buf.to_offsetˇfix
14450 4. buf.to_offsetˇ
14451 5. into_offsetˇensive
14452 6. to_offsetˇsuffix
14453 7. let to_offsetˇ //
14454 8. aato_offsetˇzz
14455 9. buf.to_offsetˇ
14456 10. buf.to_offsetˇsuffix
14457 11. to_offsetˇ
14458
14459 buf.to_offsetˇ // newest cursor
14460 "};
14461 cx.set_state(initial_state);
14462 cx.update_editor(|editor, window, cx| {
14463 editor.show_completions(&ShowCompletions, window, cx);
14464 });
14465 handle_completion_request_with_insert_and_replace(
14466 &mut cx,
14467 completion_marked_buffer,
14468 vec![(completion_text, completion_text)],
14469 Arc::new(AtomicUsize::new(0)),
14470 )
14471 .await;
14472 cx.condition(|editor, _| editor.context_menu_visible())
14473 .await;
14474 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14475 editor
14476 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14477 .unwrap()
14478 });
14479 cx.assert_editor_state(expected);
14480 handle_resolve_completion_request(&mut cx, None).await;
14481 apply_additional_edits.await.unwrap();
14482
14483 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14484 let completion_text = "foo_and_bar";
14485 let initial_state = indoc! {"
14486 1. ooanbˇ
14487 2. zooanbˇ
14488 3. ooanbˇz
14489 4. zooanbˇz
14490 5. ooanˇ
14491 6. oanbˇ
14492
14493 ooanbˇ
14494 "};
14495 let completion_marked_buffer = indoc! {"
14496 1. ooanb
14497 2. zooanb
14498 3. ooanbz
14499 4. zooanbz
14500 5. ooan
14501 6. oanb
14502
14503 <ooanb|>
14504 "};
14505 let expected = indoc! {"
14506 1. foo_and_barˇ
14507 2. zfoo_and_barˇ
14508 3. foo_and_barˇz
14509 4. zfoo_and_barˇz
14510 5. ooanfoo_and_barˇ
14511 6. oanbfoo_and_barˇ
14512
14513 foo_and_barˇ
14514 "};
14515 cx.set_state(initial_state);
14516 cx.update_editor(|editor, window, cx| {
14517 editor.show_completions(&ShowCompletions, window, cx);
14518 });
14519 handle_completion_request_with_insert_and_replace(
14520 &mut cx,
14521 completion_marked_buffer,
14522 vec![(completion_text, completion_text)],
14523 Arc::new(AtomicUsize::new(0)),
14524 )
14525 .await;
14526 cx.condition(|editor, _| editor.context_menu_visible())
14527 .await;
14528 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14529 editor
14530 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14531 .unwrap()
14532 });
14533 cx.assert_editor_state(expected);
14534 handle_resolve_completion_request(&mut cx, None).await;
14535 apply_additional_edits.await.unwrap();
14536
14537 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14538 // (expects the same as if it was inserted at the end)
14539 let completion_text = "foo_and_bar";
14540 let initial_state = indoc! {"
14541 1. ooˇanb
14542 2. zooˇanb
14543 3. ooˇanbz
14544 4. zooˇanbz
14545
14546 ooˇanb
14547 "};
14548 let completion_marked_buffer = indoc! {"
14549 1. ooanb
14550 2. zooanb
14551 3. ooanbz
14552 4. zooanbz
14553
14554 <oo|anb>
14555 "};
14556 let expected = indoc! {"
14557 1. foo_and_barˇ
14558 2. zfoo_and_barˇ
14559 3. foo_and_barˇz
14560 4. zfoo_and_barˇz
14561
14562 foo_and_barˇ
14563 "};
14564 cx.set_state(initial_state);
14565 cx.update_editor(|editor, window, cx| {
14566 editor.show_completions(&ShowCompletions, window, cx);
14567 });
14568 handle_completion_request_with_insert_and_replace(
14569 &mut cx,
14570 completion_marked_buffer,
14571 vec![(completion_text, completion_text)],
14572 Arc::new(AtomicUsize::new(0)),
14573 )
14574 .await;
14575 cx.condition(|editor, _| editor.context_menu_visible())
14576 .await;
14577 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14578 editor
14579 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14580 .unwrap()
14581 });
14582 cx.assert_editor_state(expected);
14583 handle_resolve_completion_request(&mut cx, None).await;
14584 apply_additional_edits.await.unwrap();
14585}
14586
14587// This used to crash
14588#[gpui::test]
14589async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14590 init_test(cx, |_| {});
14591
14592 let buffer_text = indoc! {"
14593 fn main() {
14594 10.satu;
14595
14596 //
14597 // separate cursors so they open in different excerpts (manually reproducible)
14598 //
14599
14600 10.satu20;
14601 }
14602 "};
14603 let multibuffer_text_with_selections = indoc! {"
14604 fn main() {
14605 10.satuˇ;
14606
14607 //
14608
14609 //
14610
14611 10.satuˇ20;
14612 }
14613 "};
14614 let expected_multibuffer = indoc! {"
14615 fn main() {
14616 10.saturating_sub()ˇ;
14617
14618 //
14619
14620 //
14621
14622 10.saturating_sub()ˇ;
14623 }
14624 "};
14625
14626 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14627 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14628
14629 let fs = FakeFs::new(cx.executor());
14630 fs.insert_tree(
14631 path!("/a"),
14632 json!({
14633 "main.rs": buffer_text,
14634 }),
14635 )
14636 .await;
14637
14638 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14639 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14640 language_registry.add(rust_lang());
14641 let mut fake_servers = language_registry.register_fake_lsp(
14642 "Rust",
14643 FakeLspAdapter {
14644 capabilities: lsp::ServerCapabilities {
14645 completion_provider: Some(lsp::CompletionOptions {
14646 resolve_provider: None,
14647 ..lsp::CompletionOptions::default()
14648 }),
14649 ..lsp::ServerCapabilities::default()
14650 },
14651 ..FakeLspAdapter::default()
14652 },
14653 );
14654 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14655 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14656 let buffer = project
14657 .update(cx, |project, cx| {
14658 project.open_local_buffer(path!("/a/main.rs"), cx)
14659 })
14660 .await
14661 .unwrap();
14662
14663 let multi_buffer = cx.new(|cx| {
14664 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14665 multi_buffer.push_excerpts(
14666 buffer.clone(),
14667 [ExcerptRange::new(0..first_excerpt_end)],
14668 cx,
14669 );
14670 multi_buffer.push_excerpts(
14671 buffer.clone(),
14672 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14673 cx,
14674 );
14675 multi_buffer
14676 });
14677
14678 let editor = workspace
14679 .update(cx, |_, window, cx| {
14680 cx.new(|cx| {
14681 Editor::new(
14682 EditorMode::Full {
14683 scale_ui_elements_with_buffer_font_size: false,
14684 show_active_line_background: false,
14685 sizing_behavior: SizingBehavior::Default,
14686 },
14687 multi_buffer.clone(),
14688 Some(project.clone()),
14689 window,
14690 cx,
14691 )
14692 })
14693 })
14694 .unwrap();
14695
14696 let pane = workspace
14697 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14698 .unwrap();
14699 pane.update_in(cx, |pane, window, cx| {
14700 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14701 });
14702
14703 let fake_server = fake_servers.next().await.unwrap();
14704
14705 editor.update_in(cx, |editor, window, cx| {
14706 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14707 s.select_ranges([
14708 Point::new(1, 11)..Point::new(1, 11),
14709 Point::new(7, 11)..Point::new(7, 11),
14710 ])
14711 });
14712
14713 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14714 });
14715
14716 editor.update_in(cx, |editor, window, cx| {
14717 editor.show_completions(&ShowCompletions, window, cx);
14718 });
14719
14720 fake_server
14721 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14722 let completion_item = lsp::CompletionItem {
14723 label: "saturating_sub()".into(),
14724 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14725 lsp::InsertReplaceEdit {
14726 new_text: "saturating_sub()".to_owned(),
14727 insert: lsp::Range::new(
14728 lsp::Position::new(7, 7),
14729 lsp::Position::new(7, 11),
14730 ),
14731 replace: lsp::Range::new(
14732 lsp::Position::new(7, 7),
14733 lsp::Position::new(7, 13),
14734 ),
14735 },
14736 )),
14737 ..lsp::CompletionItem::default()
14738 };
14739
14740 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14741 })
14742 .next()
14743 .await
14744 .unwrap();
14745
14746 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14747 .await;
14748
14749 editor
14750 .update_in(cx, |editor, window, cx| {
14751 editor
14752 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14753 .unwrap()
14754 })
14755 .await
14756 .unwrap();
14757
14758 editor.update(cx, |editor, cx| {
14759 assert_text_with_selections(editor, expected_multibuffer, cx);
14760 })
14761}
14762
14763#[gpui::test]
14764async fn test_completion(cx: &mut TestAppContext) {
14765 init_test(cx, |_| {});
14766
14767 let mut cx = EditorLspTestContext::new_rust(
14768 lsp::ServerCapabilities {
14769 completion_provider: Some(lsp::CompletionOptions {
14770 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14771 resolve_provider: Some(true),
14772 ..Default::default()
14773 }),
14774 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14775 ..Default::default()
14776 },
14777 cx,
14778 )
14779 .await;
14780 let counter = Arc::new(AtomicUsize::new(0));
14781
14782 cx.set_state(indoc! {"
14783 oneˇ
14784 two
14785 three
14786 "});
14787 cx.simulate_keystroke(".");
14788 handle_completion_request(
14789 indoc! {"
14790 one.|<>
14791 two
14792 three
14793 "},
14794 vec!["first_completion", "second_completion"],
14795 true,
14796 counter.clone(),
14797 &mut cx,
14798 )
14799 .await;
14800 cx.condition(|editor, _| editor.context_menu_visible())
14801 .await;
14802 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14803
14804 let _handler = handle_signature_help_request(
14805 &mut cx,
14806 lsp::SignatureHelp {
14807 signatures: vec![lsp::SignatureInformation {
14808 label: "test signature".to_string(),
14809 documentation: None,
14810 parameters: Some(vec![lsp::ParameterInformation {
14811 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14812 documentation: None,
14813 }]),
14814 active_parameter: None,
14815 }],
14816 active_signature: None,
14817 active_parameter: None,
14818 },
14819 );
14820 cx.update_editor(|editor, window, cx| {
14821 assert!(
14822 !editor.signature_help_state.is_shown(),
14823 "No signature help was called for"
14824 );
14825 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14826 });
14827 cx.run_until_parked();
14828 cx.update_editor(|editor, _, _| {
14829 assert!(
14830 !editor.signature_help_state.is_shown(),
14831 "No signature help should be shown when completions menu is open"
14832 );
14833 });
14834
14835 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14836 editor.context_menu_next(&Default::default(), window, cx);
14837 editor
14838 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14839 .unwrap()
14840 });
14841 cx.assert_editor_state(indoc! {"
14842 one.second_completionˇ
14843 two
14844 three
14845 "});
14846
14847 handle_resolve_completion_request(
14848 &mut cx,
14849 Some(vec![
14850 (
14851 //This overlaps with the primary completion edit which is
14852 //misbehavior from the LSP spec, test that we filter it out
14853 indoc! {"
14854 one.second_ˇcompletion
14855 two
14856 threeˇ
14857 "},
14858 "overlapping additional edit",
14859 ),
14860 (
14861 indoc! {"
14862 one.second_completion
14863 two
14864 threeˇ
14865 "},
14866 "\nadditional edit",
14867 ),
14868 ]),
14869 )
14870 .await;
14871 apply_additional_edits.await.unwrap();
14872 cx.assert_editor_state(indoc! {"
14873 one.second_completionˇ
14874 two
14875 three
14876 additional edit
14877 "});
14878
14879 cx.set_state(indoc! {"
14880 one.second_completion
14881 twoˇ
14882 threeˇ
14883 additional edit
14884 "});
14885 cx.simulate_keystroke(" ");
14886 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14887 cx.simulate_keystroke("s");
14888 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14889
14890 cx.assert_editor_state(indoc! {"
14891 one.second_completion
14892 two sˇ
14893 three sˇ
14894 additional edit
14895 "});
14896 handle_completion_request(
14897 indoc! {"
14898 one.second_completion
14899 two s
14900 three <s|>
14901 additional edit
14902 "},
14903 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14904 true,
14905 counter.clone(),
14906 &mut cx,
14907 )
14908 .await;
14909 cx.condition(|editor, _| editor.context_menu_visible())
14910 .await;
14911 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14912
14913 cx.simulate_keystroke("i");
14914
14915 handle_completion_request(
14916 indoc! {"
14917 one.second_completion
14918 two si
14919 three <si|>
14920 additional edit
14921 "},
14922 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14923 true,
14924 counter.clone(),
14925 &mut cx,
14926 )
14927 .await;
14928 cx.condition(|editor, _| editor.context_menu_visible())
14929 .await;
14930 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14931
14932 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14933 editor
14934 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14935 .unwrap()
14936 });
14937 cx.assert_editor_state(indoc! {"
14938 one.second_completion
14939 two sixth_completionˇ
14940 three sixth_completionˇ
14941 additional edit
14942 "});
14943
14944 apply_additional_edits.await.unwrap();
14945
14946 update_test_language_settings(&mut cx, |settings| {
14947 settings.defaults.show_completions_on_input = Some(false);
14948 });
14949 cx.set_state("editorˇ");
14950 cx.simulate_keystroke(".");
14951 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14952 cx.simulate_keystrokes("c l o");
14953 cx.assert_editor_state("editor.cloˇ");
14954 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14955 cx.update_editor(|editor, window, cx| {
14956 editor.show_completions(&ShowCompletions, window, cx);
14957 });
14958 handle_completion_request(
14959 "editor.<clo|>",
14960 vec!["close", "clobber"],
14961 true,
14962 counter.clone(),
14963 &mut cx,
14964 )
14965 .await;
14966 cx.condition(|editor, _| editor.context_menu_visible())
14967 .await;
14968 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14969
14970 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14971 editor
14972 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14973 .unwrap()
14974 });
14975 cx.assert_editor_state("editor.clobberˇ");
14976 handle_resolve_completion_request(&mut cx, None).await;
14977 apply_additional_edits.await.unwrap();
14978}
14979
14980#[gpui::test]
14981async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
14982 init_test(cx, |_| {});
14983
14984 let fs = FakeFs::new(cx.executor());
14985 fs.insert_tree(
14986 path!("/a"),
14987 json!({
14988 "main.rs": "",
14989 }),
14990 )
14991 .await;
14992
14993 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14994 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14995 language_registry.add(rust_lang());
14996 let command_calls = Arc::new(AtomicUsize::new(0));
14997 let registered_command = "_the/command";
14998
14999 let closure_command_calls = command_calls.clone();
15000 let mut fake_servers = language_registry.register_fake_lsp(
15001 "Rust",
15002 FakeLspAdapter {
15003 capabilities: lsp::ServerCapabilities {
15004 completion_provider: Some(lsp::CompletionOptions {
15005 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15006 ..lsp::CompletionOptions::default()
15007 }),
15008 execute_command_provider: Some(lsp::ExecuteCommandOptions {
15009 commands: vec![registered_command.to_owned()],
15010 ..lsp::ExecuteCommandOptions::default()
15011 }),
15012 ..lsp::ServerCapabilities::default()
15013 },
15014 initializer: Some(Box::new(move |fake_server| {
15015 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15016 move |params, _| async move {
15017 Ok(Some(lsp::CompletionResponse::Array(vec![
15018 lsp::CompletionItem {
15019 label: "registered_command".to_owned(),
15020 text_edit: gen_text_edit(¶ms, ""),
15021 command: Some(lsp::Command {
15022 title: registered_command.to_owned(),
15023 command: "_the/command".to_owned(),
15024 arguments: Some(vec![serde_json::Value::Bool(true)]),
15025 }),
15026 ..lsp::CompletionItem::default()
15027 },
15028 lsp::CompletionItem {
15029 label: "unregistered_command".to_owned(),
15030 text_edit: gen_text_edit(¶ms, ""),
15031 command: Some(lsp::Command {
15032 title: "????????????".to_owned(),
15033 command: "????????????".to_owned(),
15034 arguments: Some(vec![serde_json::Value::Null]),
15035 }),
15036 ..lsp::CompletionItem::default()
15037 },
15038 ])))
15039 },
15040 );
15041 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
15042 let command_calls = closure_command_calls.clone();
15043 move |params, _| {
15044 assert_eq!(params.command, registered_command);
15045 let command_calls = command_calls.clone();
15046 async move {
15047 command_calls.fetch_add(1, atomic::Ordering::Release);
15048 Ok(Some(json!(null)))
15049 }
15050 }
15051 });
15052 })),
15053 ..FakeLspAdapter::default()
15054 },
15055 );
15056 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15057 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15058 let editor = workspace
15059 .update(cx, |workspace, window, cx| {
15060 workspace.open_abs_path(
15061 PathBuf::from(path!("/a/main.rs")),
15062 OpenOptions::default(),
15063 window,
15064 cx,
15065 )
15066 })
15067 .unwrap()
15068 .await
15069 .unwrap()
15070 .downcast::<Editor>()
15071 .unwrap();
15072 let _fake_server = fake_servers.next().await.unwrap();
15073
15074 editor.update_in(cx, |editor, window, cx| {
15075 cx.focus_self(window);
15076 editor.move_to_end(&MoveToEnd, window, cx);
15077 editor.handle_input(".", window, cx);
15078 });
15079 cx.run_until_parked();
15080 editor.update(cx, |editor, _| {
15081 assert!(editor.context_menu_visible());
15082 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15083 {
15084 let completion_labels = menu
15085 .completions
15086 .borrow()
15087 .iter()
15088 .map(|c| c.label.text.clone())
15089 .collect::<Vec<_>>();
15090 assert_eq!(
15091 completion_labels,
15092 &["registered_command", "unregistered_command",],
15093 );
15094 } else {
15095 panic!("expected completion menu to be open");
15096 }
15097 });
15098
15099 editor
15100 .update_in(cx, |editor, window, cx| {
15101 editor
15102 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15103 .unwrap()
15104 })
15105 .await
15106 .unwrap();
15107 cx.run_until_parked();
15108 assert_eq!(
15109 command_calls.load(atomic::Ordering::Acquire),
15110 1,
15111 "For completion with a registered command, Zed should send a command execution request",
15112 );
15113
15114 editor.update_in(cx, |editor, window, cx| {
15115 cx.focus_self(window);
15116 editor.handle_input(".", window, cx);
15117 });
15118 cx.run_until_parked();
15119 editor.update(cx, |editor, _| {
15120 assert!(editor.context_menu_visible());
15121 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15122 {
15123 let completion_labels = menu
15124 .completions
15125 .borrow()
15126 .iter()
15127 .map(|c| c.label.text.clone())
15128 .collect::<Vec<_>>();
15129 assert_eq!(
15130 completion_labels,
15131 &["registered_command", "unregistered_command",],
15132 );
15133 } else {
15134 panic!("expected completion menu to be open");
15135 }
15136 });
15137 editor
15138 .update_in(cx, |editor, window, cx| {
15139 editor.context_menu_next(&Default::default(), window, cx);
15140 editor
15141 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15142 .unwrap()
15143 })
15144 .await
15145 .unwrap();
15146 cx.run_until_parked();
15147 assert_eq!(
15148 command_calls.load(atomic::Ordering::Acquire),
15149 1,
15150 "For completion with an unregistered command, Zed should not send a command execution request",
15151 );
15152}
15153
15154#[gpui::test]
15155async fn test_completion_reuse(cx: &mut TestAppContext) {
15156 init_test(cx, |_| {});
15157
15158 let mut cx = EditorLspTestContext::new_rust(
15159 lsp::ServerCapabilities {
15160 completion_provider: Some(lsp::CompletionOptions {
15161 trigger_characters: Some(vec![".".to_string()]),
15162 ..Default::default()
15163 }),
15164 ..Default::default()
15165 },
15166 cx,
15167 )
15168 .await;
15169
15170 let counter = Arc::new(AtomicUsize::new(0));
15171 cx.set_state("objˇ");
15172 cx.simulate_keystroke(".");
15173
15174 // Initial completion request returns complete results
15175 let is_incomplete = false;
15176 handle_completion_request(
15177 "obj.|<>",
15178 vec!["a", "ab", "abc"],
15179 is_incomplete,
15180 counter.clone(),
15181 &mut cx,
15182 )
15183 .await;
15184 cx.run_until_parked();
15185 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15186 cx.assert_editor_state("obj.ˇ");
15187 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15188
15189 // Type "a" - filters existing completions
15190 cx.simulate_keystroke("a");
15191 cx.run_until_parked();
15192 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15193 cx.assert_editor_state("obj.aˇ");
15194 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15195
15196 // Type "b" - filters existing completions
15197 cx.simulate_keystroke("b");
15198 cx.run_until_parked();
15199 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15200 cx.assert_editor_state("obj.abˇ");
15201 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15202
15203 // Type "c" - filters existing completions
15204 cx.simulate_keystroke("c");
15205 cx.run_until_parked();
15206 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15207 cx.assert_editor_state("obj.abcˇ");
15208 check_displayed_completions(vec!["abc"], &mut cx);
15209
15210 // Backspace to delete "c" - filters existing completions
15211 cx.update_editor(|editor, window, cx| {
15212 editor.backspace(&Backspace, window, cx);
15213 });
15214 cx.run_until_parked();
15215 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15216 cx.assert_editor_state("obj.abˇ");
15217 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15218
15219 // Moving cursor to the left dismisses menu.
15220 cx.update_editor(|editor, window, cx| {
15221 editor.move_left(&MoveLeft, window, cx);
15222 });
15223 cx.run_until_parked();
15224 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15225 cx.assert_editor_state("obj.aˇb");
15226 cx.update_editor(|editor, _, _| {
15227 assert_eq!(editor.context_menu_visible(), false);
15228 });
15229
15230 // Type "b" - new request
15231 cx.simulate_keystroke("b");
15232 let is_incomplete = false;
15233 handle_completion_request(
15234 "obj.<ab|>a",
15235 vec!["ab", "abc"],
15236 is_incomplete,
15237 counter.clone(),
15238 &mut cx,
15239 )
15240 .await;
15241 cx.run_until_parked();
15242 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15243 cx.assert_editor_state("obj.abˇb");
15244 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15245
15246 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
15247 cx.update_editor(|editor, window, cx| {
15248 editor.backspace(&Backspace, window, cx);
15249 });
15250 let is_incomplete = false;
15251 handle_completion_request(
15252 "obj.<a|>b",
15253 vec!["a", "ab", "abc"],
15254 is_incomplete,
15255 counter.clone(),
15256 &mut cx,
15257 )
15258 .await;
15259 cx.run_until_parked();
15260 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15261 cx.assert_editor_state("obj.aˇb");
15262 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15263
15264 // Backspace to delete "a" - dismisses menu.
15265 cx.update_editor(|editor, window, cx| {
15266 editor.backspace(&Backspace, window, cx);
15267 });
15268 cx.run_until_parked();
15269 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15270 cx.assert_editor_state("obj.ˇb");
15271 cx.update_editor(|editor, _, _| {
15272 assert_eq!(editor.context_menu_visible(), false);
15273 });
15274}
15275
15276#[gpui::test]
15277async fn test_word_completion(cx: &mut TestAppContext) {
15278 let lsp_fetch_timeout_ms = 10;
15279 init_test(cx, |language_settings| {
15280 language_settings.defaults.completions = Some(CompletionSettingsContent {
15281 words_min_length: Some(0),
15282 lsp_fetch_timeout_ms: Some(10),
15283 lsp_insert_mode: Some(LspInsertMode::Insert),
15284 ..Default::default()
15285 });
15286 });
15287
15288 let mut cx = EditorLspTestContext::new_rust(
15289 lsp::ServerCapabilities {
15290 completion_provider: Some(lsp::CompletionOptions {
15291 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15292 ..lsp::CompletionOptions::default()
15293 }),
15294 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15295 ..lsp::ServerCapabilities::default()
15296 },
15297 cx,
15298 )
15299 .await;
15300
15301 let throttle_completions = Arc::new(AtomicBool::new(false));
15302
15303 let lsp_throttle_completions = throttle_completions.clone();
15304 let _completion_requests_handler =
15305 cx.lsp
15306 .server
15307 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
15308 let lsp_throttle_completions = lsp_throttle_completions.clone();
15309 let cx = cx.clone();
15310 async move {
15311 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
15312 cx.background_executor()
15313 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
15314 .await;
15315 }
15316 Ok(Some(lsp::CompletionResponse::Array(vec![
15317 lsp::CompletionItem {
15318 label: "first".into(),
15319 ..lsp::CompletionItem::default()
15320 },
15321 lsp::CompletionItem {
15322 label: "last".into(),
15323 ..lsp::CompletionItem::default()
15324 },
15325 ])))
15326 }
15327 });
15328
15329 cx.set_state(indoc! {"
15330 oneˇ
15331 two
15332 three
15333 "});
15334 cx.simulate_keystroke(".");
15335 cx.executor().run_until_parked();
15336 cx.condition(|editor, _| editor.context_menu_visible())
15337 .await;
15338 cx.update_editor(|editor, window, cx| {
15339 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15340 {
15341 assert_eq!(
15342 completion_menu_entries(menu),
15343 &["first", "last"],
15344 "When LSP server is fast to reply, no fallback word completions are used"
15345 );
15346 } else {
15347 panic!("expected completion menu to be open");
15348 }
15349 editor.cancel(&Cancel, window, cx);
15350 });
15351 cx.executor().run_until_parked();
15352 cx.condition(|editor, _| !editor.context_menu_visible())
15353 .await;
15354
15355 throttle_completions.store(true, atomic::Ordering::Release);
15356 cx.simulate_keystroke(".");
15357 cx.executor()
15358 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
15359 cx.executor().run_until_parked();
15360 cx.condition(|editor, _| editor.context_menu_visible())
15361 .await;
15362 cx.update_editor(|editor, _, _| {
15363 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15364 {
15365 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
15366 "When LSP server is slow, document words can be shown instead, if configured accordingly");
15367 } else {
15368 panic!("expected completion menu to be open");
15369 }
15370 });
15371}
15372
15373#[gpui::test]
15374async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
15375 init_test(cx, |language_settings| {
15376 language_settings.defaults.completions = Some(CompletionSettingsContent {
15377 words: Some(WordsCompletionMode::Enabled),
15378 words_min_length: Some(0),
15379 lsp_insert_mode: Some(LspInsertMode::Insert),
15380 ..Default::default()
15381 });
15382 });
15383
15384 let mut cx = EditorLspTestContext::new_rust(
15385 lsp::ServerCapabilities {
15386 completion_provider: Some(lsp::CompletionOptions {
15387 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15388 ..lsp::CompletionOptions::default()
15389 }),
15390 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15391 ..lsp::ServerCapabilities::default()
15392 },
15393 cx,
15394 )
15395 .await;
15396
15397 let _completion_requests_handler =
15398 cx.lsp
15399 .server
15400 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15401 Ok(Some(lsp::CompletionResponse::Array(vec![
15402 lsp::CompletionItem {
15403 label: "first".into(),
15404 ..lsp::CompletionItem::default()
15405 },
15406 lsp::CompletionItem {
15407 label: "last".into(),
15408 ..lsp::CompletionItem::default()
15409 },
15410 ])))
15411 });
15412
15413 cx.set_state(indoc! {"ˇ
15414 first
15415 last
15416 second
15417 "});
15418 cx.simulate_keystroke(".");
15419 cx.executor().run_until_parked();
15420 cx.condition(|editor, _| editor.context_menu_visible())
15421 .await;
15422 cx.update_editor(|editor, _, _| {
15423 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15424 {
15425 assert_eq!(
15426 completion_menu_entries(menu),
15427 &["first", "last", "second"],
15428 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15429 );
15430 } else {
15431 panic!("expected completion menu to be open");
15432 }
15433 });
15434}
15435
15436#[gpui::test]
15437async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15438 init_test(cx, |language_settings| {
15439 language_settings.defaults.completions = Some(CompletionSettingsContent {
15440 words: Some(WordsCompletionMode::Disabled),
15441 words_min_length: Some(0),
15442 lsp_insert_mode: Some(LspInsertMode::Insert),
15443 ..Default::default()
15444 });
15445 });
15446
15447 let mut cx = EditorLspTestContext::new_rust(
15448 lsp::ServerCapabilities {
15449 completion_provider: Some(lsp::CompletionOptions {
15450 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15451 ..lsp::CompletionOptions::default()
15452 }),
15453 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15454 ..lsp::ServerCapabilities::default()
15455 },
15456 cx,
15457 )
15458 .await;
15459
15460 let _completion_requests_handler =
15461 cx.lsp
15462 .server
15463 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15464 panic!("LSP completions should not be queried when dealing with word completions")
15465 });
15466
15467 cx.set_state(indoc! {"ˇ
15468 first
15469 last
15470 second
15471 "});
15472 cx.update_editor(|editor, window, cx| {
15473 editor.show_word_completions(&ShowWordCompletions, window, cx);
15474 });
15475 cx.executor().run_until_parked();
15476 cx.condition(|editor, _| editor.context_menu_visible())
15477 .await;
15478 cx.update_editor(|editor, _, _| {
15479 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15480 {
15481 assert_eq!(
15482 completion_menu_entries(menu),
15483 &["first", "last", "second"],
15484 "`ShowWordCompletions` action should show word completions"
15485 );
15486 } else {
15487 panic!("expected completion menu to be open");
15488 }
15489 });
15490
15491 cx.simulate_keystroke("l");
15492 cx.executor().run_until_parked();
15493 cx.condition(|editor, _| editor.context_menu_visible())
15494 .await;
15495 cx.update_editor(|editor, _, _| {
15496 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15497 {
15498 assert_eq!(
15499 completion_menu_entries(menu),
15500 &["last"],
15501 "After showing word completions, further editing should filter them and not query the LSP"
15502 );
15503 } else {
15504 panic!("expected completion menu to be open");
15505 }
15506 });
15507}
15508
15509#[gpui::test]
15510async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15511 init_test(cx, |language_settings| {
15512 language_settings.defaults.completions = Some(CompletionSettingsContent {
15513 words_min_length: Some(0),
15514 lsp: Some(false),
15515 lsp_insert_mode: Some(LspInsertMode::Insert),
15516 ..Default::default()
15517 });
15518 });
15519
15520 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15521
15522 cx.set_state(indoc! {"ˇ
15523 0_usize
15524 let
15525 33
15526 4.5f32
15527 "});
15528 cx.update_editor(|editor, window, cx| {
15529 editor.show_completions(&ShowCompletions, window, cx);
15530 });
15531 cx.executor().run_until_parked();
15532 cx.condition(|editor, _| editor.context_menu_visible())
15533 .await;
15534 cx.update_editor(|editor, window, cx| {
15535 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15536 {
15537 assert_eq!(
15538 completion_menu_entries(menu),
15539 &["let"],
15540 "With no digits in the completion query, no digits should be in the word completions"
15541 );
15542 } else {
15543 panic!("expected completion menu to be open");
15544 }
15545 editor.cancel(&Cancel, window, cx);
15546 });
15547
15548 cx.set_state(indoc! {"3ˇ
15549 0_usize
15550 let
15551 3
15552 33.35f32
15553 "});
15554 cx.update_editor(|editor, window, cx| {
15555 editor.show_completions(&ShowCompletions, window, cx);
15556 });
15557 cx.executor().run_until_parked();
15558 cx.condition(|editor, _| editor.context_menu_visible())
15559 .await;
15560 cx.update_editor(|editor, _, _| {
15561 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15562 {
15563 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15564 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15565 } else {
15566 panic!("expected completion menu to be open");
15567 }
15568 });
15569}
15570
15571#[gpui::test]
15572async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15573 init_test(cx, |language_settings| {
15574 language_settings.defaults.completions = Some(CompletionSettingsContent {
15575 words: Some(WordsCompletionMode::Enabled),
15576 words_min_length: Some(3),
15577 lsp_insert_mode: Some(LspInsertMode::Insert),
15578 ..Default::default()
15579 });
15580 });
15581
15582 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15583 cx.set_state(indoc! {"ˇ
15584 wow
15585 wowen
15586 wowser
15587 "});
15588 cx.simulate_keystroke("w");
15589 cx.executor().run_until_parked();
15590 cx.update_editor(|editor, _, _| {
15591 if editor.context_menu.borrow_mut().is_some() {
15592 panic!(
15593 "expected completion menu to be hidden, as words completion threshold is not met"
15594 );
15595 }
15596 });
15597
15598 cx.update_editor(|editor, window, cx| {
15599 editor.show_word_completions(&ShowWordCompletions, window, cx);
15600 });
15601 cx.executor().run_until_parked();
15602 cx.update_editor(|editor, window, cx| {
15603 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15604 {
15605 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");
15606 } else {
15607 panic!("expected completion menu to be open after the word completions are called with an action");
15608 }
15609
15610 editor.cancel(&Cancel, window, cx);
15611 });
15612 cx.update_editor(|editor, _, _| {
15613 if editor.context_menu.borrow_mut().is_some() {
15614 panic!("expected completion menu to be hidden after canceling");
15615 }
15616 });
15617
15618 cx.simulate_keystroke("o");
15619 cx.executor().run_until_parked();
15620 cx.update_editor(|editor, _, _| {
15621 if editor.context_menu.borrow_mut().is_some() {
15622 panic!(
15623 "expected completion menu to be hidden, as words completion threshold is not met still"
15624 );
15625 }
15626 });
15627
15628 cx.simulate_keystroke("w");
15629 cx.executor().run_until_parked();
15630 cx.update_editor(|editor, _, _| {
15631 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15632 {
15633 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15634 } else {
15635 panic!("expected completion menu to be open after the word completions threshold is met");
15636 }
15637 });
15638}
15639
15640#[gpui::test]
15641async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15642 init_test(cx, |language_settings| {
15643 language_settings.defaults.completions = Some(CompletionSettingsContent {
15644 words: Some(WordsCompletionMode::Enabled),
15645 words_min_length: Some(0),
15646 lsp_insert_mode: Some(LspInsertMode::Insert),
15647 ..Default::default()
15648 });
15649 });
15650
15651 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15652 cx.update_editor(|editor, _, _| {
15653 editor.disable_word_completions();
15654 });
15655 cx.set_state(indoc! {"ˇ
15656 wow
15657 wowen
15658 wowser
15659 "});
15660 cx.simulate_keystroke("w");
15661 cx.executor().run_until_parked();
15662 cx.update_editor(|editor, _, _| {
15663 if editor.context_menu.borrow_mut().is_some() {
15664 panic!(
15665 "expected completion menu to be hidden, as words completion are disabled for this editor"
15666 );
15667 }
15668 });
15669
15670 cx.update_editor(|editor, window, cx| {
15671 editor.show_word_completions(&ShowWordCompletions, window, cx);
15672 });
15673 cx.executor().run_until_parked();
15674 cx.update_editor(|editor, _, _| {
15675 if editor.context_menu.borrow_mut().is_some() {
15676 panic!(
15677 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15678 );
15679 }
15680 });
15681}
15682
15683#[gpui::test]
15684async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15685 init_test(cx, |language_settings| {
15686 language_settings.defaults.completions = Some(CompletionSettingsContent {
15687 words: Some(WordsCompletionMode::Disabled),
15688 words_min_length: Some(0),
15689 lsp_insert_mode: Some(LspInsertMode::Insert),
15690 ..Default::default()
15691 });
15692 });
15693
15694 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15695 cx.update_editor(|editor, _, _| {
15696 editor.set_completion_provider(None);
15697 });
15698 cx.set_state(indoc! {"ˇ
15699 wow
15700 wowen
15701 wowser
15702 "});
15703 cx.simulate_keystroke("w");
15704 cx.executor().run_until_parked();
15705 cx.update_editor(|editor, _, _| {
15706 if editor.context_menu.borrow_mut().is_some() {
15707 panic!("expected completion menu to be hidden, as disabled in settings");
15708 }
15709 });
15710}
15711
15712fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15713 let position = || lsp::Position {
15714 line: params.text_document_position.position.line,
15715 character: params.text_document_position.position.character,
15716 };
15717 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15718 range: lsp::Range {
15719 start: position(),
15720 end: position(),
15721 },
15722 new_text: text.to_string(),
15723 }))
15724}
15725
15726#[gpui::test]
15727async fn test_multiline_completion(cx: &mut TestAppContext) {
15728 init_test(cx, |_| {});
15729
15730 let fs = FakeFs::new(cx.executor());
15731 fs.insert_tree(
15732 path!("/a"),
15733 json!({
15734 "main.ts": "a",
15735 }),
15736 )
15737 .await;
15738
15739 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15740 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15741 let typescript_language = Arc::new(Language::new(
15742 LanguageConfig {
15743 name: "TypeScript".into(),
15744 matcher: LanguageMatcher {
15745 path_suffixes: vec!["ts".to_string()],
15746 ..LanguageMatcher::default()
15747 },
15748 line_comments: vec!["// ".into()],
15749 ..LanguageConfig::default()
15750 },
15751 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15752 ));
15753 language_registry.add(typescript_language.clone());
15754 let mut fake_servers = language_registry.register_fake_lsp(
15755 "TypeScript",
15756 FakeLspAdapter {
15757 capabilities: lsp::ServerCapabilities {
15758 completion_provider: Some(lsp::CompletionOptions {
15759 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15760 ..lsp::CompletionOptions::default()
15761 }),
15762 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15763 ..lsp::ServerCapabilities::default()
15764 },
15765 // Emulate vtsls label generation
15766 label_for_completion: Some(Box::new(|item, _| {
15767 let text = if let Some(description) = item
15768 .label_details
15769 .as_ref()
15770 .and_then(|label_details| label_details.description.as_ref())
15771 {
15772 format!("{} {}", item.label, description)
15773 } else if let Some(detail) = &item.detail {
15774 format!("{} {}", item.label, detail)
15775 } else {
15776 item.label.clone()
15777 };
15778 Some(language::CodeLabel::plain(text, None))
15779 })),
15780 ..FakeLspAdapter::default()
15781 },
15782 );
15783 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15784 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15785 let worktree_id = workspace
15786 .update(cx, |workspace, _window, cx| {
15787 workspace.project().update(cx, |project, cx| {
15788 project.worktrees(cx).next().unwrap().read(cx).id()
15789 })
15790 })
15791 .unwrap();
15792 let _buffer = project
15793 .update(cx, |project, cx| {
15794 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15795 })
15796 .await
15797 .unwrap();
15798 let editor = workspace
15799 .update(cx, |workspace, window, cx| {
15800 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15801 })
15802 .unwrap()
15803 .await
15804 .unwrap()
15805 .downcast::<Editor>()
15806 .unwrap();
15807 let fake_server = fake_servers.next().await.unwrap();
15808
15809 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15810 let multiline_label_2 = "a\nb\nc\n";
15811 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15812 let multiline_description = "d\ne\nf\n";
15813 let multiline_detail_2 = "g\nh\ni\n";
15814
15815 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15816 move |params, _| async move {
15817 Ok(Some(lsp::CompletionResponse::Array(vec![
15818 lsp::CompletionItem {
15819 label: multiline_label.to_string(),
15820 text_edit: gen_text_edit(¶ms, "new_text_1"),
15821 ..lsp::CompletionItem::default()
15822 },
15823 lsp::CompletionItem {
15824 label: "single line label 1".to_string(),
15825 detail: Some(multiline_detail.to_string()),
15826 text_edit: gen_text_edit(¶ms, "new_text_2"),
15827 ..lsp::CompletionItem::default()
15828 },
15829 lsp::CompletionItem {
15830 label: "single line label 2".to_string(),
15831 label_details: Some(lsp::CompletionItemLabelDetails {
15832 description: Some(multiline_description.to_string()),
15833 detail: None,
15834 }),
15835 text_edit: gen_text_edit(¶ms, "new_text_2"),
15836 ..lsp::CompletionItem::default()
15837 },
15838 lsp::CompletionItem {
15839 label: multiline_label_2.to_string(),
15840 detail: Some(multiline_detail_2.to_string()),
15841 text_edit: gen_text_edit(¶ms, "new_text_3"),
15842 ..lsp::CompletionItem::default()
15843 },
15844 lsp::CompletionItem {
15845 label: "Label with many spaces and \t but without newlines".to_string(),
15846 detail: Some(
15847 "Details with many spaces and \t but without newlines".to_string(),
15848 ),
15849 text_edit: gen_text_edit(¶ms, "new_text_4"),
15850 ..lsp::CompletionItem::default()
15851 },
15852 ])))
15853 },
15854 );
15855
15856 editor.update_in(cx, |editor, window, cx| {
15857 cx.focus_self(window);
15858 editor.move_to_end(&MoveToEnd, window, cx);
15859 editor.handle_input(".", window, cx);
15860 });
15861 cx.run_until_parked();
15862 completion_handle.next().await.unwrap();
15863
15864 editor.update(cx, |editor, _| {
15865 assert!(editor.context_menu_visible());
15866 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15867 {
15868 let completion_labels = menu
15869 .completions
15870 .borrow()
15871 .iter()
15872 .map(|c| c.label.text.clone())
15873 .collect::<Vec<_>>();
15874 assert_eq!(
15875 completion_labels,
15876 &[
15877 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15878 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15879 "single line label 2 d e f ",
15880 "a b c g h i ",
15881 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15882 ],
15883 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15884 );
15885
15886 for completion in menu
15887 .completions
15888 .borrow()
15889 .iter() {
15890 assert_eq!(
15891 completion.label.filter_range,
15892 0..completion.label.text.len(),
15893 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15894 );
15895 }
15896 } else {
15897 panic!("expected completion menu to be open");
15898 }
15899 });
15900}
15901
15902#[gpui::test]
15903async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15904 init_test(cx, |_| {});
15905 let mut cx = EditorLspTestContext::new_rust(
15906 lsp::ServerCapabilities {
15907 completion_provider: Some(lsp::CompletionOptions {
15908 trigger_characters: Some(vec![".".to_string()]),
15909 ..Default::default()
15910 }),
15911 ..Default::default()
15912 },
15913 cx,
15914 )
15915 .await;
15916 cx.lsp
15917 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15918 Ok(Some(lsp::CompletionResponse::Array(vec![
15919 lsp::CompletionItem {
15920 label: "first".into(),
15921 ..Default::default()
15922 },
15923 lsp::CompletionItem {
15924 label: "last".into(),
15925 ..Default::default()
15926 },
15927 ])))
15928 });
15929 cx.set_state("variableˇ");
15930 cx.simulate_keystroke(".");
15931 cx.executor().run_until_parked();
15932
15933 cx.update_editor(|editor, _, _| {
15934 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15935 {
15936 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15937 } else {
15938 panic!("expected completion menu to be open");
15939 }
15940 });
15941
15942 cx.update_editor(|editor, window, cx| {
15943 editor.move_page_down(&MovePageDown::default(), window, cx);
15944 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15945 {
15946 assert!(
15947 menu.selected_item == 1,
15948 "expected PageDown to select the last item from the context menu"
15949 );
15950 } else {
15951 panic!("expected completion menu to stay open after PageDown");
15952 }
15953 });
15954
15955 cx.update_editor(|editor, window, cx| {
15956 editor.move_page_up(&MovePageUp::default(), window, cx);
15957 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15958 {
15959 assert!(
15960 menu.selected_item == 0,
15961 "expected PageUp to select the first item from the context menu"
15962 );
15963 } else {
15964 panic!("expected completion menu to stay open after PageUp");
15965 }
15966 });
15967}
15968
15969#[gpui::test]
15970async fn test_as_is_completions(cx: &mut TestAppContext) {
15971 init_test(cx, |_| {});
15972 let mut cx = EditorLspTestContext::new_rust(
15973 lsp::ServerCapabilities {
15974 completion_provider: Some(lsp::CompletionOptions {
15975 ..Default::default()
15976 }),
15977 ..Default::default()
15978 },
15979 cx,
15980 )
15981 .await;
15982 cx.lsp
15983 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15984 Ok(Some(lsp::CompletionResponse::Array(vec![
15985 lsp::CompletionItem {
15986 label: "unsafe".into(),
15987 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15988 range: lsp::Range {
15989 start: lsp::Position {
15990 line: 1,
15991 character: 2,
15992 },
15993 end: lsp::Position {
15994 line: 1,
15995 character: 3,
15996 },
15997 },
15998 new_text: "unsafe".to_string(),
15999 })),
16000 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
16001 ..Default::default()
16002 },
16003 ])))
16004 });
16005 cx.set_state("fn a() {}\n nˇ");
16006 cx.executor().run_until_parked();
16007 cx.update_editor(|editor, window, cx| {
16008 editor.trigger_completion_on_input("n", true, window, cx)
16009 });
16010 cx.executor().run_until_parked();
16011
16012 cx.update_editor(|editor, window, cx| {
16013 editor.confirm_completion(&Default::default(), window, cx)
16014 });
16015 cx.executor().run_until_parked();
16016 cx.assert_editor_state("fn a() {}\n unsafeˇ");
16017}
16018
16019#[gpui::test]
16020async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
16021 init_test(cx, |_| {});
16022 let language =
16023 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
16024 let mut cx = EditorLspTestContext::new(
16025 language,
16026 lsp::ServerCapabilities {
16027 completion_provider: Some(lsp::CompletionOptions {
16028 ..lsp::CompletionOptions::default()
16029 }),
16030 ..lsp::ServerCapabilities::default()
16031 },
16032 cx,
16033 )
16034 .await;
16035
16036 cx.set_state(
16037 "#ifndef BAR_H
16038#define BAR_H
16039
16040#include <stdbool.h>
16041
16042int fn_branch(bool do_branch1, bool do_branch2);
16043
16044#endif // BAR_H
16045ˇ",
16046 );
16047 cx.executor().run_until_parked();
16048 cx.update_editor(|editor, window, cx| {
16049 editor.handle_input("#", window, cx);
16050 });
16051 cx.executor().run_until_parked();
16052 cx.update_editor(|editor, window, cx| {
16053 editor.handle_input("i", window, cx);
16054 });
16055 cx.executor().run_until_parked();
16056 cx.update_editor(|editor, window, cx| {
16057 editor.handle_input("n", window, cx);
16058 });
16059 cx.executor().run_until_parked();
16060 cx.assert_editor_state(
16061 "#ifndef BAR_H
16062#define BAR_H
16063
16064#include <stdbool.h>
16065
16066int fn_branch(bool do_branch1, bool do_branch2);
16067
16068#endif // BAR_H
16069#inˇ",
16070 );
16071
16072 cx.lsp
16073 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16074 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16075 is_incomplete: false,
16076 item_defaults: None,
16077 items: vec![lsp::CompletionItem {
16078 kind: Some(lsp::CompletionItemKind::SNIPPET),
16079 label_details: Some(lsp::CompletionItemLabelDetails {
16080 detail: Some("header".to_string()),
16081 description: None,
16082 }),
16083 label: " include".to_string(),
16084 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16085 range: lsp::Range {
16086 start: lsp::Position {
16087 line: 8,
16088 character: 1,
16089 },
16090 end: lsp::Position {
16091 line: 8,
16092 character: 1,
16093 },
16094 },
16095 new_text: "include \"$0\"".to_string(),
16096 })),
16097 sort_text: Some("40b67681include".to_string()),
16098 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16099 filter_text: Some("include".to_string()),
16100 insert_text: Some("include \"$0\"".to_string()),
16101 ..lsp::CompletionItem::default()
16102 }],
16103 })))
16104 });
16105 cx.update_editor(|editor, window, cx| {
16106 editor.show_completions(&ShowCompletions, window, cx);
16107 });
16108 cx.executor().run_until_parked();
16109 cx.update_editor(|editor, window, cx| {
16110 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16111 });
16112 cx.executor().run_until_parked();
16113 cx.assert_editor_state(
16114 "#ifndef BAR_H
16115#define BAR_H
16116
16117#include <stdbool.h>
16118
16119int fn_branch(bool do_branch1, bool do_branch2);
16120
16121#endif // BAR_H
16122#include \"ˇ\"",
16123 );
16124
16125 cx.lsp
16126 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16127 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16128 is_incomplete: true,
16129 item_defaults: None,
16130 items: vec![lsp::CompletionItem {
16131 kind: Some(lsp::CompletionItemKind::FILE),
16132 label: "AGL/".to_string(),
16133 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16134 range: lsp::Range {
16135 start: lsp::Position {
16136 line: 8,
16137 character: 10,
16138 },
16139 end: lsp::Position {
16140 line: 8,
16141 character: 11,
16142 },
16143 },
16144 new_text: "AGL/".to_string(),
16145 })),
16146 sort_text: Some("40b67681AGL/".to_string()),
16147 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16148 filter_text: Some("AGL/".to_string()),
16149 insert_text: Some("AGL/".to_string()),
16150 ..lsp::CompletionItem::default()
16151 }],
16152 })))
16153 });
16154 cx.update_editor(|editor, window, cx| {
16155 editor.show_completions(&ShowCompletions, window, cx);
16156 });
16157 cx.executor().run_until_parked();
16158 cx.update_editor(|editor, window, cx| {
16159 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16160 });
16161 cx.executor().run_until_parked();
16162 cx.assert_editor_state(
16163 r##"#ifndef BAR_H
16164#define BAR_H
16165
16166#include <stdbool.h>
16167
16168int fn_branch(bool do_branch1, bool do_branch2);
16169
16170#endif // BAR_H
16171#include "AGL/ˇ"##,
16172 );
16173
16174 cx.update_editor(|editor, window, cx| {
16175 editor.handle_input("\"", window, cx);
16176 });
16177 cx.executor().run_until_parked();
16178 cx.assert_editor_state(
16179 r##"#ifndef BAR_H
16180#define BAR_H
16181
16182#include <stdbool.h>
16183
16184int fn_branch(bool do_branch1, bool do_branch2);
16185
16186#endif // BAR_H
16187#include "AGL/"ˇ"##,
16188 );
16189}
16190
16191#[gpui::test]
16192async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
16193 init_test(cx, |_| {});
16194
16195 let mut cx = EditorLspTestContext::new_rust(
16196 lsp::ServerCapabilities {
16197 completion_provider: Some(lsp::CompletionOptions {
16198 trigger_characters: Some(vec![".".to_string()]),
16199 resolve_provider: Some(true),
16200 ..Default::default()
16201 }),
16202 ..Default::default()
16203 },
16204 cx,
16205 )
16206 .await;
16207
16208 cx.set_state("fn main() { let a = 2ˇ; }");
16209 cx.simulate_keystroke(".");
16210 let completion_item = lsp::CompletionItem {
16211 label: "Some".into(),
16212 kind: Some(lsp::CompletionItemKind::SNIPPET),
16213 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16214 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16215 kind: lsp::MarkupKind::Markdown,
16216 value: "```rust\nSome(2)\n```".to_string(),
16217 })),
16218 deprecated: Some(false),
16219 sort_text: Some("Some".to_string()),
16220 filter_text: Some("Some".to_string()),
16221 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16222 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16223 range: lsp::Range {
16224 start: lsp::Position {
16225 line: 0,
16226 character: 22,
16227 },
16228 end: lsp::Position {
16229 line: 0,
16230 character: 22,
16231 },
16232 },
16233 new_text: "Some(2)".to_string(),
16234 })),
16235 additional_text_edits: Some(vec![lsp::TextEdit {
16236 range: lsp::Range {
16237 start: lsp::Position {
16238 line: 0,
16239 character: 20,
16240 },
16241 end: lsp::Position {
16242 line: 0,
16243 character: 22,
16244 },
16245 },
16246 new_text: "".to_string(),
16247 }]),
16248 ..Default::default()
16249 };
16250
16251 let closure_completion_item = completion_item.clone();
16252 let counter = Arc::new(AtomicUsize::new(0));
16253 let counter_clone = counter.clone();
16254 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16255 let task_completion_item = closure_completion_item.clone();
16256 counter_clone.fetch_add(1, atomic::Ordering::Release);
16257 async move {
16258 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16259 is_incomplete: true,
16260 item_defaults: None,
16261 items: vec![task_completion_item],
16262 })))
16263 }
16264 });
16265
16266 cx.condition(|editor, _| editor.context_menu_visible())
16267 .await;
16268 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
16269 assert!(request.next().await.is_some());
16270 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16271
16272 cx.simulate_keystrokes("S o m");
16273 cx.condition(|editor, _| editor.context_menu_visible())
16274 .await;
16275 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
16276 assert!(request.next().await.is_some());
16277 assert!(request.next().await.is_some());
16278 assert!(request.next().await.is_some());
16279 request.close();
16280 assert!(request.next().await.is_none());
16281 assert_eq!(
16282 counter.load(atomic::Ordering::Acquire),
16283 4,
16284 "With the completions menu open, only one LSP request should happen per input"
16285 );
16286}
16287
16288#[gpui::test]
16289async fn test_toggle_comment(cx: &mut TestAppContext) {
16290 init_test(cx, |_| {});
16291 let mut cx = EditorTestContext::new(cx).await;
16292 let language = Arc::new(Language::new(
16293 LanguageConfig {
16294 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16295 ..Default::default()
16296 },
16297 Some(tree_sitter_rust::LANGUAGE.into()),
16298 ));
16299 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16300
16301 // If multiple selections intersect a line, the line is only toggled once.
16302 cx.set_state(indoc! {"
16303 fn a() {
16304 «//b();
16305 ˇ»// «c();
16306 //ˇ» d();
16307 }
16308 "});
16309
16310 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16311
16312 cx.assert_editor_state(indoc! {"
16313 fn a() {
16314 «b();
16315 ˇ»«c();
16316 ˇ» d();
16317 }
16318 "});
16319
16320 // The comment prefix is inserted at the same column for every line in a
16321 // selection.
16322 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16323
16324 cx.assert_editor_state(indoc! {"
16325 fn a() {
16326 // «b();
16327 ˇ»// «c();
16328 ˇ» // d();
16329 }
16330 "});
16331
16332 // If a selection ends at the beginning of a line, that line is not toggled.
16333 cx.set_selections_state(indoc! {"
16334 fn a() {
16335 // b();
16336 «// c();
16337 ˇ» // d();
16338 }
16339 "});
16340
16341 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16342
16343 cx.assert_editor_state(indoc! {"
16344 fn a() {
16345 // b();
16346 «c();
16347 ˇ» // d();
16348 }
16349 "});
16350
16351 // If a selection span a single line and is empty, the line is toggled.
16352 cx.set_state(indoc! {"
16353 fn a() {
16354 a();
16355 b();
16356 ˇ
16357 }
16358 "});
16359
16360 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16361
16362 cx.assert_editor_state(indoc! {"
16363 fn a() {
16364 a();
16365 b();
16366 //•ˇ
16367 }
16368 "});
16369
16370 // If a selection span multiple lines, empty lines are not toggled.
16371 cx.set_state(indoc! {"
16372 fn a() {
16373 «a();
16374
16375 c();ˇ»
16376 }
16377 "});
16378
16379 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16380
16381 cx.assert_editor_state(indoc! {"
16382 fn a() {
16383 // «a();
16384
16385 // c();ˇ»
16386 }
16387 "});
16388
16389 // If a selection includes multiple comment prefixes, all lines are uncommented.
16390 cx.set_state(indoc! {"
16391 fn a() {
16392 «// a();
16393 /// b();
16394 //! c();ˇ»
16395 }
16396 "});
16397
16398 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16399
16400 cx.assert_editor_state(indoc! {"
16401 fn a() {
16402 «a();
16403 b();
16404 c();ˇ»
16405 }
16406 "});
16407}
16408
16409#[gpui::test]
16410async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16411 init_test(cx, |_| {});
16412 let mut cx = EditorTestContext::new(cx).await;
16413 let language = Arc::new(Language::new(
16414 LanguageConfig {
16415 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16416 ..Default::default()
16417 },
16418 Some(tree_sitter_rust::LANGUAGE.into()),
16419 ));
16420 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16421
16422 let toggle_comments = &ToggleComments {
16423 advance_downwards: false,
16424 ignore_indent: true,
16425 };
16426
16427 // If multiple selections intersect a line, the line is only toggled once.
16428 cx.set_state(indoc! {"
16429 fn a() {
16430 // «b();
16431 // c();
16432 // ˇ» d();
16433 }
16434 "});
16435
16436 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16437
16438 cx.assert_editor_state(indoc! {"
16439 fn a() {
16440 «b();
16441 c();
16442 ˇ» d();
16443 }
16444 "});
16445
16446 // The comment prefix is inserted at the beginning of each line
16447 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16448
16449 cx.assert_editor_state(indoc! {"
16450 fn a() {
16451 // «b();
16452 // c();
16453 // ˇ» d();
16454 }
16455 "});
16456
16457 // If a selection ends at the beginning of a line, that line is not toggled.
16458 cx.set_selections_state(indoc! {"
16459 fn a() {
16460 // b();
16461 // «c();
16462 ˇ»// d();
16463 }
16464 "});
16465
16466 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16467
16468 cx.assert_editor_state(indoc! {"
16469 fn a() {
16470 // b();
16471 «c();
16472 ˇ»// d();
16473 }
16474 "});
16475
16476 // If a selection span a single line and is empty, the line is toggled.
16477 cx.set_state(indoc! {"
16478 fn a() {
16479 a();
16480 b();
16481 ˇ
16482 }
16483 "});
16484
16485 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16486
16487 cx.assert_editor_state(indoc! {"
16488 fn a() {
16489 a();
16490 b();
16491 //ˇ
16492 }
16493 "});
16494
16495 // If a selection span multiple lines, empty lines are not toggled.
16496 cx.set_state(indoc! {"
16497 fn a() {
16498 «a();
16499
16500 c();ˇ»
16501 }
16502 "});
16503
16504 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16505
16506 cx.assert_editor_state(indoc! {"
16507 fn a() {
16508 // «a();
16509
16510 // c();ˇ»
16511 }
16512 "});
16513
16514 // If a selection includes multiple comment prefixes, all lines are uncommented.
16515 cx.set_state(indoc! {"
16516 fn a() {
16517 // «a();
16518 /// b();
16519 //! c();ˇ»
16520 }
16521 "});
16522
16523 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16524
16525 cx.assert_editor_state(indoc! {"
16526 fn a() {
16527 «a();
16528 b();
16529 c();ˇ»
16530 }
16531 "});
16532}
16533
16534#[gpui::test]
16535async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16536 init_test(cx, |_| {});
16537
16538 let language = Arc::new(Language::new(
16539 LanguageConfig {
16540 line_comments: vec!["// ".into()],
16541 ..Default::default()
16542 },
16543 Some(tree_sitter_rust::LANGUAGE.into()),
16544 ));
16545
16546 let mut cx = EditorTestContext::new(cx).await;
16547
16548 cx.language_registry().add(language.clone());
16549 cx.update_buffer(|buffer, cx| {
16550 buffer.set_language(Some(language), cx);
16551 });
16552
16553 let toggle_comments = &ToggleComments {
16554 advance_downwards: true,
16555 ignore_indent: false,
16556 };
16557
16558 // Single cursor on one line -> advance
16559 // Cursor moves horizontally 3 characters as well on non-blank line
16560 cx.set_state(indoc!(
16561 "fn a() {
16562 ˇdog();
16563 cat();
16564 }"
16565 ));
16566 cx.update_editor(|editor, window, cx| {
16567 editor.toggle_comments(toggle_comments, window, cx);
16568 });
16569 cx.assert_editor_state(indoc!(
16570 "fn a() {
16571 // dog();
16572 catˇ();
16573 }"
16574 ));
16575
16576 // Single selection on one line -> don't advance
16577 cx.set_state(indoc!(
16578 "fn a() {
16579 «dog()ˇ»;
16580 cat();
16581 }"
16582 ));
16583 cx.update_editor(|editor, window, cx| {
16584 editor.toggle_comments(toggle_comments, window, cx);
16585 });
16586 cx.assert_editor_state(indoc!(
16587 "fn a() {
16588 // «dog()ˇ»;
16589 cat();
16590 }"
16591 ));
16592
16593 // Multiple cursors on one line -> advance
16594 cx.set_state(indoc!(
16595 "fn a() {
16596 ˇdˇog();
16597 cat();
16598 }"
16599 ));
16600 cx.update_editor(|editor, window, cx| {
16601 editor.toggle_comments(toggle_comments, window, cx);
16602 });
16603 cx.assert_editor_state(indoc!(
16604 "fn a() {
16605 // dog();
16606 catˇ(ˇ);
16607 }"
16608 ));
16609
16610 // Multiple cursors on one line, with selection -> don't advance
16611 cx.set_state(indoc!(
16612 "fn a() {
16613 ˇdˇog«()ˇ»;
16614 cat();
16615 }"
16616 ));
16617 cx.update_editor(|editor, window, cx| {
16618 editor.toggle_comments(toggle_comments, window, cx);
16619 });
16620 cx.assert_editor_state(indoc!(
16621 "fn a() {
16622 // ˇdˇog«()ˇ»;
16623 cat();
16624 }"
16625 ));
16626
16627 // Single cursor on one line -> advance
16628 // Cursor moves to column 0 on blank line
16629 cx.set_state(indoc!(
16630 "fn a() {
16631 ˇdog();
16632
16633 cat();
16634 }"
16635 ));
16636 cx.update_editor(|editor, window, cx| {
16637 editor.toggle_comments(toggle_comments, window, cx);
16638 });
16639 cx.assert_editor_state(indoc!(
16640 "fn a() {
16641 // dog();
16642 ˇ
16643 cat();
16644 }"
16645 ));
16646
16647 // Single cursor on one line -> advance
16648 // Cursor starts and ends at column 0
16649 cx.set_state(indoc!(
16650 "fn a() {
16651 ˇ dog();
16652 cat();
16653 }"
16654 ));
16655 cx.update_editor(|editor, window, cx| {
16656 editor.toggle_comments(toggle_comments, window, cx);
16657 });
16658 cx.assert_editor_state(indoc!(
16659 "fn a() {
16660 // dog();
16661 ˇ cat();
16662 }"
16663 ));
16664}
16665
16666#[gpui::test]
16667async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16668 init_test(cx, |_| {});
16669
16670 let mut cx = EditorTestContext::new(cx).await;
16671
16672 let html_language = Arc::new(
16673 Language::new(
16674 LanguageConfig {
16675 name: "HTML".into(),
16676 block_comment: Some(BlockCommentConfig {
16677 start: "<!-- ".into(),
16678 prefix: "".into(),
16679 end: " -->".into(),
16680 tab_size: 0,
16681 }),
16682 ..Default::default()
16683 },
16684 Some(tree_sitter_html::LANGUAGE.into()),
16685 )
16686 .with_injection_query(
16687 r#"
16688 (script_element
16689 (raw_text) @injection.content
16690 (#set! injection.language "javascript"))
16691 "#,
16692 )
16693 .unwrap(),
16694 );
16695
16696 let javascript_language = Arc::new(Language::new(
16697 LanguageConfig {
16698 name: "JavaScript".into(),
16699 line_comments: vec!["// ".into()],
16700 ..Default::default()
16701 },
16702 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16703 ));
16704
16705 cx.language_registry().add(html_language.clone());
16706 cx.language_registry().add(javascript_language);
16707 cx.update_buffer(|buffer, cx| {
16708 buffer.set_language(Some(html_language), cx);
16709 });
16710
16711 // Toggle comments for empty selections
16712 cx.set_state(
16713 &r#"
16714 <p>A</p>ˇ
16715 <p>B</p>ˇ
16716 <p>C</p>ˇ
16717 "#
16718 .unindent(),
16719 );
16720 cx.update_editor(|editor, window, cx| {
16721 editor.toggle_comments(&ToggleComments::default(), window, cx)
16722 });
16723 cx.assert_editor_state(
16724 &r#"
16725 <!-- <p>A</p>ˇ -->
16726 <!-- <p>B</p>ˇ -->
16727 <!-- <p>C</p>ˇ -->
16728 "#
16729 .unindent(),
16730 );
16731 cx.update_editor(|editor, window, cx| {
16732 editor.toggle_comments(&ToggleComments::default(), window, cx)
16733 });
16734 cx.assert_editor_state(
16735 &r#"
16736 <p>A</p>ˇ
16737 <p>B</p>ˇ
16738 <p>C</p>ˇ
16739 "#
16740 .unindent(),
16741 );
16742
16743 // Toggle comments for mixture of empty and non-empty selections, where
16744 // multiple selections occupy a given line.
16745 cx.set_state(
16746 &r#"
16747 <p>A«</p>
16748 <p>ˇ»B</p>ˇ
16749 <p>C«</p>
16750 <p>ˇ»D</p>ˇ
16751 "#
16752 .unindent(),
16753 );
16754
16755 cx.update_editor(|editor, window, cx| {
16756 editor.toggle_comments(&ToggleComments::default(), window, cx)
16757 });
16758 cx.assert_editor_state(
16759 &r#"
16760 <!-- <p>A«</p>
16761 <p>ˇ»B</p>ˇ -->
16762 <!-- <p>C«</p>
16763 <p>ˇ»D</p>ˇ -->
16764 "#
16765 .unindent(),
16766 );
16767 cx.update_editor(|editor, window, cx| {
16768 editor.toggle_comments(&ToggleComments::default(), window, cx)
16769 });
16770 cx.assert_editor_state(
16771 &r#"
16772 <p>A«</p>
16773 <p>ˇ»B</p>ˇ
16774 <p>C«</p>
16775 <p>ˇ»D</p>ˇ
16776 "#
16777 .unindent(),
16778 );
16779
16780 // Toggle comments when different languages are active for different
16781 // selections.
16782 cx.set_state(
16783 &r#"
16784 ˇ<script>
16785 ˇvar x = new Y();
16786 ˇ</script>
16787 "#
16788 .unindent(),
16789 );
16790 cx.executor().run_until_parked();
16791 cx.update_editor(|editor, window, cx| {
16792 editor.toggle_comments(&ToggleComments::default(), window, cx)
16793 });
16794 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16795 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16796 cx.assert_editor_state(
16797 &r#"
16798 <!-- ˇ<script> -->
16799 // ˇvar x = new Y();
16800 <!-- ˇ</script> -->
16801 "#
16802 .unindent(),
16803 );
16804}
16805
16806#[gpui::test]
16807fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16808 init_test(cx, |_| {});
16809
16810 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16811 let multibuffer = cx.new(|cx| {
16812 let mut multibuffer = MultiBuffer::new(ReadWrite);
16813 multibuffer.push_excerpts(
16814 buffer.clone(),
16815 [
16816 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16817 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16818 ],
16819 cx,
16820 );
16821 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16822 multibuffer
16823 });
16824
16825 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16826 editor.update_in(cx, |editor, window, cx| {
16827 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16828 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16829 s.select_ranges([
16830 Point::new(0, 0)..Point::new(0, 0),
16831 Point::new(1, 0)..Point::new(1, 0),
16832 ])
16833 });
16834
16835 editor.handle_input("X", window, cx);
16836 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16837 assert_eq!(
16838 editor.selections.ranges(&editor.display_snapshot(cx)),
16839 [
16840 Point::new(0, 1)..Point::new(0, 1),
16841 Point::new(1, 1)..Point::new(1, 1),
16842 ]
16843 );
16844
16845 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16846 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16847 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16848 });
16849 editor.backspace(&Default::default(), window, cx);
16850 assert_eq!(editor.text(cx), "Xa\nbbb");
16851 assert_eq!(
16852 editor.selections.ranges(&editor.display_snapshot(cx)),
16853 [Point::new(1, 0)..Point::new(1, 0)]
16854 );
16855
16856 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16857 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16858 });
16859 editor.backspace(&Default::default(), window, cx);
16860 assert_eq!(editor.text(cx), "X\nbb");
16861 assert_eq!(
16862 editor.selections.ranges(&editor.display_snapshot(cx)),
16863 [Point::new(0, 1)..Point::new(0, 1)]
16864 );
16865 });
16866}
16867
16868#[gpui::test]
16869fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16870 init_test(cx, |_| {});
16871
16872 let markers = vec![('[', ']').into(), ('(', ')').into()];
16873 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16874 indoc! {"
16875 [aaaa
16876 (bbbb]
16877 cccc)",
16878 },
16879 markers.clone(),
16880 );
16881 let excerpt_ranges = markers.into_iter().map(|marker| {
16882 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16883 ExcerptRange::new(context)
16884 });
16885 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16886 let multibuffer = cx.new(|cx| {
16887 let mut multibuffer = MultiBuffer::new(ReadWrite);
16888 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16889 multibuffer
16890 });
16891
16892 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16893 editor.update_in(cx, |editor, window, cx| {
16894 let (expected_text, selection_ranges) = marked_text_ranges(
16895 indoc! {"
16896 aaaa
16897 bˇbbb
16898 bˇbbˇb
16899 cccc"
16900 },
16901 true,
16902 );
16903 assert_eq!(editor.text(cx), expected_text);
16904 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16905 s.select_ranges(
16906 selection_ranges
16907 .iter()
16908 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
16909 )
16910 });
16911
16912 editor.handle_input("X", window, cx);
16913
16914 let (expected_text, expected_selections) = marked_text_ranges(
16915 indoc! {"
16916 aaaa
16917 bXˇbbXb
16918 bXˇbbXˇb
16919 cccc"
16920 },
16921 false,
16922 );
16923 assert_eq!(editor.text(cx), expected_text);
16924 assert_eq!(
16925 editor.selections.ranges(&editor.display_snapshot(cx)),
16926 expected_selections
16927 .iter()
16928 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16929 .collect::<Vec<_>>()
16930 );
16931
16932 editor.newline(&Newline, window, cx);
16933 let (expected_text, expected_selections) = marked_text_ranges(
16934 indoc! {"
16935 aaaa
16936 bX
16937 ˇbbX
16938 b
16939 bX
16940 ˇbbX
16941 ˇb
16942 cccc"
16943 },
16944 false,
16945 );
16946 assert_eq!(editor.text(cx), expected_text);
16947 assert_eq!(
16948 editor.selections.ranges(&editor.display_snapshot(cx)),
16949 expected_selections
16950 .iter()
16951 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16952 .collect::<Vec<_>>()
16953 );
16954 });
16955}
16956
16957#[gpui::test]
16958fn test_refresh_selections(cx: &mut TestAppContext) {
16959 init_test(cx, |_| {});
16960
16961 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16962 let mut excerpt1_id = None;
16963 let multibuffer = cx.new(|cx| {
16964 let mut multibuffer = MultiBuffer::new(ReadWrite);
16965 excerpt1_id = multibuffer
16966 .push_excerpts(
16967 buffer.clone(),
16968 [
16969 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16970 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16971 ],
16972 cx,
16973 )
16974 .into_iter()
16975 .next();
16976 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16977 multibuffer
16978 });
16979
16980 let editor = cx.add_window(|window, cx| {
16981 let mut editor = build_editor(multibuffer.clone(), window, cx);
16982 let snapshot = editor.snapshot(window, cx);
16983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16984 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16985 });
16986 editor.begin_selection(
16987 Point::new(2, 1).to_display_point(&snapshot),
16988 true,
16989 1,
16990 window,
16991 cx,
16992 );
16993 assert_eq!(
16994 editor.selections.ranges(&editor.display_snapshot(cx)),
16995 [
16996 Point::new(1, 3)..Point::new(1, 3),
16997 Point::new(2, 1)..Point::new(2, 1),
16998 ]
16999 );
17000 editor
17001 });
17002
17003 // Refreshing selections is a no-op when excerpts haven't changed.
17004 _ = editor.update(cx, |editor, window, cx| {
17005 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17006 assert_eq!(
17007 editor.selections.ranges(&editor.display_snapshot(cx)),
17008 [
17009 Point::new(1, 3)..Point::new(1, 3),
17010 Point::new(2, 1)..Point::new(2, 1),
17011 ]
17012 );
17013 });
17014
17015 multibuffer.update(cx, |multibuffer, cx| {
17016 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17017 });
17018 _ = editor.update(cx, |editor, window, cx| {
17019 // Removing an excerpt causes the first selection to become degenerate.
17020 assert_eq!(
17021 editor.selections.ranges(&editor.display_snapshot(cx)),
17022 [
17023 Point::new(0, 0)..Point::new(0, 0),
17024 Point::new(0, 1)..Point::new(0, 1)
17025 ]
17026 );
17027
17028 // Refreshing selections will relocate the first selection to the original buffer
17029 // location.
17030 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17031 assert_eq!(
17032 editor.selections.ranges(&editor.display_snapshot(cx)),
17033 [
17034 Point::new(0, 1)..Point::new(0, 1),
17035 Point::new(0, 3)..Point::new(0, 3)
17036 ]
17037 );
17038 assert!(editor.selections.pending_anchor().is_some());
17039 });
17040}
17041
17042#[gpui::test]
17043fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
17044 init_test(cx, |_| {});
17045
17046 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17047 let mut excerpt1_id = None;
17048 let multibuffer = cx.new(|cx| {
17049 let mut multibuffer = MultiBuffer::new(ReadWrite);
17050 excerpt1_id = multibuffer
17051 .push_excerpts(
17052 buffer.clone(),
17053 [
17054 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17055 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17056 ],
17057 cx,
17058 )
17059 .into_iter()
17060 .next();
17061 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17062 multibuffer
17063 });
17064
17065 let editor = cx.add_window(|window, cx| {
17066 let mut editor = build_editor(multibuffer.clone(), window, cx);
17067 let snapshot = editor.snapshot(window, cx);
17068 editor.begin_selection(
17069 Point::new(1, 3).to_display_point(&snapshot),
17070 false,
17071 1,
17072 window,
17073 cx,
17074 );
17075 assert_eq!(
17076 editor.selections.ranges(&editor.display_snapshot(cx)),
17077 [Point::new(1, 3)..Point::new(1, 3)]
17078 );
17079 editor
17080 });
17081
17082 multibuffer.update(cx, |multibuffer, cx| {
17083 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17084 });
17085 _ = editor.update(cx, |editor, window, cx| {
17086 assert_eq!(
17087 editor.selections.ranges(&editor.display_snapshot(cx)),
17088 [Point::new(0, 0)..Point::new(0, 0)]
17089 );
17090
17091 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
17092 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17093 assert_eq!(
17094 editor.selections.ranges(&editor.display_snapshot(cx)),
17095 [Point::new(0, 3)..Point::new(0, 3)]
17096 );
17097 assert!(editor.selections.pending_anchor().is_some());
17098 });
17099}
17100
17101#[gpui::test]
17102async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
17103 init_test(cx, |_| {});
17104
17105 let language = Arc::new(
17106 Language::new(
17107 LanguageConfig {
17108 brackets: BracketPairConfig {
17109 pairs: vec![
17110 BracketPair {
17111 start: "{".to_string(),
17112 end: "}".to_string(),
17113 close: true,
17114 surround: true,
17115 newline: true,
17116 },
17117 BracketPair {
17118 start: "/* ".to_string(),
17119 end: " */".to_string(),
17120 close: true,
17121 surround: true,
17122 newline: true,
17123 },
17124 ],
17125 ..Default::default()
17126 },
17127 ..Default::default()
17128 },
17129 Some(tree_sitter_rust::LANGUAGE.into()),
17130 )
17131 .with_indents_query("")
17132 .unwrap(),
17133 );
17134
17135 let text = concat!(
17136 "{ }\n", //
17137 " x\n", //
17138 " /* */\n", //
17139 "x\n", //
17140 "{{} }\n", //
17141 );
17142
17143 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17144 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17145 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17146 editor
17147 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
17148 .await;
17149
17150 editor.update_in(cx, |editor, window, cx| {
17151 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17152 s.select_display_ranges([
17153 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
17154 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
17155 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
17156 ])
17157 });
17158 editor.newline(&Newline, window, cx);
17159
17160 assert_eq!(
17161 editor.buffer().read(cx).read(cx).text(),
17162 concat!(
17163 "{ \n", // Suppress rustfmt
17164 "\n", //
17165 "}\n", //
17166 " x\n", //
17167 " /* \n", //
17168 " \n", //
17169 " */\n", //
17170 "x\n", //
17171 "{{} \n", //
17172 "}\n", //
17173 )
17174 );
17175 });
17176}
17177
17178#[gpui::test]
17179fn test_highlighted_ranges(cx: &mut TestAppContext) {
17180 init_test(cx, |_| {});
17181
17182 let editor = cx.add_window(|window, cx| {
17183 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
17184 build_editor(buffer, window, cx)
17185 });
17186
17187 _ = editor.update(cx, |editor, window, cx| {
17188 struct Type1;
17189 struct Type2;
17190
17191 let buffer = editor.buffer.read(cx).snapshot(cx);
17192
17193 let anchor_range =
17194 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
17195
17196 editor.highlight_background::<Type1>(
17197 &[
17198 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
17199 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
17200 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
17201 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
17202 ],
17203 |_, _| Hsla::red(),
17204 cx,
17205 );
17206 editor.highlight_background::<Type2>(
17207 &[
17208 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
17209 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
17210 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
17211 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
17212 ],
17213 |_, _| Hsla::green(),
17214 cx,
17215 );
17216
17217 let snapshot = editor.snapshot(window, cx);
17218 let highlighted_ranges = editor.sorted_background_highlights_in_range(
17219 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
17220 &snapshot,
17221 cx.theme(),
17222 );
17223 assert_eq!(
17224 highlighted_ranges,
17225 &[
17226 (
17227 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
17228 Hsla::green(),
17229 ),
17230 (
17231 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
17232 Hsla::red(),
17233 ),
17234 (
17235 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
17236 Hsla::green(),
17237 ),
17238 (
17239 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17240 Hsla::red(),
17241 ),
17242 ]
17243 );
17244 assert_eq!(
17245 editor.sorted_background_highlights_in_range(
17246 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
17247 &snapshot,
17248 cx.theme(),
17249 ),
17250 &[(
17251 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17252 Hsla::red(),
17253 )]
17254 );
17255 });
17256}
17257
17258#[gpui::test]
17259async fn test_following(cx: &mut TestAppContext) {
17260 init_test(cx, |_| {});
17261
17262 let fs = FakeFs::new(cx.executor());
17263 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17264
17265 let buffer = project.update(cx, |project, cx| {
17266 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
17267 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
17268 });
17269 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17270 let follower = cx.update(|cx| {
17271 cx.open_window(
17272 WindowOptions {
17273 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
17274 gpui::Point::new(px(0.), px(0.)),
17275 gpui::Point::new(px(10.), px(80.)),
17276 ))),
17277 ..Default::default()
17278 },
17279 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
17280 )
17281 .unwrap()
17282 });
17283
17284 let is_still_following = Rc::new(RefCell::new(true));
17285 let follower_edit_event_count = Rc::new(RefCell::new(0));
17286 let pending_update = Rc::new(RefCell::new(None));
17287 let leader_entity = leader.root(cx).unwrap();
17288 let follower_entity = follower.root(cx).unwrap();
17289 _ = follower.update(cx, {
17290 let update = pending_update.clone();
17291 let is_still_following = is_still_following.clone();
17292 let follower_edit_event_count = follower_edit_event_count.clone();
17293 |_, window, cx| {
17294 cx.subscribe_in(
17295 &leader_entity,
17296 window,
17297 move |_, leader, event, window, cx| {
17298 leader.read(cx).add_event_to_update_proto(
17299 event,
17300 &mut update.borrow_mut(),
17301 window,
17302 cx,
17303 );
17304 },
17305 )
17306 .detach();
17307
17308 cx.subscribe_in(
17309 &follower_entity,
17310 window,
17311 move |_, _, event: &EditorEvent, _window, _cx| {
17312 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
17313 *is_still_following.borrow_mut() = false;
17314 }
17315
17316 if let EditorEvent::BufferEdited = event {
17317 *follower_edit_event_count.borrow_mut() += 1;
17318 }
17319 },
17320 )
17321 .detach();
17322 }
17323 });
17324
17325 // Update the selections only
17326 _ = leader.update(cx, |leader, window, cx| {
17327 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17328 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17329 });
17330 });
17331 follower
17332 .update(cx, |follower, window, cx| {
17333 follower.apply_update_proto(
17334 &project,
17335 pending_update.borrow_mut().take().unwrap(),
17336 window,
17337 cx,
17338 )
17339 })
17340 .unwrap()
17341 .await
17342 .unwrap();
17343 _ = follower.update(cx, |follower, _, cx| {
17344 assert_eq!(
17345 follower.selections.ranges(&follower.display_snapshot(cx)),
17346 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
17347 );
17348 });
17349 assert!(*is_still_following.borrow());
17350 assert_eq!(*follower_edit_event_count.borrow(), 0);
17351
17352 // Update the scroll position only
17353 _ = leader.update(cx, |leader, window, cx| {
17354 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17355 });
17356 follower
17357 .update(cx, |follower, window, cx| {
17358 follower.apply_update_proto(
17359 &project,
17360 pending_update.borrow_mut().take().unwrap(),
17361 window,
17362 cx,
17363 )
17364 })
17365 .unwrap()
17366 .await
17367 .unwrap();
17368 assert_eq!(
17369 follower
17370 .update(cx, |follower, _, cx| follower.scroll_position(cx))
17371 .unwrap(),
17372 gpui::Point::new(1.5, 3.5)
17373 );
17374 assert!(*is_still_following.borrow());
17375 assert_eq!(*follower_edit_event_count.borrow(), 0);
17376
17377 // Update the selections and scroll position. The follower's scroll position is updated
17378 // via autoscroll, not via the leader's exact scroll position.
17379 _ = leader.update(cx, |leader, window, cx| {
17380 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17381 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
17382 });
17383 leader.request_autoscroll(Autoscroll::newest(), cx);
17384 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17385 });
17386 follower
17387 .update(cx, |follower, window, cx| {
17388 follower.apply_update_proto(
17389 &project,
17390 pending_update.borrow_mut().take().unwrap(),
17391 window,
17392 cx,
17393 )
17394 })
17395 .unwrap()
17396 .await
17397 .unwrap();
17398 _ = follower.update(cx, |follower, _, cx| {
17399 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17400 assert_eq!(
17401 follower.selections.ranges(&follower.display_snapshot(cx)),
17402 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17403 );
17404 });
17405 assert!(*is_still_following.borrow());
17406
17407 // Creating a pending selection that precedes another selection
17408 _ = leader.update(cx, |leader, window, cx| {
17409 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17410 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17411 });
17412 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17413 });
17414 follower
17415 .update(cx, |follower, window, cx| {
17416 follower.apply_update_proto(
17417 &project,
17418 pending_update.borrow_mut().take().unwrap(),
17419 window,
17420 cx,
17421 )
17422 })
17423 .unwrap()
17424 .await
17425 .unwrap();
17426 _ = follower.update(cx, |follower, _, cx| {
17427 assert_eq!(
17428 follower.selections.ranges(&follower.display_snapshot(cx)),
17429 vec![
17430 MultiBufferOffset(0)..MultiBufferOffset(0),
17431 MultiBufferOffset(1)..MultiBufferOffset(1)
17432 ]
17433 );
17434 });
17435 assert!(*is_still_following.borrow());
17436
17437 // Extend the pending selection so that it surrounds another selection
17438 _ = leader.update(cx, |leader, window, cx| {
17439 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17440 });
17441 follower
17442 .update(cx, |follower, window, cx| {
17443 follower.apply_update_proto(
17444 &project,
17445 pending_update.borrow_mut().take().unwrap(),
17446 window,
17447 cx,
17448 )
17449 })
17450 .unwrap()
17451 .await
17452 .unwrap();
17453 _ = follower.update(cx, |follower, _, cx| {
17454 assert_eq!(
17455 follower.selections.ranges(&follower.display_snapshot(cx)),
17456 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17457 );
17458 });
17459
17460 // Scrolling locally breaks the follow
17461 _ = follower.update(cx, |follower, window, cx| {
17462 let top_anchor = follower
17463 .buffer()
17464 .read(cx)
17465 .read(cx)
17466 .anchor_after(MultiBufferOffset(0));
17467 follower.set_scroll_anchor(
17468 ScrollAnchor {
17469 anchor: top_anchor,
17470 offset: gpui::Point::new(0.0, 0.5),
17471 },
17472 window,
17473 cx,
17474 );
17475 });
17476 assert!(!(*is_still_following.borrow()));
17477}
17478
17479#[gpui::test]
17480async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17481 init_test(cx, |_| {});
17482
17483 let fs = FakeFs::new(cx.executor());
17484 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17485 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17486 let pane = workspace
17487 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17488 .unwrap();
17489
17490 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17491
17492 let leader = pane.update_in(cx, |_, window, cx| {
17493 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17494 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17495 });
17496
17497 // Start following the editor when it has no excerpts.
17498 let mut state_message =
17499 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17500 let workspace_entity = workspace.root(cx).unwrap();
17501 let follower_1 = cx
17502 .update_window(*workspace.deref(), |_, window, cx| {
17503 Editor::from_state_proto(
17504 workspace_entity,
17505 ViewId {
17506 creator: CollaboratorId::PeerId(PeerId::default()),
17507 id: 0,
17508 },
17509 &mut state_message,
17510 window,
17511 cx,
17512 )
17513 })
17514 .unwrap()
17515 .unwrap()
17516 .await
17517 .unwrap();
17518
17519 let update_message = Rc::new(RefCell::new(None));
17520 follower_1.update_in(cx, {
17521 let update = update_message.clone();
17522 |_, window, cx| {
17523 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17524 leader.read(cx).add_event_to_update_proto(
17525 event,
17526 &mut update.borrow_mut(),
17527 window,
17528 cx,
17529 );
17530 })
17531 .detach();
17532 }
17533 });
17534
17535 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17536 (
17537 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17538 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17539 )
17540 });
17541
17542 // Insert some excerpts.
17543 leader.update(cx, |leader, cx| {
17544 leader.buffer.update(cx, |multibuffer, cx| {
17545 multibuffer.set_excerpts_for_path(
17546 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17547 buffer_1.clone(),
17548 vec![
17549 Point::row_range(0..3),
17550 Point::row_range(1..6),
17551 Point::row_range(12..15),
17552 ],
17553 0,
17554 cx,
17555 );
17556 multibuffer.set_excerpts_for_path(
17557 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17558 buffer_2.clone(),
17559 vec![Point::row_range(0..6), Point::row_range(8..12)],
17560 0,
17561 cx,
17562 );
17563 });
17564 });
17565
17566 // Apply the update of adding the excerpts.
17567 follower_1
17568 .update_in(cx, |follower, window, cx| {
17569 follower.apply_update_proto(
17570 &project,
17571 update_message.borrow().clone().unwrap(),
17572 window,
17573 cx,
17574 )
17575 })
17576 .await
17577 .unwrap();
17578 assert_eq!(
17579 follower_1.update(cx, |editor, cx| editor.text(cx)),
17580 leader.update(cx, |editor, cx| editor.text(cx))
17581 );
17582 update_message.borrow_mut().take();
17583
17584 // Start following separately after it already has excerpts.
17585 let mut state_message =
17586 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17587 let workspace_entity = workspace.root(cx).unwrap();
17588 let follower_2 = cx
17589 .update_window(*workspace.deref(), |_, window, cx| {
17590 Editor::from_state_proto(
17591 workspace_entity,
17592 ViewId {
17593 creator: CollaboratorId::PeerId(PeerId::default()),
17594 id: 0,
17595 },
17596 &mut state_message,
17597 window,
17598 cx,
17599 )
17600 })
17601 .unwrap()
17602 .unwrap()
17603 .await
17604 .unwrap();
17605 assert_eq!(
17606 follower_2.update(cx, |editor, cx| editor.text(cx)),
17607 leader.update(cx, |editor, cx| editor.text(cx))
17608 );
17609
17610 // Remove some excerpts.
17611 leader.update(cx, |leader, cx| {
17612 leader.buffer.update(cx, |multibuffer, cx| {
17613 let excerpt_ids = multibuffer.excerpt_ids();
17614 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17615 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17616 });
17617 });
17618
17619 // Apply the update of removing the excerpts.
17620 follower_1
17621 .update_in(cx, |follower, window, cx| {
17622 follower.apply_update_proto(
17623 &project,
17624 update_message.borrow().clone().unwrap(),
17625 window,
17626 cx,
17627 )
17628 })
17629 .await
17630 .unwrap();
17631 follower_2
17632 .update_in(cx, |follower, window, cx| {
17633 follower.apply_update_proto(
17634 &project,
17635 update_message.borrow().clone().unwrap(),
17636 window,
17637 cx,
17638 )
17639 })
17640 .await
17641 .unwrap();
17642 update_message.borrow_mut().take();
17643 assert_eq!(
17644 follower_1.update(cx, |editor, cx| editor.text(cx)),
17645 leader.update(cx, |editor, cx| editor.text(cx))
17646 );
17647}
17648
17649#[gpui::test]
17650async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17651 init_test(cx, |_| {});
17652
17653 let mut cx = EditorTestContext::new(cx).await;
17654 let lsp_store =
17655 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17656
17657 cx.set_state(indoc! {"
17658 ˇfn func(abc def: i32) -> u32 {
17659 }
17660 "});
17661
17662 cx.update(|_, cx| {
17663 lsp_store.update(cx, |lsp_store, cx| {
17664 lsp_store
17665 .update_diagnostics(
17666 LanguageServerId(0),
17667 lsp::PublishDiagnosticsParams {
17668 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17669 version: None,
17670 diagnostics: vec![
17671 lsp::Diagnostic {
17672 range: lsp::Range::new(
17673 lsp::Position::new(0, 11),
17674 lsp::Position::new(0, 12),
17675 ),
17676 severity: Some(lsp::DiagnosticSeverity::ERROR),
17677 ..Default::default()
17678 },
17679 lsp::Diagnostic {
17680 range: lsp::Range::new(
17681 lsp::Position::new(0, 12),
17682 lsp::Position::new(0, 15),
17683 ),
17684 severity: Some(lsp::DiagnosticSeverity::ERROR),
17685 ..Default::default()
17686 },
17687 lsp::Diagnostic {
17688 range: lsp::Range::new(
17689 lsp::Position::new(0, 25),
17690 lsp::Position::new(0, 28),
17691 ),
17692 severity: Some(lsp::DiagnosticSeverity::ERROR),
17693 ..Default::default()
17694 },
17695 ],
17696 },
17697 None,
17698 DiagnosticSourceKind::Pushed,
17699 &[],
17700 cx,
17701 )
17702 .unwrap()
17703 });
17704 });
17705
17706 executor.run_until_parked();
17707
17708 cx.update_editor(|editor, window, cx| {
17709 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17710 });
17711
17712 cx.assert_editor_state(indoc! {"
17713 fn func(abc def: i32) -> ˇu32 {
17714 }
17715 "});
17716
17717 cx.update_editor(|editor, window, cx| {
17718 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17719 });
17720
17721 cx.assert_editor_state(indoc! {"
17722 fn func(abc ˇdef: i32) -> u32 {
17723 }
17724 "});
17725
17726 cx.update_editor(|editor, window, cx| {
17727 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17728 });
17729
17730 cx.assert_editor_state(indoc! {"
17731 fn func(abcˇ def: i32) -> u32 {
17732 }
17733 "});
17734
17735 cx.update_editor(|editor, window, cx| {
17736 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17737 });
17738
17739 cx.assert_editor_state(indoc! {"
17740 fn func(abc def: i32) -> ˇu32 {
17741 }
17742 "});
17743}
17744
17745#[gpui::test]
17746async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17747 init_test(cx, |_| {});
17748
17749 let mut cx = EditorTestContext::new(cx).await;
17750
17751 let diff_base = r#"
17752 use some::mod;
17753
17754 const A: u32 = 42;
17755
17756 fn main() {
17757 println!("hello");
17758
17759 println!("world");
17760 }
17761 "#
17762 .unindent();
17763
17764 // Edits are modified, removed, modified, added
17765 cx.set_state(
17766 &r#"
17767 use some::modified;
17768
17769 ˇ
17770 fn main() {
17771 println!("hello there");
17772
17773 println!("around the");
17774 println!("world");
17775 }
17776 "#
17777 .unindent(),
17778 );
17779
17780 cx.set_head_text(&diff_base);
17781 executor.run_until_parked();
17782
17783 cx.update_editor(|editor, window, cx| {
17784 //Wrap around the bottom of the buffer
17785 for _ in 0..3 {
17786 editor.go_to_next_hunk(&GoToHunk, window, cx);
17787 }
17788 });
17789
17790 cx.assert_editor_state(
17791 &r#"
17792 ˇuse some::modified;
17793
17794
17795 fn main() {
17796 println!("hello there");
17797
17798 println!("around the");
17799 println!("world");
17800 }
17801 "#
17802 .unindent(),
17803 );
17804
17805 cx.update_editor(|editor, window, cx| {
17806 //Wrap around the top of the buffer
17807 for _ in 0..2 {
17808 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17809 }
17810 });
17811
17812 cx.assert_editor_state(
17813 &r#"
17814 use some::modified;
17815
17816
17817 fn main() {
17818 ˇ println!("hello there");
17819
17820 println!("around the");
17821 println!("world");
17822 }
17823 "#
17824 .unindent(),
17825 );
17826
17827 cx.update_editor(|editor, window, cx| {
17828 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17829 });
17830
17831 cx.assert_editor_state(
17832 &r#"
17833 use some::modified;
17834
17835 ˇ
17836 fn main() {
17837 println!("hello there");
17838
17839 println!("around the");
17840 println!("world");
17841 }
17842 "#
17843 .unindent(),
17844 );
17845
17846 cx.update_editor(|editor, window, cx| {
17847 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17848 });
17849
17850 cx.assert_editor_state(
17851 &r#"
17852 ˇuse some::modified;
17853
17854
17855 fn main() {
17856 println!("hello there");
17857
17858 println!("around the");
17859 println!("world");
17860 }
17861 "#
17862 .unindent(),
17863 );
17864
17865 cx.update_editor(|editor, window, cx| {
17866 for _ in 0..2 {
17867 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17868 }
17869 });
17870
17871 cx.assert_editor_state(
17872 &r#"
17873 use some::modified;
17874
17875
17876 fn main() {
17877 ˇ println!("hello there");
17878
17879 println!("around the");
17880 println!("world");
17881 }
17882 "#
17883 .unindent(),
17884 );
17885
17886 cx.update_editor(|editor, window, cx| {
17887 editor.fold(&Fold, window, cx);
17888 });
17889
17890 cx.update_editor(|editor, window, cx| {
17891 editor.go_to_next_hunk(&GoToHunk, window, cx);
17892 });
17893
17894 cx.assert_editor_state(
17895 &r#"
17896 ˇuse some::modified;
17897
17898
17899 fn main() {
17900 println!("hello there");
17901
17902 println!("around the");
17903 println!("world");
17904 }
17905 "#
17906 .unindent(),
17907 );
17908}
17909
17910#[test]
17911fn test_split_words() {
17912 fn split(text: &str) -> Vec<&str> {
17913 split_words(text).collect()
17914 }
17915
17916 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17917 assert_eq!(split("hello_world"), &["hello_", "world"]);
17918 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17919 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17920 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17921 assert_eq!(split("helloworld"), &["helloworld"]);
17922
17923 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17924}
17925
17926#[test]
17927fn test_split_words_for_snippet_prefix() {
17928 fn split(text: &str) -> Vec<&str> {
17929 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
17930 }
17931
17932 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
17933 assert_eq!(split("hello_world"), &["hello_world"]);
17934 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
17935 assert_eq!(split("Hello_World"), &["Hello_World"]);
17936 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
17937 assert_eq!(split("helloworld"), &["helloworld"]);
17938 assert_eq!(
17939 split("this@is!@#$^many . symbols"),
17940 &[
17941 "symbols",
17942 " symbols",
17943 ". symbols",
17944 " . symbols",
17945 " . symbols",
17946 " . symbols",
17947 "many . symbols",
17948 "^many . symbols",
17949 "$^many . symbols",
17950 "#$^many . symbols",
17951 "@#$^many . symbols",
17952 "!@#$^many . symbols",
17953 "is!@#$^many . symbols",
17954 "@is!@#$^many . symbols",
17955 "this@is!@#$^many . symbols",
17956 ],
17957 );
17958 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
17959}
17960
17961#[gpui::test]
17962async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17963 init_test(cx, |_| {});
17964
17965 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17966
17967 #[track_caller]
17968 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
17969 let _state_context = cx.set_state(before);
17970 cx.run_until_parked();
17971 cx.update_editor(|editor, window, cx| {
17972 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17973 });
17974 cx.run_until_parked();
17975 cx.assert_editor_state(after);
17976 }
17977
17978 // Outside bracket jumps to outside of matching bracket
17979 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
17980 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
17981
17982 // Inside bracket jumps to inside of matching bracket
17983 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
17984 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
17985
17986 // When outside a bracket and inside, favor jumping to the inside bracket
17987 assert(
17988 "console.log('foo', [1, 2, 3]ˇ);",
17989 "console.log('foo', ˇ[1, 2, 3]);",
17990 &mut cx,
17991 );
17992 assert(
17993 "console.log(ˇ'foo', [1, 2, 3]);",
17994 "console.log('foo'ˇ, [1, 2, 3]);",
17995 &mut cx,
17996 );
17997
17998 // Bias forward if two options are equally likely
17999 assert(
18000 "let result = curried_fun()ˇ();",
18001 "let result = curried_fun()()ˇ;",
18002 &mut cx,
18003 );
18004
18005 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
18006 assert(
18007 indoc! {"
18008 function test() {
18009 console.log('test')ˇ
18010 }"},
18011 indoc! {"
18012 function test() {
18013 console.logˇ('test')
18014 }"},
18015 &mut cx,
18016 );
18017}
18018
18019#[gpui::test]
18020async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
18021 init_test(cx, |_| {});
18022 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
18023 language_registry.add(markdown_lang());
18024 language_registry.add(rust_lang());
18025 let buffer = cx.new(|cx| {
18026 let mut buffer = language::Buffer::local(
18027 indoc! {"
18028 ```rs
18029 impl Worktree {
18030 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18031 }
18032 }
18033 ```
18034 "},
18035 cx,
18036 );
18037 buffer.set_language_registry(language_registry.clone());
18038 buffer.set_language(Some(markdown_lang()), cx);
18039 buffer
18040 });
18041 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18042 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
18043 cx.executor().run_until_parked();
18044 _ = editor.update(cx, |editor, window, cx| {
18045 // Case 1: Test outer enclosing brackets
18046 select_ranges(
18047 editor,
18048 &indoc! {"
18049 ```rs
18050 impl Worktree {
18051 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18052 }
18053 }ˇ
18054 ```
18055 "},
18056 window,
18057 cx,
18058 );
18059 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18060 assert_text_with_selections(
18061 editor,
18062 &indoc! {"
18063 ```rs
18064 impl Worktree ˇ{
18065 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18066 }
18067 }
18068 ```
18069 "},
18070 cx,
18071 );
18072 // Case 2: Test inner enclosing brackets
18073 select_ranges(
18074 editor,
18075 &indoc! {"
18076 ```rs
18077 impl Worktree {
18078 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18079 }ˇ
18080 }
18081 ```
18082 "},
18083 window,
18084 cx,
18085 );
18086 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18087 assert_text_with_selections(
18088 editor,
18089 &indoc! {"
18090 ```rs
18091 impl Worktree {
18092 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
18093 }
18094 }
18095 ```
18096 "},
18097 cx,
18098 );
18099 });
18100}
18101
18102#[gpui::test]
18103async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
18104 init_test(cx, |_| {});
18105
18106 let fs = FakeFs::new(cx.executor());
18107 fs.insert_tree(
18108 path!("/a"),
18109 json!({
18110 "main.rs": "fn main() { let a = 5; }",
18111 "other.rs": "// Test file",
18112 }),
18113 )
18114 .await;
18115 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18116
18117 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18118 language_registry.add(Arc::new(Language::new(
18119 LanguageConfig {
18120 name: "Rust".into(),
18121 matcher: LanguageMatcher {
18122 path_suffixes: vec!["rs".to_string()],
18123 ..Default::default()
18124 },
18125 brackets: BracketPairConfig {
18126 pairs: vec![BracketPair {
18127 start: "{".to_string(),
18128 end: "}".to_string(),
18129 close: true,
18130 surround: true,
18131 newline: true,
18132 }],
18133 disabled_scopes_by_bracket_ix: Vec::new(),
18134 },
18135 ..Default::default()
18136 },
18137 Some(tree_sitter_rust::LANGUAGE.into()),
18138 )));
18139 let mut fake_servers = language_registry.register_fake_lsp(
18140 "Rust",
18141 FakeLspAdapter {
18142 capabilities: lsp::ServerCapabilities {
18143 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18144 first_trigger_character: "{".to_string(),
18145 more_trigger_character: None,
18146 }),
18147 ..Default::default()
18148 },
18149 ..Default::default()
18150 },
18151 );
18152
18153 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18154
18155 let cx = &mut VisualTestContext::from_window(*workspace, cx);
18156
18157 let worktree_id = workspace
18158 .update(cx, |workspace, _, cx| {
18159 workspace.project().update(cx, |project, cx| {
18160 project.worktrees(cx).next().unwrap().read(cx).id()
18161 })
18162 })
18163 .unwrap();
18164
18165 let buffer = project
18166 .update(cx, |project, cx| {
18167 project.open_local_buffer(path!("/a/main.rs"), cx)
18168 })
18169 .await
18170 .unwrap();
18171 let editor_handle = workspace
18172 .update(cx, |workspace, window, cx| {
18173 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
18174 })
18175 .unwrap()
18176 .await
18177 .unwrap()
18178 .downcast::<Editor>()
18179 .unwrap();
18180
18181 cx.executor().start_waiting();
18182 let fake_server = fake_servers.next().await.unwrap();
18183
18184 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
18185 |params, _| async move {
18186 assert_eq!(
18187 params.text_document_position.text_document.uri,
18188 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
18189 );
18190 assert_eq!(
18191 params.text_document_position.position,
18192 lsp::Position::new(0, 21),
18193 );
18194
18195 Ok(Some(vec![lsp::TextEdit {
18196 new_text: "]".to_string(),
18197 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18198 }]))
18199 },
18200 );
18201
18202 editor_handle.update_in(cx, |editor, window, cx| {
18203 window.focus(&editor.focus_handle(cx), cx);
18204 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18205 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
18206 });
18207 editor.handle_input("{", window, cx);
18208 });
18209
18210 cx.executor().run_until_parked();
18211
18212 buffer.update(cx, |buffer, _| {
18213 assert_eq!(
18214 buffer.text(),
18215 "fn main() { let a = {5}; }",
18216 "No extra braces from on type formatting should appear in the buffer"
18217 )
18218 });
18219}
18220
18221#[gpui::test(iterations = 20, seeds(31))]
18222async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
18223 init_test(cx, |_| {});
18224
18225 let mut cx = EditorLspTestContext::new_rust(
18226 lsp::ServerCapabilities {
18227 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18228 first_trigger_character: ".".to_string(),
18229 more_trigger_character: None,
18230 }),
18231 ..Default::default()
18232 },
18233 cx,
18234 )
18235 .await;
18236
18237 cx.update_buffer(|buffer, _| {
18238 // This causes autoindent to be async.
18239 buffer.set_sync_parse_timeout(Duration::ZERO)
18240 });
18241
18242 cx.set_state("fn c() {\n d()ˇ\n}\n");
18243 cx.simulate_keystroke("\n");
18244 cx.run_until_parked();
18245
18246 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
18247 let mut request =
18248 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
18249 let buffer_cloned = buffer_cloned.clone();
18250 async move {
18251 buffer_cloned.update(&mut cx, |buffer, _| {
18252 assert_eq!(
18253 buffer.text(),
18254 "fn c() {\n d()\n .\n}\n",
18255 "OnTypeFormatting should triggered after autoindent applied"
18256 )
18257 })?;
18258
18259 Ok(Some(vec![]))
18260 }
18261 });
18262
18263 cx.simulate_keystroke(".");
18264 cx.run_until_parked();
18265
18266 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
18267 assert!(request.next().await.is_some());
18268 request.close();
18269 assert!(request.next().await.is_none());
18270}
18271
18272#[gpui::test]
18273async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
18274 init_test(cx, |_| {});
18275
18276 let fs = FakeFs::new(cx.executor());
18277 fs.insert_tree(
18278 path!("/a"),
18279 json!({
18280 "main.rs": "fn main() { let a = 5; }",
18281 "other.rs": "// Test file",
18282 }),
18283 )
18284 .await;
18285
18286 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18287
18288 let server_restarts = Arc::new(AtomicUsize::new(0));
18289 let closure_restarts = Arc::clone(&server_restarts);
18290 let language_server_name = "test language server";
18291 let language_name: LanguageName = "Rust".into();
18292
18293 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18294 language_registry.add(Arc::new(Language::new(
18295 LanguageConfig {
18296 name: language_name.clone(),
18297 matcher: LanguageMatcher {
18298 path_suffixes: vec!["rs".to_string()],
18299 ..Default::default()
18300 },
18301 ..Default::default()
18302 },
18303 Some(tree_sitter_rust::LANGUAGE.into()),
18304 )));
18305 let mut fake_servers = language_registry.register_fake_lsp(
18306 "Rust",
18307 FakeLspAdapter {
18308 name: language_server_name,
18309 initialization_options: Some(json!({
18310 "testOptionValue": true
18311 })),
18312 initializer: Some(Box::new(move |fake_server| {
18313 let task_restarts = Arc::clone(&closure_restarts);
18314 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
18315 task_restarts.fetch_add(1, atomic::Ordering::Release);
18316 futures::future::ready(Ok(()))
18317 });
18318 })),
18319 ..Default::default()
18320 },
18321 );
18322
18323 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18324 let _buffer = project
18325 .update(cx, |project, cx| {
18326 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
18327 })
18328 .await
18329 .unwrap();
18330 let _fake_server = fake_servers.next().await.unwrap();
18331 update_test_language_settings(cx, |language_settings| {
18332 language_settings.languages.0.insert(
18333 language_name.clone().0,
18334 LanguageSettingsContent {
18335 tab_size: NonZeroU32::new(8),
18336 ..Default::default()
18337 },
18338 );
18339 });
18340 cx.executor().run_until_parked();
18341 assert_eq!(
18342 server_restarts.load(atomic::Ordering::Acquire),
18343 0,
18344 "Should not restart LSP server on an unrelated change"
18345 );
18346
18347 update_test_project_settings(cx, |project_settings| {
18348 project_settings.lsp.insert(
18349 "Some other server name".into(),
18350 LspSettings {
18351 binary: None,
18352 settings: None,
18353 initialization_options: Some(json!({
18354 "some other init value": false
18355 })),
18356 enable_lsp_tasks: false,
18357 fetch: None,
18358 },
18359 );
18360 });
18361 cx.executor().run_until_parked();
18362 assert_eq!(
18363 server_restarts.load(atomic::Ordering::Acquire),
18364 0,
18365 "Should not restart LSP server on an unrelated LSP settings change"
18366 );
18367
18368 update_test_project_settings(cx, |project_settings| {
18369 project_settings.lsp.insert(
18370 language_server_name.into(),
18371 LspSettings {
18372 binary: None,
18373 settings: None,
18374 initialization_options: Some(json!({
18375 "anotherInitValue": false
18376 })),
18377 enable_lsp_tasks: false,
18378 fetch: None,
18379 },
18380 );
18381 });
18382 cx.executor().run_until_parked();
18383 assert_eq!(
18384 server_restarts.load(atomic::Ordering::Acquire),
18385 1,
18386 "Should restart LSP server on a related LSP settings change"
18387 );
18388
18389 update_test_project_settings(cx, |project_settings| {
18390 project_settings.lsp.insert(
18391 language_server_name.into(),
18392 LspSettings {
18393 binary: None,
18394 settings: None,
18395 initialization_options: Some(json!({
18396 "anotherInitValue": false
18397 })),
18398 enable_lsp_tasks: false,
18399 fetch: None,
18400 },
18401 );
18402 });
18403 cx.executor().run_until_parked();
18404 assert_eq!(
18405 server_restarts.load(atomic::Ordering::Acquire),
18406 1,
18407 "Should not restart LSP server on a related LSP settings change that is the same"
18408 );
18409
18410 update_test_project_settings(cx, |project_settings| {
18411 project_settings.lsp.insert(
18412 language_server_name.into(),
18413 LspSettings {
18414 binary: None,
18415 settings: None,
18416 initialization_options: None,
18417 enable_lsp_tasks: false,
18418 fetch: None,
18419 },
18420 );
18421 });
18422 cx.executor().run_until_parked();
18423 assert_eq!(
18424 server_restarts.load(atomic::Ordering::Acquire),
18425 2,
18426 "Should restart LSP server on another related LSP settings change"
18427 );
18428}
18429
18430#[gpui::test]
18431async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18432 init_test(cx, |_| {});
18433
18434 let mut cx = EditorLspTestContext::new_rust(
18435 lsp::ServerCapabilities {
18436 completion_provider: Some(lsp::CompletionOptions {
18437 trigger_characters: Some(vec![".".to_string()]),
18438 resolve_provider: Some(true),
18439 ..Default::default()
18440 }),
18441 ..Default::default()
18442 },
18443 cx,
18444 )
18445 .await;
18446
18447 cx.set_state("fn main() { let a = 2ˇ; }");
18448 cx.simulate_keystroke(".");
18449 let completion_item = lsp::CompletionItem {
18450 label: "some".into(),
18451 kind: Some(lsp::CompletionItemKind::SNIPPET),
18452 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18453 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18454 kind: lsp::MarkupKind::Markdown,
18455 value: "```rust\nSome(2)\n```".to_string(),
18456 })),
18457 deprecated: Some(false),
18458 sort_text: Some("fffffff2".to_string()),
18459 filter_text: Some("some".to_string()),
18460 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18461 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18462 range: lsp::Range {
18463 start: lsp::Position {
18464 line: 0,
18465 character: 22,
18466 },
18467 end: lsp::Position {
18468 line: 0,
18469 character: 22,
18470 },
18471 },
18472 new_text: "Some(2)".to_string(),
18473 })),
18474 additional_text_edits: Some(vec![lsp::TextEdit {
18475 range: lsp::Range {
18476 start: lsp::Position {
18477 line: 0,
18478 character: 20,
18479 },
18480 end: lsp::Position {
18481 line: 0,
18482 character: 22,
18483 },
18484 },
18485 new_text: "".to_string(),
18486 }]),
18487 ..Default::default()
18488 };
18489
18490 let closure_completion_item = completion_item.clone();
18491 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18492 let task_completion_item = closure_completion_item.clone();
18493 async move {
18494 Ok(Some(lsp::CompletionResponse::Array(vec![
18495 task_completion_item,
18496 ])))
18497 }
18498 });
18499
18500 request.next().await;
18501
18502 cx.condition(|editor, _| editor.context_menu_visible())
18503 .await;
18504 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18505 editor
18506 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18507 .unwrap()
18508 });
18509 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18510
18511 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18512 let task_completion_item = completion_item.clone();
18513 async move { Ok(task_completion_item) }
18514 })
18515 .next()
18516 .await
18517 .unwrap();
18518 apply_additional_edits.await.unwrap();
18519 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18520}
18521
18522#[gpui::test]
18523async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18524 init_test(cx, |_| {});
18525
18526 let mut cx = EditorLspTestContext::new_rust(
18527 lsp::ServerCapabilities {
18528 completion_provider: Some(lsp::CompletionOptions {
18529 trigger_characters: Some(vec![".".to_string()]),
18530 resolve_provider: Some(true),
18531 ..Default::default()
18532 }),
18533 ..Default::default()
18534 },
18535 cx,
18536 )
18537 .await;
18538
18539 cx.set_state("fn main() { let a = 2ˇ; }");
18540 cx.simulate_keystroke(".");
18541
18542 let item1 = lsp::CompletionItem {
18543 label: "method id()".to_string(),
18544 filter_text: Some("id".to_string()),
18545 detail: None,
18546 documentation: None,
18547 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18548 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18549 new_text: ".id".to_string(),
18550 })),
18551 ..lsp::CompletionItem::default()
18552 };
18553
18554 let item2 = lsp::CompletionItem {
18555 label: "other".to_string(),
18556 filter_text: Some("other".to_string()),
18557 detail: None,
18558 documentation: None,
18559 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18560 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18561 new_text: ".other".to_string(),
18562 })),
18563 ..lsp::CompletionItem::default()
18564 };
18565
18566 let item1 = item1.clone();
18567 cx.set_request_handler::<lsp::request::Completion, _, _>({
18568 let item1 = item1.clone();
18569 move |_, _, _| {
18570 let item1 = item1.clone();
18571 let item2 = item2.clone();
18572 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
18573 }
18574 })
18575 .next()
18576 .await;
18577
18578 cx.condition(|editor, _| editor.context_menu_visible())
18579 .await;
18580 cx.update_editor(|editor, _, _| {
18581 let context_menu = editor.context_menu.borrow_mut();
18582 let context_menu = context_menu
18583 .as_ref()
18584 .expect("Should have the context menu deployed");
18585 match context_menu {
18586 CodeContextMenu::Completions(completions_menu) => {
18587 let completions = completions_menu.completions.borrow_mut();
18588 assert_eq!(
18589 completions
18590 .iter()
18591 .map(|completion| &completion.label.text)
18592 .collect::<Vec<_>>(),
18593 vec!["method id()", "other"]
18594 )
18595 }
18596 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18597 }
18598 });
18599
18600 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
18601 let item1 = item1.clone();
18602 move |_, item_to_resolve, _| {
18603 let item1 = item1.clone();
18604 async move {
18605 if item1 == item_to_resolve {
18606 Ok(lsp::CompletionItem {
18607 label: "method id()".to_string(),
18608 filter_text: Some("id".to_string()),
18609 detail: Some("Now resolved!".to_string()),
18610 documentation: Some(lsp::Documentation::String("Docs".to_string())),
18611 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18612 range: lsp::Range::new(
18613 lsp::Position::new(0, 22),
18614 lsp::Position::new(0, 22),
18615 ),
18616 new_text: ".id".to_string(),
18617 })),
18618 ..lsp::CompletionItem::default()
18619 })
18620 } else {
18621 Ok(item_to_resolve)
18622 }
18623 }
18624 }
18625 })
18626 .next()
18627 .await
18628 .unwrap();
18629 cx.run_until_parked();
18630
18631 cx.update_editor(|editor, window, cx| {
18632 editor.context_menu_next(&Default::default(), window, cx);
18633 });
18634
18635 cx.update_editor(|editor, _, _| {
18636 let context_menu = editor.context_menu.borrow_mut();
18637 let context_menu = context_menu
18638 .as_ref()
18639 .expect("Should have the context menu deployed");
18640 match context_menu {
18641 CodeContextMenu::Completions(completions_menu) => {
18642 let completions = completions_menu.completions.borrow_mut();
18643 assert_eq!(
18644 completions
18645 .iter()
18646 .map(|completion| &completion.label.text)
18647 .collect::<Vec<_>>(),
18648 vec!["method id() Now resolved!", "other"],
18649 "Should update first completion label, but not second as the filter text did not match."
18650 );
18651 }
18652 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18653 }
18654 });
18655}
18656
18657#[gpui::test]
18658async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
18659 init_test(cx, |_| {});
18660 let mut cx = EditorLspTestContext::new_rust(
18661 lsp::ServerCapabilities {
18662 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
18663 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
18664 completion_provider: Some(lsp::CompletionOptions {
18665 resolve_provider: Some(true),
18666 ..Default::default()
18667 }),
18668 ..Default::default()
18669 },
18670 cx,
18671 )
18672 .await;
18673 cx.set_state(indoc! {"
18674 struct TestStruct {
18675 field: i32
18676 }
18677
18678 fn mainˇ() {
18679 let unused_var = 42;
18680 let test_struct = TestStruct { field: 42 };
18681 }
18682 "});
18683 let symbol_range = cx.lsp_range(indoc! {"
18684 struct TestStruct {
18685 field: i32
18686 }
18687
18688 «fn main»() {
18689 let unused_var = 42;
18690 let test_struct = TestStruct { field: 42 };
18691 }
18692 "});
18693 let mut hover_requests =
18694 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18695 Ok(Some(lsp::Hover {
18696 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18697 kind: lsp::MarkupKind::Markdown,
18698 value: "Function documentation".to_string(),
18699 }),
18700 range: Some(symbol_range),
18701 }))
18702 });
18703
18704 // Case 1: Test that code action menu hide hover popover
18705 cx.dispatch_action(Hover);
18706 hover_requests.next().await;
18707 cx.condition(|editor, _| editor.hover_state.visible()).await;
18708 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18709 move |_, _, _| async move {
18710 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18711 lsp::CodeAction {
18712 title: "Remove unused variable".to_string(),
18713 kind: Some(CodeActionKind::QUICKFIX),
18714 edit: Some(lsp::WorkspaceEdit {
18715 changes: Some(
18716 [(
18717 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18718 vec![lsp::TextEdit {
18719 range: lsp::Range::new(
18720 lsp::Position::new(5, 4),
18721 lsp::Position::new(5, 27),
18722 ),
18723 new_text: "".to_string(),
18724 }],
18725 )]
18726 .into_iter()
18727 .collect(),
18728 ),
18729 ..Default::default()
18730 }),
18731 ..Default::default()
18732 },
18733 )]))
18734 },
18735 );
18736 cx.update_editor(|editor, window, cx| {
18737 editor.toggle_code_actions(
18738 &ToggleCodeActions {
18739 deployed_from: None,
18740 quick_launch: false,
18741 },
18742 window,
18743 cx,
18744 );
18745 });
18746 code_action_requests.next().await;
18747 cx.run_until_parked();
18748 cx.condition(|editor, _| editor.context_menu_visible())
18749 .await;
18750 cx.update_editor(|editor, _, _| {
18751 assert!(
18752 !editor.hover_state.visible(),
18753 "Hover popover should be hidden when code action menu is shown"
18754 );
18755 // Hide code actions
18756 editor.context_menu.take();
18757 });
18758
18759 // Case 2: Test that code completions hide hover popover
18760 cx.dispatch_action(Hover);
18761 hover_requests.next().await;
18762 cx.condition(|editor, _| editor.hover_state.visible()).await;
18763 let counter = Arc::new(AtomicUsize::new(0));
18764 let mut completion_requests =
18765 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18766 let counter = counter.clone();
18767 async move {
18768 counter.fetch_add(1, atomic::Ordering::Release);
18769 Ok(Some(lsp::CompletionResponse::Array(vec![
18770 lsp::CompletionItem {
18771 label: "main".into(),
18772 kind: Some(lsp::CompletionItemKind::FUNCTION),
18773 detail: Some("() -> ()".to_string()),
18774 ..Default::default()
18775 },
18776 lsp::CompletionItem {
18777 label: "TestStruct".into(),
18778 kind: Some(lsp::CompletionItemKind::STRUCT),
18779 detail: Some("struct TestStruct".to_string()),
18780 ..Default::default()
18781 },
18782 ])))
18783 }
18784 });
18785 cx.update_editor(|editor, window, cx| {
18786 editor.show_completions(&ShowCompletions, window, cx);
18787 });
18788 completion_requests.next().await;
18789 cx.condition(|editor, _| editor.context_menu_visible())
18790 .await;
18791 cx.update_editor(|editor, _, _| {
18792 assert!(
18793 !editor.hover_state.visible(),
18794 "Hover popover should be hidden when completion menu is shown"
18795 );
18796 });
18797}
18798
18799#[gpui::test]
18800async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18801 init_test(cx, |_| {});
18802
18803 let mut cx = EditorLspTestContext::new_rust(
18804 lsp::ServerCapabilities {
18805 completion_provider: Some(lsp::CompletionOptions {
18806 trigger_characters: Some(vec![".".to_string()]),
18807 resolve_provider: Some(true),
18808 ..Default::default()
18809 }),
18810 ..Default::default()
18811 },
18812 cx,
18813 )
18814 .await;
18815
18816 cx.set_state("fn main() { let a = 2ˇ; }");
18817 cx.simulate_keystroke(".");
18818
18819 let unresolved_item_1 = lsp::CompletionItem {
18820 label: "id".to_string(),
18821 filter_text: Some("id".to_string()),
18822 detail: None,
18823 documentation: None,
18824 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18825 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18826 new_text: ".id".to_string(),
18827 })),
18828 ..lsp::CompletionItem::default()
18829 };
18830 let resolved_item_1 = lsp::CompletionItem {
18831 additional_text_edits: Some(vec![lsp::TextEdit {
18832 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18833 new_text: "!!".to_string(),
18834 }]),
18835 ..unresolved_item_1.clone()
18836 };
18837 let unresolved_item_2 = lsp::CompletionItem {
18838 label: "other".to_string(),
18839 filter_text: Some("other".to_string()),
18840 detail: None,
18841 documentation: None,
18842 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18843 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18844 new_text: ".other".to_string(),
18845 })),
18846 ..lsp::CompletionItem::default()
18847 };
18848 let resolved_item_2 = lsp::CompletionItem {
18849 additional_text_edits: Some(vec![lsp::TextEdit {
18850 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18851 new_text: "??".to_string(),
18852 }]),
18853 ..unresolved_item_2.clone()
18854 };
18855
18856 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18857 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18858 cx.lsp
18859 .server
18860 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18861 let unresolved_item_1 = unresolved_item_1.clone();
18862 let resolved_item_1 = resolved_item_1.clone();
18863 let unresolved_item_2 = unresolved_item_2.clone();
18864 let resolved_item_2 = resolved_item_2.clone();
18865 let resolve_requests_1 = resolve_requests_1.clone();
18866 let resolve_requests_2 = resolve_requests_2.clone();
18867 move |unresolved_request, _| {
18868 let unresolved_item_1 = unresolved_item_1.clone();
18869 let resolved_item_1 = resolved_item_1.clone();
18870 let unresolved_item_2 = unresolved_item_2.clone();
18871 let resolved_item_2 = resolved_item_2.clone();
18872 let resolve_requests_1 = resolve_requests_1.clone();
18873 let resolve_requests_2 = resolve_requests_2.clone();
18874 async move {
18875 if unresolved_request == unresolved_item_1 {
18876 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18877 Ok(resolved_item_1.clone())
18878 } else if unresolved_request == unresolved_item_2 {
18879 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18880 Ok(resolved_item_2.clone())
18881 } else {
18882 panic!("Unexpected completion item {unresolved_request:?}")
18883 }
18884 }
18885 }
18886 })
18887 .detach();
18888
18889 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18890 let unresolved_item_1 = unresolved_item_1.clone();
18891 let unresolved_item_2 = unresolved_item_2.clone();
18892 async move {
18893 Ok(Some(lsp::CompletionResponse::Array(vec![
18894 unresolved_item_1,
18895 unresolved_item_2,
18896 ])))
18897 }
18898 })
18899 .next()
18900 .await;
18901
18902 cx.condition(|editor, _| editor.context_menu_visible())
18903 .await;
18904 cx.update_editor(|editor, _, _| {
18905 let context_menu = editor.context_menu.borrow_mut();
18906 let context_menu = context_menu
18907 .as_ref()
18908 .expect("Should have the context menu deployed");
18909 match context_menu {
18910 CodeContextMenu::Completions(completions_menu) => {
18911 let completions = completions_menu.completions.borrow_mut();
18912 assert_eq!(
18913 completions
18914 .iter()
18915 .map(|completion| &completion.label.text)
18916 .collect::<Vec<_>>(),
18917 vec!["id", "other"]
18918 )
18919 }
18920 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18921 }
18922 });
18923 cx.run_until_parked();
18924
18925 cx.update_editor(|editor, window, cx| {
18926 editor.context_menu_next(&ContextMenuNext, window, cx);
18927 });
18928 cx.run_until_parked();
18929 cx.update_editor(|editor, window, cx| {
18930 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18931 });
18932 cx.run_until_parked();
18933 cx.update_editor(|editor, window, cx| {
18934 editor.context_menu_next(&ContextMenuNext, window, cx);
18935 });
18936 cx.run_until_parked();
18937 cx.update_editor(|editor, window, cx| {
18938 editor
18939 .compose_completion(&ComposeCompletion::default(), window, cx)
18940 .expect("No task returned")
18941 })
18942 .await
18943 .expect("Completion failed");
18944 cx.run_until_parked();
18945
18946 cx.update_editor(|editor, _, cx| {
18947 assert_eq!(
18948 resolve_requests_1.load(atomic::Ordering::Acquire),
18949 1,
18950 "Should always resolve once despite multiple selections"
18951 );
18952 assert_eq!(
18953 resolve_requests_2.load(atomic::Ordering::Acquire),
18954 1,
18955 "Should always resolve once after multiple selections and applying the completion"
18956 );
18957 assert_eq!(
18958 editor.text(cx),
18959 "fn main() { let a = ??.other; }",
18960 "Should use resolved data when applying the completion"
18961 );
18962 });
18963}
18964
18965#[gpui::test]
18966async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18967 init_test(cx, |_| {});
18968
18969 let item_0 = lsp::CompletionItem {
18970 label: "abs".into(),
18971 insert_text: Some("abs".into()),
18972 data: Some(json!({ "very": "special"})),
18973 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18974 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18975 lsp::InsertReplaceEdit {
18976 new_text: "abs".to_string(),
18977 insert: lsp::Range::default(),
18978 replace: lsp::Range::default(),
18979 },
18980 )),
18981 ..lsp::CompletionItem::default()
18982 };
18983 let items = iter::once(item_0.clone())
18984 .chain((11..51).map(|i| lsp::CompletionItem {
18985 label: format!("item_{}", i),
18986 insert_text: Some(format!("item_{}", i)),
18987 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18988 ..lsp::CompletionItem::default()
18989 }))
18990 .collect::<Vec<_>>();
18991
18992 let default_commit_characters = vec!["?".to_string()];
18993 let default_data = json!({ "default": "data"});
18994 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18995 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18996 let default_edit_range = lsp::Range {
18997 start: lsp::Position {
18998 line: 0,
18999 character: 5,
19000 },
19001 end: lsp::Position {
19002 line: 0,
19003 character: 5,
19004 },
19005 };
19006
19007 let mut cx = EditorLspTestContext::new_rust(
19008 lsp::ServerCapabilities {
19009 completion_provider: Some(lsp::CompletionOptions {
19010 trigger_characters: Some(vec![".".to_string()]),
19011 resolve_provider: Some(true),
19012 ..Default::default()
19013 }),
19014 ..Default::default()
19015 },
19016 cx,
19017 )
19018 .await;
19019
19020 cx.set_state("fn main() { let a = 2ˇ; }");
19021 cx.simulate_keystroke(".");
19022
19023 let completion_data = default_data.clone();
19024 let completion_characters = default_commit_characters.clone();
19025 let completion_items = items.clone();
19026 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19027 let default_data = completion_data.clone();
19028 let default_commit_characters = completion_characters.clone();
19029 let items = completion_items.clone();
19030 async move {
19031 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
19032 items,
19033 item_defaults: Some(lsp::CompletionListItemDefaults {
19034 data: Some(default_data.clone()),
19035 commit_characters: Some(default_commit_characters.clone()),
19036 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
19037 default_edit_range,
19038 )),
19039 insert_text_format: Some(default_insert_text_format),
19040 insert_text_mode: Some(default_insert_text_mode),
19041 }),
19042 ..lsp::CompletionList::default()
19043 })))
19044 }
19045 })
19046 .next()
19047 .await;
19048
19049 let resolved_items = Arc::new(Mutex::new(Vec::new()));
19050 cx.lsp
19051 .server
19052 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19053 let closure_resolved_items = resolved_items.clone();
19054 move |item_to_resolve, _| {
19055 let closure_resolved_items = closure_resolved_items.clone();
19056 async move {
19057 closure_resolved_items.lock().push(item_to_resolve.clone());
19058 Ok(item_to_resolve)
19059 }
19060 }
19061 })
19062 .detach();
19063
19064 cx.condition(|editor, _| editor.context_menu_visible())
19065 .await;
19066 cx.run_until_parked();
19067 cx.update_editor(|editor, _, _| {
19068 let menu = editor.context_menu.borrow_mut();
19069 match menu.as_ref().expect("should have the completions menu") {
19070 CodeContextMenu::Completions(completions_menu) => {
19071 assert_eq!(
19072 completions_menu
19073 .entries
19074 .borrow()
19075 .iter()
19076 .map(|mat| mat.string.clone())
19077 .collect::<Vec<String>>(),
19078 items
19079 .iter()
19080 .map(|completion| completion.label.clone())
19081 .collect::<Vec<String>>()
19082 );
19083 }
19084 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
19085 }
19086 });
19087 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
19088 // with 4 from the end.
19089 assert_eq!(
19090 *resolved_items.lock(),
19091 [&items[0..16], &items[items.len() - 4..items.len()]]
19092 .concat()
19093 .iter()
19094 .cloned()
19095 .map(|mut item| {
19096 if item.data.is_none() {
19097 item.data = Some(default_data.clone());
19098 }
19099 item
19100 })
19101 .collect::<Vec<lsp::CompletionItem>>(),
19102 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
19103 );
19104 resolved_items.lock().clear();
19105
19106 cx.update_editor(|editor, window, cx| {
19107 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19108 });
19109 cx.run_until_parked();
19110 // Completions that have already been resolved are skipped.
19111 assert_eq!(
19112 *resolved_items.lock(),
19113 items[items.len() - 17..items.len() - 4]
19114 .iter()
19115 .cloned()
19116 .map(|mut item| {
19117 if item.data.is_none() {
19118 item.data = Some(default_data.clone());
19119 }
19120 item
19121 })
19122 .collect::<Vec<lsp::CompletionItem>>()
19123 );
19124 resolved_items.lock().clear();
19125}
19126
19127#[gpui::test]
19128async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
19129 init_test(cx, |_| {});
19130
19131 let mut cx = EditorLspTestContext::new(
19132 Language::new(
19133 LanguageConfig {
19134 matcher: LanguageMatcher {
19135 path_suffixes: vec!["jsx".into()],
19136 ..Default::default()
19137 },
19138 overrides: [(
19139 "element".into(),
19140 LanguageConfigOverride {
19141 completion_query_characters: Override::Set(['-'].into_iter().collect()),
19142 ..Default::default()
19143 },
19144 )]
19145 .into_iter()
19146 .collect(),
19147 ..Default::default()
19148 },
19149 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
19150 )
19151 .with_override_query("(jsx_self_closing_element) @element")
19152 .unwrap(),
19153 lsp::ServerCapabilities {
19154 completion_provider: Some(lsp::CompletionOptions {
19155 trigger_characters: Some(vec![":".to_string()]),
19156 ..Default::default()
19157 }),
19158 ..Default::default()
19159 },
19160 cx,
19161 )
19162 .await;
19163
19164 cx.lsp
19165 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19166 Ok(Some(lsp::CompletionResponse::Array(vec![
19167 lsp::CompletionItem {
19168 label: "bg-blue".into(),
19169 ..Default::default()
19170 },
19171 lsp::CompletionItem {
19172 label: "bg-red".into(),
19173 ..Default::default()
19174 },
19175 lsp::CompletionItem {
19176 label: "bg-yellow".into(),
19177 ..Default::default()
19178 },
19179 ])))
19180 });
19181
19182 cx.set_state(r#"<p class="bgˇ" />"#);
19183
19184 // Trigger completion when typing a dash, because the dash is an extra
19185 // word character in the 'element' scope, which contains the cursor.
19186 cx.simulate_keystroke("-");
19187 cx.executor().run_until_parked();
19188 cx.update_editor(|editor, _, _| {
19189 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19190 {
19191 assert_eq!(
19192 completion_menu_entries(menu),
19193 &["bg-blue", "bg-red", "bg-yellow"]
19194 );
19195 } else {
19196 panic!("expected completion menu to be open");
19197 }
19198 });
19199
19200 cx.simulate_keystroke("l");
19201 cx.executor().run_until_parked();
19202 cx.update_editor(|editor, _, _| {
19203 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19204 {
19205 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
19206 } else {
19207 panic!("expected completion menu to be open");
19208 }
19209 });
19210
19211 // When filtering completions, consider the character after the '-' to
19212 // be the start of a subword.
19213 cx.set_state(r#"<p class="yelˇ" />"#);
19214 cx.simulate_keystroke("l");
19215 cx.executor().run_until_parked();
19216 cx.update_editor(|editor, _, _| {
19217 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19218 {
19219 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
19220 } else {
19221 panic!("expected completion menu to be open");
19222 }
19223 });
19224}
19225
19226fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
19227 let entries = menu.entries.borrow();
19228 entries.iter().map(|mat| mat.string.clone()).collect()
19229}
19230
19231#[gpui::test]
19232async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
19233 init_test(cx, |settings| {
19234 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19235 });
19236
19237 let fs = FakeFs::new(cx.executor());
19238 fs.insert_file(path!("/file.ts"), Default::default()).await;
19239
19240 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
19241 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19242
19243 language_registry.add(Arc::new(Language::new(
19244 LanguageConfig {
19245 name: "TypeScript".into(),
19246 matcher: LanguageMatcher {
19247 path_suffixes: vec!["ts".to_string()],
19248 ..Default::default()
19249 },
19250 ..Default::default()
19251 },
19252 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19253 )));
19254 update_test_language_settings(cx, |settings| {
19255 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19256 });
19257
19258 let test_plugin = "test_plugin";
19259 let _ = language_registry.register_fake_lsp(
19260 "TypeScript",
19261 FakeLspAdapter {
19262 prettier_plugins: vec![test_plugin],
19263 ..Default::default()
19264 },
19265 );
19266
19267 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19268 let buffer = project
19269 .update(cx, |project, cx| {
19270 project.open_local_buffer(path!("/file.ts"), cx)
19271 })
19272 .await
19273 .unwrap();
19274
19275 let buffer_text = "one\ntwo\nthree\n";
19276 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19277 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19278 editor.update_in(cx, |editor, window, cx| {
19279 editor.set_text(buffer_text, window, cx)
19280 });
19281
19282 editor
19283 .update_in(cx, |editor, window, cx| {
19284 editor.perform_format(
19285 project.clone(),
19286 FormatTrigger::Manual,
19287 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19288 window,
19289 cx,
19290 )
19291 })
19292 .unwrap()
19293 .await;
19294 assert_eq!(
19295 editor.update(cx, |editor, cx| editor.text(cx)),
19296 buffer_text.to_string() + prettier_format_suffix,
19297 "Test prettier formatting was not applied to the original buffer text",
19298 );
19299
19300 update_test_language_settings(cx, |settings| {
19301 settings.defaults.formatter = Some(FormatterList::default())
19302 });
19303 let format = editor.update_in(cx, |editor, window, cx| {
19304 editor.perform_format(
19305 project.clone(),
19306 FormatTrigger::Manual,
19307 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19308 window,
19309 cx,
19310 )
19311 });
19312 format.await.unwrap();
19313 assert_eq!(
19314 editor.update(cx, |editor, cx| editor.text(cx)),
19315 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
19316 "Autoformatting (via test prettier) was not applied to the original buffer text",
19317 );
19318}
19319
19320#[gpui::test]
19321async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
19322 init_test(cx, |settings| {
19323 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19324 });
19325
19326 let fs = FakeFs::new(cx.executor());
19327 fs.insert_file(path!("/file.settings"), Default::default())
19328 .await;
19329
19330 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
19331 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19332
19333 let ts_lang = Arc::new(Language::new(
19334 LanguageConfig {
19335 name: "TypeScript".into(),
19336 matcher: LanguageMatcher {
19337 path_suffixes: vec!["ts".to_string()],
19338 ..LanguageMatcher::default()
19339 },
19340 prettier_parser_name: Some("typescript".to_string()),
19341 ..LanguageConfig::default()
19342 },
19343 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19344 ));
19345
19346 language_registry.add(ts_lang.clone());
19347
19348 update_test_language_settings(cx, |settings| {
19349 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19350 });
19351
19352 let test_plugin = "test_plugin";
19353 let _ = language_registry.register_fake_lsp(
19354 "TypeScript",
19355 FakeLspAdapter {
19356 prettier_plugins: vec![test_plugin],
19357 ..Default::default()
19358 },
19359 );
19360
19361 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19362 let buffer = project
19363 .update(cx, |project, cx| {
19364 project.open_local_buffer(path!("/file.settings"), cx)
19365 })
19366 .await
19367 .unwrap();
19368
19369 project.update(cx, |project, cx| {
19370 project.set_language_for_buffer(&buffer, ts_lang, cx)
19371 });
19372
19373 let buffer_text = "one\ntwo\nthree\n";
19374 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19375 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19376 editor.update_in(cx, |editor, window, cx| {
19377 editor.set_text(buffer_text, window, cx)
19378 });
19379
19380 editor
19381 .update_in(cx, |editor, window, cx| {
19382 editor.perform_format(
19383 project.clone(),
19384 FormatTrigger::Manual,
19385 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19386 window,
19387 cx,
19388 )
19389 })
19390 .unwrap()
19391 .await;
19392 assert_eq!(
19393 editor.update(cx, |editor, cx| editor.text(cx)),
19394 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
19395 "Test prettier formatting was not applied to the original buffer text",
19396 );
19397
19398 update_test_language_settings(cx, |settings| {
19399 settings.defaults.formatter = Some(FormatterList::default())
19400 });
19401 let format = editor.update_in(cx, |editor, window, cx| {
19402 editor.perform_format(
19403 project.clone(),
19404 FormatTrigger::Manual,
19405 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19406 window,
19407 cx,
19408 )
19409 });
19410 format.await.unwrap();
19411
19412 assert_eq!(
19413 editor.update(cx, |editor, cx| editor.text(cx)),
19414 buffer_text.to_string()
19415 + prettier_format_suffix
19416 + "\ntypescript\n"
19417 + prettier_format_suffix
19418 + "\ntypescript",
19419 "Autoformatting (via test prettier) was not applied to the original buffer text",
19420 );
19421}
19422
19423#[gpui::test]
19424async fn test_addition_reverts(cx: &mut TestAppContext) {
19425 init_test(cx, |_| {});
19426 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19427 let base_text = indoc! {r#"
19428 struct Row;
19429 struct Row1;
19430 struct Row2;
19431
19432 struct Row4;
19433 struct Row5;
19434 struct Row6;
19435
19436 struct Row8;
19437 struct Row9;
19438 struct Row10;"#};
19439
19440 // When addition hunks are not adjacent to carets, no hunk revert is performed
19441 assert_hunk_revert(
19442 indoc! {r#"struct Row;
19443 struct Row1;
19444 struct Row1.1;
19445 struct Row1.2;
19446 struct Row2;ˇ
19447
19448 struct Row4;
19449 struct Row5;
19450 struct Row6;
19451
19452 struct Row8;
19453 ˇstruct Row9;
19454 struct Row9.1;
19455 struct Row9.2;
19456 struct Row9.3;
19457 struct Row10;"#},
19458 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19459 indoc! {r#"struct Row;
19460 struct Row1;
19461 struct Row1.1;
19462 struct Row1.2;
19463 struct Row2;ˇ
19464
19465 struct Row4;
19466 struct Row5;
19467 struct Row6;
19468
19469 struct Row8;
19470 ˇstruct Row9;
19471 struct Row9.1;
19472 struct Row9.2;
19473 struct Row9.3;
19474 struct Row10;"#},
19475 base_text,
19476 &mut cx,
19477 );
19478 // Same for selections
19479 assert_hunk_revert(
19480 indoc! {r#"struct Row;
19481 struct Row1;
19482 struct Row2;
19483 struct Row2.1;
19484 struct Row2.2;
19485 «ˇ
19486 struct Row4;
19487 struct» Row5;
19488 «struct Row6;
19489 ˇ»
19490 struct Row9.1;
19491 struct Row9.2;
19492 struct Row9.3;
19493 struct Row8;
19494 struct Row9;
19495 struct Row10;"#},
19496 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19497 indoc! {r#"struct Row;
19498 struct Row1;
19499 struct Row2;
19500 struct Row2.1;
19501 struct Row2.2;
19502 «ˇ
19503 struct Row4;
19504 struct» Row5;
19505 «struct Row6;
19506 ˇ»
19507 struct Row9.1;
19508 struct Row9.2;
19509 struct Row9.3;
19510 struct Row8;
19511 struct Row9;
19512 struct Row10;"#},
19513 base_text,
19514 &mut cx,
19515 );
19516
19517 // When carets and selections intersect the addition hunks, those are reverted.
19518 // Adjacent carets got merged.
19519 assert_hunk_revert(
19520 indoc! {r#"struct Row;
19521 ˇ// something on the top
19522 struct Row1;
19523 struct Row2;
19524 struct Roˇw3.1;
19525 struct Row2.2;
19526 struct Row2.3;ˇ
19527
19528 struct Row4;
19529 struct ˇRow5.1;
19530 struct Row5.2;
19531 struct «Rowˇ»5.3;
19532 struct Row5;
19533 struct Row6;
19534 ˇ
19535 struct Row9.1;
19536 struct «Rowˇ»9.2;
19537 struct «ˇRow»9.3;
19538 struct Row8;
19539 struct Row9;
19540 «ˇ// something on bottom»
19541 struct Row10;"#},
19542 vec![
19543 DiffHunkStatusKind::Added,
19544 DiffHunkStatusKind::Added,
19545 DiffHunkStatusKind::Added,
19546 DiffHunkStatusKind::Added,
19547 DiffHunkStatusKind::Added,
19548 ],
19549 indoc! {r#"struct Row;
19550 ˇstruct Row1;
19551 struct Row2;
19552 ˇ
19553 struct Row4;
19554 ˇstruct Row5;
19555 struct Row6;
19556 ˇ
19557 ˇstruct Row8;
19558 struct Row9;
19559 ˇstruct Row10;"#},
19560 base_text,
19561 &mut cx,
19562 );
19563}
19564
19565#[gpui::test]
19566async fn test_modification_reverts(cx: &mut TestAppContext) {
19567 init_test(cx, |_| {});
19568 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19569 let base_text = indoc! {r#"
19570 struct Row;
19571 struct Row1;
19572 struct Row2;
19573
19574 struct Row4;
19575 struct Row5;
19576 struct Row6;
19577
19578 struct Row8;
19579 struct Row9;
19580 struct Row10;"#};
19581
19582 // Modification hunks behave the same as the addition ones.
19583 assert_hunk_revert(
19584 indoc! {r#"struct Row;
19585 struct Row1;
19586 struct Row33;
19587 ˇ
19588 struct Row4;
19589 struct Row5;
19590 struct Row6;
19591 ˇ
19592 struct Row99;
19593 struct Row9;
19594 struct Row10;"#},
19595 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19596 indoc! {r#"struct Row;
19597 struct Row1;
19598 struct Row33;
19599 ˇ
19600 struct Row4;
19601 struct Row5;
19602 struct Row6;
19603 ˇ
19604 struct Row99;
19605 struct Row9;
19606 struct Row10;"#},
19607 base_text,
19608 &mut cx,
19609 );
19610 assert_hunk_revert(
19611 indoc! {r#"struct Row;
19612 struct Row1;
19613 struct Row33;
19614 «ˇ
19615 struct Row4;
19616 struct» Row5;
19617 «struct Row6;
19618 ˇ»
19619 struct Row99;
19620 struct Row9;
19621 struct Row10;"#},
19622 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19623 indoc! {r#"struct Row;
19624 struct Row1;
19625 struct Row33;
19626 «ˇ
19627 struct Row4;
19628 struct» Row5;
19629 «struct Row6;
19630 ˇ»
19631 struct Row99;
19632 struct Row9;
19633 struct Row10;"#},
19634 base_text,
19635 &mut cx,
19636 );
19637
19638 assert_hunk_revert(
19639 indoc! {r#"ˇstruct Row1.1;
19640 struct Row1;
19641 «ˇstr»uct Row22;
19642
19643 struct ˇRow44;
19644 struct Row5;
19645 struct «Rˇ»ow66;ˇ
19646
19647 «struˇ»ct Row88;
19648 struct Row9;
19649 struct Row1011;ˇ"#},
19650 vec![
19651 DiffHunkStatusKind::Modified,
19652 DiffHunkStatusKind::Modified,
19653 DiffHunkStatusKind::Modified,
19654 DiffHunkStatusKind::Modified,
19655 DiffHunkStatusKind::Modified,
19656 DiffHunkStatusKind::Modified,
19657 ],
19658 indoc! {r#"struct Row;
19659 ˇstruct Row1;
19660 struct Row2;
19661 ˇ
19662 struct Row4;
19663 ˇstruct Row5;
19664 struct Row6;
19665 ˇ
19666 struct Row8;
19667 ˇstruct Row9;
19668 struct Row10;ˇ"#},
19669 base_text,
19670 &mut cx,
19671 );
19672}
19673
19674#[gpui::test]
19675async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
19676 init_test(cx, |_| {});
19677 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19678 let base_text = indoc! {r#"
19679 one
19680
19681 two
19682 three
19683 "#};
19684
19685 cx.set_head_text(base_text);
19686 cx.set_state("\nˇ\n");
19687 cx.executor().run_until_parked();
19688 cx.update_editor(|editor, _window, cx| {
19689 editor.expand_selected_diff_hunks(cx);
19690 });
19691 cx.executor().run_until_parked();
19692 cx.update_editor(|editor, window, cx| {
19693 editor.backspace(&Default::default(), window, cx);
19694 });
19695 cx.run_until_parked();
19696 cx.assert_state_with_diff(
19697 indoc! {r#"
19698
19699 - two
19700 - threeˇ
19701 +
19702 "#}
19703 .to_string(),
19704 );
19705}
19706
19707#[gpui::test]
19708async fn test_deletion_reverts(cx: &mut TestAppContext) {
19709 init_test(cx, |_| {});
19710 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19711 let base_text = indoc! {r#"struct Row;
19712struct Row1;
19713struct Row2;
19714
19715struct Row4;
19716struct Row5;
19717struct Row6;
19718
19719struct Row8;
19720struct Row9;
19721struct Row10;"#};
19722
19723 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
19724 assert_hunk_revert(
19725 indoc! {r#"struct Row;
19726 struct Row2;
19727
19728 ˇstruct Row4;
19729 struct Row5;
19730 struct Row6;
19731 ˇ
19732 struct Row8;
19733 struct Row10;"#},
19734 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19735 indoc! {r#"struct Row;
19736 struct Row2;
19737
19738 ˇstruct Row4;
19739 struct Row5;
19740 struct Row6;
19741 ˇ
19742 struct Row8;
19743 struct Row10;"#},
19744 base_text,
19745 &mut cx,
19746 );
19747 assert_hunk_revert(
19748 indoc! {r#"struct Row;
19749 struct Row2;
19750
19751 «ˇstruct Row4;
19752 struct» Row5;
19753 «struct Row6;
19754 ˇ»
19755 struct Row8;
19756 struct Row10;"#},
19757 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19758 indoc! {r#"struct Row;
19759 struct Row2;
19760
19761 «ˇstruct Row4;
19762 struct» Row5;
19763 «struct Row6;
19764 ˇ»
19765 struct Row8;
19766 struct Row10;"#},
19767 base_text,
19768 &mut cx,
19769 );
19770
19771 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
19772 assert_hunk_revert(
19773 indoc! {r#"struct Row;
19774 ˇstruct Row2;
19775
19776 struct Row4;
19777 struct Row5;
19778 struct Row6;
19779
19780 struct Row8;ˇ
19781 struct Row10;"#},
19782 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19783 indoc! {r#"struct Row;
19784 struct Row1;
19785 ˇstruct Row2;
19786
19787 struct Row4;
19788 struct Row5;
19789 struct Row6;
19790
19791 struct Row8;ˇ
19792 struct Row9;
19793 struct Row10;"#},
19794 base_text,
19795 &mut cx,
19796 );
19797 assert_hunk_revert(
19798 indoc! {r#"struct Row;
19799 struct Row2«ˇ;
19800 struct Row4;
19801 struct» Row5;
19802 «struct Row6;
19803
19804 struct Row8;ˇ»
19805 struct Row10;"#},
19806 vec![
19807 DiffHunkStatusKind::Deleted,
19808 DiffHunkStatusKind::Deleted,
19809 DiffHunkStatusKind::Deleted,
19810 ],
19811 indoc! {r#"struct Row;
19812 struct Row1;
19813 struct Row2«ˇ;
19814
19815 struct Row4;
19816 struct» Row5;
19817 «struct Row6;
19818
19819 struct Row8;ˇ»
19820 struct Row9;
19821 struct Row10;"#},
19822 base_text,
19823 &mut cx,
19824 );
19825}
19826
19827#[gpui::test]
19828async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
19829 init_test(cx, |_| {});
19830
19831 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
19832 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
19833 let base_text_3 =
19834 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
19835
19836 let text_1 = edit_first_char_of_every_line(base_text_1);
19837 let text_2 = edit_first_char_of_every_line(base_text_2);
19838 let text_3 = edit_first_char_of_every_line(base_text_3);
19839
19840 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19841 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19842 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19843
19844 let multibuffer = cx.new(|cx| {
19845 let mut multibuffer = MultiBuffer::new(ReadWrite);
19846 multibuffer.push_excerpts(
19847 buffer_1.clone(),
19848 [
19849 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19850 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19851 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19852 ],
19853 cx,
19854 );
19855 multibuffer.push_excerpts(
19856 buffer_2.clone(),
19857 [
19858 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19859 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19860 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19861 ],
19862 cx,
19863 );
19864 multibuffer.push_excerpts(
19865 buffer_3.clone(),
19866 [
19867 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19868 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19869 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19870 ],
19871 cx,
19872 );
19873 multibuffer
19874 });
19875
19876 let fs = FakeFs::new(cx.executor());
19877 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19878 let (editor, cx) = cx
19879 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19880 editor.update_in(cx, |editor, _window, cx| {
19881 for (buffer, diff_base) in [
19882 (buffer_1.clone(), base_text_1),
19883 (buffer_2.clone(), base_text_2),
19884 (buffer_3.clone(), base_text_3),
19885 ] {
19886 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19887 editor
19888 .buffer
19889 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19890 }
19891 });
19892 cx.executor().run_until_parked();
19893
19894 editor.update_in(cx, |editor, window, cx| {
19895 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}");
19896 editor.select_all(&SelectAll, window, cx);
19897 editor.git_restore(&Default::default(), window, cx);
19898 });
19899 cx.executor().run_until_parked();
19900
19901 // When all ranges are selected, all buffer hunks are reverted.
19902 editor.update(cx, |editor, cx| {
19903 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");
19904 });
19905 buffer_1.update(cx, |buffer, _| {
19906 assert_eq!(buffer.text(), base_text_1);
19907 });
19908 buffer_2.update(cx, |buffer, _| {
19909 assert_eq!(buffer.text(), base_text_2);
19910 });
19911 buffer_3.update(cx, |buffer, _| {
19912 assert_eq!(buffer.text(), base_text_3);
19913 });
19914
19915 editor.update_in(cx, |editor, window, cx| {
19916 editor.undo(&Default::default(), window, cx);
19917 });
19918
19919 editor.update_in(cx, |editor, window, cx| {
19920 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19921 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19922 });
19923 editor.git_restore(&Default::default(), window, cx);
19924 });
19925
19926 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19927 // but not affect buffer_2 and its related excerpts.
19928 editor.update(cx, |editor, cx| {
19929 assert_eq!(
19930 editor.text(cx),
19931 "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}"
19932 );
19933 });
19934 buffer_1.update(cx, |buffer, _| {
19935 assert_eq!(buffer.text(), base_text_1);
19936 });
19937 buffer_2.update(cx, |buffer, _| {
19938 assert_eq!(
19939 buffer.text(),
19940 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19941 );
19942 });
19943 buffer_3.update(cx, |buffer, _| {
19944 assert_eq!(
19945 buffer.text(),
19946 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19947 );
19948 });
19949
19950 fn edit_first_char_of_every_line(text: &str) -> String {
19951 text.split('\n')
19952 .map(|line| format!("X{}", &line[1..]))
19953 .collect::<Vec<_>>()
19954 .join("\n")
19955 }
19956}
19957
19958#[gpui::test]
19959async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19960 init_test(cx, |_| {});
19961
19962 let cols = 4;
19963 let rows = 10;
19964 let sample_text_1 = sample_text(rows, cols, 'a');
19965 assert_eq!(
19966 sample_text_1,
19967 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19968 );
19969 let sample_text_2 = sample_text(rows, cols, 'l');
19970 assert_eq!(
19971 sample_text_2,
19972 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19973 );
19974 let sample_text_3 = sample_text(rows, cols, 'v');
19975 assert_eq!(
19976 sample_text_3,
19977 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19978 );
19979
19980 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19981 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19982 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19983
19984 let multi_buffer = cx.new(|cx| {
19985 let mut multibuffer = MultiBuffer::new(ReadWrite);
19986 multibuffer.push_excerpts(
19987 buffer_1.clone(),
19988 [
19989 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19990 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19991 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19992 ],
19993 cx,
19994 );
19995 multibuffer.push_excerpts(
19996 buffer_2.clone(),
19997 [
19998 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19999 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20000 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20001 ],
20002 cx,
20003 );
20004 multibuffer.push_excerpts(
20005 buffer_3.clone(),
20006 [
20007 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20008 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20009 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20010 ],
20011 cx,
20012 );
20013 multibuffer
20014 });
20015
20016 let fs = FakeFs::new(cx.executor());
20017 fs.insert_tree(
20018 "/a",
20019 json!({
20020 "main.rs": sample_text_1,
20021 "other.rs": sample_text_2,
20022 "lib.rs": sample_text_3,
20023 }),
20024 )
20025 .await;
20026 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20027 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20028 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20029 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20030 Editor::new(
20031 EditorMode::full(),
20032 multi_buffer,
20033 Some(project.clone()),
20034 window,
20035 cx,
20036 )
20037 });
20038 let multibuffer_item_id = workspace
20039 .update(cx, |workspace, window, cx| {
20040 assert!(
20041 workspace.active_item(cx).is_none(),
20042 "active item should be None before the first item is added"
20043 );
20044 workspace.add_item_to_active_pane(
20045 Box::new(multi_buffer_editor.clone()),
20046 None,
20047 true,
20048 window,
20049 cx,
20050 );
20051 let active_item = workspace
20052 .active_item(cx)
20053 .expect("should have an active item after adding the multi buffer");
20054 assert_eq!(
20055 active_item.buffer_kind(cx),
20056 ItemBufferKind::Multibuffer,
20057 "A multi buffer was expected to active after adding"
20058 );
20059 active_item.item_id()
20060 })
20061 .unwrap();
20062 cx.executor().run_until_parked();
20063
20064 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20065 editor.change_selections(
20066 SelectionEffects::scroll(Autoscroll::Next),
20067 window,
20068 cx,
20069 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
20070 );
20071 editor.open_excerpts(&OpenExcerpts, window, cx);
20072 });
20073 cx.executor().run_until_parked();
20074 let first_item_id = workspace
20075 .update(cx, |workspace, window, cx| {
20076 let active_item = workspace
20077 .active_item(cx)
20078 .expect("should have an active item after navigating into the 1st buffer");
20079 let first_item_id = active_item.item_id();
20080 assert_ne!(
20081 first_item_id, multibuffer_item_id,
20082 "Should navigate into the 1st buffer and activate it"
20083 );
20084 assert_eq!(
20085 active_item.buffer_kind(cx),
20086 ItemBufferKind::Singleton,
20087 "New active item should be a singleton buffer"
20088 );
20089 assert_eq!(
20090 active_item
20091 .act_as::<Editor>(cx)
20092 .expect("should have navigated into an editor for the 1st buffer")
20093 .read(cx)
20094 .text(cx),
20095 sample_text_1
20096 );
20097
20098 workspace
20099 .go_back(workspace.active_pane().downgrade(), window, cx)
20100 .detach_and_log_err(cx);
20101
20102 first_item_id
20103 })
20104 .unwrap();
20105 cx.executor().run_until_parked();
20106 workspace
20107 .update(cx, |workspace, _, cx| {
20108 let active_item = workspace
20109 .active_item(cx)
20110 .expect("should have an active item after navigating back");
20111 assert_eq!(
20112 active_item.item_id(),
20113 multibuffer_item_id,
20114 "Should navigate back to the multi buffer"
20115 );
20116 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20117 })
20118 .unwrap();
20119
20120 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20121 editor.change_selections(
20122 SelectionEffects::scroll(Autoscroll::Next),
20123 window,
20124 cx,
20125 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
20126 );
20127 editor.open_excerpts(&OpenExcerpts, window, cx);
20128 });
20129 cx.executor().run_until_parked();
20130 let second_item_id = workspace
20131 .update(cx, |workspace, window, cx| {
20132 let active_item = workspace
20133 .active_item(cx)
20134 .expect("should have an active item after navigating into the 2nd buffer");
20135 let second_item_id = active_item.item_id();
20136 assert_ne!(
20137 second_item_id, multibuffer_item_id,
20138 "Should navigate away from the multibuffer"
20139 );
20140 assert_ne!(
20141 second_item_id, first_item_id,
20142 "Should navigate into the 2nd buffer and activate it"
20143 );
20144 assert_eq!(
20145 active_item.buffer_kind(cx),
20146 ItemBufferKind::Singleton,
20147 "New active item should be a singleton buffer"
20148 );
20149 assert_eq!(
20150 active_item
20151 .act_as::<Editor>(cx)
20152 .expect("should have navigated into an editor")
20153 .read(cx)
20154 .text(cx),
20155 sample_text_2
20156 );
20157
20158 workspace
20159 .go_back(workspace.active_pane().downgrade(), window, cx)
20160 .detach_and_log_err(cx);
20161
20162 second_item_id
20163 })
20164 .unwrap();
20165 cx.executor().run_until_parked();
20166 workspace
20167 .update(cx, |workspace, _, cx| {
20168 let active_item = workspace
20169 .active_item(cx)
20170 .expect("should have an active item after navigating back from the 2nd buffer");
20171 assert_eq!(
20172 active_item.item_id(),
20173 multibuffer_item_id,
20174 "Should navigate back from the 2nd buffer to the multi buffer"
20175 );
20176 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20177 })
20178 .unwrap();
20179
20180 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20181 editor.change_selections(
20182 SelectionEffects::scroll(Autoscroll::Next),
20183 window,
20184 cx,
20185 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
20186 );
20187 editor.open_excerpts(&OpenExcerpts, window, cx);
20188 });
20189 cx.executor().run_until_parked();
20190 workspace
20191 .update(cx, |workspace, window, cx| {
20192 let active_item = workspace
20193 .active_item(cx)
20194 .expect("should have an active item after navigating into the 3rd buffer");
20195 let third_item_id = active_item.item_id();
20196 assert_ne!(
20197 third_item_id, multibuffer_item_id,
20198 "Should navigate into the 3rd buffer and activate it"
20199 );
20200 assert_ne!(third_item_id, first_item_id);
20201 assert_ne!(third_item_id, second_item_id);
20202 assert_eq!(
20203 active_item.buffer_kind(cx),
20204 ItemBufferKind::Singleton,
20205 "New active item should be a singleton buffer"
20206 );
20207 assert_eq!(
20208 active_item
20209 .act_as::<Editor>(cx)
20210 .expect("should have navigated into an editor")
20211 .read(cx)
20212 .text(cx),
20213 sample_text_3
20214 );
20215
20216 workspace
20217 .go_back(workspace.active_pane().downgrade(), window, cx)
20218 .detach_and_log_err(cx);
20219 })
20220 .unwrap();
20221 cx.executor().run_until_parked();
20222 workspace
20223 .update(cx, |workspace, _, cx| {
20224 let active_item = workspace
20225 .active_item(cx)
20226 .expect("should have an active item after navigating back from the 3rd buffer");
20227 assert_eq!(
20228 active_item.item_id(),
20229 multibuffer_item_id,
20230 "Should navigate back from the 3rd buffer to the multi buffer"
20231 );
20232 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20233 })
20234 .unwrap();
20235}
20236
20237#[gpui::test]
20238async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20239 init_test(cx, |_| {});
20240
20241 let mut cx = EditorTestContext::new(cx).await;
20242
20243 let diff_base = r#"
20244 use some::mod;
20245
20246 const A: u32 = 42;
20247
20248 fn main() {
20249 println!("hello");
20250
20251 println!("world");
20252 }
20253 "#
20254 .unindent();
20255
20256 cx.set_state(
20257 &r#"
20258 use some::modified;
20259
20260 ˇ
20261 fn main() {
20262 println!("hello there");
20263
20264 println!("around the");
20265 println!("world");
20266 }
20267 "#
20268 .unindent(),
20269 );
20270
20271 cx.set_head_text(&diff_base);
20272 executor.run_until_parked();
20273
20274 cx.update_editor(|editor, window, cx| {
20275 editor.go_to_next_hunk(&GoToHunk, window, cx);
20276 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20277 });
20278 executor.run_until_parked();
20279 cx.assert_state_with_diff(
20280 r#"
20281 use some::modified;
20282
20283
20284 fn main() {
20285 - println!("hello");
20286 + ˇ println!("hello there");
20287
20288 println!("around the");
20289 println!("world");
20290 }
20291 "#
20292 .unindent(),
20293 );
20294
20295 cx.update_editor(|editor, window, cx| {
20296 for _ in 0..2 {
20297 editor.go_to_next_hunk(&GoToHunk, window, cx);
20298 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20299 }
20300 });
20301 executor.run_until_parked();
20302 cx.assert_state_with_diff(
20303 r#"
20304 - use some::mod;
20305 + ˇuse some::modified;
20306
20307
20308 fn main() {
20309 - println!("hello");
20310 + println!("hello there");
20311
20312 + println!("around the");
20313 println!("world");
20314 }
20315 "#
20316 .unindent(),
20317 );
20318
20319 cx.update_editor(|editor, window, cx| {
20320 editor.go_to_next_hunk(&GoToHunk, window, cx);
20321 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20322 });
20323 executor.run_until_parked();
20324 cx.assert_state_with_diff(
20325 r#"
20326 - use some::mod;
20327 + use some::modified;
20328
20329 - const A: u32 = 42;
20330 ˇ
20331 fn main() {
20332 - println!("hello");
20333 + println!("hello there");
20334
20335 + println!("around the");
20336 println!("world");
20337 }
20338 "#
20339 .unindent(),
20340 );
20341
20342 cx.update_editor(|editor, window, cx| {
20343 editor.cancel(&Cancel, window, cx);
20344 });
20345
20346 cx.assert_state_with_diff(
20347 r#"
20348 use some::modified;
20349
20350 ˇ
20351 fn main() {
20352 println!("hello there");
20353
20354 println!("around the");
20355 println!("world");
20356 }
20357 "#
20358 .unindent(),
20359 );
20360}
20361
20362#[gpui::test]
20363async fn test_diff_base_change_with_expanded_diff_hunks(
20364 executor: BackgroundExecutor,
20365 cx: &mut TestAppContext,
20366) {
20367 init_test(cx, |_| {});
20368
20369 let mut cx = EditorTestContext::new(cx).await;
20370
20371 let diff_base = r#"
20372 use some::mod1;
20373 use some::mod2;
20374
20375 const A: u32 = 42;
20376 const B: u32 = 42;
20377 const C: u32 = 42;
20378
20379 fn main() {
20380 println!("hello");
20381
20382 println!("world");
20383 }
20384 "#
20385 .unindent();
20386
20387 cx.set_state(
20388 &r#"
20389 use some::mod2;
20390
20391 const A: u32 = 42;
20392 const C: u32 = 42;
20393
20394 fn main(ˇ) {
20395 //println!("hello");
20396
20397 println!("world");
20398 //
20399 //
20400 }
20401 "#
20402 .unindent(),
20403 );
20404
20405 cx.set_head_text(&diff_base);
20406 executor.run_until_parked();
20407
20408 cx.update_editor(|editor, window, cx| {
20409 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20410 });
20411 executor.run_until_parked();
20412 cx.assert_state_with_diff(
20413 r#"
20414 - use some::mod1;
20415 use some::mod2;
20416
20417 const A: u32 = 42;
20418 - const B: u32 = 42;
20419 const C: u32 = 42;
20420
20421 fn main(ˇ) {
20422 - println!("hello");
20423 + //println!("hello");
20424
20425 println!("world");
20426 + //
20427 + //
20428 }
20429 "#
20430 .unindent(),
20431 );
20432
20433 cx.set_head_text("new diff base!");
20434 executor.run_until_parked();
20435 cx.assert_state_with_diff(
20436 r#"
20437 - new diff base!
20438 + use some::mod2;
20439 +
20440 + const A: u32 = 42;
20441 + const C: u32 = 42;
20442 +
20443 + fn main(ˇ) {
20444 + //println!("hello");
20445 +
20446 + println!("world");
20447 + //
20448 + //
20449 + }
20450 "#
20451 .unindent(),
20452 );
20453}
20454
20455#[gpui::test]
20456async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
20457 init_test(cx, |_| {});
20458
20459 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20460 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20461 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20462 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20463 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
20464 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
20465
20466 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
20467 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
20468 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
20469
20470 let multi_buffer = cx.new(|cx| {
20471 let mut multibuffer = MultiBuffer::new(ReadWrite);
20472 multibuffer.push_excerpts(
20473 buffer_1.clone(),
20474 [
20475 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20476 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20477 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20478 ],
20479 cx,
20480 );
20481 multibuffer.push_excerpts(
20482 buffer_2.clone(),
20483 [
20484 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20485 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20486 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20487 ],
20488 cx,
20489 );
20490 multibuffer.push_excerpts(
20491 buffer_3.clone(),
20492 [
20493 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20494 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20495 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20496 ],
20497 cx,
20498 );
20499 multibuffer
20500 });
20501
20502 let editor =
20503 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20504 editor
20505 .update(cx, |editor, _window, cx| {
20506 for (buffer, diff_base) in [
20507 (buffer_1.clone(), file_1_old),
20508 (buffer_2.clone(), file_2_old),
20509 (buffer_3.clone(), file_3_old),
20510 ] {
20511 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
20512 editor
20513 .buffer
20514 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20515 }
20516 })
20517 .unwrap();
20518
20519 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20520 cx.run_until_parked();
20521
20522 cx.assert_editor_state(
20523 &"
20524 ˇaaa
20525 ccc
20526 ddd
20527
20528 ggg
20529 hhh
20530
20531
20532 lll
20533 mmm
20534 NNN
20535
20536 qqq
20537 rrr
20538
20539 uuu
20540 111
20541 222
20542 333
20543
20544 666
20545 777
20546
20547 000
20548 !!!"
20549 .unindent(),
20550 );
20551
20552 cx.update_editor(|editor, window, cx| {
20553 editor.select_all(&SelectAll, window, cx);
20554 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20555 });
20556 cx.executor().run_until_parked();
20557
20558 cx.assert_state_with_diff(
20559 "
20560 «aaa
20561 - bbb
20562 ccc
20563 ddd
20564
20565 ggg
20566 hhh
20567
20568
20569 lll
20570 mmm
20571 - nnn
20572 + NNN
20573
20574 qqq
20575 rrr
20576
20577 uuu
20578 111
20579 222
20580 333
20581
20582 + 666
20583 777
20584
20585 000
20586 !!!ˇ»"
20587 .unindent(),
20588 );
20589}
20590
20591#[gpui::test]
20592async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
20593 init_test(cx, |_| {});
20594
20595 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
20596 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
20597
20598 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
20599 let multi_buffer = cx.new(|cx| {
20600 let mut multibuffer = MultiBuffer::new(ReadWrite);
20601 multibuffer.push_excerpts(
20602 buffer.clone(),
20603 [
20604 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
20605 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
20606 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
20607 ],
20608 cx,
20609 );
20610 multibuffer
20611 });
20612
20613 let editor =
20614 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20615 editor
20616 .update(cx, |editor, _window, cx| {
20617 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
20618 editor
20619 .buffer
20620 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
20621 })
20622 .unwrap();
20623
20624 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20625 cx.run_until_parked();
20626
20627 cx.update_editor(|editor, window, cx| {
20628 editor.expand_all_diff_hunks(&Default::default(), window, cx)
20629 });
20630 cx.executor().run_until_parked();
20631
20632 // When the start of a hunk coincides with the start of its excerpt,
20633 // the hunk is expanded. When the start of a hunk is earlier than
20634 // the start of its excerpt, the hunk is not expanded.
20635 cx.assert_state_with_diff(
20636 "
20637 ˇaaa
20638 - bbb
20639 + BBB
20640
20641 - ddd
20642 - eee
20643 + DDD
20644 + EEE
20645 fff
20646
20647 iii
20648 "
20649 .unindent(),
20650 );
20651}
20652
20653#[gpui::test]
20654async fn test_edits_around_expanded_insertion_hunks(
20655 executor: BackgroundExecutor,
20656 cx: &mut TestAppContext,
20657) {
20658 init_test(cx, |_| {});
20659
20660 let mut cx = EditorTestContext::new(cx).await;
20661
20662 let diff_base = r#"
20663 use some::mod1;
20664 use some::mod2;
20665
20666 const A: u32 = 42;
20667
20668 fn main() {
20669 println!("hello");
20670
20671 println!("world");
20672 }
20673 "#
20674 .unindent();
20675 executor.run_until_parked();
20676 cx.set_state(
20677 &r#"
20678 use some::mod1;
20679 use some::mod2;
20680
20681 const A: u32 = 42;
20682 const B: u32 = 42;
20683 const C: u32 = 42;
20684 ˇ
20685
20686 fn main() {
20687 println!("hello");
20688
20689 println!("world");
20690 }
20691 "#
20692 .unindent(),
20693 );
20694
20695 cx.set_head_text(&diff_base);
20696 executor.run_until_parked();
20697
20698 cx.update_editor(|editor, window, cx| {
20699 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20700 });
20701 executor.run_until_parked();
20702
20703 cx.assert_state_with_diff(
20704 r#"
20705 use some::mod1;
20706 use some::mod2;
20707
20708 const A: u32 = 42;
20709 + const B: u32 = 42;
20710 + const C: u32 = 42;
20711 + ˇ
20712
20713 fn main() {
20714 println!("hello");
20715
20716 println!("world");
20717 }
20718 "#
20719 .unindent(),
20720 );
20721
20722 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
20723 executor.run_until_parked();
20724
20725 cx.assert_state_with_diff(
20726 r#"
20727 use some::mod1;
20728 use some::mod2;
20729
20730 const A: u32 = 42;
20731 + const B: u32 = 42;
20732 + const C: u32 = 42;
20733 + const D: u32 = 42;
20734 + ˇ
20735
20736 fn main() {
20737 println!("hello");
20738
20739 println!("world");
20740 }
20741 "#
20742 .unindent(),
20743 );
20744
20745 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
20746 executor.run_until_parked();
20747
20748 cx.assert_state_with_diff(
20749 r#"
20750 use some::mod1;
20751 use some::mod2;
20752
20753 const A: u32 = 42;
20754 + const B: u32 = 42;
20755 + const C: u32 = 42;
20756 + const D: u32 = 42;
20757 + const E: u32 = 42;
20758 + ˇ
20759
20760 fn main() {
20761 println!("hello");
20762
20763 println!("world");
20764 }
20765 "#
20766 .unindent(),
20767 );
20768
20769 cx.update_editor(|editor, window, cx| {
20770 editor.delete_line(&DeleteLine, window, cx);
20771 });
20772 executor.run_until_parked();
20773
20774 cx.assert_state_with_diff(
20775 r#"
20776 use some::mod1;
20777 use some::mod2;
20778
20779 const A: u32 = 42;
20780 + const B: u32 = 42;
20781 + const C: u32 = 42;
20782 + const D: u32 = 42;
20783 + const E: u32 = 42;
20784 ˇ
20785 fn main() {
20786 println!("hello");
20787
20788 println!("world");
20789 }
20790 "#
20791 .unindent(),
20792 );
20793
20794 cx.update_editor(|editor, window, cx| {
20795 editor.move_up(&MoveUp, window, cx);
20796 editor.delete_line(&DeleteLine, window, cx);
20797 editor.move_up(&MoveUp, window, cx);
20798 editor.delete_line(&DeleteLine, window, cx);
20799 editor.move_up(&MoveUp, window, cx);
20800 editor.delete_line(&DeleteLine, window, cx);
20801 });
20802 executor.run_until_parked();
20803 cx.assert_state_with_diff(
20804 r#"
20805 use some::mod1;
20806 use some::mod2;
20807
20808 const A: u32 = 42;
20809 + const B: u32 = 42;
20810 ˇ
20811 fn main() {
20812 println!("hello");
20813
20814 println!("world");
20815 }
20816 "#
20817 .unindent(),
20818 );
20819
20820 cx.update_editor(|editor, window, cx| {
20821 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
20822 editor.delete_line(&DeleteLine, window, cx);
20823 });
20824 executor.run_until_parked();
20825 cx.assert_state_with_diff(
20826 r#"
20827 ˇ
20828 fn main() {
20829 println!("hello");
20830
20831 println!("world");
20832 }
20833 "#
20834 .unindent(),
20835 );
20836}
20837
20838#[gpui::test]
20839async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
20840 init_test(cx, |_| {});
20841
20842 let mut cx = EditorTestContext::new(cx).await;
20843 cx.set_head_text(indoc! { "
20844 one
20845 two
20846 three
20847 four
20848 five
20849 "
20850 });
20851 cx.set_state(indoc! { "
20852 one
20853 ˇthree
20854 five
20855 "});
20856 cx.run_until_parked();
20857 cx.update_editor(|editor, window, cx| {
20858 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20859 });
20860 cx.assert_state_with_diff(
20861 indoc! { "
20862 one
20863 - two
20864 ˇthree
20865 - four
20866 five
20867 "}
20868 .to_string(),
20869 );
20870 cx.update_editor(|editor, window, cx| {
20871 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20872 });
20873
20874 cx.assert_state_with_diff(
20875 indoc! { "
20876 one
20877 ˇthree
20878 five
20879 "}
20880 .to_string(),
20881 );
20882
20883 cx.update_editor(|editor, window, cx| {
20884 editor.move_up(&MoveUp, window, cx);
20885 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20886 });
20887 cx.assert_state_with_diff(
20888 indoc! { "
20889 ˇone
20890 - two
20891 three
20892 five
20893 "}
20894 .to_string(),
20895 );
20896
20897 cx.update_editor(|editor, window, cx| {
20898 editor.move_down(&MoveDown, window, cx);
20899 editor.move_down(&MoveDown, window, cx);
20900 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20901 });
20902 cx.assert_state_with_diff(
20903 indoc! { "
20904 one
20905 - two
20906 ˇthree
20907 - four
20908 five
20909 "}
20910 .to_string(),
20911 );
20912
20913 cx.set_state(indoc! { "
20914 one
20915 ˇTWO
20916 three
20917 four
20918 five
20919 "});
20920 cx.run_until_parked();
20921 cx.update_editor(|editor, window, cx| {
20922 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20923 });
20924
20925 cx.assert_state_with_diff(
20926 indoc! { "
20927 one
20928 - two
20929 + ˇTWO
20930 three
20931 four
20932 five
20933 "}
20934 .to_string(),
20935 );
20936 cx.update_editor(|editor, window, cx| {
20937 editor.move_up(&Default::default(), window, cx);
20938 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20939 });
20940 cx.assert_state_with_diff(
20941 indoc! { "
20942 one
20943 ˇTWO
20944 three
20945 four
20946 five
20947 "}
20948 .to_string(),
20949 );
20950}
20951
20952#[gpui::test]
20953async fn test_toggling_adjacent_diff_hunks_2(
20954 executor: BackgroundExecutor,
20955 cx: &mut TestAppContext,
20956) {
20957 init_test(cx, |_| {});
20958
20959 let mut cx = EditorTestContext::new(cx).await;
20960
20961 let diff_base = r#"
20962 lineA
20963 lineB
20964 lineC
20965 lineD
20966 "#
20967 .unindent();
20968
20969 cx.set_state(
20970 &r#"
20971 ˇlineA1
20972 lineB
20973 lineD
20974 "#
20975 .unindent(),
20976 );
20977 cx.set_head_text(&diff_base);
20978 executor.run_until_parked();
20979
20980 cx.update_editor(|editor, window, cx| {
20981 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20982 });
20983 executor.run_until_parked();
20984 cx.assert_state_with_diff(
20985 r#"
20986 - lineA
20987 + ˇlineA1
20988 lineB
20989 lineD
20990 "#
20991 .unindent(),
20992 );
20993
20994 cx.update_editor(|editor, window, cx| {
20995 editor.move_down(&MoveDown, window, cx);
20996 editor.move_right(&MoveRight, window, cx);
20997 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20998 });
20999 executor.run_until_parked();
21000 cx.assert_state_with_diff(
21001 r#"
21002 - lineA
21003 + lineA1
21004 lˇineB
21005 - lineC
21006 lineD
21007 "#
21008 .unindent(),
21009 );
21010}
21011
21012#[gpui::test]
21013async fn test_edits_around_expanded_deletion_hunks(
21014 executor: BackgroundExecutor,
21015 cx: &mut TestAppContext,
21016) {
21017 init_test(cx, |_| {});
21018
21019 let mut cx = EditorTestContext::new(cx).await;
21020
21021 let diff_base = r#"
21022 use some::mod1;
21023 use some::mod2;
21024
21025 const A: u32 = 42;
21026 const B: u32 = 42;
21027 const C: u32 = 42;
21028
21029
21030 fn main() {
21031 println!("hello");
21032
21033 println!("world");
21034 }
21035 "#
21036 .unindent();
21037 executor.run_until_parked();
21038 cx.set_state(
21039 &r#"
21040 use some::mod1;
21041 use some::mod2;
21042
21043 ˇconst B: u32 = 42;
21044 const C: u32 = 42;
21045
21046
21047 fn main() {
21048 println!("hello");
21049
21050 println!("world");
21051 }
21052 "#
21053 .unindent(),
21054 );
21055
21056 cx.set_head_text(&diff_base);
21057 executor.run_until_parked();
21058
21059 cx.update_editor(|editor, window, cx| {
21060 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21061 });
21062 executor.run_until_parked();
21063
21064 cx.assert_state_with_diff(
21065 r#"
21066 use some::mod1;
21067 use some::mod2;
21068
21069 - const A: u32 = 42;
21070 ˇconst B: u32 = 42;
21071 const C: u32 = 42;
21072
21073
21074 fn main() {
21075 println!("hello");
21076
21077 println!("world");
21078 }
21079 "#
21080 .unindent(),
21081 );
21082
21083 cx.update_editor(|editor, window, cx| {
21084 editor.delete_line(&DeleteLine, window, cx);
21085 });
21086 executor.run_until_parked();
21087 cx.assert_state_with_diff(
21088 r#"
21089 use some::mod1;
21090 use some::mod2;
21091
21092 - const A: u32 = 42;
21093 - const B: u32 = 42;
21094 ˇconst C: u32 = 42;
21095
21096
21097 fn main() {
21098 println!("hello");
21099
21100 println!("world");
21101 }
21102 "#
21103 .unindent(),
21104 );
21105
21106 cx.update_editor(|editor, window, cx| {
21107 editor.delete_line(&DeleteLine, window, cx);
21108 });
21109 executor.run_until_parked();
21110 cx.assert_state_with_diff(
21111 r#"
21112 use some::mod1;
21113 use some::mod2;
21114
21115 - const A: u32 = 42;
21116 - const B: u32 = 42;
21117 - const C: u32 = 42;
21118 ˇ
21119
21120 fn main() {
21121 println!("hello");
21122
21123 println!("world");
21124 }
21125 "#
21126 .unindent(),
21127 );
21128
21129 cx.update_editor(|editor, window, cx| {
21130 editor.handle_input("replacement", window, cx);
21131 });
21132 executor.run_until_parked();
21133 cx.assert_state_with_diff(
21134 r#"
21135 use some::mod1;
21136 use some::mod2;
21137
21138 - const A: u32 = 42;
21139 - const B: u32 = 42;
21140 - const C: u32 = 42;
21141 -
21142 + replacementˇ
21143
21144 fn main() {
21145 println!("hello");
21146
21147 println!("world");
21148 }
21149 "#
21150 .unindent(),
21151 );
21152}
21153
21154#[gpui::test]
21155async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21156 init_test(cx, |_| {});
21157
21158 let mut cx = EditorTestContext::new(cx).await;
21159
21160 let base_text = r#"
21161 one
21162 two
21163 three
21164 four
21165 five
21166 "#
21167 .unindent();
21168 executor.run_until_parked();
21169 cx.set_state(
21170 &r#"
21171 one
21172 two
21173 fˇour
21174 five
21175 "#
21176 .unindent(),
21177 );
21178
21179 cx.set_head_text(&base_text);
21180 executor.run_until_parked();
21181
21182 cx.update_editor(|editor, window, cx| {
21183 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21184 });
21185 executor.run_until_parked();
21186
21187 cx.assert_state_with_diff(
21188 r#"
21189 one
21190 two
21191 - three
21192 fˇour
21193 five
21194 "#
21195 .unindent(),
21196 );
21197
21198 cx.update_editor(|editor, window, cx| {
21199 editor.backspace(&Backspace, window, cx);
21200 editor.backspace(&Backspace, window, cx);
21201 });
21202 executor.run_until_parked();
21203 cx.assert_state_with_diff(
21204 r#"
21205 one
21206 two
21207 - threeˇ
21208 - four
21209 + our
21210 five
21211 "#
21212 .unindent(),
21213 );
21214}
21215
21216#[gpui::test]
21217async fn test_edit_after_expanded_modification_hunk(
21218 executor: BackgroundExecutor,
21219 cx: &mut TestAppContext,
21220) {
21221 init_test(cx, |_| {});
21222
21223 let mut cx = EditorTestContext::new(cx).await;
21224
21225 let diff_base = r#"
21226 use some::mod1;
21227 use some::mod2;
21228
21229 const A: u32 = 42;
21230 const B: u32 = 42;
21231 const C: u32 = 42;
21232 const D: u32 = 42;
21233
21234
21235 fn main() {
21236 println!("hello");
21237
21238 println!("world");
21239 }"#
21240 .unindent();
21241
21242 cx.set_state(
21243 &r#"
21244 use some::mod1;
21245 use some::mod2;
21246
21247 const A: u32 = 42;
21248 const B: u32 = 42;
21249 const C: u32 = 43ˇ
21250 const D: u32 = 42;
21251
21252
21253 fn main() {
21254 println!("hello");
21255
21256 println!("world");
21257 }"#
21258 .unindent(),
21259 );
21260
21261 cx.set_head_text(&diff_base);
21262 executor.run_until_parked();
21263 cx.update_editor(|editor, window, cx| {
21264 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21265 });
21266 executor.run_until_parked();
21267
21268 cx.assert_state_with_diff(
21269 r#"
21270 use some::mod1;
21271 use some::mod2;
21272
21273 const A: u32 = 42;
21274 const B: u32 = 42;
21275 - const C: u32 = 42;
21276 + const C: u32 = 43ˇ
21277 const D: u32 = 42;
21278
21279
21280 fn main() {
21281 println!("hello");
21282
21283 println!("world");
21284 }"#
21285 .unindent(),
21286 );
21287
21288 cx.update_editor(|editor, window, cx| {
21289 editor.handle_input("\nnew_line\n", window, cx);
21290 });
21291 executor.run_until_parked();
21292
21293 cx.assert_state_with_diff(
21294 r#"
21295 use some::mod1;
21296 use some::mod2;
21297
21298 const A: u32 = 42;
21299 const B: u32 = 42;
21300 - const C: u32 = 42;
21301 + const C: u32 = 43
21302 + new_line
21303 + ˇ
21304 const D: u32 = 42;
21305
21306
21307 fn main() {
21308 println!("hello");
21309
21310 println!("world");
21311 }"#
21312 .unindent(),
21313 );
21314}
21315
21316#[gpui::test]
21317async fn test_stage_and_unstage_added_file_hunk(
21318 executor: BackgroundExecutor,
21319 cx: &mut TestAppContext,
21320) {
21321 init_test(cx, |_| {});
21322
21323 let mut cx = EditorTestContext::new(cx).await;
21324 cx.update_editor(|editor, _, cx| {
21325 editor.set_expand_all_diff_hunks(cx);
21326 });
21327
21328 let working_copy = r#"
21329 ˇfn main() {
21330 println!("hello, world!");
21331 }
21332 "#
21333 .unindent();
21334
21335 cx.set_state(&working_copy);
21336 executor.run_until_parked();
21337
21338 cx.assert_state_with_diff(
21339 r#"
21340 + ˇfn main() {
21341 + println!("hello, world!");
21342 + }
21343 "#
21344 .unindent(),
21345 );
21346 cx.assert_index_text(None);
21347
21348 cx.update_editor(|editor, window, cx| {
21349 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21350 });
21351 executor.run_until_parked();
21352 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
21353 cx.assert_state_with_diff(
21354 r#"
21355 + ˇfn main() {
21356 + println!("hello, world!");
21357 + }
21358 "#
21359 .unindent(),
21360 );
21361
21362 cx.update_editor(|editor, window, cx| {
21363 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21364 });
21365 executor.run_until_parked();
21366 cx.assert_index_text(None);
21367}
21368
21369async fn setup_indent_guides_editor(
21370 text: &str,
21371 cx: &mut TestAppContext,
21372) -> (BufferId, EditorTestContext) {
21373 init_test(cx, |_| {});
21374
21375 let mut cx = EditorTestContext::new(cx).await;
21376
21377 let buffer_id = cx.update_editor(|editor, window, cx| {
21378 editor.set_text(text, window, cx);
21379 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
21380
21381 buffer_ids[0]
21382 });
21383
21384 (buffer_id, cx)
21385}
21386
21387fn assert_indent_guides(
21388 range: Range<u32>,
21389 expected: Vec<IndentGuide>,
21390 active_indices: Option<Vec<usize>>,
21391 cx: &mut EditorTestContext,
21392) {
21393 let indent_guides = cx.update_editor(|editor, window, cx| {
21394 let snapshot = editor.snapshot(window, cx).display_snapshot;
21395 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
21396 editor,
21397 MultiBufferRow(range.start)..MultiBufferRow(range.end),
21398 true,
21399 &snapshot,
21400 cx,
21401 );
21402
21403 indent_guides.sort_by(|a, b| {
21404 a.depth.cmp(&b.depth).then(
21405 a.start_row
21406 .cmp(&b.start_row)
21407 .then(a.end_row.cmp(&b.end_row)),
21408 )
21409 });
21410 indent_guides
21411 });
21412
21413 if let Some(expected) = active_indices {
21414 let active_indices = cx.update_editor(|editor, window, cx| {
21415 let snapshot = editor.snapshot(window, cx).display_snapshot;
21416 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
21417 });
21418
21419 assert_eq!(
21420 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
21421 expected,
21422 "Active indent guide indices do not match"
21423 );
21424 }
21425
21426 assert_eq!(indent_guides, expected, "Indent guides do not match");
21427}
21428
21429fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
21430 IndentGuide {
21431 buffer_id,
21432 start_row: MultiBufferRow(start_row),
21433 end_row: MultiBufferRow(end_row),
21434 depth,
21435 tab_size: 4,
21436 settings: IndentGuideSettings {
21437 enabled: true,
21438 line_width: 1,
21439 active_line_width: 1,
21440 coloring: IndentGuideColoring::default(),
21441 background_coloring: IndentGuideBackgroundColoring::default(),
21442 },
21443 }
21444}
21445
21446#[gpui::test]
21447async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
21448 let (buffer_id, mut cx) = setup_indent_guides_editor(
21449 &"
21450 fn main() {
21451 let a = 1;
21452 }"
21453 .unindent(),
21454 cx,
21455 )
21456 .await;
21457
21458 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21459}
21460
21461#[gpui::test]
21462async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
21463 let (buffer_id, mut cx) = setup_indent_guides_editor(
21464 &"
21465 fn main() {
21466 let a = 1;
21467 let b = 2;
21468 }"
21469 .unindent(),
21470 cx,
21471 )
21472 .await;
21473
21474 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
21475}
21476
21477#[gpui::test]
21478async fn test_indent_guide_nested(cx: &mut TestAppContext) {
21479 let (buffer_id, mut cx) = setup_indent_guides_editor(
21480 &"
21481 fn main() {
21482 let a = 1;
21483 if a == 3 {
21484 let b = 2;
21485 } else {
21486 let c = 3;
21487 }
21488 }"
21489 .unindent(),
21490 cx,
21491 )
21492 .await;
21493
21494 assert_indent_guides(
21495 0..8,
21496 vec![
21497 indent_guide(buffer_id, 1, 6, 0),
21498 indent_guide(buffer_id, 3, 3, 1),
21499 indent_guide(buffer_id, 5, 5, 1),
21500 ],
21501 None,
21502 &mut cx,
21503 );
21504}
21505
21506#[gpui::test]
21507async fn test_indent_guide_tab(cx: &mut TestAppContext) {
21508 let (buffer_id, mut cx) = setup_indent_guides_editor(
21509 &"
21510 fn main() {
21511 let a = 1;
21512 let b = 2;
21513 let c = 3;
21514 }"
21515 .unindent(),
21516 cx,
21517 )
21518 .await;
21519
21520 assert_indent_guides(
21521 0..5,
21522 vec![
21523 indent_guide(buffer_id, 1, 3, 0),
21524 indent_guide(buffer_id, 2, 2, 1),
21525 ],
21526 None,
21527 &mut cx,
21528 );
21529}
21530
21531#[gpui::test]
21532async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
21533 let (buffer_id, mut cx) = setup_indent_guides_editor(
21534 &"
21535 fn main() {
21536 let a = 1;
21537
21538 let c = 3;
21539 }"
21540 .unindent(),
21541 cx,
21542 )
21543 .await;
21544
21545 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
21546}
21547
21548#[gpui::test]
21549async fn test_indent_guide_complex(cx: &mut TestAppContext) {
21550 let (buffer_id, mut cx) = setup_indent_guides_editor(
21551 &"
21552 fn main() {
21553 let a = 1;
21554
21555 let c = 3;
21556
21557 if a == 3 {
21558 let b = 2;
21559 } else {
21560 let c = 3;
21561 }
21562 }"
21563 .unindent(),
21564 cx,
21565 )
21566 .await;
21567
21568 assert_indent_guides(
21569 0..11,
21570 vec![
21571 indent_guide(buffer_id, 1, 9, 0),
21572 indent_guide(buffer_id, 6, 6, 1),
21573 indent_guide(buffer_id, 8, 8, 1),
21574 ],
21575 None,
21576 &mut cx,
21577 );
21578}
21579
21580#[gpui::test]
21581async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
21582 let (buffer_id, mut cx) = setup_indent_guides_editor(
21583 &"
21584 fn main() {
21585 let a = 1;
21586
21587 let c = 3;
21588
21589 if a == 3 {
21590 let b = 2;
21591 } else {
21592 let c = 3;
21593 }
21594 }"
21595 .unindent(),
21596 cx,
21597 )
21598 .await;
21599
21600 assert_indent_guides(
21601 1..11,
21602 vec![
21603 indent_guide(buffer_id, 1, 9, 0),
21604 indent_guide(buffer_id, 6, 6, 1),
21605 indent_guide(buffer_id, 8, 8, 1),
21606 ],
21607 None,
21608 &mut cx,
21609 );
21610}
21611
21612#[gpui::test]
21613async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
21614 let (buffer_id, mut cx) = setup_indent_guides_editor(
21615 &"
21616 fn main() {
21617 let a = 1;
21618
21619 let c = 3;
21620
21621 if a == 3 {
21622 let b = 2;
21623 } else {
21624 let c = 3;
21625 }
21626 }"
21627 .unindent(),
21628 cx,
21629 )
21630 .await;
21631
21632 assert_indent_guides(
21633 1..10,
21634 vec![
21635 indent_guide(buffer_id, 1, 9, 0),
21636 indent_guide(buffer_id, 6, 6, 1),
21637 indent_guide(buffer_id, 8, 8, 1),
21638 ],
21639 None,
21640 &mut cx,
21641 );
21642}
21643
21644#[gpui::test]
21645async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
21646 let (buffer_id, mut cx) = setup_indent_guides_editor(
21647 &"
21648 fn main() {
21649 if a {
21650 b(
21651 c,
21652 d,
21653 )
21654 } else {
21655 e(
21656 f
21657 )
21658 }
21659 }"
21660 .unindent(),
21661 cx,
21662 )
21663 .await;
21664
21665 assert_indent_guides(
21666 0..11,
21667 vec![
21668 indent_guide(buffer_id, 1, 10, 0),
21669 indent_guide(buffer_id, 2, 5, 1),
21670 indent_guide(buffer_id, 7, 9, 1),
21671 indent_guide(buffer_id, 3, 4, 2),
21672 indent_guide(buffer_id, 8, 8, 2),
21673 ],
21674 None,
21675 &mut cx,
21676 );
21677
21678 cx.update_editor(|editor, window, cx| {
21679 editor.fold_at(MultiBufferRow(2), window, cx);
21680 assert_eq!(
21681 editor.display_text(cx),
21682 "
21683 fn main() {
21684 if a {
21685 b(⋯
21686 )
21687 } else {
21688 e(
21689 f
21690 )
21691 }
21692 }"
21693 .unindent()
21694 );
21695 });
21696
21697 assert_indent_guides(
21698 0..11,
21699 vec![
21700 indent_guide(buffer_id, 1, 10, 0),
21701 indent_guide(buffer_id, 2, 5, 1),
21702 indent_guide(buffer_id, 7, 9, 1),
21703 indent_guide(buffer_id, 8, 8, 2),
21704 ],
21705 None,
21706 &mut cx,
21707 );
21708}
21709
21710#[gpui::test]
21711async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
21712 let (buffer_id, mut cx) = setup_indent_guides_editor(
21713 &"
21714 block1
21715 block2
21716 block3
21717 block4
21718 block2
21719 block1
21720 block1"
21721 .unindent(),
21722 cx,
21723 )
21724 .await;
21725
21726 assert_indent_guides(
21727 1..10,
21728 vec![
21729 indent_guide(buffer_id, 1, 4, 0),
21730 indent_guide(buffer_id, 2, 3, 1),
21731 indent_guide(buffer_id, 3, 3, 2),
21732 ],
21733 None,
21734 &mut cx,
21735 );
21736}
21737
21738#[gpui::test]
21739async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
21740 let (buffer_id, mut cx) = setup_indent_guides_editor(
21741 &"
21742 block1
21743 block2
21744 block3
21745
21746 block1
21747 block1"
21748 .unindent(),
21749 cx,
21750 )
21751 .await;
21752
21753 assert_indent_guides(
21754 0..6,
21755 vec![
21756 indent_guide(buffer_id, 1, 2, 0),
21757 indent_guide(buffer_id, 2, 2, 1),
21758 ],
21759 None,
21760 &mut cx,
21761 );
21762}
21763
21764#[gpui::test]
21765async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
21766 let (buffer_id, mut cx) = setup_indent_guides_editor(
21767 &"
21768 function component() {
21769 \treturn (
21770 \t\t\t
21771 \t\t<div>
21772 \t\t\t<abc></abc>
21773 \t\t</div>
21774 \t)
21775 }"
21776 .unindent(),
21777 cx,
21778 )
21779 .await;
21780
21781 assert_indent_guides(
21782 0..8,
21783 vec![
21784 indent_guide(buffer_id, 1, 6, 0),
21785 indent_guide(buffer_id, 2, 5, 1),
21786 indent_guide(buffer_id, 4, 4, 2),
21787 ],
21788 None,
21789 &mut cx,
21790 );
21791}
21792
21793#[gpui::test]
21794async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
21795 let (buffer_id, mut cx) = setup_indent_guides_editor(
21796 &"
21797 function component() {
21798 \treturn (
21799 \t
21800 \t\t<div>
21801 \t\t\t<abc></abc>
21802 \t\t</div>
21803 \t)
21804 }"
21805 .unindent(),
21806 cx,
21807 )
21808 .await;
21809
21810 assert_indent_guides(
21811 0..8,
21812 vec![
21813 indent_guide(buffer_id, 1, 6, 0),
21814 indent_guide(buffer_id, 2, 5, 1),
21815 indent_guide(buffer_id, 4, 4, 2),
21816 ],
21817 None,
21818 &mut cx,
21819 );
21820}
21821
21822#[gpui::test]
21823async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
21824 let (buffer_id, mut cx) = setup_indent_guides_editor(
21825 &"
21826 block1
21827
21828
21829
21830 block2
21831 "
21832 .unindent(),
21833 cx,
21834 )
21835 .await;
21836
21837 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21838}
21839
21840#[gpui::test]
21841async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
21842 let (buffer_id, mut cx) = setup_indent_guides_editor(
21843 &"
21844 def a:
21845 \tb = 3
21846 \tif True:
21847 \t\tc = 4
21848 \t\td = 5
21849 \tprint(b)
21850 "
21851 .unindent(),
21852 cx,
21853 )
21854 .await;
21855
21856 assert_indent_guides(
21857 0..6,
21858 vec![
21859 indent_guide(buffer_id, 1, 5, 0),
21860 indent_guide(buffer_id, 3, 4, 1),
21861 ],
21862 None,
21863 &mut cx,
21864 );
21865}
21866
21867#[gpui::test]
21868async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
21869 let (buffer_id, mut cx) = setup_indent_guides_editor(
21870 &"
21871 fn main() {
21872 let a = 1;
21873 }"
21874 .unindent(),
21875 cx,
21876 )
21877 .await;
21878
21879 cx.update_editor(|editor, window, cx| {
21880 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21881 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21882 });
21883 });
21884
21885 assert_indent_guides(
21886 0..3,
21887 vec![indent_guide(buffer_id, 1, 1, 0)],
21888 Some(vec![0]),
21889 &mut cx,
21890 );
21891}
21892
21893#[gpui::test]
21894async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
21895 let (buffer_id, mut cx) = setup_indent_guides_editor(
21896 &"
21897 fn main() {
21898 if 1 == 2 {
21899 let a = 1;
21900 }
21901 }"
21902 .unindent(),
21903 cx,
21904 )
21905 .await;
21906
21907 cx.update_editor(|editor, window, cx| {
21908 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21909 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21910 });
21911 });
21912
21913 assert_indent_guides(
21914 0..4,
21915 vec![
21916 indent_guide(buffer_id, 1, 3, 0),
21917 indent_guide(buffer_id, 2, 2, 1),
21918 ],
21919 Some(vec![1]),
21920 &mut cx,
21921 );
21922
21923 cx.update_editor(|editor, window, cx| {
21924 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21925 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21926 });
21927 });
21928
21929 assert_indent_guides(
21930 0..4,
21931 vec![
21932 indent_guide(buffer_id, 1, 3, 0),
21933 indent_guide(buffer_id, 2, 2, 1),
21934 ],
21935 Some(vec![1]),
21936 &mut cx,
21937 );
21938
21939 cx.update_editor(|editor, window, cx| {
21940 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21941 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21942 });
21943 });
21944
21945 assert_indent_guides(
21946 0..4,
21947 vec![
21948 indent_guide(buffer_id, 1, 3, 0),
21949 indent_guide(buffer_id, 2, 2, 1),
21950 ],
21951 Some(vec![0]),
21952 &mut cx,
21953 );
21954}
21955
21956#[gpui::test]
21957async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21958 let (buffer_id, mut cx) = setup_indent_guides_editor(
21959 &"
21960 fn main() {
21961 let a = 1;
21962
21963 let b = 2;
21964 }"
21965 .unindent(),
21966 cx,
21967 )
21968 .await;
21969
21970 cx.update_editor(|editor, window, cx| {
21971 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21972 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21973 });
21974 });
21975
21976 assert_indent_guides(
21977 0..5,
21978 vec![indent_guide(buffer_id, 1, 3, 0)],
21979 Some(vec![0]),
21980 &mut cx,
21981 );
21982}
21983
21984#[gpui::test]
21985async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21986 let (buffer_id, mut cx) = setup_indent_guides_editor(
21987 &"
21988 def m:
21989 a = 1
21990 pass"
21991 .unindent(),
21992 cx,
21993 )
21994 .await;
21995
21996 cx.update_editor(|editor, window, cx| {
21997 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21998 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21999 });
22000 });
22001
22002 assert_indent_guides(
22003 0..3,
22004 vec![indent_guide(buffer_id, 1, 2, 0)],
22005 Some(vec![0]),
22006 &mut cx,
22007 );
22008}
22009
22010#[gpui::test]
22011async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
22012 init_test(cx, |_| {});
22013 let mut cx = EditorTestContext::new(cx).await;
22014 let text = indoc! {
22015 "
22016 impl A {
22017 fn b() {
22018 0;
22019 3;
22020 5;
22021 6;
22022 7;
22023 }
22024 }
22025 "
22026 };
22027 let base_text = indoc! {
22028 "
22029 impl A {
22030 fn b() {
22031 0;
22032 1;
22033 2;
22034 3;
22035 4;
22036 }
22037 fn c() {
22038 5;
22039 6;
22040 7;
22041 }
22042 }
22043 "
22044 };
22045
22046 cx.update_editor(|editor, window, cx| {
22047 editor.set_text(text, window, cx);
22048
22049 editor.buffer().update(cx, |multibuffer, cx| {
22050 let buffer = multibuffer.as_singleton().unwrap();
22051 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
22052
22053 multibuffer.set_all_diff_hunks_expanded(cx);
22054 multibuffer.add_diff(diff, cx);
22055
22056 buffer.read(cx).remote_id()
22057 })
22058 });
22059 cx.run_until_parked();
22060
22061 cx.assert_state_with_diff(
22062 indoc! { "
22063 impl A {
22064 fn b() {
22065 0;
22066 - 1;
22067 - 2;
22068 3;
22069 - 4;
22070 - }
22071 - fn c() {
22072 5;
22073 6;
22074 7;
22075 }
22076 }
22077 ˇ"
22078 }
22079 .to_string(),
22080 );
22081
22082 let mut actual_guides = cx.update_editor(|editor, window, cx| {
22083 editor
22084 .snapshot(window, cx)
22085 .buffer_snapshot()
22086 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
22087 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
22088 .collect::<Vec<_>>()
22089 });
22090 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
22091 assert_eq!(
22092 actual_guides,
22093 vec![
22094 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
22095 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
22096 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
22097 ]
22098 );
22099}
22100
22101#[gpui::test]
22102async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22103 init_test(cx, |_| {});
22104 let mut cx = EditorTestContext::new(cx).await;
22105
22106 let diff_base = r#"
22107 a
22108 b
22109 c
22110 "#
22111 .unindent();
22112
22113 cx.set_state(
22114 &r#"
22115 ˇA
22116 b
22117 C
22118 "#
22119 .unindent(),
22120 );
22121 cx.set_head_text(&diff_base);
22122 cx.update_editor(|editor, window, cx| {
22123 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22124 });
22125 executor.run_until_parked();
22126
22127 let both_hunks_expanded = r#"
22128 - a
22129 + ˇA
22130 b
22131 - c
22132 + C
22133 "#
22134 .unindent();
22135
22136 cx.assert_state_with_diff(both_hunks_expanded.clone());
22137
22138 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22139 let snapshot = editor.snapshot(window, cx);
22140 let hunks = editor
22141 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22142 .collect::<Vec<_>>();
22143 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22144 hunks
22145 .into_iter()
22146 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22147 .collect::<Vec<_>>()
22148 });
22149 assert_eq!(hunk_ranges.len(), 2);
22150
22151 cx.update_editor(|editor, _, cx| {
22152 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22153 });
22154 executor.run_until_parked();
22155
22156 let second_hunk_expanded = r#"
22157 ˇA
22158 b
22159 - c
22160 + C
22161 "#
22162 .unindent();
22163
22164 cx.assert_state_with_diff(second_hunk_expanded);
22165
22166 cx.update_editor(|editor, _, cx| {
22167 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22168 });
22169 executor.run_until_parked();
22170
22171 cx.assert_state_with_diff(both_hunks_expanded.clone());
22172
22173 cx.update_editor(|editor, _, cx| {
22174 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22175 });
22176 executor.run_until_parked();
22177
22178 let first_hunk_expanded = r#"
22179 - a
22180 + ˇA
22181 b
22182 C
22183 "#
22184 .unindent();
22185
22186 cx.assert_state_with_diff(first_hunk_expanded);
22187
22188 cx.update_editor(|editor, _, cx| {
22189 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22190 });
22191 executor.run_until_parked();
22192
22193 cx.assert_state_with_diff(both_hunks_expanded);
22194
22195 cx.set_state(
22196 &r#"
22197 ˇA
22198 b
22199 "#
22200 .unindent(),
22201 );
22202 cx.run_until_parked();
22203
22204 // TODO this cursor position seems bad
22205 cx.assert_state_with_diff(
22206 r#"
22207 - ˇa
22208 + A
22209 b
22210 "#
22211 .unindent(),
22212 );
22213
22214 cx.update_editor(|editor, window, cx| {
22215 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22216 });
22217
22218 cx.assert_state_with_diff(
22219 r#"
22220 - ˇa
22221 + A
22222 b
22223 - c
22224 "#
22225 .unindent(),
22226 );
22227
22228 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22229 let snapshot = editor.snapshot(window, cx);
22230 let hunks = editor
22231 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22232 .collect::<Vec<_>>();
22233 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22234 hunks
22235 .into_iter()
22236 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22237 .collect::<Vec<_>>()
22238 });
22239 assert_eq!(hunk_ranges.len(), 2);
22240
22241 cx.update_editor(|editor, _, cx| {
22242 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22243 });
22244 executor.run_until_parked();
22245
22246 cx.assert_state_with_diff(
22247 r#"
22248 - ˇa
22249 + A
22250 b
22251 "#
22252 .unindent(),
22253 );
22254}
22255
22256#[gpui::test]
22257async fn test_toggle_deletion_hunk_at_start_of_file(
22258 executor: BackgroundExecutor,
22259 cx: &mut TestAppContext,
22260) {
22261 init_test(cx, |_| {});
22262 let mut cx = EditorTestContext::new(cx).await;
22263
22264 let diff_base = r#"
22265 a
22266 b
22267 c
22268 "#
22269 .unindent();
22270
22271 cx.set_state(
22272 &r#"
22273 ˇb
22274 c
22275 "#
22276 .unindent(),
22277 );
22278 cx.set_head_text(&diff_base);
22279 cx.update_editor(|editor, window, cx| {
22280 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22281 });
22282 executor.run_until_parked();
22283
22284 let hunk_expanded = r#"
22285 - a
22286 ˇb
22287 c
22288 "#
22289 .unindent();
22290
22291 cx.assert_state_with_diff(hunk_expanded.clone());
22292
22293 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22294 let snapshot = editor.snapshot(window, cx);
22295 let hunks = editor
22296 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22297 .collect::<Vec<_>>();
22298 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22299 hunks
22300 .into_iter()
22301 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22302 .collect::<Vec<_>>()
22303 });
22304 assert_eq!(hunk_ranges.len(), 1);
22305
22306 cx.update_editor(|editor, _, cx| {
22307 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22308 });
22309 executor.run_until_parked();
22310
22311 let hunk_collapsed = r#"
22312 ˇb
22313 c
22314 "#
22315 .unindent();
22316
22317 cx.assert_state_with_diff(hunk_collapsed);
22318
22319 cx.update_editor(|editor, _, cx| {
22320 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22321 });
22322 executor.run_until_parked();
22323
22324 cx.assert_state_with_diff(hunk_expanded);
22325}
22326
22327#[gpui::test]
22328async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
22329 executor: BackgroundExecutor,
22330 cx: &mut TestAppContext,
22331) {
22332 init_test(cx, |_| {});
22333 let mut cx = EditorTestContext::new(cx).await;
22334
22335 cx.set_state("ˇnew\nsecond\nthird\n");
22336 cx.set_head_text("old\nsecond\nthird\n");
22337 cx.update_editor(|editor, window, cx| {
22338 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
22339 });
22340 executor.run_until_parked();
22341 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22342
22343 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
22344 cx.update_editor(|editor, window, cx| {
22345 let snapshot = editor.snapshot(window, cx);
22346 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22347 let hunks = editor
22348 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22349 .collect::<Vec<_>>();
22350 assert_eq!(hunks.len(), 1);
22351 let hunk_range = Anchor::range_in_buffer(excerpt_id, hunks[0].buffer_range.clone());
22352 editor.toggle_single_diff_hunk(hunk_range, cx)
22353 });
22354 executor.run_until_parked();
22355 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
22356
22357 // Keep the editor scrolled to the top so the full hunk remains visible.
22358 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22359}
22360
22361#[gpui::test]
22362async fn test_display_diff_hunks(cx: &mut TestAppContext) {
22363 init_test(cx, |_| {});
22364
22365 let fs = FakeFs::new(cx.executor());
22366 fs.insert_tree(
22367 path!("/test"),
22368 json!({
22369 ".git": {},
22370 "file-1": "ONE\n",
22371 "file-2": "TWO\n",
22372 "file-3": "THREE\n",
22373 }),
22374 )
22375 .await;
22376
22377 fs.set_head_for_repo(
22378 path!("/test/.git").as_ref(),
22379 &[
22380 ("file-1", "one\n".into()),
22381 ("file-2", "two\n".into()),
22382 ("file-3", "three\n".into()),
22383 ],
22384 "deadbeef",
22385 );
22386
22387 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
22388 let mut buffers = vec![];
22389 for i in 1..=3 {
22390 let buffer = project
22391 .update(cx, |project, cx| {
22392 let path = format!(path!("/test/file-{}"), i);
22393 project.open_local_buffer(path, cx)
22394 })
22395 .await
22396 .unwrap();
22397 buffers.push(buffer);
22398 }
22399
22400 let multibuffer = cx.new(|cx| {
22401 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
22402 multibuffer.set_all_diff_hunks_expanded(cx);
22403 for buffer in &buffers {
22404 let snapshot = buffer.read(cx).snapshot();
22405 multibuffer.set_excerpts_for_path(
22406 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
22407 buffer.clone(),
22408 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
22409 2,
22410 cx,
22411 );
22412 }
22413 multibuffer
22414 });
22415
22416 let editor = cx.add_window(|window, cx| {
22417 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
22418 });
22419 cx.run_until_parked();
22420
22421 let snapshot = editor
22422 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22423 .unwrap();
22424 let hunks = snapshot
22425 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
22426 .map(|hunk| match hunk {
22427 DisplayDiffHunk::Unfolded {
22428 display_row_range, ..
22429 } => display_row_range,
22430 DisplayDiffHunk::Folded { .. } => unreachable!(),
22431 })
22432 .collect::<Vec<_>>();
22433 assert_eq!(
22434 hunks,
22435 [
22436 DisplayRow(2)..DisplayRow(4),
22437 DisplayRow(7)..DisplayRow(9),
22438 DisplayRow(12)..DisplayRow(14),
22439 ]
22440 );
22441}
22442
22443#[gpui::test]
22444async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
22445 init_test(cx, |_| {});
22446
22447 let mut cx = EditorTestContext::new(cx).await;
22448 cx.set_head_text(indoc! { "
22449 one
22450 two
22451 three
22452 four
22453 five
22454 "
22455 });
22456 cx.set_index_text(indoc! { "
22457 one
22458 two
22459 three
22460 four
22461 five
22462 "
22463 });
22464 cx.set_state(indoc! {"
22465 one
22466 TWO
22467 ˇTHREE
22468 FOUR
22469 five
22470 "});
22471 cx.run_until_parked();
22472 cx.update_editor(|editor, window, cx| {
22473 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22474 });
22475 cx.run_until_parked();
22476 cx.assert_index_text(Some(indoc! {"
22477 one
22478 TWO
22479 THREE
22480 FOUR
22481 five
22482 "}));
22483 cx.set_state(indoc! { "
22484 one
22485 TWO
22486 ˇTHREE-HUNDRED
22487 FOUR
22488 five
22489 "});
22490 cx.run_until_parked();
22491 cx.update_editor(|editor, window, cx| {
22492 let snapshot = editor.snapshot(window, cx);
22493 let hunks = editor
22494 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22495 .collect::<Vec<_>>();
22496 assert_eq!(hunks.len(), 1);
22497 assert_eq!(
22498 hunks[0].status(),
22499 DiffHunkStatus {
22500 kind: DiffHunkStatusKind::Modified,
22501 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
22502 }
22503 );
22504
22505 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22506 });
22507 cx.run_until_parked();
22508 cx.assert_index_text(Some(indoc! {"
22509 one
22510 TWO
22511 THREE-HUNDRED
22512 FOUR
22513 five
22514 "}));
22515}
22516
22517#[gpui::test]
22518fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
22519 init_test(cx, |_| {});
22520
22521 let editor = cx.add_window(|window, cx| {
22522 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
22523 build_editor(buffer, window, cx)
22524 });
22525
22526 let render_args = Arc::new(Mutex::new(None));
22527 let snapshot = editor
22528 .update(cx, |editor, window, cx| {
22529 let snapshot = editor.buffer().read(cx).snapshot(cx);
22530 let range =
22531 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
22532
22533 struct RenderArgs {
22534 row: MultiBufferRow,
22535 folded: bool,
22536 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
22537 }
22538
22539 let crease = Crease::inline(
22540 range,
22541 FoldPlaceholder::test(),
22542 {
22543 let toggle_callback = render_args.clone();
22544 move |row, folded, callback, _window, _cx| {
22545 *toggle_callback.lock() = Some(RenderArgs {
22546 row,
22547 folded,
22548 callback,
22549 });
22550 div()
22551 }
22552 },
22553 |_row, _folded, _window, _cx| div(),
22554 );
22555
22556 editor.insert_creases(Some(crease), cx);
22557 let snapshot = editor.snapshot(window, cx);
22558 let _div =
22559 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
22560 snapshot
22561 })
22562 .unwrap();
22563
22564 let render_args = render_args.lock().take().unwrap();
22565 assert_eq!(render_args.row, MultiBufferRow(1));
22566 assert!(!render_args.folded);
22567 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22568
22569 cx.update_window(*editor, |_, window, cx| {
22570 (render_args.callback)(true, window, cx)
22571 })
22572 .unwrap();
22573 let snapshot = editor
22574 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22575 .unwrap();
22576 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
22577
22578 cx.update_window(*editor, |_, window, cx| {
22579 (render_args.callback)(false, window, cx)
22580 })
22581 .unwrap();
22582 let snapshot = editor
22583 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22584 .unwrap();
22585 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22586}
22587
22588#[gpui::test]
22589async fn test_input_text(cx: &mut TestAppContext) {
22590 init_test(cx, |_| {});
22591 let mut cx = EditorTestContext::new(cx).await;
22592
22593 cx.set_state(
22594 &r#"ˇone
22595 two
22596
22597 three
22598 fourˇ
22599 five
22600
22601 siˇx"#
22602 .unindent(),
22603 );
22604
22605 cx.dispatch_action(HandleInput(String::new()));
22606 cx.assert_editor_state(
22607 &r#"ˇone
22608 two
22609
22610 three
22611 fourˇ
22612 five
22613
22614 siˇx"#
22615 .unindent(),
22616 );
22617
22618 cx.dispatch_action(HandleInput("AAAA".to_string()));
22619 cx.assert_editor_state(
22620 &r#"AAAAˇone
22621 two
22622
22623 three
22624 fourAAAAˇ
22625 five
22626
22627 siAAAAˇx"#
22628 .unindent(),
22629 );
22630}
22631
22632#[gpui::test]
22633async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22634 init_test(cx, |_| {});
22635
22636 let mut cx = EditorTestContext::new(cx).await;
22637 cx.set_state(
22638 r#"let foo = 1;
22639let foo = 2;
22640let foo = 3;
22641let fooˇ = 4;
22642let foo = 5;
22643let foo = 6;
22644let foo = 7;
22645let foo = 8;
22646let foo = 9;
22647let foo = 10;
22648let foo = 11;
22649let foo = 12;
22650let foo = 13;
22651let foo = 14;
22652let foo = 15;"#,
22653 );
22654
22655 cx.update_editor(|e, window, cx| {
22656 assert_eq!(
22657 e.next_scroll_position,
22658 NextScrollCursorCenterTopBottom::Center,
22659 "Default next scroll direction is center",
22660 );
22661
22662 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22663 assert_eq!(
22664 e.next_scroll_position,
22665 NextScrollCursorCenterTopBottom::Top,
22666 "After center, next scroll direction should be top",
22667 );
22668
22669 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22670 assert_eq!(
22671 e.next_scroll_position,
22672 NextScrollCursorCenterTopBottom::Bottom,
22673 "After top, next scroll direction should be bottom",
22674 );
22675
22676 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22677 assert_eq!(
22678 e.next_scroll_position,
22679 NextScrollCursorCenterTopBottom::Center,
22680 "After bottom, scrolling should start over",
22681 );
22682
22683 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22684 assert_eq!(
22685 e.next_scroll_position,
22686 NextScrollCursorCenterTopBottom::Top,
22687 "Scrolling continues if retriggered fast enough"
22688 );
22689 });
22690
22691 cx.executor()
22692 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22693 cx.executor().run_until_parked();
22694 cx.update_editor(|e, _, _| {
22695 assert_eq!(
22696 e.next_scroll_position,
22697 NextScrollCursorCenterTopBottom::Center,
22698 "If scrolling is not triggered fast enough, it should reset"
22699 );
22700 });
22701}
22702
22703#[gpui::test]
22704async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22705 init_test(cx, |_| {});
22706 let mut cx = EditorLspTestContext::new_rust(
22707 lsp::ServerCapabilities {
22708 definition_provider: Some(lsp::OneOf::Left(true)),
22709 references_provider: Some(lsp::OneOf::Left(true)),
22710 ..lsp::ServerCapabilities::default()
22711 },
22712 cx,
22713 )
22714 .await;
22715
22716 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22717 let go_to_definition = cx
22718 .lsp
22719 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22720 move |params, _| async move {
22721 if empty_go_to_definition {
22722 Ok(None)
22723 } else {
22724 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22725 uri: params.text_document_position_params.text_document.uri,
22726 range: lsp::Range::new(
22727 lsp::Position::new(4, 3),
22728 lsp::Position::new(4, 6),
22729 ),
22730 })))
22731 }
22732 },
22733 );
22734 let references = cx
22735 .lsp
22736 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22737 Ok(Some(vec![lsp::Location {
22738 uri: params.text_document_position.text_document.uri,
22739 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22740 }]))
22741 });
22742 (go_to_definition, references)
22743 };
22744
22745 cx.set_state(
22746 &r#"fn one() {
22747 let mut a = ˇtwo();
22748 }
22749
22750 fn two() {}"#
22751 .unindent(),
22752 );
22753 set_up_lsp_handlers(false, &mut cx);
22754 let navigated = cx
22755 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22756 .await
22757 .expect("Failed to navigate to definition");
22758 assert_eq!(
22759 navigated,
22760 Navigated::Yes,
22761 "Should have navigated to definition from the GetDefinition response"
22762 );
22763 cx.assert_editor_state(
22764 &r#"fn one() {
22765 let mut a = two();
22766 }
22767
22768 fn «twoˇ»() {}"#
22769 .unindent(),
22770 );
22771
22772 let editors = cx.update_workspace(|workspace, _, cx| {
22773 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22774 });
22775 cx.update_editor(|_, _, test_editor_cx| {
22776 assert_eq!(
22777 editors.len(),
22778 1,
22779 "Initially, only one, test, editor should be open in the workspace"
22780 );
22781 assert_eq!(
22782 test_editor_cx.entity(),
22783 editors.last().expect("Asserted len is 1").clone()
22784 );
22785 });
22786
22787 set_up_lsp_handlers(true, &mut cx);
22788 let navigated = cx
22789 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22790 .await
22791 .expect("Failed to navigate to lookup references");
22792 assert_eq!(
22793 navigated,
22794 Navigated::Yes,
22795 "Should have navigated to references as a fallback after empty GoToDefinition response"
22796 );
22797 // We should not change the selections in the existing file,
22798 // if opening another milti buffer with the references
22799 cx.assert_editor_state(
22800 &r#"fn one() {
22801 let mut a = two();
22802 }
22803
22804 fn «twoˇ»() {}"#
22805 .unindent(),
22806 );
22807 let editors = cx.update_workspace(|workspace, _, cx| {
22808 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22809 });
22810 cx.update_editor(|_, _, test_editor_cx| {
22811 assert_eq!(
22812 editors.len(),
22813 2,
22814 "After falling back to references search, we open a new editor with the results"
22815 );
22816 let references_fallback_text = editors
22817 .into_iter()
22818 .find(|new_editor| *new_editor != test_editor_cx.entity())
22819 .expect("Should have one non-test editor now")
22820 .read(test_editor_cx)
22821 .text(test_editor_cx);
22822 assert_eq!(
22823 references_fallback_text, "fn one() {\n let mut a = two();\n}",
22824 "Should use the range from the references response and not the GoToDefinition one"
22825 );
22826 });
22827}
22828
22829#[gpui::test]
22830async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
22831 init_test(cx, |_| {});
22832 cx.update(|cx| {
22833 let mut editor_settings = EditorSettings::get_global(cx).clone();
22834 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
22835 EditorSettings::override_global(editor_settings, cx);
22836 });
22837 let mut cx = EditorLspTestContext::new_rust(
22838 lsp::ServerCapabilities {
22839 definition_provider: Some(lsp::OneOf::Left(true)),
22840 references_provider: Some(lsp::OneOf::Left(true)),
22841 ..lsp::ServerCapabilities::default()
22842 },
22843 cx,
22844 )
22845 .await;
22846 let original_state = r#"fn one() {
22847 let mut a = ˇtwo();
22848 }
22849
22850 fn two() {}"#
22851 .unindent();
22852 cx.set_state(&original_state);
22853
22854 let mut go_to_definition = cx
22855 .lsp
22856 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22857 move |_, _| async move { Ok(None) },
22858 );
22859 let _references = cx
22860 .lsp
22861 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
22862 panic!("Should not call for references with no go to definition fallback")
22863 });
22864
22865 let navigated = cx
22866 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22867 .await
22868 .expect("Failed to navigate to lookup references");
22869 go_to_definition
22870 .next()
22871 .await
22872 .expect("Should have called the go_to_definition handler");
22873
22874 assert_eq!(
22875 navigated,
22876 Navigated::No,
22877 "Should have navigated to references as a fallback after empty GoToDefinition response"
22878 );
22879 cx.assert_editor_state(&original_state);
22880 let editors = cx.update_workspace(|workspace, _, cx| {
22881 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22882 });
22883 cx.update_editor(|_, _, _| {
22884 assert_eq!(
22885 editors.len(),
22886 1,
22887 "After unsuccessful fallback, no other editor should have been opened"
22888 );
22889 });
22890}
22891
22892#[gpui::test]
22893async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
22894 init_test(cx, |_| {});
22895 let mut cx = EditorLspTestContext::new_rust(
22896 lsp::ServerCapabilities {
22897 references_provider: Some(lsp::OneOf::Left(true)),
22898 ..lsp::ServerCapabilities::default()
22899 },
22900 cx,
22901 )
22902 .await;
22903
22904 cx.set_state(
22905 &r#"
22906 fn one() {
22907 let mut a = two();
22908 }
22909
22910 fn ˇtwo() {}"#
22911 .unindent(),
22912 );
22913 cx.lsp
22914 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22915 Ok(Some(vec![
22916 lsp::Location {
22917 uri: params.text_document_position.text_document.uri.clone(),
22918 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22919 },
22920 lsp::Location {
22921 uri: params.text_document_position.text_document.uri,
22922 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
22923 },
22924 ]))
22925 });
22926 let navigated = cx
22927 .update_editor(|editor, window, cx| {
22928 editor.find_all_references(&FindAllReferences::default(), window, cx)
22929 })
22930 .unwrap()
22931 .await
22932 .expect("Failed to navigate to references");
22933 assert_eq!(
22934 navigated,
22935 Navigated::Yes,
22936 "Should have navigated to references from the FindAllReferences response"
22937 );
22938 cx.assert_editor_state(
22939 &r#"fn one() {
22940 let mut a = two();
22941 }
22942
22943 fn ˇtwo() {}"#
22944 .unindent(),
22945 );
22946
22947 let editors = cx.update_workspace(|workspace, _, cx| {
22948 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22949 });
22950 cx.update_editor(|_, _, _| {
22951 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
22952 });
22953
22954 cx.set_state(
22955 &r#"fn one() {
22956 let mut a = ˇtwo();
22957 }
22958
22959 fn two() {}"#
22960 .unindent(),
22961 );
22962 let navigated = cx
22963 .update_editor(|editor, window, cx| {
22964 editor.find_all_references(&FindAllReferences::default(), window, cx)
22965 })
22966 .unwrap()
22967 .await
22968 .expect("Failed to navigate to references");
22969 assert_eq!(
22970 navigated,
22971 Navigated::Yes,
22972 "Should have navigated to references from the FindAllReferences response"
22973 );
22974 cx.assert_editor_state(
22975 &r#"fn one() {
22976 let mut a = ˇtwo();
22977 }
22978
22979 fn two() {}"#
22980 .unindent(),
22981 );
22982 let editors = cx.update_workspace(|workspace, _, cx| {
22983 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22984 });
22985 cx.update_editor(|_, _, _| {
22986 assert_eq!(
22987 editors.len(),
22988 2,
22989 "should have re-used the previous multibuffer"
22990 );
22991 });
22992
22993 cx.set_state(
22994 &r#"fn one() {
22995 let mut a = ˇtwo();
22996 }
22997 fn three() {}
22998 fn two() {}"#
22999 .unindent(),
23000 );
23001 cx.lsp
23002 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23003 Ok(Some(vec![
23004 lsp::Location {
23005 uri: params.text_document_position.text_document.uri.clone(),
23006 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
23007 },
23008 lsp::Location {
23009 uri: params.text_document_position.text_document.uri,
23010 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
23011 },
23012 ]))
23013 });
23014 let navigated = cx
23015 .update_editor(|editor, window, cx| {
23016 editor.find_all_references(&FindAllReferences::default(), window, cx)
23017 })
23018 .unwrap()
23019 .await
23020 .expect("Failed to navigate to references");
23021 assert_eq!(
23022 navigated,
23023 Navigated::Yes,
23024 "Should have navigated to references from the FindAllReferences response"
23025 );
23026 cx.assert_editor_state(
23027 &r#"fn one() {
23028 let mut a = ˇtwo();
23029 }
23030 fn three() {}
23031 fn two() {}"#
23032 .unindent(),
23033 );
23034 let editors = cx.update_workspace(|workspace, _, cx| {
23035 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23036 });
23037 cx.update_editor(|_, _, _| {
23038 assert_eq!(
23039 editors.len(),
23040 3,
23041 "should have used a new multibuffer as offsets changed"
23042 );
23043 });
23044}
23045#[gpui::test]
23046async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
23047 init_test(cx, |_| {});
23048
23049 let language = Arc::new(Language::new(
23050 LanguageConfig::default(),
23051 Some(tree_sitter_rust::LANGUAGE.into()),
23052 ));
23053
23054 let text = r#"
23055 #[cfg(test)]
23056 mod tests() {
23057 #[test]
23058 fn runnable_1() {
23059 let a = 1;
23060 }
23061
23062 #[test]
23063 fn runnable_2() {
23064 let a = 1;
23065 let b = 2;
23066 }
23067 }
23068 "#
23069 .unindent();
23070
23071 let fs = FakeFs::new(cx.executor());
23072 fs.insert_file("/file.rs", Default::default()).await;
23073
23074 let project = Project::test(fs, ["/a".as_ref()], cx).await;
23075 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23076 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23077 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
23078 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
23079
23080 let editor = cx.new_window_entity(|window, cx| {
23081 Editor::new(
23082 EditorMode::full(),
23083 multi_buffer,
23084 Some(project.clone()),
23085 window,
23086 cx,
23087 )
23088 });
23089
23090 editor.update_in(cx, |editor, window, cx| {
23091 let snapshot = editor.buffer().read(cx).snapshot(cx);
23092 editor.tasks.insert(
23093 (buffer.read(cx).remote_id(), 3),
23094 RunnableTasks {
23095 templates: vec![],
23096 offset: snapshot.anchor_before(MultiBufferOffset(43)),
23097 column: 0,
23098 extra_variables: HashMap::default(),
23099 context_range: BufferOffset(43)..BufferOffset(85),
23100 },
23101 );
23102 editor.tasks.insert(
23103 (buffer.read(cx).remote_id(), 8),
23104 RunnableTasks {
23105 templates: vec![],
23106 offset: snapshot.anchor_before(MultiBufferOffset(86)),
23107 column: 0,
23108 extra_variables: HashMap::default(),
23109 context_range: BufferOffset(86)..BufferOffset(191),
23110 },
23111 );
23112
23113 // Test finding task when cursor is inside function body
23114 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23115 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
23116 });
23117 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23118 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
23119
23120 // Test finding task when cursor is on function name
23121 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23122 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
23123 });
23124 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23125 assert_eq!(row, 8, "Should find task when cursor is on function name");
23126 });
23127}
23128
23129#[gpui::test]
23130async fn test_folding_buffers(cx: &mut TestAppContext) {
23131 init_test(cx, |_| {});
23132
23133 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23134 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
23135 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
23136
23137 let fs = FakeFs::new(cx.executor());
23138 fs.insert_tree(
23139 path!("/a"),
23140 json!({
23141 "first.rs": sample_text_1,
23142 "second.rs": sample_text_2,
23143 "third.rs": sample_text_3,
23144 }),
23145 )
23146 .await;
23147 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23148 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23149 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23150 let worktree = project.update(cx, |project, cx| {
23151 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23152 assert_eq!(worktrees.len(), 1);
23153 worktrees.pop().unwrap()
23154 });
23155 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23156
23157 let buffer_1 = project
23158 .update(cx, |project, cx| {
23159 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23160 })
23161 .await
23162 .unwrap();
23163 let buffer_2 = project
23164 .update(cx, |project, cx| {
23165 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23166 })
23167 .await
23168 .unwrap();
23169 let buffer_3 = project
23170 .update(cx, |project, cx| {
23171 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23172 })
23173 .await
23174 .unwrap();
23175
23176 let multi_buffer = cx.new(|cx| {
23177 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23178 multi_buffer.push_excerpts(
23179 buffer_1.clone(),
23180 [
23181 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23182 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23183 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23184 ],
23185 cx,
23186 );
23187 multi_buffer.push_excerpts(
23188 buffer_2.clone(),
23189 [
23190 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23191 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23192 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23193 ],
23194 cx,
23195 );
23196 multi_buffer.push_excerpts(
23197 buffer_3.clone(),
23198 [
23199 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23200 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23201 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23202 ],
23203 cx,
23204 );
23205 multi_buffer
23206 });
23207 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23208 Editor::new(
23209 EditorMode::full(),
23210 multi_buffer.clone(),
23211 Some(project.clone()),
23212 window,
23213 cx,
23214 )
23215 });
23216
23217 assert_eq!(
23218 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23219 "\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",
23220 );
23221
23222 multi_buffer_editor.update(cx, |editor, cx| {
23223 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23224 });
23225 assert_eq!(
23226 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23227 "\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",
23228 "After folding the first buffer, its text should not be displayed"
23229 );
23230
23231 multi_buffer_editor.update(cx, |editor, cx| {
23232 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23233 });
23234 assert_eq!(
23235 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23236 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
23237 "After folding the second buffer, its text should not be displayed"
23238 );
23239
23240 multi_buffer_editor.update(cx, |editor, cx| {
23241 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23242 });
23243 assert_eq!(
23244 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23245 "\n\n\n\n\n",
23246 "After folding the third buffer, its text should not be displayed"
23247 );
23248
23249 // Emulate selection inside the fold logic, that should work
23250 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23251 editor
23252 .snapshot(window, cx)
23253 .next_line_boundary(Point::new(0, 4));
23254 });
23255
23256 multi_buffer_editor.update(cx, |editor, cx| {
23257 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23258 });
23259 assert_eq!(
23260 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23261 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
23262 "After unfolding the second buffer, its text should be displayed"
23263 );
23264
23265 // Typing inside of buffer 1 causes that buffer to be unfolded.
23266 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23267 assert_eq!(
23268 multi_buffer
23269 .read(cx)
23270 .snapshot(cx)
23271 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
23272 .collect::<String>(),
23273 "bbbb"
23274 );
23275 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23276 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
23277 });
23278 editor.handle_input("B", window, cx);
23279 });
23280
23281 assert_eq!(
23282 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23283 "\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",
23284 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
23285 );
23286
23287 multi_buffer_editor.update(cx, |editor, cx| {
23288 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23289 });
23290 assert_eq!(
23291 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23292 "\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",
23293 "After unfolding the all buffers, all original text should be displayed"
23294 );
23295}
23296
23297#[gpui::test]
23298async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
23299 init_test(cx, |_| {});
23300
23301 let sample_text_1 = "1111\n2222\n3333".to_string();
23302 let sample_text_2 = "4444\n5555\n6666".to_string();
23303 let sample_text_3 = "7777\n8888\n9999".to_string();
23304
23305 let fs = FakeFs::new(cx.executor());
23306 fs.insert_tree(
23307 path!("/a"),
23308 json!({
23309 "first.rs": sample_text_1,
23310 "second.rs": sample_text_2,
23311 "third.rs": sample_text_3,
23312 }),
23313 )
23314 .await;
23315 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23316 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23317 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23318 let worktree = project.update(cx, |project, cx| {
23319 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23320 assert_eq!(worktrees.len(), 1);
23321 worktrees.pop().unwrap()
23322 });
23323 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23324
23325 let buffer_1 = project
23326 .update(cx, |project, cx| {
23327 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23328 })
23329 .await
23330 .unwrap();
23331 let buffer_2 = project
23332 .update(cx, |project, cx| {
23333 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23334 })
23335 .await
23336 .unwrap();
23337 let buffer_3 = project
23338 .update(cx, |project, cx| {
23339 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23340 })
23341 .await
23342 .unwrap();
23343
23344 let multi_buffer = cx.new(|cx| {
23345 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23346 multi_buffer.push_excerpts(
23347 buffer_1.clone(),
23348 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23349 cx,
23350 );
23351 multi_buffer.push_excerpts(
23352 buffer_2.clone(),
23353 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23354 cx,
23355 );
23356 multi_buffer.push_excerpts(
23357 buffer_3.clone(),
23358 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23359 cx,
23360 );
23361 multi_buffer
23362 });
23363
23364 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23365 Editor::new(
23366 EditorMode::full(),
23367 multi_buffer,
23368 Some(project.clone()),
23369 window,
23370 cx,
23371 )
23372 });
23373
23374 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
23375 assert_eq!(
23376 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23377 full_text,
23378 );
23379
23380 multi_buffer_editor.update(cx, |editor, cx| {
23381 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23382 });
23383 assert_eq!(
23384 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23385 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
23386 "After folding the first buffer, its text should not be displayed"
23387 );
23388
23389 multi_buffer_editor.update(cx, |editor, cx| {
23390 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23391 });
23392
23393 assert_eq!(
23394 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23395 "\n\n\n\n\n\n7777\n8888\n9999",
23396 "After folding the second buffer, its text should not be displayed"
23397 );
23398
23399 multi_buffer_editor.update(cx, |editor, cx| {
23400 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23401 });
23402 assert_eq!(
23403 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23404 "\n\n\n\n\n",
23405 "After folding the third buffer, its text should not be displayed"
23406 );
23407
23408 multi_buffer_editor.update(cx, |editor, cx| {
23409 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23410 });
23411 assert_eq!(
23412 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23413 "\n\n\n\n4444\n5555\n6666\n\n",
23414 "After unfolding the second buffer, its text should be displayed"
23415 );
23416
23417 multi_buffer_editor.update(cx, |editor, cx| {
23418 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
23419 });
23420 assert_eq!(
23421 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23422 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
23423 "After unfolding the first buffer, its text should be displayed"
23424 );
23425
23426 multi_buffer_editor.update(cx, |editor, cx| {
23427 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23428 });
23429 assert_eq!(
23430 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23431 full_text,
23432 "After unfolding all buffers, all original text should be displayed"
23433 );
23434}
23435
23436#[gpui::test]
23437async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
23438 init_test(cx, |_| {});
23439
23440 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23441
23442 let fs = FakeFs::new(cx.executor());
23443 fs.insert_tree(
23444 path!("/a"),
23445 json!({
23446 "main.rs": sample_text,
23447 }),
23448 )
23449 .await;
23450 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23451 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23452 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23453 let worktree = project.update(cx, |project, cx| {
23454 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23455 assert_eq!(worktrees.len(), 1);
23456 worktrees.pop().unwrap()
23457 });
23458 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23459
23460 let buffer_1 = project
23461 .update(cx, |project, cx| {
23462 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23463 })
23464 .await
23465 .unwrap();
23466
23467 let multi_buffer = cx.new(|cx| {
23468 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23469 multi_buffer.push_excerpts(
23470 buffer_1.clone(),
23471 [ExcerptRange::new(
23472 Point::new(0, 0)
23473 ..Point::new(
23474 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
23475 0,
23476 ),
23477 )],
23478 cx,
23479 );
23480 multi_buffer
23481 });
23482 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23483 Editor::new(
23484 EditorMode::full(),
23485 multi_buffer,
23486 Some(project.clone()),
23487 window,
23488 cx,
23489 )
23490 });
23491
23492 let selection_range = Point::new(1, 0)..Point::new(2, 0);
23493 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23494 enum TestHighlight {}
23495 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
23496 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
23497 editor.highlight_text::<TestHighlight>(
23498 vec![highlight_range.clone()],
23499 HighlightStyle::color(Hsla::green()),
23500 cx,
23501 );
23502 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23503 s.select_ranges(Some(highlight_range))
23504 });
23505 });
23506
23507 let full_text = format!("\n\n{sample_text}");
23508 assert_eq!(
23509 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23510 full_text,
23511 );
23512}
23513
23514#[gpui::test]
23515async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
23516 init_test(cx, |_| {});
23517 cx.update(|cx| {
23518 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
23519 "keymaps/default-linux.json",
23520 cx,
23521 )
23522 .unwrap();
23523 cx.bind_keys(default_key_bindings);
23524 });
23525
23526 let (editor, cx) = cx.add_window_view(|window, cx| {
23527 let multi_buffer = MultiBuffer::build_multi(
23528 [
23529 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
23530 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
23531 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
23532 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
23533 ],
23534 cx,
23535 );
23536 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
23537
23538 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
23539 // fold all but the second buffer, so that we test navigating between two
23540 // adjacent folded buffers, as well as folded buffers at the start and
23541 // end the multibuffer
23542 editor.fold_buffer(buffer_ids[0], cx);
23543 editor.fold_buffer(buffer_ids[2], cx);
23544 editor.fold_buffer(buffer_ids[3], cx);
23545
23546 editor
23547 });
23548 cx.simulate_resize(size(px(1000.), px(1000.)));
23549
23550 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
23551 cx.assert_excerpts_with_selections(indoc! {"
23552 [EXCERPT]
23553 ˇ[FOLDED]
23554 [EXCERPT]
23555 a1
23556 b1
23557 [EXCERPT]
23558 [FOLDED]
23559 [EXCERPT]
23560 [FOLDED]
23561 "
23562 });
23563 cx.simulate_keystroke("down");
23564 cx.assert_excerpts_with_selections(indoc! {"
23565 [EXCERPT]
23566 [FOLDED]
23567 [EXCERPT]
23568 ˇa1
23569 b1
23570 [EXCERPT]
23571 [FOLDED]
23572 [EXCERPT]
23573 [FOLDED]
23574 "
23575 });
23576 cx.simulate_keystroke("down");
23577 cx.assert_excerpts_with_selections(indoc! {"
23578 [EXCERPT]
23579 [FOLDED]
23580 [EXCERPT]
23581 a1
23582 ˇb1
23583 [EXCERPT]
23584 [FOLDED]
23585 [EXCERPT]
23586 [FOLDED]
23587 "
23588 });
23589 cx.simulate_keystroke("down");
23590 cx.assert_excerpts_with_selections(indoc! {"
23591 [EXCERPT]
23592 [FOLDED]
23593 [EXCERPT]
23594 a1
23595 b1
23596 ˇ[EXCERPT]
23597 [FOLDED]
23598 [EXCERPT]
23599 [FOLDED]
23600 "
23601 });
23602 cx.simulate_keystroke("down");
23603 cx.assert_excerpts_with_selections(indoc! {"
23604 [EXCERPT]
23605 [FOLDED]
23606 [EXCERPT]
23607 a1
23608 b1
23609 [EXCERPT]
23610 ˇ[FOLDED]
23611 [EXCERPT]
23612 [FOLDED]
23613 "
23614 });
23615 for _ in 0..5 {
23616 cx.simulate_keystroke("down");
23617 cx.assert_excerpts_with_selections(indoc! {"
23618 [EXCERPT]
23619 [FOLDED]
23620 [EXCERPT]
23621 a1
23622 b1
23623 [EXCERPT]
23624 [FOLDED]
23625 [EXCERPT]
23626 ˇ[FOLDED]
23627 "
23628 });
23629 }
23630
23631 cx.simulate_keystroke("up");
23632 cx.assert_excerpts_with_selections(indoc! {"
23633 [EXCERPT]
23634 [FOLDED]
23635 [EXCERPT]
23636 a1
23637 b1
23638 [EXCERPT]
23639 ˇ[FOLDED]
23640 [EXCERPT]
23641 [FOLDED]
23642 "
23643 });
23644 cx.simulate_keystroke("up");
23645 cx.assert_excerpts_with_selections(indoc! {"
23646 [EXCERPT]
23647 [FOLDED]
23648 [EXCERPT]
23649 a1
23650 b1
23651 ˇ[EXCERPT]
23652 [FOLDED]
23653 [EXCERPT]
23654 [FOLDED]
23655 "
23656 });
23657 cx.simulate_keystroke("up");
23658 cx.assert_excerpts_with_selections(indoc! {"
23659 [EXCERPT]
23660 [FOLDED]
23661 [EXCERPT]
23662 a1
23663 ˇb1
23664 [EXCERPT]
23665 [FOLDED]
23666 [EXCERPT]
23667 [FOLDED]
23668 "
23669 });
23670 cx.simulate_keystroke("up");
23671 cx.assert_excerpts_with_selections(indoc! {"
23672 [EXCERPT]
23673 [FOLDED]
23674 [EXCERPT]
23675 ˇa1
23676 b1
23677 [EXCERPT]
23678 [FOLDED]
23679 [EXCERPT]
23680 [FOLDED]
23681 "
23682 });
23683 for _ in 0..5 {
23684 cx.simulate_keystroke("up");
23685 cx.assert_excerpts_with_selections(indoc! {"
23686 [EXCERPT]
23687 ˇ[FOLDED]
23688 [EXCERPT]
23689 a1
23690 b1
23691 [EXCERPT]
23692 [FOLDED]
23693 [EXCERPT]
23694 [FOLDED]
23695 "
23696 });
23697 }
23698}
23699
23700#[gpui::test]
23701async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23702 init_test(cx, |_| {});
23703
23704 // Simple insertion
23705 assert_highlighted_edits(
23706 "Hello, world!",
23707 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23708 true,
23709 cx,
23710 |highlighted_edits, cx| {
23711 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23712 assert_eq!(highlighted_edits.highlights.len(), 1);
23713 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23714 assert_eq!(
23715 highlighted_edits.highlights[0].1.background_color,
23716 Some(cx.theme().status().created_background)
23717 );
23718 },
23719 )
23720 .await;
23721
23722 // Replacement
23723 assert_highlighted_edits(
23724 "This is a test.",
23725 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23726 false,
23727 cx,
23728 |highlighted_edits, cx| {
23729 assert_eq!(highlighted_edits.text, "That is a test.");
23730 assert_eq!(highlighted_edits.highlights.len(), 1);
23731 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23732 assert_eq!(
23733 highlighted_edits.highlights[0].1.background_color,
23734 Some(cx.theme().status().created_background)
23735 );
23736 },
23737 )
23738 .await;
23739
23740 // Multiple edits
23741 assert_highlighted_edits(
23742 "Hello, world!",
23743 vec![
23744 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23745 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23746 ],
23747 false,
23748 cx,
23749 |highlighted_edits, cx| {
23750 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23751 assert_eq!(highlighted_edits.highlights.len(), 2);
23752 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23753 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23754 assert_eq!(
23755 highlighted_edits.highlights[0].1.background_color,
23756 Some(cx.theme().status().created_background)
23757 );
23758 assert_eq!(
23759 highlighted_edits.highlights[1].1.background_color,
23760 Some(cx.theme().status().created_background)
23761 );
23762 },
23763 )
23764 .await;
23765
23766 // Multiple lines with edits
23767 assert_highlighted_edits(
23768 "First line\nSecond line\nThird line\nFourth line",
23769 vec![
23770 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
23771 (
23772 Point::new(2, 0)..Point::new(2, 10),
23773 "New third line".to_string(),
23774 ),
23775 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
23776 ],
23777 false,
23778 cx,
23779 |highlighted_edits, cx| {
23780 assert_eq!(
23781 highlighted_edits.text,
23782 "Second modified\nNew third line\nFourth updated line"
23783 );
23784 assert_eq!(highlighted_edits.highlights.len(), 3);
23785 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
23786 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
23787 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
23788 for highlight in &highlighted_edits.highlights {
23789 assert_eq!(
23790 highlight.1.background_color,
23791 Some(cx.theme().status().created_background)
23792 );
23793 }
23794 },
23795 )
23796 .await;
23797}
23798
23799#[gpui::test]
23800async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
23801 init_test(cx, |_| {});
23802
23803 // Deletion
23804 assert_highlighted_edits(
23805 "Hello, world!",
23806 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
23807 true,
23808 cx,
23809 |highlighted_edits, cx| {
23810 assert_eq!(highlighted_edits.text, "Hello, world!");
23811 assert_eq!(highlighted_edits.highlights.len(), 1);
23812 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
23813 assert_eq!(
23814 highlighted_edits.highlights[0].1.background_color,
23815 Some(cx.theme().status().deleted_background)
23816 );
23817 },
23818 )
23819 .await;
23820
23821 // Insertion
23822 assert_highlighted_edits(
23823 "Hello, world!",
23824 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
23825 true,
23826 cx,
23827 |highlighted_edits, cx| {
23828 assert_eq!(highlighted_edits.highlights.len(), 1);
23829 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
23830 assert_eq!(
23831 highlighted_edits.highlights[0].1.background_color,
23832 Some(cx.theme().status().created_background)
23833 );
23834 },
23835 )
23836 .await;
23837}
23838
23839async fn assert_highlighted_edits(
23840 text: &str,
23841 edits: Vec<(Range<Point>, String)>,
23842 include_deletions: bool,
23843 cx: &mut TestAppContext,
23844 assertion_fn: impl Fn(HighlightedText, &App),
23845) {
23846 let window = cx.add_window(|window, cx| {
23847 let buffer = MultiBuffer::build_simple(text, cx);
23848 Editor::new(EditorMode::full(), buffer, None, window, cx)
23849 });
23850 let cx = &mut VisualTestContext::from_window(*window, cx);
23851
23852 let (buffer, snapshot) = window
23853 .update(cx, |editor, _window, cx| {
23854 (
23855 editor.buffer().clone(),
23856 editor.buffer().read(cx).snapshot(cx),
23857 )
23858 })
23859 .unwrap();
23860
23861 let edits = edits
23862 .into_iter()
23863 .map(|(range, edit)| {
23864 (
23865 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
23866 edit,
23867 )
23868 })
23869 .collect::<Vec<_>>();
23870
23871 let text_anchor_edits = edits
23872 .clone()
23873 .into_iter()
23874 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
23875 .collect::<Vec<_>>();
23876
23877 let edit_preview = window
23878 .update(cx, |_, _window, cx| {
23879 buffer
23880 .read(cx)
23881 .as_singleton()
23882 .unwrap()
23883 .read(cx)
23884 .preview_edits(text_anchor_edits.into(), cx)
23885 })
23886 .unwrap()
23887 .await;
23888
23889 cx.update(|_window, cx| {
23890 let highlighted_edits = edit_prediction_edit_text(
23891 snapshot.as_singleton().unwrap().2,
23892 &edits,
23893 &edit_preview,
23894 include_deletions,
23895 cx,
23896 );
23897 assertion_fn(highlighted_edits, cx)
23898 });
23899}
23900
23901#[track_caller]
23902fn assert_breakpoint(
23903 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
23904 path: &Arc<Path>,
23905 expected: Vec<(u32, Breakpoint)>,
23906) {
23907 if expected.is_empty() {
23908 assert!(!breakpoints.contains_key(path), "{}", path.display());
23909 } else {
23910 let mut breakpoint = breakpoints
23911 .get(path)
23912 .unwrap()
23913 .iter()
23914 .map(|breakpoint| {
23915 (
23916 breakpoint.row,
23917 Breakpoint {
23918 message: breakpoint.message.clone(),
23919 state: breakpoint.state,
23920 condition: breakpoint.condition.clone(),
23921 hit_condition: breakpoint.hit_condition.clone(),
23922 },
23923 )
23924 })
23925 .collect::<Vec<_>>();
23926
23927 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
23928
23929 assert_eq!(expected, breakpoint);
23930 }
23931}
23932
23933fn add_log_breakpoint_at_cursor(
23934 editor: &mut Editor,
23935 log_message: &str,
23936 window: &mut Window,
23937 cx: &mut Context<Editor>,
23938) {
23939 let (anchor, bp) = editor
23940 .breakpoints_at_cursors(window, cx)
23941 .first()
23942 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
23943 .unwrap_or_else(|| {
23944 let snapshot = editor.snapshot(window, cx);
23945 let cursor_position: Point =
23946 editor.selections.newest(&snapshot.display_snapshot).head();
23947
23948 let breakpoint_position = snapshot
23949 .buffer_snapshot()
23950 .anchor_before(Point::new(cursor_position.row, 0));
23951
23952 (breakpoint_position, Breakpoint::new_log(log_message))
23953 });
23954
23955 editor.edit_breakpoint_at_anchor(
23956 anchor,
23957 bp,
23958 BreakpointEditAction::EditLogMessage(log_message.into()),
23959 cx,
23960 );
23961}
23962
23963#[gpui::test]
23964async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23965 init_test(cx, |_| {});
23966
23967 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23968 let fs = FakeFs::new(cx.executor());
23969 fs.insert_tree(
23970 path!("/a"),
23971 json!({
23972 "main.rs": sample_text,
23973 }),
23974 )
23975 .await;
23976 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23977 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23978 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23979
23980 let fs = FakeFs::new(cx.executor());
23981 fs.insert_tree(
23982 path!("/a"),
23983 json!({
23984 "main.rs": sample_text,
23985 }),
23986 )
23987 .await;
23988 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23989 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23990 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23991 let worktree_id = workspace
23992 .update(cx, |workspace, _window, cx| {
23993 workspace.project().update(cx, |project, cx| {
23994 project.worktrees(cx).next().unwrap().read(cx).id()
23995 })
23996 })
23997 .unwrap();
23998
23999 let buffer = project
24000 .update(cx, |project, cx| {
24001 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24002 })
24003 .await
24004 .unwrap();
24005
24006 let (editor, cx) = cx.add_window_view(|window, cx| {
24007 Editor::new(
24008 EditorMode::full(),
24009 MultiBuffer::build_from_buffer(buffer, cx),
24010 Some(project.clone()),
24011 window,
24012 cx,
24013 )
24014 });
24015
24016 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24017 let abs_path = project.read_with(cx, |project, cx| {
24018 project
24019 .absolute_path(&project_path, cx)
24020 .map(Arc::from)
24021 .unwrap()
24022 });
24023
24024 // assert we can add breakpoint on the first line
24025 editor.update_in(cx, |editor, window, cx| {
24026 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24027 editor.move_to_end(&MoveToEnd, window, cx);
24028 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24029 });
24030
24031 let breakpoints = editor.update(cx, |editor, cx| {
24032 editor
24033 .breakpoint_store()
24034 .as_ref()
24035 .unwrap()
24036 .read(cx)
24037 .all_source_breakpoints(cx)
24038 });
24039
24040 assert_eq!(1, breakpoints.len());
24041 assert_breakpoint(
24042 &breakpoints,
24043 &abs_path,
24044 vec![
24045 (0, Breakpoint::new_standard()),
24046 (3, Breakpoint::new_standard()),
24047 ],
24048 );
24049
24050 editor.update_in(cx, |editor, window, cx| {
24051 editor.move_to_beginning(&MoveToBeginning, window, cx);
24052 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24053 });
24054
24055 let breakpoints = editor.update(cx, |editor, cx| {
24056 editor
24057 .breakpoint_store()
24058 .as_ref()
24059 .unwrap()
24060 .read(cx)
24061 .all_source_breakpoints(cx)
24062 });
24063
24064 assert_eq!(1, breakpoints.len());
24065 assert_breakpoint(
24066 &breakpoints,
24067 &abs_path,
24068 vec![(3, Breakpoint::new_standard())],
24069 );
24070
24071 editor.update_in(cx, |editor, window, cx| {
24072 editor.move_to_end(&MoveToEnd, window, cx);
24073 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24074 });
24075
24076 let breakpoints = editor.update(cx, |editor, cx| {
24077 editor
24078 .breakpoint_store()
24079 .as_ref()
24080 .unwrap()
24081 .read(cx)
24082 .all_source_breakpoints(cx)
24083 });
24084
24085 assert_eq!(0, breakpoints.len());
24086 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24087}
24088
24089#[gpui::test]
24090async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
24091 init_test(cx, |_| {});
24092
24093 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24094
24095 let fs = FakeFs::new(cx.executor());
24096 fs.insert_tree(
24097 path!("/a"),
24098 json!({
24099 "main.rs": sample_text,
24100 }),
24101 )
24102 .await;
24103 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24104 let (workspace, cx) =
24105 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24106
24107 let worktree_id = workspace.update(cx, |workspace, cx| {
24108 workspace.project().update(cx, |project, cx| {
24109 project.worktrees(cx).next().unwrap().read(cx).id()
24110 })
24111 });
24112
24113 let buffer = project
24114 .update(cx, |project, cx| {
24115 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24116 })
24117 .await
24118 .unwrap();
24119
24120 let (editor, cx) = cx.add_window_view(|window, cx| {
24121 Editor::new(
24122 EditorMode::full(),
24123 MultiBuffer::build_from_buffer(buffer, cx),
24124 Some(project.clone()),
24125 window,
24126 cx,
24127 )
24128 });
24129
24130 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24131 let abs_path = project.read_with(cx, |project, cx| {
24132 project
24133 .absolute_path(&project_path, cx)
24134 .map(Arc::from)
24135 .unwrap()
24136 });
24137
24138 editor.update_in(cx, |editor, window, cx| {
24139 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24140 });
24141
24142 let breakpoints = editor.update(cx, |editor, cx| {
24143 editor
24144 .breakpoint_store()
24145 .as_ref()
24146 .unwrap()
24147 .read(cx)
24148 .all_source_breakpoints(cx)
24149 });
24150
24151 assert_breakpoint(
24152 &breakpoints,
24153 &abs_path,
24154 vec![(0, Breakpoint::new_log("hello world"))],
24155 );
24156
24157 // Removing a log message from a log breakpoint should remove it
24158 editor.update_in(cx, |editor, window, cx| {
24159 add_log_breakpoint_at_cursor(editor, "", window, cx);
24160 });
24161
24162 let breakpoints = editor.update(cx, |editor, cx| {
24163 editor
24164 .breakpoint_store()
24165 .as_ref()
24166 .unwrap()
24167 .read(cx)
24168 .all_source_breakpoints(cx)
24169 });
24170
24171 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24172
24173 editor.update_in(cx, |editor, window, cx| {
24174 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24175 editor.move_to_end(&MoveToEnd, window, cx);
24176 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24177 // Not adding a log message to a standard breakpoint shouldn't remove it
24178 add_log_breakpoint_at_cursor(editor, "", window, cx);
24179 });
24180
24181 let breakpoints = editor.update(cx, |editor, cx| {
24182 editor
24183 .breakpoint_store()
24184 .as_ref()
24185 .unwrap()
24186 .read(cx)
24187 .all_source_breakpoints(cx)
24188 });
24189
24190 assert_breakpoint(
24191 &breakpoints,
24192 &abs_path,
24193 vec![
24194 (0, Breakpoint::new_standard()),
24195 (3, Breakpoint::new_standard()),
24196 ],
24197 );
24198
24199 editor.update_in(cx, |editor, window, cx| {
24200 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24201 });
24202
24203 let breakpoints = editor.update(cx, |editor, cx| {
24204 editor
24205 .breakpoint_store()
24206 .as_ref()
24207 .unwrap()
24208 .read(cx)
24209 .all_source_breakpoints(cx)
24210 });
24211
24212 assert_breakpoint(
24213 &breakpoints,
24214 &abs_path,
24215 vec![
24216 (0, Breakpoint::new_standard()),
24217 (3, Breakpoint::new_log("hello world")),
24218 ],
24219 );
24220
24221 editor.update_in(cx, |editor, window, cx| {
24222 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
24223 });
24224
24225 let breakpoints = editor.update(cx, |editor, cx| {
24226 editor
24227 .breakpoint_store()
24228 .as_ref()
24229 .unwrap()
24230 .read(cx)
24231 .all_source_breakpoints(cx)
24232 });
24233
24234 assert_breakpoint(
24235 &breakpoints,
24236 &abs_path,
24237 vec![
24238 (0, Breakpoint::new_standard()),
24239 (3, Breakpoint::new_log("hello Earth!!")),
24240 ],
24241 );
24242}
24243
24244/// This also tests that Editor::breakpoint_at_cursor_head is working properly
24245/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
24246/// or when breakpoints were placed out of order. This tests for a regression too
24247#[gpui::test]
24248async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
24249 init_test(cx, |_| {});
24250
24251 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24252 let fs = FakeFs::new(cx.executor());
24253 fs.insert_tree(
24254 path!("/a"),
24255 json!({
24256 "main.rs": sample_text,
24257 }),
24258 )
24259 .await;
24260 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24261 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24262 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24263
24264 let fs = FakeFs::new(cx.executor());
24265 fs.insert_tree(
24266 path!("/a"),
24267 json!({
24268 "main.rs": sample_text,
24269 }),
24270 )
24271 .await;
24272 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24273 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24274 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24275 let worktree_id = workspace
24276 .update(cx, |workspace, _window, cx| {
24277 workspace.project().update(cx, |project, cx| {
24278 project.worktrees(cx).next().unwrap().read(cx).id()
24279 })
24280 })
24281 .unwrap();
24282
24283 let buffer = project
24284 .update(cx, |project, cx| {
24285 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24286 })
24287 .await
24288 .unwrap();
24289
24290 let (editor, cx) = cx.add_window_view(|window, cx| {
24291 Editor::new(
24292 EditorMode::full(),
24293 MultiBuffer::build_from_buffer(buffer, cx),
24294 Some(project.clone()),
24295 window,
24296 cx,
24297 )
24298 });
24299
24300 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24301 let abs_path = project.read_with(cx, |project, cx| {
24302 project
24303 .absolute_path(&project_path, cx)
24304 .map(Arc::from)
24305 .unwrap()
24306 });
24307
24308 // assert we can add breakpoint on the first line
24309 editor.update_in(cx, |editor, window, cx| {
24310 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24311 editor.move_to_end(&MoveToEnd, window, cx);
24312 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24313 editor.move_up(&MoveUp, window, cx);
24314 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24315 });
24316
24317 let breakpoints = editor.update(cx, |editor, cx| {
24318 editor
24319 .breakpoint_store()
24320 .as_ref()
24321 .unwrap()
24322 .read(cx)
24323 .all_source_breakpoints(cx)
24324 });
24325
24326 assert_eq!(1, breakpoints.len());
24327 assert_breakpoint(
24328 &breakpoints,
24329 &abs_path,
24330 vec![
24331 (0, Breakpoint::new_standard()),
24332 (2, Breakpoint::new_standard()),
24333 (3, Breakpoint::new_standard()),
24334 ],
24335 );
24336
24337 editor.update_in(cx, |editor, window, cx| {
24338 editor.move_to_beginning(&MoveToBeginning, window, cx);
24339 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24340 editor.move_to_end(&MoveToEnd, window, cx);
24341 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24342 // Disabling a breakpoint that doesn't exist should do nothing
24343 editor.move_up(&MoveUp, window, cx);
24344 editor.move_up(&MoveUp, window, cx);
24345 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24346 });
24347
24348 let breakpoints = editor.update(cx, |editor, cx| {
24349 editor
24350 .breakpoint_store()
24351 .as_ref()
24352 .unwrap()
24353 .read(cx)
24354 .all_source_breakpoints(cx)
24355 });
24356
24357 let disable_breakpoint = {
24358 let mut bp = Breakpoint::new_standard();
24359 bp.state = BreakpointState::Disabled;
24360 bp
24361 };
24362
24363 assert_eq!(1, breakpoints.len());
24364 assert_breakpoint(
24365 &breakpoints,
24366 &abs_path,
24367 vec![
24368 (0, disable_breakpoint.clone()),
24369 (2, Breakpoint::new_standard()),
24370 (3, disable_breakpoint.clone()),
24371 ],
24372 );
24373
24374 editor.update_in(cx, |editor, window, cx| {
24375 editor.move_to_beginning(&MoveToBeginning, window, cx);
24376 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24377 editor.move_to_end(&MoveToEnd, window, cx);
24378 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24379 editor.move_up(&MoveUp, window, cx);
24380 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24381 });
24382
24383 let breakpoints = editor.update(cx, |editor, cx| {
24384 editor
24385 .breakpoint_store()
24386 .as_ref()
24387 .unwrap()
24388 .read(cx)
24389 .all_source_breakpoints(cx)
24390 });
24391
24392 assert_eq!(1, breakpoints.len());
24393 assert_breakpoint(
24394 &breakpoints,
24395 &abs_path,
24396 vec![
24397 (0, Breakpoint::new_standard()),
24398 (2, disable_breakpoint),
24399 (3, Breakpoint::new_standard()),
24400 ],
24401 );
24402}
24403
24404#[gpui::test]
24405async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
24406 init_test(cx, |_| {});
24407 let capabilities = lsp::ServerCapabilities {
24408 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
24409 prepare_provider: Some(true),
24410 work_done_progress_options: Default::default(),
24411 })),
24412 ..Default::default()
24413 };
24414 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24415
24416 cx.set_state(indoc! {"
24417 struct Fˇoo {}
24418 "});
24419
24420 cx.update_editor(|editor, _, cx| {
24421 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24422 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24423 editor.highlight_background::<DocumentHighlightRead>(
24424 &[highlight_range],
24425 |_, theme| theme.colors().editor_document_highlight_read_background,
24426 cx,
24427 );
24428 });
24429
24430 let mut prepare_rename_handler = cx
24431 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
24432 move |_, _, _| async move {
24433 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
24434 start: lsp::Position {
24435 line: 0,
24436 character: 7,
24437 },
24438 end: lsp::Position {
24439 line: 0,
24440 character: 10,
24441 },
24442 })))
24443 },
24444 );
24445 let prepare_rename_task = cx
24446 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24447 .expect("Prepare rename was not started");
24448 prepare_rename_handler.next().await.unwrap();
24449 prepare_rename_task.await.expect("Prepare rename failed");
24450
24451 let mut rename_handler =
24452 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24453 let edit = lsp::TextEdit {
24454 range: lsp::Range {
24455 start: lsp::Position {
24456 line: 0,
24457 character: 7,
24458 },
24459 end: lsp::Position {
24460 line: 0,
24461 character: 10,
24462 },
24463 },
24464 new_text: "FooRenamed".to_string(),
24465 };
24466 Ok(Some(lsp::WorkspaceEdit::new(
24467 // Specify the same edit twice
24468 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
24469 )))
24470 });
24471 let rename_task = cx
24472 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24473 .expect("Confirm rename was not started");
24474 rename_handler.next().await.unwrap();
24475 rename_task.await.expect("Confirm rename failed");
24476 cx.run_until_parked();
24477
24478 // Despite two edits, only one is actually applied as those are identical
24479 cx.assert_editor_state(indoc! {"
24480 struct FooRenamedˇ {}
24481 "});
24482}
24483
24484#[gpui::test]
24485async fn test_rename_without_prepare(cx: &mut TestAppContext) {
24486 init_test(cx, |_| {});
24487 // These capabilities indicate that the server does not support prepare rename.
24488 let capabilities = lsp::ServerCapabilities {
24489 rename_provider: Some(lsp::OneOf::Left(true)),
24490 ..Default::default()
24491 };
24492 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24493
24494 cx.set_state(indoc! {"
24495 struct Fˇoo {}
24496 "});
24497
24498 cx.update_editor(|editor, _window, cx| {
24499 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24500 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24501 editor.highlight_background::<DocumentHighlightRead>(
24502 &[highlight_range],
24503 |_, theme| theme.colors().editor_document_highlight_read_background,
24504 cx,
24505 );
24506 });
24507
24508 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24509 .expect("Prepare rename was not started")
24510 .await
24511 .expect("Prepare rename failed");
24512
24513 let mut rename_handler =
24514 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24515 let edit = lsp::TextEdit {
24516 range: lsp::Range {
24517 start: lsp::Position {
24518 line: 0,
24519 character: 7,
24520 },
24521 end: lsp::Position {
24522 line: 0,
24523 character: 10,
24524 },
24525 },
24526 new_text: "FooRenamed".to_string(),
24527 };
24528 Ok(Some(lsp::WorkspaceEdit::new(
24529 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
24530 )))
24531 });
24532 let rename_task = cx
24533 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24534 .expect("Confirm rename was not started");
24535 rename_handler.next().await.unwrap();
24536 rename_task.await.expect("Confirm rename failed");
24537 cx.run_until_parked();
24538
24539 // Correct range is renamed, as `surrounding_word` is used to find it.
24540 cx.assert_editor_state(indoc! {"
24541 struct FooRenamedˇ {}
24542 "});
24543}
24544
24545#[gpui::test]
24546async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
24547 init_test(cx, |_| {});
24548 let mut cx = EditorTestContext::new(cx).await;
24549
24550 let language = Arc::new(
24551 Language::new(
24552 LanguageConfig::default(),
24553 Some(tree_sitter_html::LANGUAGE.into()),
24554 )
24555 .with_brackets_query(
24556 r#"
24557 ("<" @open "/>" @close)
24558 ("</" @open ">" @close)
24559 ("<" @open ">" @close)
24560 ("\"" @open "\"" @close)
24561 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
24562 "#,
24563 )
24564 .unwrap(),
24565 );
24566 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24567
24568 cx.set_state(indoc! {"
24569 <span>ˇ</span>
24570 "});
24571 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24572 cx.assert_editor_state(indoc! {"
24573 <span>
24574 ˇ
24575 </span>
24576 "});
24577
24578 cx.set_state(indoc! {"
24579 <span><span></span>ˇ</span>
24580 "});
24581 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24582 cx.assert_editor_state(indoc! {"
24583 <span><span></span>
24584 ˇ</span>
24585 "});
24586
24587 cx.set_state(indoc! {"
24588 <span>ˇ
24589 </span>
24590 "});
24591 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24592 cx.assert_editor_state(indoc! {"
24593 <span>
24594 ˇ
24595 </span>
24596 "});
24597}
24598
24599#[gpui::test(iterations = 10)]
24600async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
24601 init_test(cx, |_| {});
24602
24603 let fs = FakeFs::new(cx.executor());
24604 fs.insert_tree(
24605 path!("/dir"),
24606 json!({
24607 "a.ts": "a",
24608 }),
24609 )
24610 .await;
24611
24612 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
24613 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24614 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24615
24616 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24617 language_registry.add(Arc::new(Language::new(
24618 LanguageConfig {
24619 name: "TypeScript".into(),
24620 matcher: LanguageMatcher {
24621 path_suffixes: vec!["ts".to_string()],
24622 ..Default::default()
24623 },
24624 ..Default::default()
24625 },
24626 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24627 )));
24628 let mut fake_language_servers = language_registry.register_fake_lsp(
24629 "TypeScript",
24630 FakeLspAdapter {
24631 capabilities: lsp::ServerCapabilities {
24632 code_lens_provider: Some(lsp::CodeLensOptions {
24633 resolve_provider: Some(true),
24634 }),
24635 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24636 commands: vec!["_the/command".to_string()],
24637 ..lsp::ExecuteCommandOptions::default()
24638 }),
24639 ..lsp::ServerCapabilities::default()
24640 },
24641 ..FakeLspAdapter::default()
24642 },
24643 );
24644
24645 let editor = workspace
24646 .update(cx, |workspace, window, cx| {
24647 workspace.open_abs_path(
24648 PathBuf::from(path!("/dir/a.ts")),
24649 OpenOptions::default(),
24650 window,
24651 cx,
24652 )
24653 })
24654 .unwrap()
24655 .await
24656 .unwrap()
24657 .downcast::<Editor>()
24658 .unwrap();
24659 cx.executor().run_until_parked();
24660
24661 let fake_server = fake_language_servers.next().await.unwrap();
24662
24663 let buffer = editor.update(cx, |editor, cx| {
24664 editor
24665 .buffer()
24666 .read(cx)
24667 .as_singleton()
24668 .expect("have opened a single file by path")
24669 });
24670
24671 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24672 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24673 drop(buffer_snapshot);
24674 let actions = cx
24675 .update_window(*workspace, |_, window, cx| {
24676 project.code_actions(&buffer, anchor..anchor, window, cx)
24677 })
24678 .unwrap();
24679
24680 fake_server
24681 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24682 Ok(Some(vec![
24683 lsp::CodeLens {
24684 range: lsp::Range::default(),
24685 command: Some(lsp::Command {
24686 title: "Code lens command".to_owned(),
24687 command: "_the/command".to_owned(),
24688 arguments: None,
24689 }),
24690 data: None,
24691 },
24692 lsp::CodeLens {
24693 range: lsp::Range::default(),
24694 command: Some(lsp::Command {
24695 title: "Command not in capabilities".to_owned(),
24696 command: "not in capabilities".to_owned(),
24697 arguments: None,
24698 }),
24699 data: None,
24700 },
24701 lsp::CodeLens {
24702 range: lsp::Range {
24703 start: lsp::Position {
24704 line: 1,
24705 character: 1,
24706 },
24707 end: lsp::Position {
24708 line: 1,
24709 character: 1,
24710 },
24711 },
24712 command: Some(lsp::Command {
24713 title: "Command not in range".to_owned(),
24714 command: "_the/command".to_owned(),
24715 arguments: None,
24716 }),
24717 data: None,
24718 },
24719 ]))
24720 })
24721 .next()
24722 .await;
24723
24724 let actions = actions.await.unwrap();
24725 assert_eq!(
24726 actions.len(),
24727 1,
24728 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24729 );
24730 let action = actions[0].clone();
24731 let apply = project.update(cx, |project, cx| {
24732 project.apply_code_action(buffer.clone(), action, true, cx)
24733 });
24734
24735 // Resolving the code action does not populate its edits. In absence of
24736 // edits, we must execute the given command.
24737 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24738 |mut lens, _| async move {
24739 let lens_command = lens.command.as_mut().expect("should have a command");
24740 assert_eq!(lens_command.title, "Code lens command");
24741 lens_command.arguments = Some(vec![json!("the-argument")]);
24742 Ok(lens)
24743 },
24744 );
24745
24746 // While executing the command, the language server sends the editor
24747 // a `workspaceEdit` request.
24748 fake_server
24749 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24750 let fake = fake_server.clone();
24751 move |params, _| {
24752 assert_eq!(params.command, "_the/command");
24753 let fake = fake.clone();
24754 async move {
24755 fake.server
24756 .request::<lsp::request::ApplyWorkspaceEdit>(
24757 lsp::ApplyWorkspaceEditParams {
24758 label: None,
24759 edit: lsp::WorkspaceEdit {
24760 changes: Some(
24761 [(
24762 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
24763 vec![lsp::TextEdit {
24764 range: lsp::Range::new(
24765 lsp::Position::new(0, 0),
24766 lsp::Position::new(0, 0),
24767 ),
24768 new_text: "X".into(),
24769 }],
24770 )]
24771 .into_iter()
24772 .collect(),
24773 ),
24774 ..lsp::WorkspaceEdit::default()
24775 },
24776 },
24777 )
24778 .await
24779 .into_response()
24780 .unwrap();
24781 Ok(Some(json!(null)))
24782 }
24783 }
24784 })
24785 .next()
24786 .await;
24787
24788 // Applying the code lens command returns a project transaction containing the edits
24789 // sent by the language server in its `workspaceEdit` request.
24790 let transaction = apply.await.unwrap();
24791 assert!(transaction.0.contains_key(&buffer));
24792 buffer.update(cx, |buffer, cx| {
24793 assert_eq!(buffer.text(), "Xa");
24794 buffer.undo(cx);
24795 assert_eq!(buffer.text(), "a");
24796 });
24797
24798 let actions_after_edits = cx
24799 .update_window(*workspace, |_, window, cx| {
24800 project.code_actions(&buffer, anchor..anchor, window, cx)
24801 })
24802 .unwrap()
24803 .await
24804 .unwrap();
24805 assert_eq!(
24806 actions, actions_after_edits,
24807 "For the same selection, same code lens actions should be returned"
24808 );
24809
24810 let _responses =
24811 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24812 panic!("No more code lens requests are expected");
24813 });
24814 editor.update_in(cx, |editor, window, cx| {
24815 editor.select_all(&SelectAll, window, cx);
24816 });
24817 cx.executor().run_until_parked();
24818 let new_actions = cx
24819 .update_window(*workspace, |_, window, cx| {
24820 project.code_actions(&buffer, anchor..anchor, window, cx)
24821 })
24822 .unwrap()
24823 .await
24824 .unwrap();
24825 assert_eq!(
24826 actions, new_actions,
24827 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
24828 );
24829}
24830
24831#[gpui::test]
24832async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
24833 init_test(cx, |_| {});
24834
24835 let fs = FakeFs::new(cx.executor());
24836 let main_text = r#"fn main() {
24837println!("1");
24838println!("2");
24839println!("3");
24840println!("4");
24841println!("5");
24842}"#;
24843 let lib_text = "mod foo {}";
24844 fs.insert_tree(
24845 path!("/a"),
24846 json!({
24847 "lib.rs": lib_text,
24848 "main.rs": main_text,
24849 }),
24850 )
24851 .await;
24852
24853 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24854 let (workspace, cx) =
24855 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24856 let worktree_id = workspace.update(cx, |workspace, cx| {
24857 workspace.project().update(cx, |project, cx| {
24858 project.worktrees(cx).next().unwrap().read(cx).id()
24859 })
24860 });
24861
24862 let expected_ranges = vec![
24863 Point::new(0, 0)..Point::new(0, 0),
24864 Point::new(1, 0)..Point::new(1, 1),
24865 Point::new(2, 0)..Point::new(2, 2),
24866 Point::new(3, 0)..Point::new(3, 3),
24867 ];
24868
24869 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24870 let editor_1 = workspace
24871 .update_in(cx, |workspace, window, cx| {
24872 workspace.open_path(
24873 (worktree_id, rel_path("main.rs")),
24874 Some(pane_1.downgrade()),
24875 true,
24876 window,
24877 cx,
24878 )
24879 })
24880 .unwrap()
24881 .await
24882 .downcast::<Editor>()
24883 .unwrap();
24884 pane_1.update(cx, |pane, cx| {
24885 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24886 open_editor.update(cx, |editor, cx| {
24887 assert_eq!(
24888 editor.display_text(cx),
24889 main_text,
24890 "Original main.rs text on initial open",
24891 );
24892 assert_eq!(
24893 editor
24894 .selections
24895 .all::<Point>(&editor.display_snapshot(cx))
24896 .into_iter()
24897 .map(|s| s.range())
24898 .collect::<Vec<_>>(),
24899 vec![Point::zero()..Point::zero()],
24900 "Default selections on initial open",
24901 );
24902 })
24903 });
24904 editor_1.update_in(cx, |editor, window, cx| {
24905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24906 s.select_ranges(expected_ranges.clone());
24907 });
24908 });
24909
24910 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
24911 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
24912 });
24913 let editor_2 = workspace
24914 .update_in(cx, |workspace, window, cx| {
24915 workspace.open_path(
24916 (worktree_id, rel_path("main.rs")),
24917 Some(pane_2.downgrade()),
24918 true,
24919 window,
24920 cx,
24921 )
24922 })
24923 .unwrap()
24924 .await
24925 .downcast::<Editor>()
24926 .unwrap();
24927 pane_2.update(cx, |pane, cx| {
24928 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24929 open_editor.update(cx, |editor, cx| {
24930 assert_eq!(
24931 editor.display_text(cx),
24932 main_text,
24933 "Original main.rs text on initial open in another panel",
24934 );
24935 assert_eq!(
24936 editor
24937 .selections
24938 .all::<Point>(&editor.display_snapshot(cx))
24939 .into_iter()
24940 .map(|s| s.range())
24941 .collect::<Vec<_>>(),
24942 vec![Point::zero()..Point::zero()],
24943 "Default selections on initial open in another panel",
24944 );
24945 })
24946 });
24947
24948 editor_2.update_in(cx, |editor, window, cx| {
24949 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
24950 });
24951
24952 let _other_editor_1 = workspace
24953 .update_in(cx, |workspace, window, cx| {
24954 workspace.open_path(
24955 (worktree_id, rel_path("lib.rs")),
24956 Some(pane_1.downgrade()),
24957 true,
24958 window,
24959 cx,
24960 )
24961 })
24962 .unwrap()
24963 .await
24964 .downcast::<Editor>()
24965 .unwrap();
24966 pane_1
24967 .update_in(cx, |pane, window, cx| {
24968 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24969 })
24970 .await
24971 .unwrap();
24972 drop(editor_1);
24973 pane_1.update(cx, |pane, cx| {
24974 pane.active_item()
24975 .unwrap()
24976 .downcast::<Editor>()
24977 .unwrap()
24978 .update(cx, |editor, cx| {
24979 assert_eq!(
24980 editor.display_text(cx),
24981 lib_text,
24982 "Other file should be open and active",
24983 );
24984 });
24985 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24986 });
24987
24988 let _other_editor_2 = workspace
24989 .update_in(cx, |workspace, window, cx| {
24990 workspace.open_path(
24991 (worktree_id, rel_path("lib.rs")),
24992 Some(pane_2.downgrade()),
24993 true,
24994 window,
24995 cx,
24996 )
24997 })
24998 .unwrap()
24999 .await
25000 .downcast::<Editor>()
25001 .unwrap();
25002 pane_2
25003 .update_in(cx, |pane, window, cx| {
25004 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
25005 })
25006 .await
25007 .unwrap();
25008 drop(editor_2);
25009 pane_2.update(cx, |pane, cx| {
25010 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25011 open_editor.update(cx, |editor, cx| {
25012 assert_eq!(
25013 editor.display_text(cx),
25014 lib_text,
25015 "Other file should be open and active in another panel too",
25016 );
25017 });
25018 assert_eq!(
25019 pane.items().count(),
25020 1,
25021 "No other editors should be open in another pane",
25022 );
25023 });
25024
25025 let _editor_1_reopened = workspace
25026 .update_in(cx, |workspace, window, cx| {
25027 workspace.open_path(
25028 (worktree_id, rel_path("main.rs")),
25029 Some(pane_1.downgrade()),
25030 true,
25031 window,
25032 cx,
25033 )
25034 })
25035 .unwrap()
25036 .await
25037 .downcast::<Editor>()
25038 .unwrap();
25039 let _editor_2_reopened = workspace
25040 .update_in(cx, |workspace, window, cx| {
25041 workspace.open_path(
25042 (worktree_id, rel_path("main.rs")),
25043 Some(pane_2.downgrade()),
25044 true,
25045 window,
25046 cx,
25047 )
25048 })
25049 .unwrap()
25050 .await
25051 .downcast::<Editor>()
25052 .unwrap();
25053 pane_1.update(cx, |pane, cx| {
25054 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25055 open_editor.update(cx, |editor, cx| {
25056 assert_eq!(
25057 editor.display_text(cx),
25058 main_text,
25059 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
25060 );
25061 assert_eq!(
25062 editor
25063 .selections
25064 .all::<Point>(&editor.display_snapshot(cx))
25065 .into_iter()
25066 .map(|s| s.range())
25067 .collect::<Vec<_>>(),
25068 expected_ranges,
25069 "Previous editor in the 1st panel had selections and should get them restored on reopen",
25070 );
25071 })
25072 });
25073 pane_2.update(cx, |pane, cx| {
25074 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25075 open_editor.update(cx, |editor, cx| {
25076 assert_eq!(
25077 editor.display_text(cx),
25078 r#"fn main() {
25079⋯rintln!("1");
25080⋯intln!("2");
25081⋯ntln!("3");
25082println!("4");
25083println!("5");
25084}"#,
25085 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
25086 );
25087 assert_eq!(
25088 editor
25089 .selections
25090 .all::<Point>(&editor.display_snapshot(cx))
25091 .into_iter()
25092 .map(|s| s.range())
25093 .collect::<Vec<_>>(),
25094 vec![Point::zero()..Point::zero()],
25095 "Previous editor in the 2nd pane had no selections changed hence should restore none",
25096 );
25097 })
25098 });
25099}
25100
25101#[gpui::test]
25102async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
25103 init_test(cx, |_| {});
25104
25105 let fs = FakeFs::new(cx.executor());
25106 let main_text = r#"fn main() {
25107println!("1");
25108println!("2");
25109println!("3");
25110println!("4");
25111println!("5");
25112}"#;
25113 let lib_text = "mod foo {}";
25114 fs.insert_tree(
25115 path!("/a"),
25116 json!({
25117 "lib.rs": lib_text,
25118 "main.rs": main_text,
25119 }),
25120 )
25121 .await;
25122
25123 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25124 let (workspace, cx) =
25125 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25126 let worktree_id = workspace.update(cx, |workspace, cx| {
25127 workspace.project().update(cx, |project, cx| {
25128 project.worktrees(cx).next().unwrap().read(cx).id()
25129 })
25130 });
25131
25132 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25133 let editor = workspace
25134 .update_in(cx, |workspace, window, cx| {
25135 workspace.open_path(
25136 (worktree_id, rel_path("main.rs")),
25137 Some(pane.downgrade()),
25138 true,
25139 window,
25140 cx,
25141 )
25142 })
25143 .unwrap()
25144 .await
25145 .downcast::<Editor>()
25146 .unwrap();
25147 pane.update(cx, |pane, cx| {
25148 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25149 open_editor.update(cx, |editor, cx| {
25150 assert_eq!(
25151 editor.display_text(cx),
25152 main_text,
25153 "Original main.rs text on initial open",
25154 );
25155 })
25156 });
25157 editor.update_in(cx, |editor, window, cx| {
25158 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
25159 });
25160
25161 cx.update_global(|store: &mut SettingsStore, cx| {
25162 store.update_user_settings(cx, |s| {
25163 s.workspace.restore_on_file_reopen = Some(false);
25164 });
25165 });
25166 editor.update_in(cx, |editor, window, cx| {
25167 editor.fold_ranges(
25168 vec![
25169 Point::new(1, 0)..Point::new(1, 1),
25170 Point::new(2, 0)..Point::new(2, 2),
25171 Point::new(3, 0)..Point::new(3, 3),
25172 ],
25173 false,
25174 window,
25175 cx,
25176 );
25177 });
25178 pane.update_in(cx, |pane, window, cx| {
25179 pane.close_all_items(&CloseAllItems::default(), window, cx)
25180 })
25181 .await
25182 .unwrap();
25183 pane.update(cx, |pane, _| {
25184 assert!(pane.active_item().is_none());
25185 });
25186 cx.update_global(|store: &mut SettingsStore, cx| {
25187 store.update_user_settings(cx, |s| {
25188 s.workspace.restore_on_file_reopen = Some(true);
25189 });
25190 });
25191
25192 let _editor_reopened = workspace
25193 .update_in(cx, |workspace, window, cx| {
25194 workspace.open_path(
25195 (worktree_id, rel_path("main.rs")),
25196 Some(pane.downgrade()),
25197 true,
25198 window,
25199 cx,
25200 )
25201 })
25202 .unwrap()
25203 .await
25204 .downcast::<Editor>()
25205 .unwrap();
25206 pane.update(cx, |pane, cx| {
25207 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25208 open_editor.update(cx, |editor, cx| {
25209 assert_eq!(
25210 editor.display_text(cx),
25211 main_text,
25212 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
25213 );
25214 })
25215 });
25216}
25217
25218#[gpui::test]
25219async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
25220 struct EmptyModalView {
25221 focus_handle: gpui::FocusHandle,
25222 }
25223 impl EventEmitter<DismissEvent> for EmptyModalView {}
25224 impl Render for EmptyModalView {
25225 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
25226 div()
25227 }
25228 }
25229 impl Focusable for EmptyModalView {
25230 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
25231 self.focus_handle.clone()
25232 }
25233 }
25234 impl workspace::ModalView for EmptyModalView {}
25235 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
25236 EmptyModalView {
25237 focus_handle: cx.focus_handle(),
25238 }
25239 }
25240
25241 init_test(cx, |_| {});
25242
25243 let fs = FakeFs::new(cx.executor());
25244 let project = Project::test(fs, [], cx).await;
25245 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25246 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
25247 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
25248 let editor = cx.new_window_entity(|window, cx| {
25249 Editor::new(
25250 EditorMode::full(),
25251 buffer,
25252 Some(project.clone()),
25253 window,
25254 cx,
25255 )
25256 });
25257 workspace
25258 .update(cx, |workspace, window, cx| {
25259 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
25260 })
25261 .unwrap();
25262 editor.update_in(cx, |editor, window, cx| {
25263 editor.open_context_menu(&OpenContextMenu, window, cx);
25264 assert!(editor.mouse_context_menu.is_some());
25265 });
25266 workspace
25267 .update(cx, |workspace, window, cx| {
25268 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
25269 })
25270 .unwrap();
25271 cx.read(|cx| {
25272 assert!(editor.read(cx).mouse_context_menu.is_none());
25273 });
25274}
25275
25276fn set_linked_edit_ranges(
25277 opening: (Point, Point),
25278 closing: (Point, Point),
25279 editor: &mut Editor,
25280 cx: &mut Context<Editor>,
25281) {
25282 let Some((buffer, _)) = editor
25283 .buffer
25284 .read(cx)
25285 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
25286 else {
25287 panic!("Failed to get buffer for selection position");
25288 };
25289 let buffer = buffer.read(cx);
25290 let buffer_id = buffer.remote_id();
25291 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
25292 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
25293 let mut linked_ranges = HashMap::default();
25294 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
25295 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
25296}
25297
25298#[gpui::test]
25299async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
25300 init_test(cx, |_| {});
25301
25302 let fs = FakeFs::new(cx.executor());
25303 fs.insert_file(path!("/file.html"), Default::default())
25304 .await;
25305
25306 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
25307
25308 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25309 let html_language = Arc::new(Language::new(
25310 LanguageConfig {
25311 name: "HTML".into(),
25312 matcher: LanguageMatcher {
25313 path_suffixes: vec!["html".to_string()],
25314 ..LanguageMatcher::default()
25315 },
25316 brackets: BracketPairConfig {
25317 pairs: vec![BracketPair {
25318 start: "<".into(),
25319 end: ">".into(),
25320 close: true,
25321 ..Default::default()
25322 }],
25323 ..Default::default()
25324 },
25325 ..Default::default()
25326 },
25327 Some(tree_sitter_html::LANGUAGE.into()),
25328 ));
25329 language_registry.add(html_language);
25330 let mut fake_servers = language_registry.register_fake_lsp(
25331 "HTML",
25332 FakeLspAdapter {
25333 capabilities: lsp::ServerCapabilities {
25334 completion_provider: Some(lsp::CompletionOptions {
25335 resolve_provider: Some(true),
25336 ..Default::default()
25337 }),
25338 ..Default::default()
25339 },
25340 ..Default::default()
25341 },
25342 );
25343
25344 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25345 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25346
25347 let worktree_id = workspace
25348 .update(cx, |workspace, _window, cx| {
25349 workspace.project().update(cx, |project, cx| {
25350 project.worktrees(cx).next().unwrap().read(cx).id()
25351 })
25352 })
25353 .unwrap();
25354 project
25355 .update(cx, |project, cx| {
25356 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
25357 })
25358 .await
25359 .unwrap();
25360 let editor = workspace
25361 .update(cx, |workspace, window, cx| {
25362 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
25363 })
25364 .unwrap()
25365 .await
25366 .unwrap()
25367 .downcast::<Editor>()
25368 .unwrap();
25369
25370 let fake_server = fake_servers.next().await.unwrap();
25371 editor.update_in(cx, |editor, window, cx| {
25372 editor.set_text("<ad></ad>", window, cx);
25373 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
25374 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
25375 });
25376 set_linked_edit_ranges(
25377 (Point::new(0, 1), Point::new(0, 3)),
25378 (Point::new(0, 6), Point::new(0, 8)),
25379 editor,
25380 cx,
25381 );
25382 });
25383 let mut completion_handle =
25384 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25385 Ok(Some(lsp::CompletionResponse::Array(vec![
25386 lsp::CompletionItem {
25387 label: "head".to_string(),
25388 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25389 lsp::InsertReplaceEdit {
25390 new_text: "head".to_string(),
25391 insert: lsp::Range::new(
25392 lsp::Position::new(0, 1),
25393 lsp::Position::new(0, 3),
25394 ),
25395 replace: lsp::Range::new(
25396 lsp::Position::new(0, 1),
25397 lsp::Position::new(0, 3),
25398 ),
25399 },
25400 )),
25401 ..Default::default()
25402 },
25403 ])))
25404 });
25405 editor.update_in(cx, |editor, window, cx| {
25406 editor.show_completions(&ShowCompletions, window, cx);
25407 });
25408 cx.run_until_parked();
25409 completion_handle.next().await.unwrap();
25410 editor.update(cx, |editor, _| {
25411 assert!(
25412 editor.context_menu_visible(),
25413 "Completion menu should be visible"
25414 );
25415 });
25416 editor.update_in(cx, |editor, window, cx| {
25417 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
25418 });
25419 cx.executor().run_until_parked();
25420 editor.update(cx, |editor, cx| {
25421 assert_eq!(editor.text(cx), "<head></head>");
25422 });
25423}
25424
25425#[gpui::test]
25426async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
25427 init_test(cx, |_| {});
25428
25429 let mut cx = EditorTestContext::new(cx).await;
25430 let language = Arc::new(Language::new(
25431 LanguageConfig {
25432 name: "TSX".into(),
25433 matcher: LanguageMatcher {
25434 path_suffixes: vec!["tsx".to_string()],
25435 ..LanguageMatcher::default()
25436 },
25437 brackets: BracketPairConfig {
25438 pairs: vec![BracketPair {
25439 start: "<".into(),
25440 end: ">".into(),
25441 close: true,
25442 ..Default::default()
25443 }],
25444 ..Default::default()
25445 },
25446 linked_edit_characters: HashSet::from_iter(['.']),
25447 ..Default::default()
25448 },
25449 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
25450 ));
25451 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25452
25453 // Test typing > does not extend linked pair
25454 cx.set_state("<divˇ<div></div>");
25455 cx.update_editor(|editor, _, cx| {
25456 set_linked_edit_ranges(
25457 (Point::new(0, 1), Point::new(0, 4)),
25458 (Point::new(0, 11), Point::new(0, 14)),
25459 editor,
25460 cx,
25461 );
25462 });
25463 cx.update_editor(|editor, window, cx| {
25464 editor.handle_input(">", window, cx);
25465 });
25466 cx.assert_editor_state("<div>ˇ<div></div>");
25467
25468 // Test typing . do extend linked pair
25469 cx.set_state("<Animatedˇ></Animated>");
25470 cx.update_editor(|editor, _, cx| {
25471 set_linked_edit_ranges(
25472 (Point::new(0, 1), Point::new(0, 9)),
25473 (Point::new(0, 12), Point::new(0, 20)),
25474 editor,
25475 cx,
25476 );
25477 });
25478 cx.update_editor(|editor, window, cx| {
25479 editor.handle_input(".", window, cx);
25480 });
25481 cx.assert_editor_state("<Animated.ˇ></Animated.>");
25482 cx.update_editor(|editor, _, cx| {
25483 set_linked_edit_ranges(
25484 (Point::new(0, 1), Point::new(0, 10)),
25485 (Point::new(0, 13), Point::new(0, 21)),
25486 editor,
25487 cx,
25488 );
25489 });
25490 cx.update_editor(|editor, window, cx| {
25491 editor.handle_input("V", window, cx);
25492 });
25493 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
25494}
25495
25496#[gpui::test]
25497async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
25498 init_test(cx, |_| {});
25499
25500 let fs = FakeFs::new(cx.executor());
25501 fs.insert_tree(
25502 path!("/root"),
25503 json!({
25504 "a": {
25505 "main.rs": "fn main() {}",
25506 },
25507 "foo": {
25508 "bar": {
25509 "external_file.rs": "pub mod external {}",
25510 }
25511 }
25512 }),
25513 )
25514 .await;
25515
25516 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
25517 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25518 language_registry.add(rust_lang());
25519 let _fake_servers = language_registry.register_fake_lsp(
25520 "Rust",
25521 FakeLspAdapter {
25522 ..FakeLspAdapter::default()
25523 },
25524 );
25525 let (workspace, cx) =
25526 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25527 let worktree_id = workspace.update(cx, |workspace, cx| {
25528 workspace.project().update(cx, |project, cx| {
25529 project.worktrees(cx).next().unwrap().read(cx).id()
25530 })
25531 });
25532
25533 let assert_language_servers_count =
25534 |expected: usize, context: &str, cx: &mut VisualTestContext| {
25535 project.update(cx, |project, cx| {
25536 let current = project
25537 .lsp_store()
25538 .read(cx)
25539 .as_local()
25540 .unwrap()
25541 .language_servers
25542 .len();
25543 assert_eq!(expected, current, "{context}");
25544 });
25545 };
25546
25547 assert_language_servers_count(
25548 0,
25549 "No servers should be running before any file is open",
25550 cx,
25551 );
25552 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25553 let main_editor = workspace
25554 .update_in(cx, |workspace, window, cx| {
25555 workspace.open_path(
25556 (worktree_id, rel_path("main.rs")),
25557 Some(pane.downgrade()),
25558 true,
25559 window,
25560 cx,
25561 )
25562 })
25563 .unwrap()
25564 .await
25565 .downcast::<Editor>()
25566 .unwrap();
25567 pane.update(cx, |pane, cx| {
25568 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25569 open_editor.update(cx, |editor, cx| {
25570 assert_eq!(
25571 editor.display_text(cx),
25572 "fn main() {}",
25573 "Original main.rs text on initial open",
25574 );
25575 });
25576 assert_eq!(open_editor, main_editor);
25577 });
25578 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
25579
25580 let external_editor = workspace
25581 .update_in(cx, |workspace, window, cx| {
25582 workspace.open_abs_path(
25583 PathBuf::from("/root/foo/bar/external_file.rs"),
25584 OpenOptions::default(),
25585 window,
25586 cx,
25587 )
25588 })
25589 .await
25590 .expect("opening external file")
25591 .downcast::<Editor>()
25592 .expect("downcasted external file's open element to editor");
25593 pane.update(cx, |pane, cx| {
25594 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25595 open_editor.update(cx, |editor, cx| {
25596 assert_eq!(
25597 editor.display_text(cx),
25598 "pub mod external {}",
25599 "External file is open now",
25600 );
25601 });
25602 assert_eq!(open_editor, external_editor);
25603 });
25604 assert_language_servers_count(
25605 1,
25606 "Second, external, *.rs file should join the existing server",
25607 cx,
25608 );
25609
25610 pane.update_in(cx, |pane, window, cx| {
25611 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25612 })
25613 .await
25614 .unwrap();
25615 pane.update_in(cx, |pane, window, cx| {
25616 pane.navigate_backward(&Default::default(), window, cx);
25617 });
25618 cx.run_until_parked();
25619 pane.update(cx, |pane, cx| {
25620 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25621 open_editor.update(cx, |editor, cx| {
25622 assert_eq!(
25623 editor.display_text(cx),
25624 "pub mod external {}",
25625 "External file is open now",
25626 );
25627 });
25628 });
25629 assert_language_servers_count(
25630 1,
25631 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25632 cx,
25633 );
25634
25635 cx.update(|_, cx| {
25636 workspace::reload(cx);
25637 });
25638 assert_language_servers_count(
25639 1,
25640 "After reloading the worktree with local and external files opened, only one project should be started",
25641 cx,
25642 );
25643}
25644
25645#[gpui::test]
25646async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25647 init_test(cx, |_| {});
25648
25649 let mut cx = EditorTestContext::new(cx).await;
25650 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25651 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25652
25653 // test cursor move to start of each line on tab
25654 // for `if`, `elif`, `else`, `while`, `with` and `for`
25655 cx.set_state(indoc! {"
25656 def main():
25657 ˇ for item in items:
25658 ˇ while item.active:
25659 ˇ if item.value > 10:
25660 ˇ continue
25661 ˇ elif item.value < 0:
25662 ˇ break
25663 ˇ else:
25664 ˇ with item.context() as ctx:
25665 ˇ yield count
25666 ˇ else:
25667 ˇ log('while else')
25668 ˇ else:
25669 ˇ log('for else')
25670 "});
25671 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25672 cx.wait_for_autoindent_applied().await;
25673 cx.assert_editor_state(indoc! {"
25674 def main():
25675 ˇfor item in items:
25676 ˇwhile item.active:
25677 ˇif item.value > 10:
25678 ˇcontinue
25679 ˇelif item.value < 0:
25680 ˇbreak
25681 ˇelse:
25682 ˇwith item.context() as ctx:
25683 ˇyield count
25684 ˇelse:
25685 ˇlog('while else')
25686 ˇelse:
25687 ˇlog('for else')
25688 "});
25689 // test relative indent is preserved when tab
25690 // for `if`, `elif`, `else`, `while`, `with` and `for`
25691 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25692 cx.wait_for_autoindent_applied().await;
25693 cx.assert_editor_state(indoc! {"
25694 def main():
25695 ˇfor item in items:
25696 ˇwhile item.active:
25697 ˇif item.value > 10:
25698 ˇcontinue
25699 ˇelif item.value < 0:
25700 ˇbreak
25701 ˇelse:
25702 ˇwith item.context() as ctx:
25703 ˇyield count
25704 ˇelse:
25705 ˇlog('while else')
25706 ˇelse:
25707 ˇlog('for else')
25708 "});
25709
25710 // test cursor move to start of each line on tab
25711 // for `try`, `except`, `else`, `finally`, `match` and `def`
25712 cx.set_state(indoc! {"
25713 def main():
25714 ˇ try:
25715 ˇ fetch()
25716 ˇ except ValueError:
25717 ˇ handle_error()
25718 ˇ else:
25719 ˇ match value:
25720 ˇ case _:
25721 ˇ finally:
25722 ˇ def status():
25723 ˇ return 0
25724 "});
25725 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25726 cx.wait_for_autoindent_applied().await;
25727 cx.assert_editor_state(indoc! {"
25728 def main():
25729 ˇtry:
25730 ˇfetch()
25731 ˇexcept ValueError:
25732 ˇhandle_error()
25733 ˇelse:
25734 ˇmatch value:
25735 ˇcase _:
25736 ˇfinally:
25737 ˇdef status():
25738 ˇreturn 0
25739 "});
25740 // test relative indent is preserved when tab
25741 // for `try`, `except`, `else`, `finally`, `match` and `def`
25742 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25743 cx.wait_for_autoindent_applied().await;
25744 cx.assert_editor_state(indoc! {"
25745 def main():
25746 ˇtry:
25747 ˇfetch()
25748 ˇexcept ValueError:
25749 ˇhandle_error()
25750 ˇelse:
25751 ˇmatch value:
25752 ˇcase _:
25753 ˇfinally:
25754 ˇdef status():
25755 ˇreturn 0
25756 "});
25757}
25758
25759#[gpui::test]
25760async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
25761 init_test(cx, |_| {});
25762
25763 let mut cx = EditorTestContext::new(cx).await;
25764 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25765 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25766
25767 // test `else` auto outdents when typed inside `if` block
25768 cx.set_state(indoc! {"
25769 def main():
25770 if i == 2:
25771 return
25772 ˇ
25773 "});
25774 cx.update_editor(|editor, window, cx| {
25775 editor.handle_input("else:", window, cx);
25776 });
25777 cx.wait_for_autoindent_applied().await;
25778 cx.assert_editor_state(indoc! {"
25779 def main():
25780 if i == 2:
25781 return
25782 else:ˇ
25783 "});
25784
25785 // test `except` auto outdents when typed inside `try` block
25786 cx.set_state(indoc! {"
25787 def main():
25788 try:
25789 i = 2
25790 ˇ
25791 "});
25792 cx.update_editor(|editor, window, cx| {
25793 editor.handle_input("except:", window, cx);
25794 });
25795 cx.wait_for_autoindent_applied().await;
25796 cx.assert_editor_state(indoc! {"
25797 def main():
25798 try:
25799 i = 2
25800 except:ˇ
25801 "});
25802
25803 // test `else` auto outdents when typed inside `except` block
25804 cx.set_state(indoc! {"
25805 def main():
25806 try:
25807 i = 2
25808 except:
25809 j = 2
25810 ˇ
25811 "});
25812 cx.update_editor(|editor, window, cx| {
25813 editor.handle_input("else:", window, cx);
25814 });
25815 cx.wait_for_autoindent_applied().await;
25816 cx.assert_editor_state(indoc! {"
25817 def main():
25818 try:
25819 i = 2
25820 except:
25821 j = 2
25822 else:ˇ
25823 "});
25824
25825 // test `finally` auto outdents when typed inside `else` block
25826 cx.set_state(indoc! {"
25827 def main():
25828 try:
25829 i = 2
25830 except:
25831 j = 2
25832 else:
25833 k = 2
25834 ˇ
25835 "});
25836 cx.update_editor(|editor, window, cx| {
25837 editor.handle_input("finally:", window, cx);
25838 });
25839 cx.wait_for_autoindent_applied().await;
25840 cx.assert_editor_state(indoc! {"
25841 def main():
25842 try:
25843 i = 2
25844 except:
25845 j = 2
25846 else:
25847 k = 2
25848 finally:ˇ
25849 "});
25850
25851 // test `else` does not outdents when typed inside `except` block right after for block
25852 cx.set_state(indoc! {"
25853 def main():
25854 try:
25855 i = 2
25856 except:
25857 for i in range(n):
25858 pass
25859 ˇ
25860 "});
25861 cx.update_editor(|editor, window, cx| {
25862 editor.handle_input("else:", window, cx);
25863 });
25864 cx.wait_for_autoindent_applied().await;
25865 cx.assert_editor_state(indoc! {"
25866 def main():
25867 try:
25868 i = 2
25869 except:
25870 for i in range(n):
25871 pass
25872 else:ˇ
25873 "});
25874
25875 // test `finally` auto outdents when typed inside `else` block right after for block
25876 cx.set_state(indoc! {"
25877 def main():
25878 try:
25879 i = 2
25880 except:
25881 j = 2
25882 else:
25883 for i in range(n):
25884 pass
25885 ˇ
25886 "});
25887 cx.update_editor(|editor, window, cx| {
25888 editor.handle_input("finally:", window, cx);
25889 });
25890 cx.wait_for_autoindent_applied().await;
25891 cx.assert_editor_state(indoc! {"
25892 def main():
25893 try:
25894 i = 2
25895 except:
25896 j = 2
25897 else:
25898 for i in range(n):
25899 pass
25900 finally:ˇ
25901 "});
25902
25903 // test `except` outdents to inner "try" block
25904 cx.set_state(indoc! {"
25905 def main():
25906 try:
25907 i = 2
25908 if i == 2:
25909 try:
25910 i = 3
25911 ˇ
25912 "});
25913 cx.update_editor(|editor, window, cx| {
25914 editor.handle_input("except:", window, cx);
25915 });
25916 cx.wait_for_autoindent_applied().await;
25917 cx.assert_editor_state(indoc! {"
25918 def main():
25919 try:
25920 i = 2
25921 if i == 2:
25922 try:
25923 i = 3
25924 except:ˇ
25925 "});
25926
25927 // test `except` outdents to outer "try" block
25928 cx.set_state(indoc! {"
25929 def main():
25930 try:
25931 i = 2
25932 if i == 2:
25933 try:
25934 i = 3
25935 ˇ
25936 "});
25937 cx.update_editor(|editor, window, cx| {
25938 editor.handle_input("except:", window, cx);
25939 });
25940 cx.wait_for_autoindent_applied().await;
25941 cx.assert_editor_state(indoc! {"
25942 def main():
25943 try:
25944 i = 2
25945 if i == 2:
25946 try:
25947 i = 3
25948 except:ˇ
25949 "});
25950
25951 // test `else` stays at correct indent when typed after `for` block
25952 cx.set_state(indoc! {"
25953 def main():
25954 for i in range(10):
25955 if i == 3:
25956 break
25957 ˇ
25958 "});
25959 cx.update_editor(|editor, window, cx| {
25960 editor.handle_input("else:", window, cx);
25961 });
25962 cx.wait_for_autoindent_applied().await;
25963 cx.assert_editor_state(indoc! {"
25964 def main():
25965 for i in range(10):
25966 if i == 3:
25967 break
25968 else:ˇ
25969 "});
25970
25971 // test does not outdent on typing after line with square brackets
25972 cx.set_state(indoc! {"
25973 def f() -> list[str]:
25974 ˇ
25975 "});
25976 cx.update_editor(|editor, window, cx| {
25977 editor.handle_input("a", window, cx);
25978 });
25979 cx.wait_for_autoindent_applied().await;
25980 cx.assert_editor_state(indoc! {"
25981 def f() -> list[str]:
25982 aˇ
25983 "});
25984
25985 // test does not outdent on typing : after case keyword
25986 cx.set_state(indoc! {"
25987 match 1:
25988 caseˇ
25989 "});
25990 cx.update_editor(|editor, window, cx| {
25991 editor.handle_input(":", window, cx);
25992 });
25993 cx.wait_for_autoindent_applied().await;
25994 cx.assert_editor_state(indoc! {"
25995 match 1:
25996 case:ˇ
25997 "});
25998}
25999
26000#[gpui::test]
26001async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
26002 init_test(cx, |_| {});
26003 update_test_language_settings(cx, |settings| {
26004 settings.defaults.extend_comment_on_newline = Some(false);
26005 });
26006 let mut cx = EditorTestContext::new(cx).await;
26007 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26008 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26009
26010 // test correct indent after newline on comment
26011 cx.set_state(indoc! {"
26012 # COMMENT:ˇ
26013 "});
26014 cx.update_editor(|editor, window, cx| {
26015 editor.newline(&Newline, window, cx);
26016 });
26017 cx.wait_for_autoindent_applied().await;
26018 cx.assert_editor_state(indoc! {"
26019 # COMMENT:
26020 ˇ
26021 "});
26022
26023 // test correct indent after newline in brackets
26024 cx.set_state(indoc! {"
26025 {ˇ}
26026 "});
26027 cx.update_editor(|editor, window, cx| {
26028 editor.newline(&Newline, window, cx);
26029 });
26030 cx.wait_for_autoindent_applied().await;
26031 cx.assert_editor_state(indoc! {"
26032 {
26033 ˇ
26034 }
26035 "});
26036
26037 cx.set_state(indoc! {"
26038 (ˇ)
26039 "});
26040 cx.update_editor(|editor, window, cx| {
26041 editor.newline(&Newline, window, cx);
26042 });
26043 cx.run_until_parked();
26044 cx.assert_editor_state(indoc! {"
26045 (
26046 ˇ
26047 )
26048 "});
26049
26050 // do not indent after empty lists or dictionaries
26051 cx.set_state(indoc! {"
26052 a = []ˇ
26053 "});
26054 cx.update_editor(|editor, window, cx| {
26055 editor.newline(&Newline, window, cx);
26056 });
26057 cx.run_until_parked();
26058 cx.assert_editor_state(indoc! {"
26059 a = []
26060 ˇ
26061 "});
26062}
26063
26064#[gpui::test]
26065async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
26066 init_test(cx, |_| {});
26067
26068 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
26069 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
26070 language_registry.add(markdown_lang());
26071 language_registry.add(python_lang);
26072
26073 let mut cx = EditorTestContext::new(cx).await;
26074 cx.update_buffer(|buffer, cx| {
26075 buffer.set_language_registry(language_registry);
26076 buffer.set_language(Some(markdown_lang()), cx);
26077 });
26078
26079 // Test that `else:` correctly outdents to match `if:` inside the Python code block
26080 cx.set_state(indoc! {"
26081 # Heading
26082
26083 ```python
26084 def main():
26085 if condition:
26086 pass
26087 ˇ
26088 ```
26089 "});
26090 cx.update_editor(|editor, window, cx| {
26091 editor.handle_input("else:", window, cx);
26092 });
26093 cx.run_until_parked();
26094 cx.assert_editor_state(indoc! {"
26095 # Heading
26096
26097 ```python
26098 def main():
26099 if condition:
26100 pass
26101 else:ˇ
26102 ```
26103 "});
26104}
26105
26106#[gpui::test]
26107async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
26108 init_test(cx, |_| {});
26109
26110 let mut cx = EditorTestContext::new(cx).await;
26111 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26112 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26113
26114 // test cursor move to start of each line on tab
26115 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
26116 cx.set_state(indoc! {"
26117 function main() {
26118 ˇ for item in $items; do
26119 ˇ while [ -n \"$item\" ]; do
26120 ˇ if [ \"$value\" -gt 10 ]; then
26121 ˇ continue
26122 ˇ elif [ \"$value\" -lt 0 ]; then
26123 ˇ break
26124 ˇ else
26125 ˇ echo \"$item\"
26126 ˇ fi
26127 ˇ done
26128 ˇ done
26129 ˇ}
26130 "});
26131 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26132 cx.wait_for_autoindent_applied().await;
26133 cx.assert_editor_state(indoc! {"
26134 function main() {
26135 ˇfor item in $items; do
26136 ˇwhile [ -n \"$item\" ]; do
26137 ˇif [ \"$value\" -gt 10 ]; then
26138 ˇcontinue
26139 ˇelif [ \"$value\" -lt 0 ]; then
26140 ˇbreak
26141 ˇelse
26142 ˇecho \"$item\"
26143 ˇfi
26144 ˇdone
26145 ˇdone
26146 ˇ}
26147 "});
26148 // test relative indent is preserved when tab
26149 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26150 cx.wait_for_autoindent_applied().await;
26151 cx.assert_editor_state(indoc! {"
26152 function main() {
26153 ˇfor item in $items; do
26154 ˇwhile [ -n \"$item\" ]; do
26155 ˇif [ \"$value\" -gt 10 ]; then
26156 ˇcontinue
26157 ˇelif [ \"$value\" -lt 0 ]; then
26158 ˇbreak
26159 ˇelse
26160 ˇecho \"$item\"
26161 ˇfi
26162 ˇdone
26163 ˇdone
26164 ˇ}
26165 "});
26166
26167 // test cursor move to start of each line on tab
26168 // for `case` statement with patterns
26169 cx.set_state(indoc! {"
26170 function handle() {
26171 ˇ case \"$1\" in
26172 ˇ start)
26173 ˇ echo \"a\"
26174 ˇ ;;
26175 ˇ stop)
26176 ˇ echo \"b\"
26177 ˇ ;;
26178 ˇ *)
26179 ˇ echo \"c\"
26180 ˇ ;;
26181 ˇ esac
26182 ˇ}
26183 "});
26184 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26185 cx.wait_for_autoindent_applied().await;
26186 cx.assert_editor_state(indoc! {"
26187 function handle() {
26188 ˇcase \"$1\" in
26189 ˇstart)
26190 ˇecho \"a\"
26191 ˇ;;
26192 ˇstop)
26193 ˇecho \"b\"
26194 ˇ;;
26195 ˇ*)
26196 ˇecho \"c\"
26197 ˇ;;
26198 ˇesac
26199 ˇ}
26200 "});
26201}
26202
26203#[gpui::test]
26204async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
26205 init_test(cx, |_| {});
26206
26207 let mut cx = EditorTestContext::new(cx).await;
26208 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26209 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26210
26211 // test indents on comment insert
26212 cx.set_state(indoc! {"
26213 function main() {
26214 ˇ for item in $items; do
26215 ˇ while [ -n \"$item\" ]; do
26216 ˇ if [ \"$value\" -gt 10 ]; then
26217 ˇ continue
26218 ˇ elif [ \"$value\" -lt 0 ]; then
26219 ˇ break
26220 ˇ else
26221 ˇ echo \"$item\"
26222 ˇ fi
26223 ˇ done
26224 ˇ done
26225 ˇ}
26226 "});
26227 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
26228 cx.wait_for_autoindent_applied().await;
26229 cx.assert_editor_state(indoc! {"
26230 function main() {
26231 #ˇ for item in $items; do
26232 #ˇ while [ -n \"$item\" ]; do
26233 #ˇ if [ \"$value\" -gt 10 ]; then
26234 #ˇ continue
26235 #ˇ elif [ \"$value\" -lt 0 ]; then
26236 #ˇ break
26237 #ˇ else
26238 #ˇ echo \"$item\"
26239 #ˇ fi
26240 #ˇ done
26241 #ˇ done
26242 #ˇ}
26243 "});
26244}
26245
26246#[gpui::test]
26247async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
26248 init_test(cx, |_| {});
26249
26250 let mut cx = EditorTestContext::new(cx).await;
26251 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26252 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26253
26254 // test `else` auto outdents when typed inside `if` block
26255 cx.set_state(indoc! {"
26256 if [ \"$1\" = \"test\" ]; then
26257 echo \"foo bar\"
26258 ˇ
26259 "});
26260 cx.update_editor(|editor, window, cx| {
26261 editor.handle_input("else", window, cx);
26262 });
26263 cx.wait_for_autoindent_applied().await;
26264 cx.assert_editor_state(indoc! {"
26265 if [ \"$1\" = \"test\" ]; then
26266 echo \"foo bar\"
26267 elseˇ
26268 "});
26269
26270 // test `elif` auto outdents when typed inside `if` block
26271 cx.set_state(indoc! {"
26272 if [ \"$1\" = \"test\" ]; then
26273 echo \"foo bar\"
26274 ˇ
26275 "});
26276 cx.update_editor(|editor, window, cx| {
26277 editor.handle_input("elif", window, cx);
26278 });
26279 cx.wait_for_autoindent_applied().await;
26280 cx.assert_editor_state(indoc! {"
26281 if [ \"$1\" = \"test\" ]; then
26282 echo \"foo bar\"
26283 elifˇ
26284 "});
26285
26286 // test `fi` auto outdents when typed inside `else` block
26287 cx.set_state(indoc! {"
26288 if [ \"$1\" = \"test\" ]; then
26289 echo \"foo bar\"
26290 else
26291 echo \"bar baz\"
26292 ˇ
26293 "});
26294 cx.update_editor(|editor, window, cx| {
26295 editor.handle_input("fi", window, cx);
26296 });
26297 cx.wait_for_autoindent_applied().await;
26298 cx.assert_editor_state(indoc! {"
26299 if [ \"$1\" = \"test\" ]; then
26300 echo \"foo bar\"
26301 else
26302 echo \"bar baz\"
26303 fiˇ
26304 "});
26305
26306 // test `done` auto outdents when typed inside `while` block
26307 cx.set_state(indoc! {"
26308 while read line; do
26309 echo \"$line\"
26310 ˇ
26311 "});
26312 cx.update_editor(|editor, window, cx| {
26313 editor.handle_input("done", window, cx);
26314 });
26315 cx.wait_for_autoindent_applied().await;
26316 cx.assert_editor_state(indoc! {"
26317 while read line; do
26318 echo \"$line\"
26319 doneˇ
26320 "});
26321
26322 // test `done` auto outdents when typed inside `for` block
26323 cx.set_state(indoc! {"
26324 for file in *.txt; do
26325 cat \"$file\"
26326 ˇ
26327 "});
26328 cx.update_editor(|editor, window, cx| {
26329 editor.handle_input("done", window, cx);
26330 });
26331 cx.wait_for_autoindent_applied().await;
26332 cx.assert_editor_state(indoc! {"
26333 for file in *.txt; do
26334 cat \"$file\"
26335 doneˇ
26336 "});
26337
26338 // test `esac` auto outdents when typed inside `case` block
26339 cx.set_state(indoc! {"
26340 case \"$1\" in
26341 start)
26342 echo \"foo bar\"
26343 ;;
26344 stop)
26345 echo \"bar baz\"
26346 ;;
26347 ˇ
26348 "});
26349 cx.update_editor(|editor, window, cx| {
26350 editor.handle_input("esac", window, cx);
26351 });
26352 cx.wait_for_autoindent_applied().await;
26353 cx.assert_editor_state(indoc! {"
26354 case \"$1\" in
26355 start)
26356 echo \"foo bar\"
26357 ;;
26358 stop)
26359 echo \"bar baz\"
26360 ;;
26361 esacˇ
26362 "});
26363
26364 // test `*)` auto outdents when typed inside `case` block
26365 cx.set_state(indoc! {"
26366 case \"$1\" in
26367 start)
26368 echo \"foo bar\"
26369 ;;
26370 ˇ
26371 "});
26372 cx.update_editor(|editor, window, cx| {
26373 editor.handle_input("*)", window, cx);
26374 });
26375 cx.wait_for_autoindent_applied().await;
26376 cx.assert_editor_state(indoc! {"
26377 case \"$1\" in
26378 start)
26379 echo \"foo bar\"
26380 ;;
26381 *)ˇ
26382 "});
26383
26384 // test `fi` outdents to correct level with nested if blocks
26385 cx.set_state(indoc! {"
26386 if [ \"$1\" = \"test\" ]; then
26387 echo \"outer if\"
26388 if [ \"$2\" = \"debug\" ]; then
26389 echo \"inner if\"
26390 ˇ
26391 "});
26392 cx.update_editor(|editor, window, cx| {
26393 editor.handle_input("fi", window, cx);
26394 });
26395 cx.wait_for_autoindent_applied().await;
26396 cx.assert_editor_state(indoc! {"
26397 if [ \"$1\" = \"test\" ]; then
26398 echo \"outer if\"
26399 if [ \"$2\" = \"debug\" ]; then
26400 echo \"inner if\"
26401 fiˇ
26402 "});
26403}
26404
26405#[gpui::test]
26406async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
26407 init_test(cx, |_| {});
26408 update_test_language_settings(cx, |settings| {
26409 settings.defaults.extend_comment_on_newline = Some(false);
26410 });
26411 let mut cx = EditorTestContext::new(cx).await;
26412 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26413 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26414
26415 // test correct indent after newline on comment
26416 cx.set_state(indoc! {"
26417 # COMMENT:ˇ
26418 "});
26419 cx.update_editor(|editor, window, cx| {
26420 editor.newline(&Newline, window, cx);
26421 });
26422 cx.wait_for_autoindent_applied().await;
26423 cx.assert_editor_state(indoc! {"
26424 # COMMENT:
26425 ˇ
26426 "});
26427
26428 // test correct indent after newline after `then`
26429 cx.set_state(indoc! {"
26430
26431 if [ \"$1\" = \"test\" ]; thenˇ
26432 "});
26433 cx.update_editor(|editor, window, cx| {
26434 editor.newline(&Newline, window, cx);
26435 });
26436 cx.wait_for_autoindent_applied().await;
26437 cx.assert_editor_state(indoc! {"
26438
26439 if [ \"$1\" = \"test\" ]; then
26440 ˇ
26441 "});
26442
26443 // test correct indent after newline after `else`
26444 cx.set_state(indoc! {"
26445 if [ \"$1\" = \"test\" ]; then
26446 elseˇ
26447 "});
26448 cx.update_editor(|editor, window, cx| {
26449 editor.newline(&Newline, window, cx);
26450 });
26451 cx.wait_for_autoindent_applied().await;
26452 cx.assert_editor_state(indoc! {"
26453 if [ \"$1\" = \"test\" ]; then
26454 else
26455 ˇ
26456 "});
26457
26458 // test correct indent after newline after `elif`
26459 cx.set_state(indoc! {"
26460 if [ \"$1\" = \"test\" ]; then
26461 elifˇ
26462 "});
26463 cx.update_editor(|editor, window, cx| {
26464 editor.newline(&Newline, window, cx);
26465 });
26466 cx.wait_for_autoindent_applied().await;
26467 cx.assert_editor_state(indoc! {"
26468 if [ \"$1\" = \"test\" ]; then
26469 elif
26470 ˇ
26471 "});
26472
26473 // test correct indent after newline after `do`
26474 cx.set_state(indoc! {"
26475 for file in *.txt; doˇ
26476 "});
26477 cx.update_editor(|editor, window, cx| {
26478 editor.newline(&Newline, window, cx);
26479 });
26480 cx.wait_for_autoindent_applied().await;
26481 cx.assert_editor_state(indoc! {"
26482 for file in *.txt; do
26483 ˇ
26484 "});
26485
26486 // test correct indent after newline after case pattern
26487 cx.set_state(indoc! {"
26488 case \"$1\" in
26489 start)ˇ
26490 "});
26491 cx.update_editor(|editor, window, cx| {
26492 editor.newline(&Newline, window, cx);
26493 });
26494 cx.wait_for_autoindent_applied().await;
26495 cx.assert_editor_state(indoc! {"
26496 case \"$1\" in
26497 start)
26498 ˇ
26499 "});
26500
26501 // test correct indent after newline after case pattern
26502 cx.set_state(indoc! {"
26503 case \"$1\" in
26504 start)
26505 ;;
26506 *)ˇ
26507 "});
26508 cx.update_editor(|editor, window, cx| {
26509 editor.newline(&Newline, window, cx);
26510 });
26511 cx.wait_for_autoindent_applied().await;
26512 cx.assert_editor_state(indoc! {"
26513 case \"$1\" in
26514 start)
26515 ;;
26516 *)
26517 ˇ
26518 "});
26519
26520 // test correct indent after newline after function opening brace
26521 cx.set_state(indoc! {"
26522 function test() {ˇ}
26523 "});
26524 cx.update_editor(|editor, window, cx| {
26525 editor.newline(&Newline, window, cx);
26526 });
26527 cx.wait_for_autoindent_applied().await;
26528 cx.assert_editor_state(indoc! {"
26529 function test() {
26530 ˇ
26531 }
26532 "});
26533
26534 // test no extra indent after semicolon on same line
26535 cx.set_state(indoc! {"
26536 echo \"test\";ˇ
26537 "});
26538 cx.update_editor(|editor, window, cx| {
26539 editor.newline(&Newline, window, cx);
26540 });
26541 cx.wait_for_autoindent_applied().await;
26542 cx.assert_editor_state(indoc! {"
26543 echo \"test\";
26544 ˇ
26545 "});
26546}
26547
26548fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
26549 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
26550 point..point
26551}
26552
26553#[track_caller]
26554fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
26555 let (text, ranges) = marked_text_ranges(marked_text, true);
26556 assert_eq!(editor.text(cx), text);
26557 assert_eq!(
26558 editor.selections.ranges(&editor.display_snapshot(cx)),
26559 ranges
26560 .iter()
26561 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
26562 .collect::<Vec<_>>(),
26563 "Assert selections are {}",
26564 marked_text
26565 );
26566}
26567
26568pub fn handle_signature_help_request(
26569 cx: &mut EditorLspTestContext,
26570 mocked_response: lsp::SignatureHelp,
26571) -> impl Future<Output = ()> + use<> {
26572 let mut request =
26573 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
26574 let mocked_response = mocked_response.clone();
26575 async move { Ok(Some(mocked_response)) }
26576 });
26577
26578 async move {
26579 request.next().await;
26580 }
26581}
26582
26583#[track_caller]
26584pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
26585 cx.update_editor(|editor, _, _| {
26586 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
26587 let entries = menu.entries.borrow();
26588 let entries = entries
26589 .iter()
26590 .map(|entry| entry.string.as_str())
26591 .collect::<Vec<_>>();
26592 assert_eq!(entries, expected);
26593 } else {
26594 panic!("Expected completions menu");
26595 }
26596 });
26597}
26598
26599#[gpui::test]
26600async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
26601 init_test(cx, |_| {});
26602 let mut cx = EditorLspTestContext::new_rust(
26603 lsp::ServerCapabilities {
26604 completion_provider: Some(lsp::CompletionOptions {
26605 ..Default::default()
26606 }),
26607 ..Default::default()
26608 },
26609 cx,
26610 )
26611 .await;
26612 cx.lsp
26613 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
26614 Ok(Some(lsp::CompletionResponse::Array(vec![
26615 lsp::CompletionItem {
26616 label: "unsafe".into(),
26617 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26618 range: lsp::Range {
26619 start: lsp::Position {
26620 line: 0,
26621 character: 9,
26622 },
26623 end: lsp::Position {
26624 line: 0,
26625 character: 11,
26626 },
26627 },
26628 new_text: "unsafe".to_string(),
26629 })),
26630 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
26631 ..Default::default()
26632 },
26633 ])))
26634 });
26635
26636 cx.update_editor(|editor, _, cx| {
26637 editor.project().unwrap().update(cx, |project, cx| {
26638 project.snippets().update(cx, |snippets, _cx| {
26639 snippets.add_snippet_for_test(
26640 None,
26641 PathBuf::from("test_snippets.json"),
26642 vec![
26643 Arc::new(project::snippet_provider::Snippet {
26644 prefix: vec![
26645 "unlimited word count".to_string(),
26646 "unlimit word count".to_string(),
26647 "unlimited unknown".to_string(),
26648 ],
26649 body: "this is many words".to_string(),
26650 description: Some("description".to_string()),
26651 name: "multi-word snippet test".to_string(),
26652 }),
26653 Arc::new(project::snippet_provider::Snippet {
26654 prefix: vec!["unsnip".to_string(), "@few".to_string()],
26655 body: "fewer words".to_string(),
26656 description: Some("alt description".to_string()),
26657 name: "other name".to_string(),
26658 }),
26659 Arc::new(project::snippet_provider::Snippet {
26660 prefix: vec!["ab aa".to_string()],
26661 body: "abcd".to_string(),
26662 description: None,
26663 name: "alphabet".to_string(),
26664 }),
26665 ],
26666 );
26667 });
26668 })
26669 });
26670
26671 let get_completions = |cx: &mut EditorLspTestContext| {
26672 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
26673 Some(CodeContextMenu::Completions(context_menu)) => {
26674 let entries = context_menu.entries.borrow();
26675 entries
26676 .iter()
26677 .map(|entry| entry.string.clone())
26678 .collect_vec()
26679 }
26680 _ => vec![],
26681 })
26682 };
26683
26684 // snippets:
26685 // @foo
26686 // foo bar
26687 //
26688 // when typing:
26689 //
26690 // when typing:
26691 // - if I type a symbol "open the completions with snippets only"
26692 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
26693 //
26694 // stuff we need:
26695 // - filtering logic change?
26696 // - remember how far back the completion started.
26697
26698 let test_cases: &[(&str, &[&str])] = &[
26699 (
26700 "un",
26701 &[
26702 "unsafe",
26703 "unlimit word count",
26704 "unlimited unknown",
26705 "unlimited word count",
26706 "unsnip",
26707 ],
26708 ),
26709 (
26710 "u ",
26711 &[
26712 "unlimit word count",
26713 "unlimited unknown",
26714 "unlimited word count",
26715 ],
26716 ),
26717 ("u a", &["ab aa", "unsafe"]), // unsAfe
26718 (
26719 "u u",
26720 &[
26721 "unsafe",
26722 "unlimit word count",
26723 "unlimited unknown", // ranked highest among snippets
26724 "unlimited word count",
26725 "unsnip",
26726 ],
26727 ),
26728 ("uw c", &["unlimit word count", "unlimited word count"]),
26729 (
26730 "u w",
26731 &[
26732 "unlimit word count",
26733 "unlimited word count",
26734 "unlimited unknown",
26735 ],
26736 ),
26737 ("u w ", &["unlimit word count", "unlimited word count"]),
26738 (
26739 "u ",
26740 &[
26741 "unlimit word count",
26742 "unlimited unknown",
26743 "unlimited word count",
26744 ],
26745 ),
26746 ("wor", &[]),
26747 ("uf", &["unsafe"]),
26748 ("af", &["unsafe"]),
26749 ("afu", &[]),
26750 (
26751 "ue",
26752 &["unsafe", "unlimited unknown", "unlimited word count"],
26753 ),
26754 ("@", &["@few"]),
26755 ("@few", &["@few"]),
26756 ("@ ", &[]),
26757 ("a@", &["@few"]),
26758 ("a@f", &["@few", "unsafe"]),
26759 ("a@fw", &["@few"]),
26760 ("a", &["ab aa", "unsafe"]),
26761 ("aa", &["ab aa"]),
26762 ("aaa", &["ab aa"]),
26763 ("ab", &["ab aa"]),
26764 ("ab ", &["ab aa"]),
26765 ("ab a", &["ab aa", "unsafe"]),
26766 ("ab ab", &["ab aa"]),
26767 ("ab ab aa", &["ab aa"]),
26768 ];
26769
26770 for &(input_to_simulate, expected_completions) in test_cases {
26771 cx.set_state("fn a() { ˇ }\n");
26772 for c in input_to_simulate.split("") {
26773 cx.simulate_input(c);
26774 cx.run_until_parked();
26775 }
26776 let expected_completions = expected_completions
26777 .iter()
26778 .map(|s| s.to_string())
26779 .collect_vec();
26780 assert_eq!(
26781 get_completions(&mut cx),
26782 expected_completions,
26783 "< actual / expected >, input = {input_to_simulate:?}",
26784 );
26785 }
26786}
26787
26788/// Handle completion request passing a marked string specifying where the completion
26789/// should be triggered from using '|' character, what range should be replaced, and what completions
26790/// should be returned using '<' and '>' to delimit the range.
26791///
26792/// Also see `handle_completion_request_with_insert_and_replace`.
26793#[track_caller]
26794pub fn handle_completion_request(
26795 marked_string: &str,
26796 completions: Vec<&'static str>,
26797 is_incomplete: bool,
26798 counter: Arc<AtomicUsize>,
26799 cx: &mut EditorLspTestContext,
26800) -> impl Future<Output = ()> {
26801 let complete_from_marker: TextRangeMarker = '|'.into();
26802 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26803 let (_, mut marked_ranges) = marked_text_ranges_by(
26804 marked_string,
26805 vec![complete_from_marker.clone(), replace_range_marker.clone()],
26806 );
26807
26808 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26809 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26810 ));
26811 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26812 let replace_range =
26813 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26814
26815 let mut request =
26816 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26817 let completions = completions.clone();
26818 counter.fetch_add(1, atomic::Ordering::Release);
26819 async move {
26820 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26821 assert_eq!(
26822 params.text_document_position.position,
26823 complete_from_position
26824 );
26825 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
26826 is_incomplete,
26827 item_defaults: None,
26828 items: completions
26829 .iter()
26830 .map(|completion_text| lsp::CompletionItem {
26831 label: completion_text.to_string(),
26832 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26833 range: replace_range,
26834 new_text: completion_text.to_string(),
26835 })),
26836 ..Default::default()
26837 })
26838 .collect(),
26839 })))
26840 }
26841 });
26842
26843 async move {
26844 request.next().await;
26845 }
26846}
26847
26848/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
26849/// given instead, which also contains an `insert` range.
26850///
26851/// This function uses markers to define ranges:
26852/// - `|` marks the cursor position
26853/// - `<>` marks the replace range
26854/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
26855pub fn handle_completion_request_with_insert_and_replace(
26856 cx: &mut EditorLspTestContext,
26857 marked_string: &str,
26858 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
26859 counter: Arc<AtomicUsize>,
26860) -> impl Future<Output = ()> {
26861 let complete_from_marker: TextRangeMarker = '|'.into();
26862 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26863 let insert_range_marker: TextRangeMarker = ('{', '}').into();
26864
26865 let (_, mut marked_ranges) = marked_text_ranges_by(
26866 marked_string,
26867 vec![
26868 complete_from_marker.clone(),
26869 replace_range_marker.clone(),
26870 insert_range_marker.clone(),
26871 ],
26872 );
26873
26874 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26875 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26876 ));
26877 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26878 let replace_range =
26879 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26880
26881 let insert_range = match marked_ranges.remove(&insert_range_marker) {
26882 Some(ranges) if !ranges.is_empty() => {
26883 let range1 = ranges[0].clone();
26884 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
26885 }
26886 _ => lsp::Range {
26887 start: replace_range.start,
26888 end: complete_from_position,
26889 },
26890 };
26891
26892 let mut request =
26893 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26894 let completions = completions.clone();
26895 counter.fetch_add(1, atomic::Ordering::Release);
26896 async move {
26897 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26898 assert_eq!(
26899 params.text_document_position.position, complete_from_position,
26900 "marker `|` position doesn't match",
26901 );
26902 Ok(Some(lsp::CompletionResponse::Array(
26903 completions
26904 .iter()
26905 .map(|(label, new_text)| lsp::CompletionItem {
26906 label: label.to_string(),
26907 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
26908 lsp::InsertReplaceEdit {
26909 insert: insert_range,
26910 replace: replace_range,
26911 new_text: new_text.to_string(),
26912 },
26913 )),
26914 ..Default::default()
26915 })
26916 .collect(),
26917 )))
26918 }
26919 });
26920
26921 async move {
26922 request.next().await;
26923 }
26924}
26925
26926fn handle_resolve_completion_request(
26927 cx: &mut EditorLspTestContext,
26928 edits: Option<Vec<(&'static str, &'static str)>>,
26929) -> impl Future<Output = ()> {
26930 let edits = edits.map(|edits| {
26931 edits
26932 .iter()
26933 .map(|(marked_string, new_text)| {
26934 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
26935 let replace_range = cx.to_lsp_range(
26936 MultiBufferOffset(marked_ranges[0].start)
26937 ..MultiBufferOffset(marked_ranges[0].end),
26938 );
26939 lsp::TextEdit::new(replace_range, new_text.to_string())
26940 })
26941 .collect::<Vec<_>>()
26942 });
26943
26944 let mut request =
26945 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
26946 let edits = edits.clone();
26947 async move {
26948 Ok(lsp::CompletionItem {
26949 additional_text_edits: edits,
26950 ..Default::default()
26951 })
26952 }
26953 });
26954
26955 async move {
26956 request.next().await;
26957 }
26958}
26959
26960pub(crate) fn update_test_language_settings(
26961 cx: &mut TestAppContext,
26962 f: impl Fn(&mut AllLanguageSettingsContent),
26963) {
26964 cx.update(|cx| {
26965 SettingsStore::update_global(cx, |store, cx| {
26966 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
26967 });
26968 });
26969}
26970
26971pub(crate) fn update_test_project_settings(
26972 cx: &mut TestAppContext,
26973 f: impl Fn(&mut ProjectSettingsContent),
26974) {
26975 cx.update(|cx| {
26976 SettingsStore::update_global(cx, |store, cx| {
26977 store.update_user_settings(cx, |settings| f(&mut settings.project));
26978 });
26979 });
26980}
26981
26982pub(crate) fn update_test_editor_settings(
26983 cx: &mut TestAppContext,
26984 f: impl Fn(&mut EditorSettingsContent),
26985) {
26986 cx.update(|cx| {
26987 SettingsStore::update_global(cx, |store, cx| {
26988 store.update_user_settings(cx, |settings| f(&mut settings.editor));
26989 })
26990 })
26991}
26992
26993pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
26994 cx.update(|cx| {
26995 assets::Assets.load_test_fonts(cx);
26996 let store = SettingsStore::test(cx);
26997 cx.set_global(store);
26998 theme::init(theme::LoadThemes::JustBase, cx);
26999 release_channel::init(semver::Version::new(0, 0, 0), cx);
27000 crate::init(cx);
27001 });
27002 zlog::init_test();
27003 update_test_language_settings(cx, f);
27004}
27005
27006#[track_caller]
27007fn assert_hunk_revert(
27008 not_reverted_text_with_selections: &str,
27009 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
27010 expected_reverted_text_with_selections: &str,
27011 base_text: &str,
27012 cx: &mut EditorLspTestContext,
27013) {
27014 cx.set_state(not_reverted_text_with_selections);
27015 cx.set_head_text(base_text);
27016 cx.executor().run_until_parked();
27017
27018 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
27019 let snapshot = editor.snapshot(window, cx);
27020 let reverted_hunk_statuses = snapshot
27021 .buffer_snapshot()
27022 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
27023 .map(|hunk| hunk.status().kind)
27024 .collect::<Vec<_>>();
27025
27026 editor.git_restore(&Default::default(), window, cx);
27027 reverted_hunk_statuses
27028 });
27029 cx.executor().run_until_parked();
27030 cx.assert_editor_state(expected_reverted_text_with_selections);
27031 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
27032}
27033
27034#[gpui::test(iterations = 10)]
27035async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
27036 init_test(cx, |_| {});
27037
27038 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
27039 let counter = diagnostic_requests.clone();
27040
27041 let fs = FakeFs::new(cx.executor());
27042 fs.insert_tree(
27043 path!("/a"),
27044 json!({
27045 "first.rs": "fn main() { let a = 5; }",
27046 "second.rs": "// Test file",
27047 }),
27048 )
27049 .await;
27050
27051 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27052 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27053 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27054
27055 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27056 language_registry.add(rust_lang());
27057 let mut fake_servers = language_registry.register_fake_lsp(
27058 "Rust",
27059 FakeLspAdapter {
27060 capabilities: lsp::ServerCapabilities {
27061 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
27062 lsp::DiagnosticOptions {
27063 identifier: None,
27064 inter_file_dependencies: true,
27065 workspace_diagnostics: true,
27066 work_done_progress_options: Default::default(),
27067 },
27068 )),
27069 ..Default::default()
27070 },
27071 ..Default::default()
27072 },
27073 );
27074
27075 let editor = workspace
27076 .update(cx, |workspace, window, cx| {
27077 workspace.open_abs_path(
27078 PathBuf::from(path!("/a/first.rs")),
27079 OpenOptions::default(),
27080 window,
27081 cx,
27082 )
27083 })
27084 .unwrap()
27085 .await
27086 .unwrap()
27087 .downcast::<Editor>()
27088 .unwrap();
27089 let fake_server = fake_servers.next().await.unwrap();
27090 let server_id = fake_server.server.server_id();
27091 let mut first_request = fake_server
27092 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
27093 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
27094 let result_id = Some(new_result_id.to_string());
27095 assert_eq!(
27096 params.text_document.uri,
27097 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27098 );
27099 async move {
27100 Ok(lsp::DocumentDiagnosticReportResult::Report(
27101 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
27102 related_documents: None,
27103 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
27104 items: Vec::new(),
27105 result_id,
27106 },
27107 }),
27108 ))
27109 }
27110 });
27111
27112 let ensure_result_id = |expected: Option<SharedString>, cx: &mut TestAppContext| {
27113 project.update(cx, |project, cx| {
27114 let buffer_id = editor
27115 .read(cx)
27116 .buffer()
27117 .read(cx)
27118 .as_singleton()
27119 .expect("created a singleton buffer")
27120 .read(cx)
27121 .remote_id();
27122 let buffer_result_id = project
27123 .lsp_store()
27124 .read(cx)
27125 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
27126 assert_eq!(expected, buffer_result_id);
27127 });
27128 };
27129
27130 ensure_result_id(None, cx);
27131 cx.executor().advance_clock(Duration::from_millis(60));
27132 cx.executor().run_until_parked();
27133 assert_eq!(
27134 diagnostic_requests.load(atomic::Ordering::Acquire),
27135 1,
27136 "Opening file should trigger diagnostic request"
27137 );
27138 first_request
27139 .next()
27140 .await
27141 .expect("should have sent the first diagnostics pull request");
27142 ensure_result_id(Some(SharedString::new("1")), cx);
27143
27144 // Editing should trigger diagnostics
27145 editor.update_in(cx, |editor, window, cx| {
27146 editor.handle_input("2", window, cx)
27147 });
27148 cx.executor().advance_clock(Duration::from_millis(60));
27149 cx.executor().run_until_parked();
27150 assert_eq!(
27151 diagnostic_requests.load(atomic::Ordering::Acquire),
27152 2,
27153 "Editing should trigger diagnostic request"
27154 );
27155 ensure_result_id(Some(SharedString::new("2")), cx);
27156
27157 // Moving cursor should not trigger diagnostic request
27158 editor.update_in(cx, |editor, window, cx| {
27159 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27160 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
27161 });
27162 });
27163 cx.executor().advance_clock(Duration::from_millis(60));
27164 cx.executor().run_until_parked();
27165 assert_eq!(
27166 diagnostic_requests.load(atomic::Ordering::Acquire),
27167 2,
27168 "Cursor movement should not trigger diagnostic request"
27169 );
27170 ensure_result_id(Some(SharedString::new("2")), cx);
27171 // Multiple rapid edits should be debounced
27172 for _ in 0..5 {
27173 editor.update_in(cx, |editor, window, cx| {
27174 editor.handle_input("x", window, cx)
27175 });
27176 }
27177 cx.executor().advance_clock(Duration::from_millis(60));
27178 cx.executor().run_until_parked();
27179
27180 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
27181 assert!(
27182 final_requests <= 4,
27183 "Multiple rapid edits should be debounced (got {final_requests} requests)",
27184 );
27185 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
27186}
27187
27188#[gpui::test]
27189async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
27190 // Regression test for issue #11671
27191 // Previously, adding a cursor after moving multiple cursors would reset
27192 // the cursor count instead of adding to the existing cursors.
27193 init_test(cx, |_| {});
27194 let mut cx = EditorTestContext::new(cx).await;
27195
27196 // Create a simple buffer with cursor at start
27197 cx.set_state(indoc! {"
27198 ˇaaaa
27199 bbbb
27200 cccc
27201 dddd
27202 eeee
27203 ffff
27204 gggg
27205 hhhh"});
27206
27207 // Add 2 cursors below (so we have 3 total)
27208 cx.update_editor(|editor, window, cx| {
27209 editor.add_selection_below(&Default::default(), window, cx);
27210 editor.add_selection_below(&Default::default(), window, cx);
27211 });
27212
27213 // Verify we have 3 cursors
27214 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
27215 assert_eq!(
27216 initial_count, 3,
27217 "Should have 3 cursors after adding 2 below"
27218 );
27219
27220 // Move down one line
27221 cx.update_editor(|editor, window, cx| {
27222 editor.move_down(&MoveDown, window, cx);
27223 });
27224
27225 // Add another cursor below
27226 cx.update_editor(|editor, window, cx| {
27227 editor.add_selection_below(&Default::default(), window, cx);
27228 });
27229
27230 // Should now have 4 cursors (3 original + 1 new)
27231 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
27232 assert_eq!(
27233 final_count, 4,
27234 "Should have 4 cursors after moving and adding another"
27235 );
27236}
27237
27238#[gpui::test]
27239async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
27240 init_test(cx, |_| {});
27241
27242 let mut cx = EditorTestContext::new(cx).await;
27243
27244 cx.set_state(indoc!(
27245 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
27246 Second line here"#
27247 ));
27248
27249 cx.update_editor(|editor, window, cx| {
27250 // Enable soft wrapping with a narrow width to force soft wrapping and
27251 // confirm that more than 2 rows are being displayed.
27252 editor.set_wrap_width(Some(100.0.into()), cx);
27253 assert!(editor.display_text(cx).lines().count() > 2);
27254
27255 editor.add_selection_below(
27256 &AddSelectionBelow {
27257 skip_soft_wrap: true,
27258 },
27259 window,
27260 cx,
27261 );
27262
27263 assert_eq!(
27264 display_ranges(editor, cx),
27265 &[
27266 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27267 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
27268 ]
27269 );
27270
27271 editor.add_selection_above(
27272 &AddSelectionAbove {
27273 skip_soft_wrap: true,
27274 },
27275 window,
27276 cx,
27277 );
27278
27279 assert_eq!(
27280 display_ranges(editor, cx),
27281 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27282 );
27283
27284 editor.add_selection_below(
27285 &AddSelectionBelow {
27286 skip_soft_wrap: false,
27287 },
27288 window,
27289 cx,
27290 );
27291
27292 assert_eq!(
27293 display_ranges(editor, cx),
27294 &[
27295 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27296 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
27297 ]
27298 );
27299
27300 editor.add_selection_above(
27301 &AddSelectionAbove {
27302 skip_soft_wrap: false,
27303 },
27304 window,
27305 cx,
27306 );
27307
27308 assert_eq!(
27309 display_ranges(editor, cx),
27310 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27311 );
27312 });
27313}
27314
27315#[gpui::test]
27316async fn test_insert_snippet(cx: &mut TestAppContext) {
27317 init_test(cx, |_| {});
27318 let mut cx = EditorTestContext::new(cx).await;
27319
27320 cx.update_editor(|editor, _, cx| {
27321 editor.project().unwrap().update(cx, |project, cx| {
27322 project.snippets().update(cx, |snippets, _cx| {
27323 let snippet = project::snippet_provider::Snippet {
27324 prefix: vec![], // no prefix needed!
27325 body: "an Unspecified".to_string(),
27326 description: Some("shhhh it's a secret".to_string()),
27327 name: "super secret snippet".to_string(),
27328 };
27329 snippets.add_snippet_for_test(
27330 None,
27331 PathBuf::from("test_snippets.json"),
27332 vec![Arc::new(snippet)],
27333 );
27334
27335 let snippet = project::snippet_provider::Snippet {
27336 prefix: vec![], // no prefix needed!
27337 body: " Location".to_string(),
27338 description: Some("the word 'location'".to_string()),
27339 name: "location word".to_string(),
27340 };
27341 snippets.add_snippet_for_test(
27342 Some("Markdown".to_string()),
27343 PathBuf::from("test_snippets.json"),
27344 vec![Arc::new(snippet)],
27345 );
27346 });
27347 })
27348 });
27349
27350 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
27351
27352 cx.update_editor(|editor, window, cx| {
27353 editor.insert_snippet_at_selections(
27354 &InsertSnippet {
27355 language: None,
27356 name: Some("super secret snippet".to_string()),
27357 snippet: None,
27358 },
27359 window,
27360 cx,
27361 );
27362
27363 // Language is specified in the action,
27364 // so the buffer language does not need to match
27365 editor.insert_snippet_at_selections(
27366 &InsertSnippet {
27367 language: Some("Markdown".to_string()),
27368 name: Some("location word".to_string()),
27369 snippet: None,
27370 },
27371 window,
27372 cx,
27373 );
27374
27375 editor.insert_snippet_at_selections(
27376 &InsertSnippet {
27377 language: None,
27378 name: None,
27379 snippet: Some("$0 after".to_string()),
27380 },
27381 window,
27382 cx,
27383 );
27384 });
27385
27386 cx.assert_editor_state(
27387 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
27388 );
27389}
27390
27391#[gpui::test(iterations = 10)]
27392async fn test_document_colors(cx: &mut TestAppContext) {
27393 let expected_color = Rgba {
27394 r: 0.33,
27395 g: 0.33,
27396 b: 0.33,
27397 a: 0.33,
27398 };
27399
27400 init_test(cx, |_| {});
27401
27402 let fs = FakeFs::new(cx.executor());
27403 fs.insert_tree(
27404 path!("/a"),
27405 json!({
27406 "first.rs": "fn main() { let a = 5; }",
27407 }),
27408 )
27409 .await;
27410
27411 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27412 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27413 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27414
27415 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27416 language_registry.add(rust_lang());
27417 let mut fake_servers = language_registry.register_fake_lsp(
27418 "Rust",
27419 FakeLspAdapter {
27420 capabilities: lsp::ServerCapabilities {
27421 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
27422 ..lsp::ServerCapabilities::default()
27423 },
27424 name: "rust-analyzer",
27425 ..FakeLspAdapter::default()
27426 },
27427 );
27428 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
27429 "Rust",
27430 FakeLspAdapter {
27431 capabilities: lsp::ServerCapabilities {
27432 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
27433 ..lsp::ServerCapabilities::default()
27434 },
27435 name: "not-rust-analyzer",
27436 ..FakeLspAdapter::default()
27437 },
27438 );
27439
27440 let editor = workspace
27441 .update(cx, |workspace, window, cx| {
27442 workspace.open_abs_path(
27443 PathBuf::from(path!("/a/first.rs")),
27444 OpenOptions::default(),
27445 window,
27446 cx,
27447 )
27448 })
27449 .unwrap()
27450 .await
27451 .unwrap()
27452 .downcast::<Editor>()
27453 .unwrap();
27454 let fake_language_server = fake_servers.next().await.unwrap();
27455 let fake_language_server_without_capabilities =
27456 fake_servers_without_capabilities.next().await.unwrap();
27457 let requests_made = Arc::new(AtomicUsize::new(0));
27458 let closure_requests_made = Arc::clone(&requests_made);
27459 let mut color_request_handle = fake_language_server
27460 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27461 let requests_made = Arc::clone(&closure_requests_made);
27462 async move {
27463 assert_eq!(
27464 params.text_document.uri,
27465 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27466 );
27467 requests_made.fetch_add(1, atomic::Ordering::Release);
27468 Ok(vec![
27469 lsp::ColorInformation {
27470 range: lsp::Range {
27471 start: lsp::Position {
27472 line: 0,
27473 character: 0,
27474 },
27475 end: lsp::Position {
27476 line: 0,
27477 character: 1,
27478 },
27479 },
27480 color: lsp::Color {
27481 red: 0.33,
27482 green: 0.33,
27483 blue: 0.33,
27484 alpha: 0.33,
27485 },
27486 },
27487 lsp::ColorInformation {
27488 range: lsp::Range {
27489 start: lsp::Position {
27490 line: 0,
27491 character: 0,
27492 },
27493 end: lsp::Position {
27494 line: 0,
27495 character: 1,
27496 },
27497 },
27498 color: lsp::Color {
27499 red: 0.33,
27500 green: 0.33,
27501 blue: 0.33,
27502 alpha: 0.33,
27503 },
27504 },
27505 ])
27506 }
27507 });
27508
27509 let _handle = fake_language_server_without_capabilities
27510 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
27511 panic!("Should not be called");
27512 });
27513 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27514 color_request_handle.next().await.unwrap();
27515 cx.run_until_parked();
27516 assert_eq!(
27517 1,
27518 requests_made.load(atomic::Ordering::Acquire),
27519 "Should query for colors once per editor open"
27520 );
27521 editor.update_in(cx, |editor, _, cx| {
27522 assert_eq!(
27523 vec![expected_color],
27524 extract_color_inlays(editor, cx),
27525 "Should have an initial inlay"
27526 );
27527 });
27528
27529 // opening another file in a split should not influence the LSP query counter
27530 workspace
27531 .update(cx, |workspace, window, cx| {
27532 assert_eq!(
27533 workspace.panes().len(),
27534 1,
27535 "Should have one pane with one editor"
27536 );
27537 workspace.move_item_to_pane_in_direction(
27538 &MoveItemToPaneInDirection {
27539 direction: SplitDirection::Right,
27540 focus: false,
27541 clone: true,
27542 },
27543 window,
27544 cx,
27545 );
27546 })
27547 .unwrap();
27548 cx.run_until_parked();
27549 workspace
27550 .update(cx, |workspace, _, cx| {
27551 let panes = workspace.panes();
27552 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
27553 for pane in panes {
27554 let editor = pane
27555 .read(cx)
27556 .active_item()
27557 .and_then(|item| item.downcast::<Editor>())
27558 .expect("Should have opened an editor in each split");
27559 let editor_file = editor
27560 .read(cx)
27561 .buffer()
27562 .read(cx)
27563 .as_singleton()
27564 .expect("test deals with singleton buffers")
27565 .read(cx)
27566 .file()
27567 .expect("test buffese should have a file")
27568 .path();
27569 assert_eq!(
27570 editor_file.as_ref(),
27571 rel_path("first.rs"),
27572 "Both editors should be opened for the same file"
27573 )
27574 }
27575 })
27576 .unwrap();
27577
27578 cx.executor().advance_clock(Duration::from_millis(500));
27579 let save = editor.update_in(cx, |editor, window, cx| {
27580 editor.move_to_end(&MoveToEnd, window, cx);
27581 editor.handle_input("dirty", window, cx);
27582 editor.save(
27583 SaveOptions {
27584 format: true,
27585 autosave: true,
27586 },
27587 project.clone(),
27588 window,
27589 cx,
27590 )
27591 });
27592 save.await.unwrap();
27593
27594 color_request_handle.next().await.unwrap();
27595 cx.run_until_parked();
27596 assert_eq!(
27597 2,
27598 requests_made.load(atomic::Ordering::Acquire),
27599 "Should query for colors once per save (deduplicated) and once per formatting after save"
27600 );
27601
27602 drop(editor);
27603 let close = workspace
27604 .update(cx, |workspace, window, cx| {
27605 workspace.active_pane().update(cx, |pane, cx| {
27606 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27607 })
27608 })
27609 .unwrap();
27610 close.await.unwrap();
27611 let close = workspace
27612 .update(cx, |workspace, window, cx| {
27613 workspace.active_pane().update(cx, |pane, cx| {
27614 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27615 })
27616 })
27617 .unwrap();
27618 close.await.unwrap();
27619 assert_eq!(
27620 2,
27621 requests_made.load(atomic::Ordering::Acquire),
27622 "After saving and closing all editors, no extra requests should be made"
27623 );
27624 workspace
27625 .update(cx, |workspace, _, cx| {
27626 assert!(
27627 workspace.active_item(cx).is_none(),
27628 "Should close all editors"
27629 )
27630 })
27631 .unwrap();
27632
27633 workspace
27634 .update(cx, |workspace, window, cx| {
27635 workspace.active_pane().update(cx, |pane, cx| {
27636 pane.navigate_backward(&workspace::GoBack, window, cx);
27637 })
27638 })
27639 .unwrap();
27640 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27641 cx.run_until_parked();
27642 let editor = workspace
27643 .update(cx, |workspace, _, cx| {
27644 workspace
27645 .active_item(cx)
27646 .expect("Should have reopened the editor again after navigating back")
27647 .downcast::<Editor>()
27648 .expect("Should be an editor")
27649 })
27650 .unwrap();
27651
27652 assert_eq!(
27653 2,
27654 requests_made.load(atomic::Ordering::Acquire),
27655 "Cache should be reused on buffer close and reopen"
27656 );
27657 editor.update(cx, |editor, cx| {
27658 assert_eq!(
27659 vec![expected_color],
27660 extract_color_inlays(editor, cx),
27661 "Should have an initial inlay"
27662 );
27663 });
27664
27665 drop(color_request_handle);
27666 let closure_requests_made = Arc::clone(&requests_made);
27667 let mut empty_color_request_handle = fake_language_server
27668 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27669 let requests_made = Arc::clone(&closure_requests_made);
27670 async move {
27671 assert_eq!(
27672 params.text_document.uri,
27673 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27674 );
27675 requests_made.fetch_add(1, atomic::Ordering::Release);
27676 Ok(Vec::new())
27677 }
27678 });
27679 let save = editor.update_in(cx, |editor, window, cx| {
27680 editor.move_to_end(&MoveToEnd, window, cx);
27681 editor.handle_input("dirty_again", window, cx);
27682 editor.save(
27683 SaveOptions {
27684 format: false,
27685 autosave: true,
27686 },
27687 project.clone(),
27688 window,
27689 cx,
27690 )
27691 });
27692 save.await.unwrap();
27693
27694 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27695 empty_color_request_handle.next().await.unwrap();
27696 cx.run_until_parked();
27697 assert_eq!(
27698 3,
27699 requests_made.load(atomic::Ordering::Acquire),
27700 "Should query for colors once per save only, as formatting was not requested"
27701 );
27702 editor.update(cx, |editor, cx| {
27703 assert_eq!(
27704 Vec::<Rgba>::new(),
27705 extract_color_inlays(editor, cx),
27706 "Should clear all colors when the server returns an empty response"
27707 );
27708 });
27709}
27710
27711#[gpui::test]
27712async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
27713 init_test(cx, |_| {});
27714 let (editor, cx) = cx.add_window_view(Editor::single_line);
27715 editor.update_in(cx, |editor, window, cx| {
27716 editor.set_text("oops\n\nwow\n", window, cx)
27717 });
27718 cx.run_until_parked();
27719 editor.update(cx, |editor, cx| {
27720 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
27721 });
27722 editor.update(cx, |editor, cx| {
27723 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
27724 });
27725 cx.run_until_parked();
27726 editor.update(cx, |editor, cx| {
27727 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
27728 });
27729}
27730
27731#[gpui::test]
27732async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
27733 init_test(cx, |_| {});
27734
27735 cx.update(|cx| {
27736 register_project_item::<Editor>(cx);
27737 });
27738
27739 let fs = FakeFs::new(cx.executor());
27740 fs.insert_tree("/root1", json!({})).await;
27741 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
27742 .await;
27743
27744 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
27745 let (workspace, cx) =
27746 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
27747
27748 let worktree_id = project.update(cx, |project, cx| {
27749 project.worktrees(cx).next().unwrap().read(cx).id()
27750 });
27751
27752 let handle = workspace
27753 .update_in(cx, |workspace, window, cx| {
27754 let project_path = (worktree_id, rel_path("one.pdf"));
27755 workspace.open_path(project_path, None, true, window, cx)
27756 })
27757 .await
27758 .unwrap();
27759 // The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
27760 // Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
27761 // With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
27762 assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
27763}
27764
27765#[gpui::test]
27766async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
27767 init_test(cx, |_| {});
27768
27769 let language = Arc::new(Language::new(
27770 LanguageConfig::default(),
27771 Some(tree_sitter_rust::LANGUAGE.into()),
27772 ));
27773
27774 // Test hierarchical sibling navigation
27775 let text = r#"
27776 fn outer() {
27777 if condition {
27778 let a = 1;
27779 }
27780 let b = 2;
27781 }
27782
27783 fn another() {
27784 let c = 3;
27785 }
27786 "#;
27787
27788 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
27789 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
27790 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
27791
27792 // Wait for parsing to complete
27793 editor
27794 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
27795 .await;
27796
27797 editor.update_in(cx, |editor, window, cx| {
27798 // Start by selecting "let a = 1;" inside the if block
27799 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27800 s.select_display_ranges([
27801 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
27802 ]);
27803 });
27804
27805 let initial_selection = editor
27806 .selections
27807 .display_ranges(&editor.display_snapshot(cx));
27808 assert_eq!(initial_selection.len(), 1, "Should have one selection");
27809
27810 // Test select next sibling - should move up levels to find the next sibling
27811 // Since "let a = 1;" has no siblings in the if block, it should move up
27812 // to find "let b = 2;" which is a sibling of the if block
27813 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27814 let next_selection = editor
27815 .selections
27816 .display_ranges(&editor.display_snapshot(cx));
27817
27818 // Should have a selection and it should be different from the initial
27819 assert_eq!(
27820 next_selection.len(),
27821 1,
27822 "Should have one selection after next"
27823 );
27824 assert_ne!(
27825 next_selection[0], initial_selection[0],
27826 "Next sibling selection should be different"
27827 );
27828
27829 // Test hierarchical navigation by going to the end of the current function
27830 // and trying to navigate to the next function
27831 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27832 s.select_display_ranges([
27833 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
27834 ]);
27835 });
27836
27837 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27838 let function_next_selection = editor
27839 .selections
27840 .display_ranges(&editor.display_snapshot(cx));
27841
27842 // Should move to the next function
27843 assert_eq!(
27844 function_next_selection.len(),
27845 1,
27846 "Should have one selection after function next"
27847 );
27848
27849 // Test select previous sibling navigation
27850 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
27851 let prev_selection = editor
27852 .selections
27853 .display_ranges(&editor.display_snapshot(cx));
27854
27855 // Should have a selection and it should be different
27856 assert_eq!(
27857 prev_selection.len(),
27858 1,
27859 "Should have one selection after prev"
27860 );
27861 assert_ne!(
27862 prev_selection[0], function_next_selection[0],
27863 "Previous sibling selection should be different from next"
27864 );
27865 });
27866}
27867
27868#[gpui::test]
27869async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
27870 init_test(cx, |_| {});
27871
27872 let mut cx = EditorTestContext::new(cx).await;
27873 cx.set_state(
27874 "let ˇvariable = 42;
27875let another = variable + 1;
27876let result = variable * 2;",
27877 );
27878
27879 // Set up document highlights manually (simulating LSP response)
27880 cx.update_editor(|editor, _window, cx| {
27881 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
27882
27883 // Create highlights for "variable" occurrences
27884 let highlight_ranges = [
27885 Point::new(0, 4)..Point::new(0, 12), // First "variable"
27886 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
27887 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
27888 ];
27889
27890 let anchor_ranges: Vec<_> = highlight_ranges
27891 .iter()
27892 .map(|range| range.clone().to_anchors(&buffer_snapshot))
27893 .collect();
27894
27895 editor.highlight_background::<DocumentHighlightRead>(
27896 &anchor_ranges,
27897 |_, theme| theme.colors().editor_document_highlight_read_background,
27898 cx,
27899 );
27900 });
27901
27902 // Go to next highlight - should move to second "variable"
27903 cx.update_editor(|editor, window, cx| {
27904 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27905 });
27906 cx.assert_editor_state(
27907 "let variable = 42;
27908let another = ˇvariable + 1;
27909let result = variable * 2;",
27910 );
27911
27912 // Go to next highlight - should move to third "variable"
27913 cx.update_editor(|editor, window, cx| {
27914 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27915 });
27916 cx.assert_editor_state(
27917 "let variable = 42;
27918let another = variable + 1;
27919let result = ˇvariable * 2;",
27920 );
27921
27922 // Go to next highlight - should stay at third "variable" (no wrap-around)
27923 cx.update_editor(|editor, window, cx| {
27924 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27925 });
27926 cx.assert_editor_state(
27927 "let variable = 42;
27928let another = variable + 1;
27929let result = ˇvariable * 2;",
27930 );
27931
27932 // Now test going backwards from third position
27933 cx.update_editor(|editor, window, cx| {
27934 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27935 });
27936 cx.assert_editor_state(
27937 "let variable = 42;
27938let another = ˇvariable + 1;
27939let result = variable * 2;",
27940 );
27941
27942 // Go to previous highlight - should move to first "variable"
27943 cx.update_editor(|editor, window, cx| {
27944 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27945 });
27946 cx.assert_editor_state(
27947 "let ˇvariable = 42;
27948let another = variable + 1;
27949let result = variable * 2;",
27950 );
27951
27952 // Go to previous highlight - should stay on first "variable"
27953 cx.update_editor(|editor, window, cx| {
27954 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27955 });
27956 cx.assert_editor_state(
27957 "let ˇvariable = 42;
27958let another = variable + 1;
27959let result = variable * 2;",
27960 );
27961}
27962
27963#[gpui::test]
27964async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
27965 cx: &mut gpui::TestAppContext,
27966) {
27967 init_test(cx, |_| {});
27968
27969 let url = "https://zed.dev";
27970
27971 let markdown_language = Arc::new(Language::new(
27972 LanguageConfig {
27973 name: "Markdown".into(),
27974 ..LanguageConfig::default()
27975 },
27976 None,
27977 ));
27978
27979 let mut cx = EditorTestContext::new(cx).await;
27980 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27981 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
27982
27983 cx.update_editor(|editor, window, cx| {
27984 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27985 editor.paste(&Paste, window, cx);
27986 });
27987
27988 cx.assert_editor_state(&format!(
27989 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
27990 ));
27991}
27992
27993#[gpui::test]
27994async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
27995 init_test(cx, |_| {});
27996
27997 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
27998 let mut cx = EditorTestContext::new(cx).await;
27999
28000 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28001
28002 // Case 1: Test if adding a character with multi cursors preserves nested list indents
28003 cx.set_state(&indoc! {"
28004 - [ ] Item 1
28005 - [ ] Item 1.a
28006 - [ˇ] Item 2
28007 - [ˇ] Item 2.a
28008 - [ˇ] Item 2.b
28009 "
28010 });
28011 cx.update_editor(|editor, window, cx| {
28012 editor.handle_input("x", window, cx);
28013 });
28014 cx.run_until_parked();
28015 cx.assert_editor_state(indoc! {"
28016 - [ ] Item 1
28017 - [ ] Item 1.a
28018 - [xˇ] Item 2
28019 - [xˇ] Item 2.a
28020 - [xˇ] Item 2.b
28021 "
28022 });
28023
28024 // Case 2: Test adding new line after nested list preserves indent of previous line
28025 cx.set_state(&indoc! {"
28026 - [ ] Item 1
28027 - [ ] Item 1.a
28028 - [x] Item 2
28029 - [x] Item 2.a
28030 - [x] Item 2.bˇ"
28031 });
28032 cx.update_editor(|editor, window, cx| {
28033 editor.newline(&Newline, window, cx);
28034 });
28035 cx.assert_editor_state(indoc! {"
28036 - [ ] Item 1
28037 - [ ] Item 1.a
28038 - [x] Item 2
28039 - [x] Item 2.a
28040 - [x] Item 2.b
28041 ˇ"
28042 });
28043
28044 // Case 3: Test adding a new nested list item preserves indent
28045 cx.set_state(&indoc! {"
28046 - [ ] Item 1
28047 - [ ] Item 1.a
28048 - [x] Item 2
28049 - [x] Item 2.a
28050 - [x] Item 2.b
28051 ˇ"
28052 });
28053 cx.update_editor(|editor, window, cx| {
28054 editor.handle_input("-", window, cx);
28055 });
28056 cx.run_until_parked();
28057 cx.assert_editor_state(indoc! {"
28058 - [ ] Item 1
28059 - [ ] Item 1.a
28060 - [x] Item 2
28061 - [x] Item 2.a
28062 - [x] Item 2.b
28063 -ˇ"
28064 });
28065 cx.update_editor(|editor, window, cx| {
28066 editor.handle_input(" [x] Item 2.c", window, cx);
28067 });
28068 cx.run_until_parked();
28069 cx.assert_editor_state(indoc! {"
28070 - [ ] Item 1
28071 - [ ] Item 1.a
28072 - [x] Item 2
28073 - [x] Item 2.a
28074 - [x] Item 2.b
28075 - [x] Item 2.cˇ"
28076 });
28077
28078 // Case 4: Test adding new line after nested ordered list preserves indent of previous line
28079 cx.set_state(indoc! {"
28080 1. Item 1
28081 1. Item 1.a
28082 2. Item 2
28083 1. Item 2.a
28084 2. Item 2.bˇ"
28085 });
28086 cx.update_editor(|editor, window, cx| {
28087 editor.newline(&Newline, window, cx);
28088 });
28089 cx.assert_editor_state(indoc! {"
28090 1. Item 1
28091 1. Item 1.a
28092 2. Item 2
28093 1. Item 2.a
28094 2. Item 2.b
28095 ˇ"
28096 });
28097
28098 // Case 5: Adding new ordered list item preserves indent
28099 cx.set_state(indoc! {"
28100 1. Item 1
28101 1. Item 1.a
28102 2. Item 2
28103 1. Item 2.a
28104 2. Item 2.b
28105 ˇ"
28106 });
28107 cx.update_editor(|editor, window, cx| {
28108 editor.handle_input("3", window, cx);
28109 });
28110 cx.run_until_parked();
28111 cx.assert_editor_state(indoc! {"
28112 1. Item 1
28113 1. Item 1.a
28114 2. Item 2
28115 1. Item 2.a
28116 2. Item 2.b
28117 3ˇ"
28118 });
28119 cx.update_editor(|editor, window, cx| {
28120 editor.handle_input(".", window, cx);
28121 });
28122 cx.run_until_parked();
28123 cx.assert_editor_state(indoc! {"
28124 1. Item 1
28125 1. Item 1.a
28126 2. Item 2
28127 1. Item 2.a
28128 2. Item 2.b
28129 3.ˇ"
28130 });
28131 cx.update_editor(|editor, window, cx| {
28132 editor.handle_input(" Item 2.c", window, cx);
28133 });
28134 cx.run_until_parked();
28135 cx.assert_editor_state(indoc! {"
28136 1. Item 1
28137 1. Item 1.a
28138 2. Item 2
28139 1. Item 2.a
28140 2. Item 2.b
28141 3. Item 2.cˇ"
28142 });
28143
28144 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
28145 cx.set_state(indoc! {"
28146 - Item 1
28147 - Item 1.a
28148 - Item 1.a
28149 ˇ"});
28150 cx.update_editor(|editor, window, cx| {
28151 editor.handle_input("-", window, cx);
28152 });
28153 cx.run_until_parked();
28154 cx.assert_editor_state(indoc! {"
28155 - Item 1
28156 - Item 1.a
28157 - Item 1.a
28158 -ˇ"});
28159
28160 // Case 7: Test blockquote newline preserves something
28161 cx.set_state(indoc! {"
28162 > Item 1ˇ"
28163 });
28164 cx.update_editor(|editor, window, cx| {
28165 editor.newline(&Newline, window, cx);
28166 });
28167 cx.assert_editor_state(indoc! {"
28168 > Item 1
28169 ˇ"
28170 });
28171}
28172
28173#[gpui::test]
28174async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
28175 cx: &mut gpui::TestAppContext,
28176) {
28177 init_test(cx, |_| {});
28178
28179 let url = "https://zed.dev";
28180
28181 let markdown_language = Arc::new(Language::new(
28182 LanguageConfig {
28183 name: "Markdown".into(),
28184 ..LanguageConfig::default()
28185 },
28186 None,
28187 ));
28188
28189 let mut cx = EditorTestContext::new(cx).await;
28190 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28191 cx.set_state(&format!(
28192 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
28193 ));
28194
28195 cx.update_editor(|editor, window, cx| {
28196 editor.copy(&Copy, window, cx);
28197 });
28198
28199 cx.set_state(&format!(
28200 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
28201 ));
28202
28203 cx.update_editor(|editor, window, cx| {
28204 editor.paste(&Paste, window, cx);
28205 });
28206
28207 cx.assert_editor_state(&format!(
28208 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
28209 ));
28210}
28211
28212#[gpui::test]
28213async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
28214 cx: &mut gpui::TestAppContext,
28215) {
28216 init_test(cx, |_| {});
28217
28218 let url = "https://zed.dev";
28219
28220 let markdown_language = Arc::new(Language::new(
28221 LanguageConfig {
28222 name: "Markdown".into(),
28223 ..LanguageConfig::default()
28224 },
28225 None,
28226 ));
28227
28228 let mut cx = EditorTestContext::new(cx).await;
28229 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28230 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
28231
28232 cx.update_editor(|editor, window, cx| {
28233 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28234 editor.paste(&Paste, window, cx);
28235 });
28236
28237 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
28238}
28239
28240#[gpui::test]
28241async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
28242 cx: &mut gpui::TestAppContext,
28243) {
28244 init_test(cx, |_| {});
28245
28246 let text = "Awesome";
28247
28248 let markdown_language = Arc::new(Language::new(
28249 LanguageConfig {
28250 name: "Markdown".into(),
28251 ..LanguageConfig::default()
28252 },
28253 None,
28254 ));
28255
28256 let mut cx = EditorTestContext::new(cx).await;
28257 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28258 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
28259
28260 cx.update_editor(|editor, window, cx| {
28261 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
28262 editor.paste(&Paste, window, cx);
28263 });
28264
28265 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
28266}
28267
28268#[gpui::test]
28269async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
28270 cx: &mut gpui::TestAppContext,
28271) {
28272 init_test(cx, |_| {});
28273
28274 let url = "https://zed.dev";
28275
28276 let markdown_language = Arc::new(Language::new(
28277 LanguageConfig {
28278 name: "Rust".into(),
28279 ..LanguageConfig::default()
28280 },
28281 None,
28282 ));
28283
28284 let mut cx = EditorTestContext::new(cx).await;
28285 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28286 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
28287
28288 cx.update_editor(|editor, window, cx| {
28289 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28290 editor.paste(&Paste, window, cx);
28291 });
28292
28293 cx.assert_editor_state(&format!(
28294 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
28295 ));
28296}
28297
28298#[gpui::test]
28299async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
28300 cx: &mut TestAppContext,
28301) {
28302 init_test(cx, |_| {});
28303
28304 let url = "https://zed.dev";
28305
28306 let markdown_language = Arc::new(Language::new(
28307 LanguageConfig {
28308 name: "Markdown".into(),
28309 ..LanguageConfig::default()
28310 },
28311 None,
28312 ));
28313
28314 let (editor, cx) = cx.add_window_view(|window, cx| {
28315 let multi_buffer = MultiBuffer::build_multi(
28316 [
28317 ("this will embed -> link", vec![Point::row_range(0..1)]),
28318 ("this will replace -> link", vec![Point::row_range(0..1)]),
28319 ],
28320 cx,
28321 );
28322 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
28323 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28324 s.select_ranges(vec![
28325 Point::new(0, 19)..Point::new(0, 23),
28326 Point::new(1, 21)..Point::new(1, 25),
28327 ])
28328 });
28329 let first_buffer_id = multi_buffer
28330 .read(cx)
28331 .excerpt_buffer_ids()
28332 .into_iter()
28333 .next()
28334 .unwrap();
28335 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
28336 first_buffer.update(cx, |buffer, cx| {
28337 buffer.set_language(Some(markdown_language.clone()), cx);
28338 });
28339
28340 editor
28341 });
28342 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28343
28344 cx.update_editor(|editor, window, cx| {
28345 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28346 editor.paste(&Paste, window, cx);
28347 });
28348
28349 cx.assert_editor_state(&format!(
28350 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
28351 ));
28352}
28353
28354#[gpui::test]
28355async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
28356 init_test(cx, |_| {});
28357
28358 let fs = FakeFs::new(cx.executor());
28359 fs.insert_tree(
28360 path!("/project"),
28361 json!({
28362 "first.rs": "# First Document\nSome content here.",
28363 "second.rs": "Plain text content for second file.",
28364 }),
28365 )
28366 .await;
28367
28368 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
28369 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
28370 let cx = &mut VisualTestContext::from_window(*workspace, cx);
28371
28372 let language = rust_lang();
28373 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28374 language_registry.add(language.clone());
28375 let mut fake_servers = language_registry.register_fake_lsp(
28376 "Rust",
28377 FakeLspAdapter {
28378 ..FakeLspAdapter::default()
28379 },
28380 );
28381
28382 let buffer1 = project
28383 .update(cx, |project, cx| {
28384 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
28385 })
28386 .await
28387 .unwrap();
28388 let buffer2 = project
28389 .update(cx, |project, cx| {
28390 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
28391 })
28392 .await
28393 .unwrap();
28394
28395 let multi_buffer = cx.new(|cx| {
28396 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
28397 multi_buffer.set_excerpts_for_path(
28398 PathKey::for_buffer(&buffer1, cx),
28399 buffer1.clone(),
28400 [Point::zero()..buffer1.read(cx).max_point()],
28401 3,
28402 cx,
28403 );
28404 multi_buffer.set_excerpts_for_path(
28405 PathKey::for_buffer(&buffer2, cx),
28406 buffer2.clone(),
28407 [Point::zero()..buffer1.read(cx).max_point()],
28408 3,
28409 cx,
28410 );
28411 multi_buffer
28412 });
28413
28414 let (editor, cx) = cx.add_window_view(|window, cx| {
28415 Editor::new(
28416 EditorMode::full(),
28417 multi_buffer,
28418 Some(project.clone()),
28419 window,
28420 cx,
28421 )
28422 });
28423
28424 let fake_language_server = fake_servers.next().await.unwrap();
28425
28426 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
28427
28428 let save = editor.update_in(cx, |editor, window, cx| {
28429 assert!(editor.is_dirty(cx));
28430
28431 editor.save(
28432 SaveOptions {
28433 format: true,
28434 autosave: true,
28435 },
28436 project,
28437 window,
28438 cx,
28439 )
28440 });
28441 let (start_edit_tx, start_edit_rx) = oneshot::channel();
28442 let (done_edit_tx, done_edit_rx) = oneshot::channel();
28443 let mut done_edit_rx = Some(done_edit_rx);
28444 let mut start_edit_tx = Some(start_edit_tx);
28445
28446 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
28447 start_edit_tx.take().unwrap().send(()).unwrap();
28448 let done_edit_rx = done_edit_rx.take().unwrap();
28449 async move {
28450 done_edit_rx.await.unwrap();
28451 Ok(None)
28452 }
28453 });
28454
28455 start_edit_rx.await.unwrap();
28456 buffer2
28457 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
28458 .unwrap();
28459
28460 done_edit_tx.send(()).unwrap();
28461
28462 save.await.unwrap();
28463 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
28464}
28465
28466#[track_caller]
28467fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
28468 editor
28469 .all_inlays(cx)
28470 .into_iter()
28471 .filter_map(|inlay| inlay.get_color())
28472 .map(Rgba::from)
28473 .collect()
28474}
28475
28476#[gpui::test]
28477fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
28478 init_test(cx, |_| {});
28479
28480 let editor = cx.add_window(|window, cx| {
28481 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
28482 build_editor(buffer, window, cx)
28483 });
28484
28485 editor
28486 .update(cx, |editor, window, cx| {
28487 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28488 s.select_display_ranges([
28489 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
28490 ])
28491 });
28492
28493 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
28494
28495 assert_eq!(
28496 editor.display_text(cx),
28497 "line1\nline2\nline2",
28498 "Duplicating last line upward should create duplicate above, not on same line"
28499 );
28500
28501 assert_eq!(
28502 editor
28503 .selections
28504 .display_ranges(&editor.display_snapshot(cx)),
28505 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
28506 "Selection should move to the duplicated line"
28507 );
28508 })
28509 .unwrap();
28510}
28511
28512#[gpui::test]
28513async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
28514 init_test(cx, |_| {});
28515
28516 let mut cx = EditorTestContext::new(cx).await;
28517
28518 cx.set_state("line1\nline2ˇ");
28519
28520 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28521
28522 let clipboard_text = cx
28523 .read_from_clipboard()
28524 .and_then(|item| item.text().as_deref().map(str::to_string));
28525
28526 assert_eq!(
28527 clipboard_text,
28528 Some("line2\n".to_string()),
28529 "Copying a line without trailing newline should include a newline"
28530 );
28531
28532 cx.set_state("line1\nˇ");
28533
28534 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28535
28536 cx.assert_editor_state("line1\nline2\nˇ");
28537}
28538
28539#[gpui::test]
28540async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28541 init_test(cx, |_| {});
28542
28543 let mut cx = EditorTestContext::new(cx).await;
28544
28545 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28546
28547 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28548
28549 let clipboard_text = cx
28550 .read_from_clipboard()
28551 .and_then(|item| item.text().as_deref().map(str::to_string));
28552
28553 assert_eq!(
28554 clipboard_text,
28555 Some("line1\nline2\nline3\n".to_string()),
28556 "Copying multiple lines should include a single newline between lines"
28557 );
28558
28559 cx.set_state("lineA\nˇ");
28560
28561 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28562
28563 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28564}
28565
28566#[gpui::test]
28567async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28568 init_test(cx, |_| {});
28569
28570 let mut cx = EditorTestContext::new(cx).await;
28571
28572 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28573
28574 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
28575
28576 let clipboard_text = cx
28577 .read_from_clipboard()
28578 .and_then(|item| item.text().as_deref().map(str::to_string));
28579
28580 assert_eq!(
28581 clipboard_text,
28582 Some("line1\nline2\nline3\n".to_string()),
28583 "Copying multiple lines should include a single newline between lines"
28584 );
28585
28586 cx.set_state("lineA\nˇ");
28587
28588 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28589
28590 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28591}
28592
28593#[gpui::test]
28594async fn test_end_of_editor_context(cx: &mut TestAppContext) {
28595 init_test(cx, |_| {});
28596
28597 let mut cx = EditorTestContext::new(cx).await;
28598
28599 cx.set_state("line1\nline2ˇ");
28600 cx.update_editor(|e, window, cx| {
28601 e.set_mode(EditorMode::SingleLine);
28602 assert!(e.key_context(window, cx).contains("end_of_input"));
28603 });
28604 cx.set_state("ˇline1\nline2");
28605 cx.update_editor(|e, window, cx| {
28606 assert!(!e.key_context(window, cx).contains("end_of_input"));
28607 });
28608 cx.set_state("line1ˇ\nline2");
28609 cx.update_editor(|e, window, cx| {
28610 assert!(!e.key_context(window, cx).contains("end_of_input"));
28611 });
28612}
28613
28614#[gpui::test]
28615async fn test_sticky_scroll(cx: &mut TestAppContext) {
28616 init_test(cx, |_| {});
28617 let mut cx = EditorTestContext::new(cx).await;
28618
28619 let buffer = indoc! {"
28620 ˇfn foo() {
28621 let abc = 123;
28622 }
28623 struct Bar;
28624 impl Bar {
28625 fn new() -> Self {
28626 Self
28627 }
28628 }
28629 fn baz() {
28630 }
28631 "};
28632 cx.set_state(&buffer);
28633
28634 cx.update_editor(|e, _, cx| {
28635 e.buffer()
28636 .read(cx)
28637 .as_singleton()
28638 .unwrap()
28639 .update(cx, |buffer, cx| {
28640 buffer.set_language(Some(rust_lang()), cx);
28641 })
28642 });
28643
28644 let mut sticky_headers = |offset: ScrollOffset| {
28645 cx.update_editor(|e, window, cx| {
28646 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
28647 let style = e.style(cx).clone();
28648 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), &style, cx)
28649 .into_iter()
28650 .map(
28651 |StickyHeader {
28652 start_point,
28653 offset,
28654 ..
28655 }| { (start_point, offset) },
28656 )
28657 .collect::<Vec<_>>()
28658 })
28659 };
28660
28661 let fn_foo = Point { row: 0, column: 0 };
28662 let impl_bar = Point { row: 4, column: 0 };
28663 let fn_new = Point { row: 5, column: 4 };
28664
28665 assert_eq!(sticky_headers(0.0), vec![]);
28666 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
28667 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
28668 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
28669 assert_eq!(sticky_headers(2.0), vec![]);
28670 assert_eq!(sticky_headers(2.5), vec![]);
28671 assert_eq!(sticky_headers(3.0), vec![]);
28672 assert_eq!(sticky_headers(3.5), vec![]);
28673 assert_eq!(sticky_headers(4.0), vec![]);
28674 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28675 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28676 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
28677 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
28678 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
28679 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
28680 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
28681 assert_eq!(sticky_headers(8.0), vec![]);
28682 assert_eq!(sticky_headers(8.5), vec![]);
28683 assert_eq!(sticky_headers(9.0), vec![]);
28684 assert_eq!(sticky_headers(9.5), vec![]);
28685 assert_eq!(sticky_headers(10.0), vec![]);
28686}
28687
28688#[gpui::test]
28689async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
28690 init_test(cx, |_| {});
28691 cx.update(|cx| {
28692 SettingsStore::update_global(cx, |store, cx| {
28693 store.update_user_settings(cx, |settings| {
28694 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
28695 enabled: Some(true),
28696 })
28697 });
28698 });
28699 });
28700 let mut cx = EditorTestContext::new(cx).await;
28701
28702 let line_height = cx.update_editor(|editor, window, cx| {
28703 editor
28704 .style(cx)
28705 .text
28706 .line_height_in_pixels(window.rem_size())
28707 });
28708
28709 let buffer = indoc! {"
28710 ˇfn foo() {
28711 let abc = 123;
28712 }
28713 struct Bar;
28714 impl Bar {
28715 fn new() -> Self {
28716 Self
28717 }
28718 }
28719 fn baz() {
28720 }
28721 "};
28722 cx.set_state(&buffer);
28723
28724 cx.update_editor(|e, _, cx| {
28725 e.buffer()
28726 .read(cx)
28727 .as_singleton()
28728 .unwrap()
28729 .update(cx, |buffer, cx| {
28730 buffer.set_language(Some(rust_lang()), cx);
28731 })
28732 });
28733
28734 let fn_foo = || empty_range(0, 0);
28735 let impl_bar = || empty_range(4, 0);
28736 let fn_new = || empty_range(5, 4);
28737
28738 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
28739 cx.update_editor(|e, window, cx| {
28740 e.scroll(
28741 gpui::Point {
28742 x: 0.,
28743 y: scroll_offset,
28744 },
28745 None,
28746 window,
28747 cx,
28748 );
28749 });
28750 cx.simulate_click(
28751 gpui::Point {
28752 x: px(0.),
28753 y: click_offset as f32 * line_height,
28754 },
28755 Modifiers::none(),
28756 );
28757 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
28758 };
28759
28760 assert_eq!(
28761 scroll_and_click(
28762 4.5, // impl Bar is halfway off the screen
28763 0.0 // click top of screen
28764 ),
28765 // scrolled to impl Bar
28766 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28767 );
28768
28769 assert_eq!(
28770 scroll_and_click(
28771 4.5, // impl Bar is halfway off the screen
28772 0.25 // click middle of impl Bar
28773 ),
28774 // scrolled to impl Bar
28775 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28776 );
28777
28778 assert_eq!(
28779 scroll_and_click(
28780 4.5, // impl Bar is halfway off the screen
28781 1.5 // click below impl Bar (e.g. fn new())
28782 ),
28783 // scrolled to fn new() - this is below the impl Bar header which has persisted
28784 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28785 );
28786
28787 assert_eq!(
28788 scroll_and_click(
28789 5.5, // fn new is halfway underneath impl Bar
28790 0.75 // click on the overlap of impl Bar and fn new()
28791 ),
28792 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28793 );
28794
28795 assert_eq!(
28796 scroll_and_click(
28797 5.5, // fn new is halfway underneath impl Bar
28798 1.25 // click on the visible part of fn new()
28799 ),
28800 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28801 );
28802
28803 assert_eq!(
28804 scroll_and_click(
28805 1.5, // fn foo is halfway off the screen
28806 0.0 // click top of screen
28807 ),
28808 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
28809 );
28810
28811 assert_eq!(
28812 scroll_and_click(
28813 1.5, // fn foo is halfway off the screen
28814 0.75 // click visible part of let abc...
28815 )
28816 .0,
28817 // no change in scroll
28818 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
28819 (gpui::Point { x: 0., y: 1.5 })
28820 );
28821}
28822
28823#[gpui::test]
28824async fn test_next_prev_reference(cx: &mut TestAppContext) {
28825 const CYCLE_POSITIONS: &[&'static str] = &[
28826 indoc! {"
28827 fn foo() {
28828 let ˇabc = 123;
28829 let x = abc + 1;
28830 let y = abc + 2;
28831 let z = abc + 2;
28832 }
28833 "},
28834 indoc! {"
28835 fn foo() {
28836 let abc = 123;
28837 let x = ˇabc + 1;
28838 let y = abc + 2;
28839 let z = abc + 2;
28840 }
28841 "},
28842 indoc! {"
28843 fn foo() {
28844 let abc = 123;
28845 let x = abc + 1;
28846 let y = ˇabc + 2;
28847 let z = abc + 2;
28848 }
28849 "},
28850 indoc! {"
28851 fn foo() {
28852 let abc = 123;
28853 let x = abc + 1;
28854 let y = abc + 2;
28855 let z = ˇabc + 2;
28856 }
28857 "},
28858 ];
28859
28860 init_test(cx, |_| {});
28861
28862 let mut cx = EditorLspTestContext::new_rust(
28863 lsp::ServerCapabilities {
28864 references_provider: Some(lsp::OneOf::Left(true)),
28865 ..Default::default()
28866 },
28867 cx,
28868 )
28869 .await;
28870
28871 // importantly, the cursor is in the middle
28872 cx.set_state(indoc! {"
28873 fn foo() {
28874 let aˇbc = 123;
28875 let x = abc + 1;
28876 let y = abc + 2;
28877 let z = abc + 2;
28878 }
28879 "});
28880
28881 let reference_ranges = [
28882 lsp::Position::new(1, 8),
28883 lsp::Position::new(2, 12),
28884 lsp::Position::new(3, 12),
28885 lsp::Position::new(4, 12),
28886 ]
28887 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
28888
28889 cx.lsp
28890 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
28891 Ok(Some(
28892 reference_ranges
28893 .map(|range| lsp::Location {
28894 uri: params.text_document_position.text_document.uri.clone(),
28895 range,
28896 })
28897 .to_vec(),
28898 ))
28899 });
28900
28901 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
28902 cx.update_editor(|editor, window, cx| {
28903 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
28904 })
28905 .unwrap()
28906 .await
28907 .unwrap()
28908 };
28909
28910 _move(Direction::Next, 1, &mut cx).await;
28911 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28912
28913 _move(Direction::Next, 1, &mut cx).await;
28914 cx.assert_editor_state(CYCLE_POSITIONS[2]);
28915
28916 _move(Direction::Next, 1, &mut cx).await;
28917 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28918
28919 // loops back to the start
28920 _move(Direction::Next, 1, &mut cx).await;
28921 cx.assert_editor_state(CYCLE_POSITIONS[0]);
28922
28923 // loops back to the end
28924 _move(Direction::Prev, 1, &mut cx).await;
28925 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28926
28927 _move(Direction::Prev, 1, &mut cx).await;
28928 cx.assert_editor_state(CYCLE_POSITIONS[2]);
28929
28930 _move(Direction::Prev, 1, &mut cx).await;
28931 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28932
28933 _move(Direction::Prev, 1, &mut cx).await;
28934 cx.assert_editor_state(CYCLE_POSITIONS[0]);
28935
28936 _move(Direction::Next, 3, &mut cx).await;
28937 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28938
28939 _move(Direction::Prev, 2, &mut cx).await;
28940 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28941}
28942
28943#[gpui::test]
28944async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
28945 init_test(cx, |_| {});
28946
28947 let (editor, cx) = cx.add_window_view(|window, cx| {
28948 let multi_buffer = MultiBuffer::build_multi(
28949 [
28950 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28951 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28952 ],
28953 cx,
28954 );
28955 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28956 });
28957
28958 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28959 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
28960
28961 cx.assert_excerpts_with_selections(indoc! {"
28962 [EXCERPT]
28963 ˇ1
28964 2
28965 3
28966 [EXCERPT]
28967 1
28968 2
28969 3
28970 "});
28971
28972 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
28973 cx.update_editor(|editor, window, cx| {
28974 editor.change_selections(None.into(), window, cx, |s| {
28975 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28976 });
28977 });
28978 cx.assert_excerpts_with_selections(indoc! {"
28979 [EXCERPT]
28980 1
28981 2ˇ
28982 3
28983 [EXCERPT]
28984 1
28985 2
28986 3
28987 "});
28988
28989 cx.update_editor(|editor, window, cx| {
28990 editor
28991 .select_all_matches(&SelectAllMatches, window, cx)
28992 .unwrap();
28993 });
28994 cx.assert_excerpts_with_selections(indoc! {"
28995 [EXCERPT]
28996 1
28997 2ˇ
28998 3
28999 [EXCERPT]
29000 1
29001 2ˇ
29002 3
29003 "});
29004
29005 cx.update_editor(|editor, window, cx| {
29006 editor.handle_input("X", window, cx);
29007 });
29008 cx.assert_excerpts_with_selections(indoc! {"
29009 [EXCERPT]
29010 1
29011 Xˇ
29012 3
29013 [EXCERPT]
29014 1
29015 Xˇ
29016 3
29017 "});
29018
29019 // Scenario 2: Select "2", then fold second buffer before insertion
29020 cx.update_multibuffer(|mb, cx| {
29021 for buffer_id in buffer_ids.iter() {
29022 let buffer = mb.buffer(*buffer_id).unwrap();
29023 buffer.update(cx, |buffer, cx| {
29024 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29025 });
29026 }
29027 });
29028
29029 // Select "2" and select all matches
29030 cx.update_editor(|editor, window, cx| {
29031 editor.change_selections(None.into(), window, cx, |s| {
29032 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29033 });
29034 editor
29035 .select_all_matches(&SelectAllMatches, window, cx)
29036 .unwrap();
29037 });
29038
29039 // Fold second buffer - should remove selections from folded buffer
29040 cx.update_editor(|editor, _, cx| {
29041 editor.fold_buffer(buffer_ids[1], cx);
29042 });
29043 cx.assert_excerpts_with_selections(indoc! {"
29044 [EXCERPT]
29045 1
29046 2ˇ
29047 3
29048 [EXCERPT]
29049 [FOLDED]
29050 "});
29051
29052 // Insert text - should only affect first buffer
29053 cx.update_editor(|editor, window, cx| {
29054 editor.handle_input("Y", window, cx);
29055 });
29056 cx.update_editor(|editor, _, cx| {
29057 editor.unfold_buffer(buffer_ids[1], cx);
29058 });
29059 cx.assert_excerpts_with_selections(indoc! {"
29060 [EXCERPT]
29061 1
29062 Yˇ
29063 3
29064 [EXCERPT]
29065 1
29066 2
29067 3
29068 "});
29069
29070 // Scenario 3: Select "2", then fold first buffer before insertion
29071 cx.update_multibuffer(|mb, cx| {
29072 for buffer_id in buffer_ids.iter() {
29073 let buffer = mb.buffer(*buffer_id).unwrap();
29074 buffer.update(cx, |buffer, cx| {
29075 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29076 });
29077 }
29078 });
29079
29080 // Select "2" and select all matches
29081 cx.update_editor(|editor, window, cx| {
29082 editor.change_selections(None.into(), window, cx, |s| {
29083 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29084 });
29085 editor
29086 .select_all_matches(&SelectAllMatches, window, cx)
29087 .unwrap();
29088 });
29089
29090 // Fold first buffer - should remove selections from folded buffer
29091 cx.update_editor(|editor, _, cx| {
29092 editor.fold_buffer(buffer_ids[0], cx);
29093 });
29094 cx.assert_excerpts_with_selections(indoc! {"
29095 [EXCERPT]
29096 [FOLDED]
29097 [EXCERPT]
29098 1
29099 2ˇ
29100 3
29101 "});
29102
29103 // Insert text - should only affect second buffer
29104 cx.update_editor(|editor, window, cx| {
29105 editor.handle_input("Z", window, cx);
29106 });
29107 cx.update_editor(|editor, _, cx| {
29108 editor.unfold_buffer(buffer_ids[0], cx);
29109 });
29110 cx.assert_excerpts_with_selections(indoc! {"
29111 [EXCERPT]
29112 1
29113 2
29114 3
29115 [EXCERPT]
29116 1
29117 Zˇ
29118 3
29119 "});
29120
29121 // Test correct folded header is selected upon fold
29122 cx.update_editor(|editor, _, cx| {
29123 editor.fold_buffer(buffer_ids[0], cx);
29124 editor.fold_buffer(buffer_ids[1], cx);
29125 });
29126 cx.assert_excerpts_with_selections(indoc! {"
29127 [EXCERPT]
29128 [FOLDED]
29129 [EXCERPT]
29130 ˇ[FOLDED]
29131 "});
29132
29133 // Test selection inside folded buffer unfolds it on type
29134 cx.update_editor(|editor, window, cx| {
29135 editor.handle_input("W", window, cx);
29136 });
29137 cx.update_editor(|editor, _, cx| {
29138 editor.unfold_buffer(buffer_ids[0], cx);
29139 });
29140 cx.assert_excerpts_with_selections(indoc! {"
29141 [EXCERPT]
29142 1
29143 2
29144 3
29145 [EXCERPT]
29146 Wˇ1
29147 Z
29148 3
29149 "});
29150}
29151
29152#[gpui::test]
29153async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) {
29154 init_test(cx, |_| {});
29155 let mut leader_cx = EditorTestContext::new(cx).await;
29156
29157 let diff_base = indoc!(
29158 r#"
29159 one
29160 two
29161 three
29162 four
29163 five
29164 six
29165 "#
29166 );
29167
29168 let initial_state = indoc!(
29169 r#"
29170 ˇone
29171 two
29172 THREE
29173 four
29174 five
29175 six
29176 "#
29177 );
29178
29179 leader_cx.set_state(initial_state);
29180
29181 leader_cx.set_head_text(&diff_base);
29182 leader_cx.run_until_parked();
29183
29184 let follower = leader_cx.update_multibuffer(|leader, cx| {
29185 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
29186 leader.set_all_diff_hunks_expanded(cx);
29187 leader.get_or_create_follower(cx)
29188 });
29189 follower.update(cx, |follower, cx| {
29190 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
29191 follower.set_all_diff_hunks_expanded(cx);
29192 });
29193
29194 let follower_editor =
29195 leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx));
29196 // leader_cx.window.focus(&follower_editor.focus_handle(cx));
29197
29198 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await;
29199 cx.run_until_parked();
29200
29201 leader_cx.assert_editor_state(initial_state);
29202 follower_cx.assert_editor_state(indoc! {
29203 r#"
29204 ˇone
29205 two
29206 three
29207 four
29208 five
29209 six
29210 "#
29211 });
29212
29213 follower_cx.editor(|editor, _window, cx| {
29214 assert!(editor.read_only(cx));
29215 });
29216
29217 leader_cx.update_editor(|editor, _window, cx| {
29218 editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx);
29219 });
29220 cx.run_until_parked();
29221
29222 leader_cx.assert_editor_state(indoc! {
29223 r#"
29224 ˇone
29225 two
29226 THREE
29227 four
29228 FIVE
29229 six
29230 "#
29231 });
29232
29233 follower_cx.assert_editor_state(indoc! {
29234 r#"
29235 ˇone
29236 two
29237 three
29238 four
29239 five
29240 six
29241 "#
29242 });
29243
29244 leader_cx.update_editor(|editor, _window, cx| {
29245 editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx);
29246 });
29247 cx.run_until_parked();
29248
29249 leader_cx.assert_editor_state(indoc! {
29250 r#"
29251 ˇone
29252 two
29253 THREE
29254 four
29255 FIVE
29256 six
29257 SEVEN"#
29258 });
29259
29260 follower_cx.assert_editor_state(indoc! {
29261 r#"
29262 ˇone
29263 two
29264 three
29265 four
29266 five
29267 six
29268 "#
29269 });
29270
29271 leader_cx.update_editor(|editor, window, cx| {
29272 editor.move_down(&MoveDown, window, cx);
29273 editor.refresh_selected_text_highlights(true, window, cx);
29274 });
29275 leader_cx.run_until_parked();
29276}
29277
29278#[gpui::test]
29279async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) {
29280 init_test(cx, |_| {});
29281 let base_text = "base\n";
29282 let buffer_text = "buffer\n";
29283
29284 let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx));
29285 let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx));
29286
29287 let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx));
29288 let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx));
29289 let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx));
29290 let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx));
29291
29292 let leader = cx.new(|cx| {
29293 let mut leader = MultiBuffer::new(Capability::ReadWrite);
29294 leader.set_all_diff_hunks_expanded(cx);
29295 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
29296 leader
29297 });
29298 let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx));
29299 follower.update(cx, |follower, _| {
29300 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
29301 });
29302
29303 leader.update(cx, |leader, cx| {
29304 leader.insert_excerpts_after(
29305 ExcerptId::min(),
29306 extra_buffer_2.clone(),
29307 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29308 cx,
29309 );
29310 leader.add_diff(extra_diff_2.clone(), cx);
29311
29312 leader.insert_excerpts_after(
29313 ExcerptId::min(),
29314 extra_buffer_1.clone(),
29315 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29316 cx,
29317 );
29318 leader.add_diff(extra_diff_1.clone(), cx);
29319
29320 leader.insert_excerpts_after(
29321 ExcerptId::min(),
29322 buffer1.clone(),
29323 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29324 cx,
29325 );
29326 leader.add_diff(diff1.clone(), cx);
29327 });
29328
29329 cx.run_until_parked();
29330 let mut cx = cx.add_empty_window();
29331
29332 let leader_editor = cx
29333 .new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx));
29334 let follower_editor = cx.new_window_entity(|window, cx| {
29335 Editor::for_multibuffer(follower.clone(), None, window, cx)
29336 });
29337
29338 let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await;
29339 leader_cx.assert_editor_state(indoc! {"
29340 ˇbuffer
29341
29342 dummy text 1
29343
29344 dummy text 2
29345 "});
29346 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await;
29347 follower_cx.assert_editor_state(indoc! {"
29348 ˇbase
29349
29350
29351 "});
29352}
29353
29354#[gpui::test]
29355async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
29356 init_test(cx, |_| {});
29357
29358 let (editor, cx) = cx.add_window_view(|window, cx| {
29359 let multi_buffer = MultiBuffer::build_multi(
29360 [
29361 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29362 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
29363 ],
29364 cx,
29365 );
29366 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29367 });
29368
29369 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29370
29371 cx.assert_excerpts_with_selections(indoc! {"
29372 [EXCERPT]
29373 ˇ1
29374 2
29375 3
29376 [EXCERPT]
29377 1
29378 2
29379 3
29380 4
29381 5
29382 6
29383 7
29384 8
29385 9
29386 "});
29387
29388 cx.update_editor(|editor, window, cx| {
29389 editor.change_selections(None.into(), window, cx, |s| {
29390 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
29391 });
29392 });
29393
29394 cx.assert_excerpts_with_selections(indoc! {"
29395 [EXCERPT]
29396 1
29397 2
29398 3
29399 [EXCERPT]
29400 1
29401 2
29402 3
29403 4
29404 5
29405 6
29406 ˇ7
29407 8
29408 9
29409 "});
29410
29411 cx.update_editor(|editor, _window, cx| {
29412 editor.set_vertical_scroll_margin(0, cx);
29413 });
29414
29415 cx.update_editor(|editor, window, cx| {
29416 assert_eq!(editor.vertical_scroll_margin(), 0);
29417 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29418 assert_eq!(
29419 editor.snapshot(window, cx).scroll_position(),
29420 gpui::Point::new(0., 12.0)
29421 );
29422 });
29423
29424 cx.update_editor(|editor, _window, cx| {
29425 editor.set_vertical_scroll_margin(3, cx);
29426 });
29427
29428 cx.update_editor(|editor, window, cx| {
29429 assert_eq!(editor.vertical_scroll_margin(), 3);
29430 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29431 assert_eq!(
29432 editor.snapshot(window, cx).scroll_position(),
29433 gpui::Point::new(0., 9.0)
29434 );
29435 });
29436}
29437
29438#[gpui::test]
29439async fn test_find_references_single_case(cx: &mut TestAppContext) {
29440 init_test(cx, |_| {});
29441 let mut cx = EditorLspTestContext::new_rust(
29442 lsp::ServerCapabilities {
29443 references_provider: Some(lsp::OneOf::Left(true)),
29444 ..lsp::ServerCapabilities::default()
29445 },
29446 cx,
29447 )
29448 .await;
29449
29450 let before = indoc!(
29451 r#"
29452 fn main() {
29453 let aˇbc = 123;
29454 let xyz = abc;
29455 }
29456 "#
29457 );
29458 let after = indoc!(
29459 r#"
29460 fn main() {
29461 let abc = 123;
29462 let xyz = ˇabc;
29463 }
29464 "#
29465 );
29466
29467 cx.lsp
29468 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
29469 Ok(Some(vec![
29470 lsp::Location {
29471 uri: params.text_document_position.text_document.uri.clone(),
29472 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
29473 },
29474 lsp::Location {
29475 uri: params.text_document_position.text_document.uri,
29476 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
29477 },
29478 ]))
29479 });
29480
29481 cx.set_state(before);
29482
29483 let action = FindAllReferences {
29484 always_open_multibuffer: false,
29485 };
29486
29487 let navigated = cx
29488 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
29489 .expect("should have spawned a task")
29490 .await
29491 .unwrap();
29492
29493 assert_eq!(navigated, Navigated::No);
29494
29495 cx.run_until_parked();
29496
29497 cx.assert_editor_state(after);
29498}
29499
29500#[gpui::test]
29501async fn test_local_worktree_trust(cx: &mut TestAppContext) {
29502 init_test(cx, |_| {});
29503 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), None, None, cx));
29504
29505 cx.update(|cx| {
29506 SettingsStore::update_global(cx, |store, cx| {
29507 store.update_user_settings(cx, |settings| {
29508 settings.project.all_languages.defaults.inlay_hints =
29509 Some(InlayHintSettingsContent {
29510 enabled: Some(true),
29511 ..InlayHintSettingsContent::default()
29512 });
29513 });
29514 });
29515 });
29516
29517 let fs = FakeFs::new(cx.executor());
29518 fs.insert_tree(
29519 path!("/project"),
29520 json!({
29521 ".zed": {
29522 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
29523 },
29524 "main.rs": "fn main() {}"
29525 }),
29526 )
29527 .await;
29528
29529 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
29530 let server_name = "override-rust-analyzer";
29531 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
29532
29533 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
29534 language_registry.add(rust_lang());
29535
29536 let capabilities = lsp::ServerCapabilities {
29537 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
29538 ..lsp::ServerCapabilities::default()
29539 };
29540 let mut fake_language_servers = language_registry.register_fake_lsp(
29541 "Rust",
29542 FakeLspAdapter {
29543 name: server_name,
29544 capabilities,
29545 initializer: Some(Box::new({
29546 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
29547 move |fake_server| {
29548 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
29549 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
29550 move |_params, _| {
29551 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
29552 async move {
29553 Ok(Some(vec![lsp::InlayHint {
29554 position: lsp::Position::new(0, 0),
29555 label: lsp::InlayHintLabel::String("hint".to_string()),
29556 kind: None,
29557 text_edits: None,
29558 tooltip: None,
29559 padding_left: None,
29560 padding_right: None,
29561 data: None,
29562 }]))
29563 }
29564 },
29565 );
29566 }
29567 })),
29568 ..FakeLspAdapter::default()
29569 },
29570 );
29571
29572 cx.run_until_parked();
29573
29574 let worktree_id = project.read_with(cx, |project, cx| {
29575 project
29576 .worktrees(cx)
29577 .next()
29578 .map(|wt| wt.read(cx).id())
29579 .expect("should have a worktree")
29580 });
29581
29582 let trusted_worktrees =
29583 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
29584
29585 let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
29586 assert!(!can_trust, "worktree should be restricted initially");
29587
29588 let buffer_before_approval = project
29589 .update(cx, |project, cx| {
29590 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
29591 })
29592 .await
29593 .unwrap();
29594
29595 let (editor, cx) = cx.add_window_view(|window, cx| {
29596 Editor::new(
29597 EditorMode::full(),
29598 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
29599 Some(project.clone()),
29600 window,
29601 cx,
29602 )
29603 });
29604 cx.run_until_parked();
29605 let fake_language_server = fake_language_servers.next();
29606
29607 cx.read(|cx| {
29608 let file = buffer_before_approval.read(cx).file();
29609 assert_eq!(
29610 language::language_settings::language_settings(Some("Rust".into()), file, cx)
29611 .language_servers,
29612 ["...".to_string()],
29613 "local .zed/settings.json must not apply before trust approval"
29614 )
29615 });
29616
29617 editor.update_in(cx, |editor, window, cx| {
29618 editor.handle_input("1", window, cx);
29619 });
29620 cx.run_until_parked();
29621 cx.executor()
29622 .advance_clock(std::time::Duration::from_secs(1));
29623 assert_eq!(
29624 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
29625 0,
29626 "inlay hints must not be queried before trust approval"
29627 );
29628
29629 trusted_worktrees.update(cx, |store, cx| {
29630 store.trust(
29631 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
29632 None,
29633 cx,
29634 );
29635 });
29636 cx.run_until_parked();
29637
29638 cx.read(|cx| {
29639 let file = buffer_before_approval.read(cx).file();
29640 assert_eq!(
29641 language::language_settings::language_settings(Some("Rust".into()), file, cx)
29642 .language_servers,
29643 ["override-rust-analyzer".to_string()],
29644 "local .zed/settings.json should apply after trust approval"
29645 )
29646 });
29647 let _fake_language_server = fake_language_server.await.unwrap();
29648 editor.update_in(cx, |editor, window, cx| {
29649 editor.handle_input("1", window, cx);
29650 });
29651 cx.run_until_parked();
29652 cx.executor()
29653 .advance_clock(std::time::Duration::from_secs(1));
29654 assert!(
29655 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
29656 "inlay hints should be queried after trust approval"
29657 );
29658
29659 let can_trust_after =
29660 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
29661 assert!(can_trust_after, "worktree should be trusted after trust()");
29662}
29663
29664#[gpui::test]
29665fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
29666 // This test reproduces a bug where drawing an editor at a position above the viewport
29667 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
29668 // causes an infinite loop in blocks_in_range.
29669 //
29670 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
29671 // the content mask intersection produces visible_bounds with origin at the viewport top.
29672 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
29673 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
29674 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
29675 init_test(cx, |_| {});
29676
29677 let window = cx.add_window(|_, _| gpui::Empty);
29678 let mut cx = VisualTestContext::from_window(*window, cx);
29679
29680 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
29681 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
29682
29683 // Simulate a small viewport (500x500 pixels at origin 0,0)
29684 cx.simulate_resize(gpui::size(px(500.), px(500.)));
29685
29686 // Draw the editor at a very negative Y position, simulating an editor that's been
29687 // scrolled way above the visible viewport (like in a List that has scrolled past it).
29688 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
29689 // This should NOT hang - it should just render nothing.
29690 cx.draw(
29691 gpui::point(px(0.), px(-10000.)),
29692 gpui::size(px(500.), px(3000.)),
29693 |_, _| editor.clone(),
29694 );
29695
29696 // If we get here without hanging, the test passes
29697}