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.set_state(indoc! { "
20884 one
20885 ˇTWO
20886 three
20887 four
20888 five
20889 "});
20890 cx.run_until_parked();
20891 cx.update_editor(|editor, window, cx| {
20892 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20893 });
20894
20895 cx.assert_state_with_diff(
20896 indoc! { "
20897 one
20898 - two
20899 + ˇTWO
20900 three
20901 four
20902 five
20903 "}
20904 .to_string(),
20905 );
20906 cx.update_editor(|editor, window, cx| {
20907 editor.move_up(&Default::default(), window, cx);
20908 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20909 });
20910 cx.assert_state_with_diff(
20911 indoc! { "
20912 one
20913 ˇTWO
20914 three
20915 four
20916 five
20917 "}
20918 .to_string(),
20919 );
20920}
20921
20922#[gpui::test]
20923async fn test_edits_around_expanded_deletion_hunks(
20924 executor: BackgroundExecutor,
20925 cx: &mut TestAppContext,
20926) {
20927 init_test(cx, |_| {});
20928
20929 let mut cx = EditorTestContext::new(cx).await;
20930
20931 let diff_base = r#"
20932 use some::mod1;
20933 use some::mod2;
20934
20935 const A: u32 = 42;
20936 const B: u32 = 42;
20937 const C: u32 = 42;
20938
20939
20940 fn main() {
20941 println!("hello");
20942
20943 println!("world");
20944 }
20945 "#
20946 .unindent();
20947 executor.run_until_parked();
20948 cx.set_state(
20949 &r#"
20950 use some::mod1;
20951 use some::mod2;
20952
20953 ˇconst B: u32 = 42;
20954 const C: u32 = 42;
20955
20956
20957 fn main() {
20958 println!("hello");
20959
20960 println!("world");
20961 }
20962 "#
20963 .unindent(),
20964 );
20965
20966 cx.set_head_text(&diff_base);
20967 executor.run_until_parked();
20968
20969 cx.update_editor(|editor, window, cx| {
20970 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20971 });
20972 executor.run_until_parked();
20973
20974 cx.assert_state_with_diff(
20975 r#"
20976 use some::mod1;
20977 use some::mod2;
20978
20979 - const A: u32 = 42;
20980 ˇconst B: u32 = 42;
20981 const C: u32 = 42;
20982
20983
20984 fn main() {
20985 println!("hello");
20986
20987 println!("world");
20988 }
20989 "#
20990 .unindent(),
20991 );
20992
20993 cx.update_editor(|editor, window, cx| {
20994 editor.delete_line(&DeleteLine, window, cx);
20995 });
20996 executor.run_until_parked();
20997 cx.assert_state_with_diff(
20998 r#"
20999 use some::mod1;
21000 use some::mod2;
21001
21002 - const A: u32 = 42;
21003 - const B: u32 = 42;
21004 ˇconst C: u32 = 42;
21005
21006
21007 fn main() {
21008 println!("hello");
21009
21010 println!("world");
21011 }
21012 "#
21013 .unindent(),
21014 );
21015
21016 cx.update_editor(|editor, window, cx| {
21017 editor.delete_line(&DeleteLine, window, cx);
21018 });
21019 executor.run_until_parked();
21020 cx.assert_state_with_diff(
21021 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 );
21038
21039 cx.update_editor(|editor, window, cx| {
21040 editor.handle_input("replacement", window, cx);
21041 });
21042 executor.run_until_parked();
21043 cx.assert_state_with_diff(
21044 r#"
21045 use some::mod1;
21046 use some::mod2;
21047
21048 - const A: u32 = 42;
21049 - const B: u32 = 42;
21050 - const C: u32 = 42;
21051 -
21052 + replacementˇ
21053
21054 fn main() {
21055 println!("hello");
21056
21057 println!("world");
21058 }
21059 "#
21060 .unindent(),
21061 );
21062}
21063
21064#[gpui::test]
21065async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21066 init_test(cx, |_| {});
21067
21068 let mut cx = EditorTestContext::new(cx).await;
21069
21070 let base_text = r#"
21071 one
21072 two
21073 three
21074 four
21075 five
21076 "#
21077 .unindent();
21078 executor.run_until_parked();
21079 cx.set_state(
21080 &r#"
21081 one
21082 two
21083 fˇour
21084 five
21085 "#
21086 .unindent(),
21087 );
21088
21089 cx.set_head_text(&base_text);
21090 executor.run_until_parked();
21091
21092 cx.update_editor(|editor, window, cx| {
21093 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21094 });
21095 executor.run_until_parked();
21096
21097 cx.assert_state_with_diff(
21098 r#"
21099 one
21100 two
21101 - three
21102 fˇour
21103 five
21104 "#
21105 .unindent(),
21106 );
21107
21108 cx.update_editor(|editor, window, cx| {
21109 editor.backspace(&Backspace, window, cx);
21110 editor.backspace(&Backspace, window, cx);
21111 });
21112 executor.run_until_parked();
21113 cx.assert_state_with_diff(
21114 r#"
21115 one
21116 two
21117 - threeˇ
21118 - four
21119 + our
21120 five
21121 "#
21122 .unindent(),
21123 );
21124}
21125
21126#[gpui::test]
21127async fn test_edit_after_expanded_modification_hunk(
21128 executor: BackgroundExecutor,
21129 cx: &mut TestAppContext,
21130) {
21131 init_test(cx, |_| {});
21132
21133 let mut cx = EditorTestContext::new(cx).await;
21134
21135 let diff_base = r#"
21136 use some::mod1;
21137 use some::mod2;
21138
21139 const A: u32 = 42;
21140 const B: u32 = 42;
21141 const C: u32 = 42;
21142 const D: u32 = 42;
21143
21144
21145 fn main() {
21146 println!("hello");
21147
21148 println!("world");
21149 }"#
21150 .unindent();
21151
21152 cx.set_state(
21153 &r#"
21154 use some::mod1;
21155 use some::mod2;
21156
21157 const A: u32 = 42;
21158 const B: u32 = 42;
21159 const C: u32 = 43ˇ
21160 const D: u32 = 42;
21161
21162
21163 fn main() {
21164 println!("hello");
21165
21166 println!("world");
21167 }"#
21168 .unindent(),
21169 );
21170
21171 cx.set_head_text(&diff_base);
21172 executor.run_until_parked();
21173 cx.update_editor(|editor, window, cx| {
21174 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21175 });
21176 executor.run_until_parked();
21177
21178 cx.assert_state_with_diff(
21179 r#"
21180 use some::mod1;
21181 use some::mod2;
21182
21183 const A: u32 = 42;
21184 const B: u32 = 42;
21185 - const C: u32 = 42;
21186 + const C: u32 = 43ˇ
21187 const D: u32 = 42;
21188
21189
21190 fn main() {
21191 println!("hello");
21192
21193 println!("world");
21194 }"#
21195 .unindent(),
21196 );
21197
21198 cx.update_editor(|editor, window, cx| {
21199 editor.handle_input("\nnew_line\n", window, cx);
21200 });
21201 executor.run_until_parked();
21202
21203 cx.assert_state_with_diff(
21204 r#"
21205 use some::mod1;
21206 use some::mod2;
21207
21208 const A: u32 = 42;
21209 const B: u32 = 42;
21210 - const C: u32 = 42;
21211 + const C: u32 = 43
21212 + new_line
21213 + ˇ
21214 const D: u32 = 42;
21215
21216
21217 fn main() {
21218 println!("hello");
21219
21220 println!("world");
21221 }"#
21222 .unindent(),
21223 );
21224}
21225
21226#[gpui::test]
21227async fn test_stage_and_unstage_added_file_hunk(
21228 executor: BackgroundExecutor,
21229 cx: &mut TestAppContext,
21230) {
21231 init_test(cx, |_| {});
21232
21233 let mut cx = EditorTestContext::new(cx).await;
21234 cx.update_editor(|editor, _, cx| {
21235 editor.set_expand_all_diff_hunks(cx);
21236 });
21237
21238 let working_copy = r#"
21239 ˇfn main() {
21240 println!("hello, world!");
21241 }
21242 "#
21243 .unindent();
21244
21245 cx.set_state(&working_copy);
21246 executor.run_until_parked();
21247
21248 cx.assert_state_with_diff(
21249 r#"
21250 + ˇfn main() {
21251 + println!("hello, world!");
21252 + }
21253 "#
21254 .unindent(),
21255 );
21256 cx.assert_index_text(None);
21257
21258 cx.update_editor(|editor, window, cx| {
21259 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21260 });
21261 executor.run_until_parked();
21262 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
21263 cx.assert_state_with_diff(
21264 r#"
21265 + ˇfn main() {
21266 + println!("hello, world!");
21267 + }
21268 "#
21269 .unindent(),
21270 );
21271
21272 cx.update_editor(|editor, window, cx| {
21273 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21274 });
21275 executor.run_until_parked();
21276 cx.assert_index_text(None);
21277}
21278
21279async fn setup_indent_guides_editor(
21280 text: &str,
21281 cx: &mut TestAppContext,
21282) -> (BufferId, EditorTestContext) {
21283 init_test(cx, |_| {});
21284
21285 let mut cx = EditorTestContext::new(cx).await;
21286
21287 let buffer_id = cx.update_editor(|editor, window, cx| {
21288 editor.set_text(text, window, cx);
21289 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
21290
21291 buffer_ids[0]
21292 });
21293
21294 (buffer_id, cx)
21295}
21296
21297fn assert_indent_guides(
21298 range: Range<u32>,
21299 expected: Vec<IndentGuide>,
21300 active_indices: Option<Vec<usize>>,
21301 cx: &mut EditorTestContext,
21302) {
21303 let indent_guides = cx.update_editor(|editor, window, cx| {
21304 let snapshot = editor.snapshot(window, cx).display_snapshot;
21305 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
21306 editor,
21307 MultiBufferRow(range.start)..MultiBufferRow(range.end),
21308 true,
21309 &snapshot,
21310 cx,
21311 );
21312
21313 indent_guides.sort_by(|a, b| {
21314 a.depth.cmp(&b.depth).then(
21315 a.start_row
21316 .cmp(&b.start_row)
21317 .then(a.end_row.cmp(&b.end_row)),
21318 )
21319 });
21320 indent_guides
21321 });
21322
21323 if let Some(expected) = active_indices {
21324 let active_indices = cx.update_editor(|editor, window, cx| {
21325 let snapshot = editor.snapshot(window, cx).display_snapshot;
21326 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
21327 });
21328
21329 assert_eq!(
21330 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
21331 expected,
21332 "Active indent guide indices do not match"
21333 );
21334 }
21335
21336 assert_eq!(indent_guides, expected, "Indent guides do not match");
21337}
21338
21339fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
21340 IndentGuide {
21341 buffer_id,
21342 start_row: MultiBufferRow(start_row),
21343 end_row: MultiBufferRow(end_row),
21344 depth,
21345 tab_size: 4,
21346 settings: IndentGuideSettings {
21347 enabled: true,
21348 line_width: 1,
21349 active_line_width: 1,
21350 coloring: IndentGuideColoring::default(),
21351 background_coloring: IndentGuideBackgroundColoring::default(),
21352 },
21353 }
21354}
21355
21356#[gpui::test]
21357async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
21358 let (buffer_id, mut cx) = setup_indent_guides_editor(
21359 &"
21360 fn main() {
21361 let a = 1;
21362 }"
21363 .unindent(),
21364 cx,
21365 )
21366 .await;
21367
21368 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21369}
21370
21371#[gpui::test]
21372async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
21373 let (buffer_id, mut cx) = setup_indent_guides_editor(
21374 &"
21375 fn main() {
21376 let a = 1;
21377 let b = 2;
21378 }"
21379 .unindent(),
21380 cx,
21381 )
21382 .await;
21383
21384 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
21385}
21386
21387#[gpui::test]
21388async fn test_indent_guide_nested(cx: &mut TestAppContext) {
21389 let (buffer_id, mut cx) = setup_indent_guides_editor(
21390 &"
21391 fn main() {
21392 let a = 1;
21393 if a == 3 {
21394 let b = 2;
21395 } else {
21396 let c = 3;
21397 }
21398 }"
21399 .unindent(),
21400 cx,
21401 )
21402 .await;
21403
21404 assert_indent_guides(
21405 0..8,
21406 vec![
21407 indent_guide(buffer_id, 1, 6, 0),
21408 indent_guide(buffer_id, 3, 3, 1),
21409 indent_guide(buffer_id, 5, 5, 1),
21410 ],
21411 None,
21412 &mut cx,
21413 );
21414}
21415
21416#[gpui::test]
21417async fn test_indent_guide_tab(cx: &mut TestAppContext) {
21418 let (buffer_id, mut cx) = setup_indent_guides_editor(
21419 &"
21420 fn main() {
21421 let a = 1;
21422 let b = 2;
21423 let c = 3;
21424 }"
21425 .unindent(),
21426 cx,
21427 )
21428 .await;
21429
21430 assert_indent_guides(
21431 0..5,
21432 vec![
21433 indent_guide(buffer_id, 1, 3, 0),
21434 indent_guide(buffer_id, 2, 2, 1),
21435 ],
21436 None,
21437 &mut cx,
21438 );
21439}
21440
21441#[gpui::test]
21442async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
21443 let (buffer_id, mut cx) = setup_indent_guides_editor(
21444 &"
21445 fn main() {
21446 let a = 1;
21447
21448 let c = 3;
21449 }"
21450 .unindent(),
21451 cx,
21452 )
21453 .await;
21454
21455 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
21456}
21457
21458#[gpui::test]
21459async fn test_indent_guide_complex(cx: &mut TestAppContext) {
21460 let (buffer_id, mut cx) = setup_indent_guides_editor(
21461 &"
21462 fn main() {
21463 let a = 1;
21464
21465 let c = 3;
21466
21467 if a == 3 {
21468 let b = 2;
21469 } else {
21470 let c = 3;
21471 }
21472 }"
21473 .unindent(),
21474 cx,
21475 )
21476 .await;
21477
21478 assert_indent_guides(
21479 0..11,
21480 vec![
21481 indent_guide(buffer_id, 1, 9, 0),
21482 indent_guide(buffer_id, 6, 6, 1),
21483 indent_guide(buffer_id, 8, 8, 1),
21484 ],
21485 None,
21486 &mut cx,
21487 );
21488}
21489
21490#[gpui::test]
21491async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
21492 let (buffer_id, mut cx) = setup_indent_guides_editor(
21493 &"
21494 fn main() {
21495 let a = 1;
21496
21497 let c = 3;
21498
21499 if a == 3 {
21500 let b = 2;
21501 } else {
21502 let c = 3;
21503 }
21504 }"
21505 .unindent(),
21506 cx,
21507 )
21508 .await;
21509
21510 assert_indent_guides(
21511 1..11,
21512 vec![
21513 indent_guide(buffer_id, 1, 9, 0),
21514 indent_guide(buffer_id, 6, 6, 1),
21515 indent_guide(buffer_id, 8, 8, 1),
21516 ],
21517 None,
21518 &mut cx,
21519 );
21520}
21521
21522#[gpui::test]
21523async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
21524 let (buffer_id, mut cx) = setup_indent_guides_editor(
21525 &"
21526 fn main() {
21527 let a = 1;
21528
21529 let c = 3;
21530
21531 if a == 3 {
21532 let b = 2;
21533 } else {
21534 let c = 3;
21535 }
21536 }"
21537 .unindent(),
21538 cx,
21539 )
21540 .await;
21541
21542 assert_indent_guides(
21543 1..10,
21544 vec![
21545 indent_guide(buffer_id, 1, 9, 0),
21546 indent_guide(buffer_id, 6, 6, 1),
21547 indent_guide(buffer_id, 8, 8, 1),
21548 ],
21549 None,
21550 &mut cx,
21551 );
21552}
21553
21554#[gpui::test]
21555async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
21556 let (buffer_id, mut cx) = setup_indent_guides_editor(
21557 &"
21558 fn main() {
21559 if a {
21560 b(
21561 c,
21562 d,
21563 )
21564 } else {
21565 e(
21566 f
21567 )
21568 }
21569 }"
21570 .unindent(),
21571 cx,
21572 )
21573 .await;
21574
21575 assert_indent_guides(
21576 0..11,
21577 vec![
21578 indent_guide(buffer_id, 1, 10, 0),
21579 indent_guide(buffer_id, 2, 5, 1),
21580 indent_guide(buffer_id, 7, 9, 1),
21581 indent_guide(buffer_id, 3, 4, 2),
21582 indent_guide(buffer_id, 8, 8, 2),
21583 ],
21584 None,
21585 &mut cx,
21586 );
21587
21588 cx.update_editor(|editor, window, cx| {
21589 editor.fold_at(MultiBufferRow(2), window, cx);
21590 assert_eq!(
21591 editor.display_text(cx),
21592 "
21593 fn main() {
21594 if a {
21595 b(⋯
21596 )
21597 } else {
21598 e(
21599 f
21600 )
21601 }
21602 }"
21603 .unindent()
21604 );
21605 });
21606
21607 assert_indent_guides(
21608 0..11,
21609 vec![
21610 indent_guide(buffer_id, 1, 10, 0),
21611 indent_guide(buffer_id, 2, 5, 1),
21612 indent_guide(buffer_id, 7, 9, 1),
21613 indent_guide(buffer_id, 8, 8, 2),
21614 ],
21615 None,
21616 &mut cx,
21617 );
21618}
21619
21620#[gpui::test]
21621async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
21622 let (buffer_id, mut cx) = setup_indent_guides_editor(
21623 &"
21624 block1
21625 block2
21626 block3
21627 block4
21628 block2
21629 block1
21630 block1"
21631 .unindent(),
21632 cx,
21633 )
21634 .await;
21635
21636 assert_indent_guides(
21637 1..10,
21638 vec![
21639 indent_guide(buffer_id, 1, 4, 0),
21640 indent_guide(buffer_id, 2, 3, 1),
21641 indent_guide(buffer_id, 3, 3, 2),
21642 ],
21643 None,
21644 &mut cx,
21645 );
21646}
21647
21648#[gpui::test]
21649async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
21650 let (buffer_id, mut cx) = setup_indent_guides_editor(
21651 &"
21652 block1
21653 block2
21654 block3
21655
21656 block1
21657 block1"
21658 .unindent(),
21659 cx,
21660 )
21661 .await;
21662
21663 assert_indent_guides(
21664 0..6,
21665 vec![
21666 indent_guide(buffer_id, 1, 2, 0),
21667 indent_guide(buffer_id, 2, 2, 1),
21668 ],
21669 None,
21670 &mut cx,
21671 );
21672}
21673
21674#[gpui::test]
21675async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
21676 let (buffer_id, mut cx) = setup_indent_guides_editor(
21677 &"
21678 function component() {
21679 \treturn (
21680 \t\t\t
21681 \t\t<div>
21682 \t\t\t<abc></abc>
21683 \t\t</div>
21684 \t)
21685 }"
21686 .unindent(),
21687 cx,
21688 )
21689 .await;
21690
21691 assert_indent_guides(
21692 0..8,
21693 vec![
21694 indent_guide(buffer_id, 1, 6, 0),
21695 indent_guide(buffer_id, 2, 5, 1),
21696 indent_guide(buffer_id, 4, 4, 2),
21697 ],
21698 None,
21699 &mut cx,
21700 );
21701}
21702
21703#[gpui::test]
21704async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
21705 let (buffer_id, mut cx) = setup_indent_guides_editor(
21706 &"
21707 function component() {
21708 \treturn (
21709 \t
21710 \t\t<div>
21711 \t\t\t<abc></abc>
21712 \t\t</div>
21713 \t)
21714 }"
21715 .unindent(),
21716 cx,
21717 )
21718 .await;
21719
21720 assert_indent_guides(
21721 0..8,
21722 vec![
21723 indent_guide(buffer_id, 1, 6, 0),
21724 indent_guide(buffer_id, 2, 5, 1),
21725 indent_guide(buffer_id, 4, 4, 2),
21726 ],
21727 None,
21728 &mut cx,
21729 );
21730}
21731
21732#[gpui::test]
21733async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
21734 let (buffer_id, mut cx) = setup_indent_guides_editor(
21735 &"
21736 block1
21737
21738
21739
21740 block2
21741 "
21742 .unindent(),
21743 cx,
21744 )
21745 .await;
21746
21747 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21748}
21749
21750#[gpui::test]
21751async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
21752 let (buffer_id, mut cx) = setup_indent_guides_editor(
21753 &"
21754 def a:
21755 \tb = 3
21756 \tif True:
21757 \t\tc = 4
21758 \t\td = 5
21759 \tprint(b)
21760 "
21761 .unindent(),
21762 cx,
21763 )
21764 .await;
21765
21766 assert_indent_guides(
21767 0..6,
21768 vec![
21769 indent_guide(buffer_id, 1, 5, 0),
21770 indent_guide(buffer_id, 3, 4, 1),
21771 ],
21772 None,
21773 &mut cx,
21774 );
21775}
21776
21777#[gpui::test]
21778async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
21779 let (buffer_id, mut cx) = setup_indent_guides_editor(
21780 &"
21781 fn main() {
21782 let a = 1;
21783 }"
21784 .unindent(),
21785 cx,
21786 )
21787 .await;
21788
21789 cx.update_editor(|editor, window, cx| {
21790 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21791 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21792 });
21793 });
21794
21795 assert_indent_guides(
21796 0..3,
21797 vec![indent_guide(buffer_id, 1, 1, 0)],
21798 Some(vec![0]),
21799 &mut cx,
21800 );
21801}
21802
21803#[gpui::test]
21804async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
21805 let (buffer_id, mut cx) = setup_indent_guides_editor(
21806 &"
21807 fn main() {
21808 if 1 == 2 {
21809 let a = 1;
21810 }
21811 }"
21812 .unindent(),
21813 cx,
21814 )
21815 .await;
21816
21817 cx.update_editor(|editor, window, cx| {
21818 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21819 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21820 });
21821 });
21822
21823 assert_indent_guides(
21824 0..4,
21825 vec![
21826 indent_guide(buffer_id, 1, 3, 0),
21827 indent_guide(buffer_id, 2, 2, 1),
21828 ],
21829 Some(vec![1]),
21830 &mut cx,
21831 );
21832
21833 cx.update_editor(|editor, window, cx| {
21834 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21835 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21836 });
21837 });
21838
21839 assert_indent_guides(
21840 0..4,
21841 vec![
21842 indent_guide(buffer_id, 1, 3, 0),
21843 indent_guide(buffer_id, 2, 2, 1),
21844 ],
21845 Some(vec![1]),
21846 &mut cx,
21847 );
21848
21849 cx.update_editor(|editor, window, cx| {
21850 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21851 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21852 });
21853 });
21854
21855 assert_indent_guides(
21856 0..4,
21857 vec![
21858 indent_guide(buffer_id, 1, 3, 0),
21859 indent_guide(buffer_id, 2, 2, 1),
21860 ],
21861 Some(vec![0]),
21862 &mut cx,
21863 );
21864}
21865
21866#[gpui::test]
21867async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21868 let (buffer_id, mut cx) = setup_indent_guides_editor(
21869 &"
21870 fn main() {
21871 let a = 1;
21872
21873 let b = 2;
21874 }"
21875 .unindent(),
21876 cx,
21877 )
21878 .await;
21879
21880 cx.update_editor(|editor, window, cx| {
21881 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21882 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21883 });
21884 });
21885
21886 assert_indent_guides(
21887 0..5,
21888 vec![indent_guide(buffer_id, 1, 3, 0)],
21889 Some(vec![0]),
21890 &mut cx,
21891 );
21892}
21893
21894#[gpui::test]
21895async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21896 let (buffer_id, mut cx) = setup_indent_guides_editor(
21897 &"
21898 def m:
21899 a = 1
21900 pass"
21901 .unindent(),
21902 cx,
21903 )
21904 .await;
21905
21906 cx.update_editor(|editor, window, cx| {
21907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21908 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21909 });
21910 });
21911
21912 assert_indent_guides(
21913 0..3,
21914 vec![indent_guide(buffer_id, 1, 2, 0)],
21915 Some(vec![0]),
21916 &mut cx,
21917 );
21918}
21919
21920#[gpui::test]
21921async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21922 init_test(cx, |_| {});
21923 let mut cx = EditorTestContext::new(cx).await;
21924 let text = indoc! {
21925 "
21926 impl A {
21927 fn b() {
21928 0;
21929 3;
21930 5;
21931 6;
21932 7;
21933 }
21934 }
21935 "
21936 };
21937 let base_text = indoc! {
21938 "
21939 impl A {
21940 fn b() {
21941 0;
21942 1;
21943 2;
21944 3;
21945 4;
21946 }
21947 fn c() {
21948 5;
21949 6;
21950 7;
21951 }
21952 }
21953 "
21954 };
21955
21956 cx.update_editor(|editor, window, cx| {
21957 editor.set_text(text, window, cx);
21958
21959 editor.buffer().update(cx, |multibuffer, cx| {
21960 let buffer = multibuffer.as_singleton().unwrap();
21961 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21962
21963 multibuffer.set_all_diff_hunks_expanded(cx);
21964 multibuffer.add_diff(diff, cx);
21965
21966 buffer.read(cx).remote_id()
21967 })
21968 });
21969 cx.run_until_parked();
21970
21971 cx.assert_state_with_diff(
21972 indoc! { "
21973 impl A {
21974 fn b() {
21975 0;
21976 - 1;
21977 - 2;
21978 3;
21979 - 4;
21980 - }
21981 - fn c() {
21982 5;
21983 6;
21984 7;
21985 }
21986 }
21987 ˇ"
21988 }
21989 .to_string(),
21990 );
21991
21992 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21993 editor
21994 .snapshot(window, cx)
21995 .buffer_snapshot()
21996 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21997 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21998 .collect::<Vec<_>>()
21999 });
22000 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
22001 assert_eq!(
22002 actual_guides,
22003 vec![
22004 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
22005 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
22006 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
22007 ]
22008 );
22009}
22010
22011#[gpui::test]
22012async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22013 init_test(cx, |_| {});
22014 let mut cx = EditorTestContext::new(cx).await;
22015
22016 let diff_base = r#"
22017 a
22018 b
22019 c
22020 "#
22021 .unindent();
22022
22023 cx.set_state(
22024 &r#"
22025 ˇA
22026 b
22027 C
22028 "#
22029 .unindent(),
22030 );
22031 cx.set_head_text(&diff_base);
22032 cx.update_editor(|editor, window, cx| {
22033 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22034 });
22035 executor.run_until_parked();
22036
22037 let both_hunks_expanded = r#"
22038 - a
22039 + ˇA
22040 b
22041 - c
22042 + C
22043 "#
22044 .unindent();
22045
22046 cx.assert_state_with_diff(both_hunks_expanded.clone());
22047
22048 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22049 let snapshot = editor.snapshot(window, cx);
22050 let hunks = editor
22051 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22052 .collect::<Vec<_>>();
22053 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22054 hunks
22055 .into_iter()
22056 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22057 .collect::<Vec<_>>()
22058 });
22059 assert_eq!(hunk_ranges.len(), 2);
22060
22061 cx.update_editor(|editor, _, cx| {
22062 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22063 });
22064 executor.run_until_parked();
22065
22066 let second_hunk_expanded = r#"
22067 ˇA
22068 b
22069 - c
22070 + C
22071 "#
22072 .unindent();
22073
22074 cx.assert_state_with_diff(second_hunk_expanded);
22075
22076 cx.update_editor(|editor, _, cx| {
22077 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22078 });
22079 executor.run_until_parked();
22080
22081 cx.assert_state_with_diff(both_hunks_expanded.clone());
22082
22083 cx.update_editor(|editor, _, cx| {
22084 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22085 });
22086 executor.run_until_parked();
22087
22088 let first_hunk_expanded = r#"
22089 - a
22090 + ˇA
22091 b
22092 C
22093 "#
22094 .unindent();
22095
22096 cx.assert_state_with_diff(first_hunk_expanded);
22097
22098 cx.update_editor(|editor, _, cx| {
22099 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22100 });
22101 executor.run_until_parked();
22102
22103 cx.assert_state_with_diff(both_hunks_expanded);
22104
22105 cx.set_state(
22106 &r#"
22107 ˇA
22108 b
22109 "#
22110 .unindent(),
22111 );
22112 cx.run_until_parked();
22113
22114 // TODO this cursor position seems bad
22115 cx.assert_state_with_diff(
22116 r#"
22117 - ˇa
22118 + A
22119 b
22120 "#
22121 .unindent(),
22122 );
22123
22124 cx.update_editor(|editor, window, cx| {
22125 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22126 });
22127
22128 cx.assert_state_with_diff(
22129 r#"
22130 - ˇa
22131 + A
22132 b
22133 - c
22134 "#
22135 .unindent(),
22136 );
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[1].clone(), cx);
22153 });
22154 executor.run_until_parked();
22155
22156 cx.assert_state_with_diff(
22157 r#"
22158 - ˇa
22159 + A
22160 b
22161 "#
22162 .unindent(),
22163 );
22164}
22165
22166#[gpui::test]
22167async fn test_toggle_deletion_hunk_at_start_of_file(
22168 executor: BackgroundExecutor,
22169 cx: &mut TestAppContext,
22170) {
22171 init_test(cx, |_| {});
22172 let mut cx = EditorTestContext::new(cx).await;
22173
22174 let diff_base = r#"
22175 a
22176 b
22177 c
22178 "#
22179 .unindent();
22180
22181 cx.set_state(
22182 &r#"
22183 ˇb
22184 c
22185 "#
22186 .unindent(),
22187 );
22188 cx.set_head_text(&diff_base);
22189 cx.update_editor(|editor, window, cx| {
22190 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22191 });
22192 executor.run_until_parked();
22193
22194 let hunk_expanded = r#"
22195 - a
22196 ˇb
22197 c
22198 "#
22199 .unindent();
22200
22201 cx.assert_state_with_diff(hunk_expanded.clone());
22202
22203 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22204 let snapshot = editor.snapshot(window, cx);
22205 let hunks = editor
22206 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22207 .collect::<Vec<_>>();
22208 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22209 hunks
22210 .into_iter()
22211 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22212 .collect::<Vec<_>>()
22213 });
22214 assert_eq!(hunk_ranges.len(), 1);
22215
22216 cx.update_editor(|editor, _, cx| {
22217 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22218 });
22219 executor.run_until_parked();
22220
22221 let hunk_collapsed = r#"
22222 ˇb
22223 c
22224 "#
22225 .unindent();
22226
22227 cx.assert_state_with_diff(hunk_collapsed);
22228
22229 cx.update_editor(|editor, _, cx| {
22230 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22231 });
22232 executor.run_until_parked();
22233
22234 cx.assert_state_with_diff(hunk_expanded);
22235}
22236
22237#[gpui::test]
22238async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
22239 executor: BackgroundExecutor,
22240 cx: &mut TestAppContext,
22241) {
22242 init_test(cx, |_| {});
22243 let mut cx = EditorTestContext::new(cx).await;
22244
22245 cx.set_state("ˇnew\nsecond\nthird\n");
22246 cx.set_head_text("old\nsecond\nthird\n");
22247 cx.update_editor(|editor, window, cx| {
22248 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
22249 });
22250 executor.run_until_parked();
22251 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22252
22253 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
22254 cx.update_editor(|editor, window, cx| {
22255 let snapshot = editor.snapshot(window, cx);
22256 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22257 let hunks = editor
22258 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22259 .collect::<Vec<_>>();
22260 assert_eq!(hunks.len(), 1);
22261 let hunk_range = Anchor::range_in_buffer(excerpt_id, hunks[0].buffer_range.clone());
22262 editor.toggle_single_diff_hunk(hunk_range, cx)
22263 });
22264 executor.run_until_parked();
22265 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
22266
22267 // Keep the editor scrolled to the top so the full hunk remains visible.
22268 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22269}
22270
22271#[gpui::test]
22272async fn test_display_diff_hunks(cx: &mut TestAppContext) {
22273 init_test(cx, |_| {});
22274
22275 let fs = FakeFs::new(cx.executor());
22276 fs.insert_tree(
22277 path!("/test"),
22278 json!({
22279 ".git": {},
22280 "file-1": "ONE\n",
22281 "file-2": "TWO\n",
22282 "file-3": "THREE\n",
22283 }),
22284 )
22285 .await;
22286
22287 fs.set_head_for_repo(
22288 path!("/test/.git").as_ref(),
22289 &[
22290 ("file-1", "one\n".into()),
22291 ("file-2", "two\n".into()),
22292 ("file-3", "three\n".into()),
22293 ],
22294 "deadbeef",
22295 );
22296
22297 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
22298 let mut buffers = vec![];
22299 for i in 1..=3 {
22300 let buffer = project
22301 .update(cx, |project, cx| {
22302 let path = format!(path!("/test/file-{}"), i);
22303 project.open_local_buffer(path, cx)
22304 })
22305 .await
22306 .unwrap();
22307 buffers.push(buffer);
22308 }
22309
22310 let multibuffer = cx.new(|cx| {
22311 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
22312 multibuffer.set_all_diff_hunks_expanded(cx);
22313 for buffer in &buffers {
22314 let snapshot = buffer.read(cx).snapshot();
22315 multibuffer.set_excerpts_for_path(
22316 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
22317 buffer.clone(),
22318 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
22319 2,
22320 cx,
22321 );
22322 }
22323 multibuffer
22324 });
22325
22326 let editor = cx.add_window(|window, cx| {
22327 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
22328 });
22329 cx.run_until_parked();
22330
22331 let snapshot = editor
22332 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22333 .unwrap();
22334 let hunks = snapshot
22335 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
22336 .map(|hunk| match hunk {
22337 DisplayDiffHunk::Unfolded {
22338 display_row_range, ..
22339 } => display_row_range,
22340 DisplayDiffHunk::Folded { .. } => unreachable!(),
22341 })
22342 .collect::<Vec<_>>();
22343 assert_eq!(
22344 hunks,
22345 [
22346 DisplayRow(2)..DisplayRow(4),
22347 DisplayRow(7)..DisplayRow(9),
22348 DisplayRow(12)..DisplayRow(14),
22349 ]
22350 );
22351}
22352
22353#[gpui::test]
22354async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
22355 init_test(cx, |_| {});
22356
22357 let mut cx = EditorTestContext::new(cx).await;
22358 cx.set_head_text(indoc! { "
22359 one
22360 two
22361 three
22362 four
22363 five
22364 "
22365 });
22366 cx.set_index_text(indoc! { "
22367 one
22368 two
22369 three
22370 four
22371 five
22372 "
22373 });
22374 cx.set_state(indoc! {"
22375 one
22376 TWO
22377 ˇTHREE
22378 FOUR
22379 five
22380 "});
22381 cx.run_until_parked();
22382 cx.update_editor(|editor, window, cx| {
22383 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22384 });
22385 cx.run_until_parked();
22386 cx.assert_index_text(Some(indoc! {"
22387 one
22388 TWO
22389 THREE
22390 FOUR
22391 five
22392 "}));
22393 cx.set_state(indoc! { "
22394 one
22395 TWO
22396 ˇTHREE-HUNDRED
22397 FOUR
22398 five
22399 "});
22400 cx.run_until_parked();
22401 cx.update_editor(|editor, window, cx| {
22402 let snapshot = editor.snapshot(window, cx);
22403 let hunks = editor
22404 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22405 .collect::<Vec<_>>();
22406 assert_eq!(hunks.len(), 1);
22407 assert_eq!(
22408 hunks[0].status(),
22409 DiffHunkStatus {
22410 kind: DiffHunkStatusKind::Modified,
22411 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
22412 }
22413 );
22414
22415 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22416 });
22417 cx.run_until_parked();
22418 cx.assert_index_text(Some(indoc! {"
22419 one
22420 TWO
22421 THREE-HUNDRED
22422 FOUR
22423 five
22424 "}));
22425}
22426
22427#[gpui::test]
22428fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
22429 init_test(cx, |_| {});
22430
22431 let editor = cx.add_window(|window, cx| {
22432 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
22433 build_editor(buffer, window, cx)
22434 });
22435
22436 let render_args = Arc::new(Mutex::new(None));
22437 let snapshot = editor
22438 .update(cx, |editor, window, cx| {
22439 let snapshot = editor.buffer().read(cx).snapshot(cx);
22440 let range =
22441 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
22442
22443 struct RenderArgs {
22444 row: MultiBufferRow,
22445 folded: bool,
22446 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
22447 }
22448
22449 let crease = Crease::inline(
22450 range,
22451 FoldPlaceholder::test(),
22452 {
22453 let toggle_callback = render_args.clone();
22454 move |row, folded, callback, _window, _cx| {
22455 *toggle_callback.lock() = Some(RenderArgs {
22456 row,
22457 folded,
22458 callback,
22459 });
22460 div()
22461 }
22462 },
22463 |_row, _folded, _window, _cx| div(),
22464 );
22465
22466 editor.insert_creases(Some(crease), cx);
22467 let snapshot = editor.snapshot(window, cx);
22468 let _div =
22469 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
22470 snapshot
22471 })
22472 .unwrap();
22473
22474 let render_args = render_args.lock().take().unwrap();
22475 assert_eq!(render_args.row, MultiBufferRow(1));
22476 assert!(!render_args.folded);
22477 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22478
22479 cx.update_window(*editor, |_, window, cx| {
22480 (render_args.callback)(true, window, cx)
22481 })
22482 .unwrap();
22483 let snapshot = editor
22484 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22485 .unwrap();
22486 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
22487
22488 cx.update_window(*editor, |_, window, cx| {
22489 (render_args.callback)(false, window, cx)
22490 })
22491 .unwrap();
22492 let snapshot = editor
22493 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22494 .unwrap();
22495 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22496}
22497
22498#[gpui::test]
22499async fn test_input_text(cx: &mut TestAppContext) {
22500 init_test(cx, |_| {});
22501 let mut cx = EditorTestContext::new(cx).await;
22502
22503 cx.set_state(
22504 &r#"ˇone
22505 two
22506
22507 three
22508 fourˇ
22509 five
22510
22511 siˇx"#
22512 .unindent(),
22513 );
22514
22515 cx.dispatch_action(HandleInput(String::new()));
22516 cx.assert_editor_state(
22517 &r#"ˇone
22518 two
22519
22520 three
22521 fourˇ
22522 five
22523
22524 siˇx"#
22525 .unindent(),
22526 );
22527
22528 cx.dispatch_action(HandleInput("AAAA".to_string()));
22529 cx.assert_editor_state(
22530 &r#"AAAAˇone
22531 two
22532
22533 three
22534 fourAAAAˇ
22535 five
22536
22537 siAAAAˇx"#
22538 .unindent(),
22539 );
22540}
22541
22542#[gpui::test]
22543async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22544 init_test(cx, |_| {});
22545
22546 let mut cx = EditorTestContext::new(cx).await;
22547 cx.set_state(
22548 r#"let foo = 1;
22549let foo = 2;
22550let foo = 3;
22551let fooˇ = 4;
22552let foo = 5;
22553let foo = 6;
22554let foo = 7;
22555let foo = 8;
22556let foo = 9;
22557let foo = 10;
22558let foo = 11;
22559let foo = 12;
22560let foo = 13;
22561let foo = 14;
22562let foo = 15;"#,
22563 );
22564
22565 cx.update_editor(|e, window, cx| {
22566 assert_eq!(
22567 e.next_scroll_position,
22568 NextScrollCursorCenterTopBottom::Center,
22569 "Default next scroll direction is center",
22570 );
22571
22572 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22573 assert_eq!(
22574 e.next_scroll_position,
22575 NextScrollCursorCenterTopBottom::Top,
22576 "After center, next scroll direction should be top",
22577 );
22578
22579 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22580 assert_eq!(
22581 e.next_scroll_position,
22582 NextScrollCursorCenterTopBottom::Bottom,
22583 "After top, next scroll direction should be bottom",
22584 );
22585
22586 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22587 assert_eq!(
22588 e.next_scroll_position,
22589 NextScrollCursorCenterTopBottom::Center,
22590 "After bottom, scrolling should start over",
22591 );
22592
22593 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22594 assert_eq!(
22595 e.next_scroll_position,
22596 NextScrollCursorCenterTopBottom::Top,
22597 "Scrolling continues if retriggered fast enough"
22598 );
22599 });
22600
22601 cx.executor()
22602 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22603 cx.executor().run_until_parked();
22604 cx.update_editor(|e, _, _| {
22605 assert_eq!(
22606 e.next_scroll_position,
22607 NextScrollCursorCenterTopBottom::Center,
22608 "If scrolling is not triggered fast enough, it should reset"
22609 );
22610 });
22611}
22612
22613#[gpui::test]
22614async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22615 init_test(cx, |_| {});
22616 let mut cx = EditorLspTestContext::new_rust(
22617 lsp::ServerCapabilities {
22618 definition_provider: Some(lsp::OneOf::Left(true)),
22619 references_provider: Some(lsp::OneOf::Left(true)),
22620 ..lsp::ServerCapabilities::default()
22621 },
22622 cx,
22623 )
22624 .await;
22625
22626 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22627 let go_to_definition = cx
22628 .lsp
22629 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22630 move |params, _| async move {
22631 if empty_go_to_definition {
22632 Ok(None)
22633 } else {
22634 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22635 uri: params.text_document_position_params.text_document.uri,
22636 range: lsp::Range::new(
22637 lsp::Position::new(4, 3),
22638 lsp::Position::new(4, 6),
22639 ),
22640 })))
22641 }
22642 },
22643 );
22644 let references = cx
22645 .lsp
22646 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22647 Ok(Some(vec![lsp::Location {
22648 uri: params.text_document_position.text_document.uri,
22649 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22650 }]))
22651 });
22652 (go_to_definition, references)
22653 };
22654
22655 cx.set_state(
22656 &r#"fn one() {
22657 let mut a = ˇtwo();
22658 }
22659
22660 fn two() {}"#
22661 .unindent(),
22662 );
22663 set_up_lsp_handlers(false, &mut cx);
22664 let navigated = cx
22665 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22666 .await
22667 .expect("Failed to navigate to definition");
22668 assert_eq!(
22669 navigated,
22670 Navigated::Yes,
22671 "Should have navigated to definition from the GetDefinition response"
22672 );
22673 cx.assert_editor_state(
22674 &r#"fn one() {
22675 let mut a = two();
22676 }
22677
22678 fn «twoˇ»() {}"#
22679 .unindent(),
22680 );
22681
22682 let editors = cx.update_workspace(|workspace, _, cx| {
22683 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22684 });
22685 cx.update_editor(|_, _, test_editor_cx| {
22686 assert_eq!(
22687 editors.len(),
22688 1,
22689 "Initially, only one, test, editor should be open in the workspace"
22690 );
22691 assert_eq!(
22692 test_editor_cx.entity(),
22693 editors.last().expect("Asserted len is 1").clone()
22694 );
22695 });
22696
22697 set_up_lsp_handlers(true, &mut cx);
22698 let navigated = cx
22699 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22700 .await
22701 .expect("Failed to navigate to lookup references");
22702 assert_eq!(
22703 navigated,
22704 Navigated::Yes,
22705 "Should have navigated to references as a fallback after empty GoToDefinition response"
22706 );
22707 // We should not change the selections in the existing file,
22708 // if opening another milti buffer with the references
22709 cx.assert_editor_state(
22710 &r#"fn one() {
22711 let mut a = two();
22712 }
22713
22714 fn «twoˇ»() {}"#
22715 .unindent(),
22716 );
22717 let editors = cx.update_workspace(|workspace, _, cx| {
22718 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22719 });
22720 cx.update_editor(|_, _, test_editor_cx| {
22721 assert_eq!(
22722 editors.len(),
22723 2,
22724 "After falling back to references search, we open a new editor with the results"
22725 );
22726 let references_fallback_text = editors
22727 .into_iter()
22728 .find(|new_editor| *new_editor != test_editor_cx.entity())
22729 .expect("Should have one non-test editor now")
22730 .read(test_editor_cx)
22731 .text(test_editor_cx);
22732 assert_eq!(
22733 references_fallback_text, "fn one() {\n let mut a = two();\n}",
22734 "Should use the range from the references response and not the GoToDefinition one"
22735 );
22736 });
22737}
22738
22739#[gpui::test]
22740async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
22741 init_test(cx, |_| {});
22742 cx.update(|cx| {
22743 let mut editor_settings = EditorSettings::get_global(cx).clone();
22744 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
22745 EditorSettings::override_global(editor_settings, cx);
22746 });
22747 let mut cx = EditorLspTestContext::new_rust(
22748 lsp::ServerCapabilities {
22749 definition_provider: Some(lsp::OneOf::Left(true)),
22750 references_provider: Some(lsp::OneOf::Left(true)),
22751 ..lsp::ServerCapabilities::default()
22752 },
22753 cx,
22754 )
22755 .await;
22756 let original_state = r#"fn one() {
22757 let mut a = ˇtwo();
22758 }
22759
22760 fn two() {}"#
22761 .unindent();
22762 cx.set_state(&original_state);
22763
22764 let mut go_to_definition = cx
22765 .lsp
22766 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22767 move |_, _| async move { Ok(None) },
22768 );
22769 let _references = cx
22770 .lsp
22771 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
22772 panic!("Should not call for references with no go to definition fallback")
22773 });
22774
22775 let navigated = cx
22776 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22777 .await
22778 .expect("Failed to navigate to lookup references");
22779 go_to_definition
22780 .next()
22781 .await
22782 .expect("Should have called the go_to_definition handler");
22783
22784 assert_eq!(
22785 navigated,
22786 Navigated::No,
22787 "Should have navigated to references as a fallback after empty GoToDefinition response"
22788 );
22789 cx.assert_editor_state(&original_state);
22790 let editors = cx.update_workspace(|workspace, _, cx| {
22791 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22792 });
22793 cx.update_editor(|_, _, _| {
22794 assert_eq!(
22795 editors.len(),
22796 1,
22797 "After unsuccessful fallback, no other editor should have been opened"
22798 );
22799 });
22800}
22801
22802#[gpui::test]
22803async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
22804 init_test(cx, |_| {});
22805 let mut cx = EditorLspTestContext::new_rust(
22806 lsp::ServerCapabilities {
22807 references_provider: Some(lsp::OneOf::Left(true)),
22808 ..lsp::ServerCapabilities::default()
22809 },
22810 cx,
22811 )
22812 .await;
22813
22814 cx.set_state(
22815 &r#"
22816 fn one() {
22817 let mut a = two();
22818 }
22819
22820 fn ˇtwo() {}"#
22821 .unindent(),
22822 );
22823 cx.lsp
22824 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22825 Ok(Some(vec![
22826 lsp::Location {
22827 uri: params.text_document_position.text_document.uri.clone(),
22828 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22829 },
22830 lsp::Location {
22831 uri: params.text_document_position.text_document.uri,
22832 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
22833 },
22834 ]))
22835 });
22836 let navigated = cx
22837 .update_editor(|editor, window, cx| {
22838 editor.find_all_references(&FindAllReferences::default(), window, cx)
22839 })
22840 .unwrap()
22841 .await
22842 .expect("Failed to navigate to references");
22843 assert_eq!(
22844 navigated,
22845 Navigated::Yes,
22846 "Should have navigated to references from the FindAllReferences response"
22847 );
22848 cx.assert_editor_state(
22849 &r#"fn one() {
22850 let mut a = two();
22851 }
22852
22853 fn ˇtwo() {}"#
22854 .unindent(),
22855 );
22856
22857 let editors = cx.update_workspace(|workspace, _, cx| {
22858 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22859 });
22860 cx.update_editor(|_, _, _| {
22861 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
22862 });
22863
22864 cx.set_state(
22865 &r#"fn one() {
22866 let mut a = ˇtwo();
22867 }
22868
22869 fn two() {}"#
22870 .unindent(),
22871 );
22872 let navigated = cx
22873 .update_editor(|editor, window, cx| {
22874 editor.find_all_references(&FindAllReferences::default(), window, cx)
22875 })
22876 .unwrap()
22877 .await
22878 .expect("Failed to navigate to references");
22879 assert_eq!(
22880 navigated,
22881 Navigated::Yes,
22882 "Should have navigated to references from the FindAllReferences response"
22883 );
22884 cx.assert_editor_state(
22885 &r#"fn one() {
22886 let mut a = ˇtwo();
22887 }
22888
22889 fn two() {}"#
22890 .unindent(),
22891 );
22892 let editors = cx.update_workspace(|workspace, _, cx| {
22893 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22894 });
22895 cx.update_editor(|_, _, _| {
22896 assert_eq!(
22897 editors.len(),
22898 2,
22899 "should have re-used the previous multibuffer"
22900 );
22901 });
22902
22903 cx.set_state(
22904 &r#"fn one() {
22905 let mut a = ˇtwo();
22906 }
22907 fn three() {}
22908 fn two() {}"#
22909 .unindent(),
22910 );
22911 cx.lsp
22912 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22913 Ok(Some(vec![
22914 lsp::Location {
22915 uri: params.text_document_position.text_document.uri.clone(),
22916 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22917 },
22918 lsp::Location {
22919 uri: params.text_document_position.text_document.uri,
22920 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22921 },
22922 ]))
22923 });
22924 let navigated = cx
22925 .update_editor(|editor, window, cx| {
22926 editor.find_all_references(&FindAllReferences::default(), window, cx)
22927 })
22928 .unwrap()
22929 .await
22930 .expect("Failed to navigate to references");
22931 assert_eq!(
22932 navigated,
22933 Navigated::Yes,
22934 "Should have navigated to references from the FindAllReferences response"
22935 );
22936 cx.assert_editor_state(
22937 &r#"fn one() {
22938 let mut a = ˇtwo();
22939 }
22940 fn three() {}
22941 fn two() {}"#
22942 .unindent(),
22943 );
22944 let editors = cx.update_workspace(|workspace, _, cx| {
22945 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22946 });
22947 cx.update_editor(|_, _, _| {
22948 assert_eq!(
22949 editors.len(),
22950 3,
22951 "should have used a new multibuffer as offsets changed"
22952 );
22953 });
22954}
22955#[gpui::test]
22956async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22957 init_test(cx, |_| {});
22958
22959 let language = Arc::new(Language::new(
22960 LanguageConfig::default(),
22961 Some(tree_sitter_rust::LANGUAGE.into()),
22962 ));
22963
22964 let text = r#"
22965 #[cfg(test)]
22966 mod tests() {
22967 #[test]
22968 fn runnable_1() {
22969 let a = 1;
22970 }
22971
22972 #[test]
22973 fn runnable_2() {
22974 let a = 1;
22975 let b = 2;
22976 }
22977 }
22978 "#
22979 .unindent();
22980
22981 let fs = FakeFs::new(cx.executor());
22982 fs.insert_file("/file.rs", Default::default()).await;
22983
22984 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22985 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22986 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22987 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22988 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22989
22990 let editor = cx.new_window_entity(|window, cx| {
22991 Editor::new(
22992 EditorMode::full(),
22993 multi_buffer,
22994 Some(project.clone()),
22995 window,
22996 cx,
22997 )
22998 });
22999
23000 editor.update_in(cx, |editor, window, cx| {
23001 let snapshot = editor.buffer().read(cx).snapshot(cx);
23002 editor.tasks.insert(
23003 (buffer.read(cx).remote_id(), 3),
23004 RunnableTasks {
23005 templates: vec![],
23006 offset: snapshot.anchor_before(MultiBufferOffset(43)),
23007 column: 0,
23008 extra_variables: HashMap::default(),
23009 context_range: BufferOffset(43)..BufferOffset(85),
23010 },
23011 );
23012 editor.tasks.insert(
23013 (buffer.read(cx).remote_id(), 8),
23014 RunnableTasks {
23015 templates: vec![],
23016 offset: snapshot.anchor_before(MultiBufferOffset(86)),
23017 column: 0,
23018 extra_variables: HashMap::default(),
23019 context_range: BufferOffset(86)..BufferOffset(191),
23020 },
23021 );
23022
23023 // Test finding task when cursor is inside function body
23024 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23025 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
23026 });
23027 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23028 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
23029
23030 // Test finding task when cursor is on function name
23031 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23032 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
23033 });
23034 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23035 assert_eq!(row, 8, "Should find task when cursor is on function name");
23036 });
23037}
23038
23039#[gpui::test]
23040async fn test_folding_buffers(cx: &mut TestAppContext) {
23041 init_test(cx, |_| {});
23042
23043 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23044 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
23045 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
23046
23047 let fs = FakeFs::new(cx.executor());
23048 fs.insert_tree(
23049 path!("/a"),
23050 json!({
23051 "first.rs": sample_text_1,
23052 "second.rs": sample_text_2,
23053 "third.rs": sample_text_3,
23054 }),
23055 )
23056 .await;
23057 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23058 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23059 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23060 let worktree = project.update(cx, |project, cx| {
23061 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23062 assert_eq!(worktrees.len(), 1);
23063 worktrees.pop().unwrap()
23064 });
23065 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23066
23067 let buffer_1 = project
23068 .update(cx, |project, cx| {
23069 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23070 })
23071 .await
23072 .unwrap();
23073 let buffer_2 = project
23074 .update(cx, |project, cx| {
23075 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23076 })
23077 .await
23078 .unwrap();
23079 let buffer_3 = project
23080 .update(cx, |project, cx| {
23081 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23082 })
23083 .await
23084 .unwrap();
23085
23086 let multi_buffer = cx.new(|cx| {
23087 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23088 multi_buffer.push_excerpts(
23089 buffer_1.clone(),
23090 [
23091 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23092 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23093 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23094 ],
23095 cx,
23096 );
23097 multi_buffer.push_excerpts(
23098 buffer_2.clone(),
23099 [
23100 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23101 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23102 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23103 ],
23104 cx,
23105 );
23106 multi_buffer.push_excerpts(
23107 buffer_3.clone(),
23108 [
23109 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23110 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23111 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23112 ],
23113 cx,
23114 );
23115 multi_buffer
23116 });
23117 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23118 Editor::new(
23119 EditorMode::full(),
23120 multi_buffer.clone(),
23121 Some(project.clone()),
23122 window,
23123 cx,
23124 )
23125 });
23126
23127 assert_eq!(
23128 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23129 "\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",
23130 );
23131
23132 multi_buffer_editor.update(cx, |editor, cx| {
23133 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23134 });
23135 assert_eq!(
23136 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23137 "\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",
23138 "After folding the first buffer, its text should not be displayed"
23139 );
23140
23141 multi_buffer_editor.update(cx, |editor, cx| {
23142 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23143 });
23144 assert_eq!(
23145 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23146 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
23147 "After folding the second buffer, its text should not be displayed"
23148 );
23149
23150 multi_buffer_editor.update(cx, |editor, cx| {
23151 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23152 });
23153 assert_eq!(
23154 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23155 "\n\n\n\n\n",
23156 "After folding the third buffer, its text should not be displayed"
23157 );
23158
23159 // Emulate selection inside the fold logic, that should work
23160 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23161 editor
23162 .snapshot(window, cx)
23163 .next_line_boundary(Point::new(0, 4));
23164 });
23165
23166 multi_buffer_editor.update(cx, |editor, cx| {
23167 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23168 });
23169 assert_eq!(
23170 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23171 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
23172 "After unfolding the second buffer, its text should be displayed"
23173 );
23174
23175 // Typing inside of buffer 1 causes that buffer to be unfolded.
23176 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23177 assert_eq!(
23178 multi_buffer
23179 .read(cx)
23180 .snapshot(cx)
23181 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
23182 .collect::<String>(),
23183 "bbbb"
23184 );
23185 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23186 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
23187 });
23188 editor.handle_input("B", window, cx);
23189 });
23190
23191 assert_eq!(
23192 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23193 "\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",
23194 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
23195 );
23196
23197 multi_buffer_editor.update(cx, |editor, cx| {
23198 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23199 });
23200 assert_eq!(
23201 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23202 "\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",
23203 "After unfolding the all buffers, all original text should be displayed"
23204 );
23205}
23206
23207#[gpui::test]
23208async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
23209 init_test(cx, |_| {});
23210
23211 let sample_text_1 = "1111\n2222\n3333".to_string();
23212 let sample_text_2 = "4444\n5555\n6666".to_string();
23213 let sample_text_3 = "7777\n8888\n9999".to_string();
23214
23215 let fs = FakeFs::new(cx.executor());
23216 fs.insert_tree(
23217 path!("/a"),
23218 json!({
23219 "first.rs": sample_text_1,
23220 "second.rs": sample_text_2,
23221 "third.rs": sample_text_3,
23222 }),
23223 )
23224 .await;
23225 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23226 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23227 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23228 let worktree = project.update(cx, |project, cx| {
23229 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23230 assert_eq!(worktrees.len(), 1);
23231 worktrees.pop().unwrap()
23232 });
23233 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23234
23235 let buffer_1 = project
23236 .update(cx, |project, cx| {
23237 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23238 })
23239 .await
23240 .unwrap();
23241 let buffer_2 = project
23242 .update(cx, |project, cx| {
23243 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23244 })
23245 .await
23246 .unwrap();
23247 let buffer_3 = project
23248 .update(cx, |project, cx| {
23249 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23250 })
23251 .await
23252 .unwrap();
23253
23254 let multi_buffer = cx.new(|cx| {
23255 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23256 multi_buffer.push_excerpts(
23257 buffer_1.clone(),
23258 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23259 cx,
23260 );
23261 multi_buffer.push_excerpts(
23262 buffer_2.clone(),
23263 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23264 cx,
23265 );
23266 multi_buffer.push_excerpts(
23267 buffer_3.clone(),
23268 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23269 cx,
23270 );
23271 multi_buffer
23272 });
23273
23274 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23275 Editor::new(
23276 EditorMode::full(),
23277 multi_buffer,
23278 Some(project.clone()),
23279 window,
23280 cx,
23281 )
23282 });
23283
23284 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
23285 assert_eq!(
23286 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23287 full_text,
23288 );
23289
23290 multi_buffer_editor.update(cx, |editor, cx| {
23291 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23292 });
23293 assert_eq!(
23294 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23295 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
23296 "After folding the first buffer, its text should not be displayed"
23297 );
23298
23299 multi_buffer_editor.update(cx, |editor, cx| {
23300 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23301 });
23302
23303 assert_eq!(
23304 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23305 "\n\n\n\n\n\n7777\n8888\n9999",
23306 "After folding the second buffer, its text should not be displayed"
23307 );
23308
23309 multi_buffer_editor.update(cx, |editor, cx| {
23310 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23311 });
23312 assert_eq!(
23313 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23314 "\n\n\n\n\n",
23315 "After folding the third buffer, its text should not be displayed"
23316 );
23317
23318 multi_buffer_editor.update(cx, |editor, cx| {
23319 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23320 });
23321 assert_eq!(
23322 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23323 "\n\n\n\n4444\n5555\n6666\n\n",
23324 "After unfolding the second buffer, its text should be displayed"
23325 );
23326
23327 multi_buffer_editor.update(cx, |editor, cx| {
23328 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
23329 });
23330 assert_eq!(
23331 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23332 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
23333 "After unfolding the first buffer, its text should be displayed"
23334 );
23335
23336 multi_buffer_editor.update(cx, |editor, cx| {
23337 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23338 });
23339 assert_eq!(
23340 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23341 full_text,
23342 "After unfolding all buffers, all original text should be displayed"
23343 );
23344}
23345
23346#[gpui::test]
23347async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
23348 init_test(cx, |_| {});
23349
23350 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23351
23352 let fs = FakeFs::new(cx.executor());
23353 fs.insert_tree(
23354 path!("/a"),
23355 json!({
23356 "main.rs": sample_text,
23357 }),
23358 )
23359 .await;
23360 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23361 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23362 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23363 let worktree = project.update(cx, |project, cx| {
23364 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23365 assert_eq!(worktrees.len(), 1);
23366 worktrees.pop().unwrap()
23367 });
23368 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23369
23370 let buffer_1 = project
23371 .update(cx, |project, cx| {
23372 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23373 })
23374 .await
23375 .unwrap();
23376
23377 let multi_buffer = cx.new(|cx| {
23378 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23379 multi_buffer.push_excerpts(
23380 buffer_1.clone(),
23381 [ExcerptRange::new(
23382 Point::new(0, 0)
23383 ..Point::new(
23384 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
23385 0,
23386 ),
23387 )],
23388 cx,
23389 );
23390 multi_buffer
23391 });
23392 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23393 Editor::new(
23394 EditorMode::full(),
23395 multi_buffer,
23396 Some(project.clone()),
23397 window,
23398 cx,
23399 )
23400 });
23401
23402 let selection_range = Point::new(1, 0)..Point::new(2, 0);
23403 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23404 enum TestHighlight {}
23405 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
23406 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
23407 editor.highlight_text::<TestHighlight>(
23408 vec![highlight_range.clone()],
23409 HighlightStyle::color(Hsla::green()),
23410 cx,
23411 );
23412 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23413 s.select_ranges(Some(highlight_range))
23414 });
23415 });
23416
23417 let full_text = format!("\n\n{sample_text}");
23418 assert_eq!(
23419 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23420 full_text,
23421 );
23422}
23423
23424#[gpui::test]
23425async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
23426 init_test(cx, |_| {});
23427 cx.update(|cx| {
23428 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
23429 "keymaps/default-linux.json",
23430 cx,
23431 )
23432 .unwrap();
23433 cx.bind_keys(default_key_bindings);
23434 });
23435
23436 let (editor, cx) = cx.add_window_view(|window, cx| {
23437 let multi_buffer = MultiBuffer::build_multi(
23438 [
23439 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
23440 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
23441 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
23442 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
23443 ],
23444 cx,
23445 );
23446 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
23447
23448 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
23449 // fold all but the second buffer, so that we test navigating between two
23450 // adjacent folded buffers, as well as folded buffers at the start and
23451 // end the multibuffer
23452 editor.fold_buffer(buffer_ids[0], cx);
23453 editor.fold_buffer(buffer_ids[2], cx);
23454 editor.fold_buffer(buffer_ids[3], cx);
23455
23456 editor
23457 });
23458 cx.simulate_resize(size(px(1000.), px(1000.)));
23459
23460 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
23461 cx.assert_excerpts_with_selections(indoc! {"
23462 [EXCERPT]
23463 ˇ[FOLDED]
23464 [EXCERPT]
23465 a1
23466 b1
23467 [EXCERPT]
23468 [FOLDED]
23469 [EXCERPT]
23470 [FOLDED]
23471 "
23472 });
23473 cx.simulate_keystroke("down");
23474 cx.assert_excerpts_with_selections(indoc! {"
23475 [EXCERPT]
23476 [FOLDED]
23477 [EXCERPT]
23478 ˇa1
23479 b1
23480 [EXCERPT]
23481 [FOLDED]
23482 [EXCERPT]
23483 [FOLDED]
23484 "
23485 });
23486 cx.simulate_keystroke("down");
23487 cx.assert_excerpts_with_selections(indoc! {"
23488 [EXCERPT]
23489 [FOLDED]
23490 [EXCERPT]
23491 a1
23492 ˇb1
23493 [EXCERPT]
23494 [FOLDED]
23495 [EXCERPT]
23496 [FOLDED]
23497 "
23498 });
23499 cx.simulate_keystroke("down");
23500 cx.assert_excerpts_with_selections(indoc! {"
23501 [EXCERPT]
23502 [FOLDED]
23503 [EXCERPT]
23504 a1
23505 b1
23506 ˇ[EXCERPT]
23507 [FOLDED]
23508 [EXCERPT]
23509 [FOLDED]
23510 "
23511 });
23512 cx.simulate_keystroke("down");
23513 cx.assert_excerpts_with_selections(indoc! {"
23514 [EXCERPT]
23515 [FOLDED]
23516 [EXCERPT]
23517 a1
23518 b1
23519 [EXCERPT]
23520 ˇ[FOLDED]
23521 [EXCERPT]
23522 [FOLDED]
23523 "
23524 });
23525 for _ in 0..5 {
23526 cx.simulate_keystroke("down");
23527 cx.assert_excerpts_with_selections(indoc! {"
23528 [EXCERPT]
23529 [FOLDED]
23530 [EXCERPT]
23531 a1
23532 b1
23533 [EXCERPT]
23534 [FOLDED]
23535 [EXCERPT]
23536 ˇ[FOLDED]
23537 "
23538 });
23539 }
23540
23541 cx.simulate_keystroke("up");
23542 cx.assert_excerpts_with_selections(indoc! {"
23543 [EXCERPT]
23544 [FOLDED]
23545 [EXCERPT]
23546 a1
23547 b1
23548 [EXCERPT]
23549 ˇ[FOLDED]
23550 [EXCERPT]
23551 [FOLDED]
23552 "
23553 });
23554 cx.simulate_keystroke("up");
23555 cx.assert_excerpts_with_selections(indoc! {"
23556 [EXCERPT]
23557 [FOLDED]
23558 [EXCERPT]
23559 a1
23560 b1
23561 ˇ[EXCERPT]
23562 [FOLDED]
23563 [EXCERPT]
23564 [FOLDED]
23565 "
23566 });
23567 cx.simulate_keystroke("up");
23568 cx.assert_excerpts_with_selections(indoc! {"
23569 [EXCERPT]
23570 [FOLDED]
23571 [EXCERPT]
23572 a1
23573 ˇb1
23574 [EXCERPT]
23575 [FOLDED]
23576 [EXCERPT]
23577 [FOLDED]
23578 "
23579 });
23580 cx.simulate_keystroke("up");
23581 cx.assert_excerpts_with_selections(indoc! {"
23582 [EXCERPT]
23583 [FOLDED]
23584 [EXCERPT]
23585 ˇa1
23586 b1
23587 [EXCERPT]
23588 [FOLDED]
23589 [EXCERPT]
23590 [FOLDED]
23591 "
23592 });
23593 for _ in 0..5 {
23594 cx.simulate_keystroke("up");
23595 cx.assert_excerpts_with_selections(indoc! {"
23596 [EXCERPT]
23597 ˇ[FOLDED]
23598 [EXCERPT]
23599 a1
23600 b1
23601 [EXCERPT]
23602 [FOLDED]
23603 [EXCERPT]
23604 [FOLDED]
23605 "
23606 });
23607 }
23608}
23609
23610#[gpui::test]
23611async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23612 init_test(cx, |_| {});
23613
23614 // Simple insertion
23615 assert_highlighted_edits(
23616 "Hello, world!",
23617 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23618 true,
23619 cx,
23620 |highlighted_edits, cx| {
23621 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23622 assert_eq!(highlighted_edits.highlights.len(), 1);
23623 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23624 assert_eq!(
23625 highlighted_edits.highlights[0].1.background_color,
23626 Some(cx.theme().status().created_background)
23627 );
23628 },
23629 )
23630 .await;
23631
23632 // Replacement
23633 assert_highlighted_edits(
23634 "This is a test.",
23635 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23636 false,
23637 cx,
23638 |highlighted_edits, cx| {
23639 assert_eq!(highlighted_edits.text, "That is a test.");
23640 assert_eq!(highlighted_edits.highlights.len(), 1);
23641 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23642 assert_eq!(
23643 highlighted_edits.highlights[0].1.background_color,
23644 Some(cx.theme().status().created_background)
23645 );
23646 },
23647 )
23648 .await;
23649
23650 // Multiple edits
23651 assert_highlighted_edits(
23652 "Hello, world!",
23653 vec![
23654 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23655 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23656 ],
23657 false,
23658 cx,
23659 |highlighted_edits, cx| {
23660 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23661 assert_eq!(highlighted_edits.highlights.len(), 2);
23662 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23663 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23664 assert_eq!(
23665 highlighted_edits.highlights[0].1.background_color,
23666 Some(cx.theme().status().created_background)
23667 );
23668 assert_eq!(
23669 highlighted_edits.highlights[1].1.background_color,
23670 Some(cx.theme().status().created_background)
23671 );
23672 },
23673 )
23674 .await;
23675
23676 // Multiple lines with edits
23677 assert_highlighted_edits(
23678 "First line\nSecond line\nThird line\nFourth line",
23679 vec![
23680 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
23681 (
23682 Point::new(2, 0)..Point::new(2, 10),
23683 "New third line".to_string(),
23684 ),
23685 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
23686 ],
23687 false,
23688 cx,
23689 |highlighted_edits, cx| {
23690 assert_eq!(
23691 highlighted_edits.text,
23692 "Second modified\nNew third line\nFourth updated line"
23693 );
23694 assert_eq!(highlighted_edits.highlights.len(), 3);
23695 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
23696 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
23697 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
23698 for highlight in &highlighted_edits.highlights {
23699 assert_eq!(
23700 highlight.1.background_color,
23701 Some(cx.theme().status().created_background)
23702 );
23703 }
23704 },
23705 )
23706 .await;
23707}
23708
23709#[gpui::test]
23710async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
23711 init_test(cx, |_| {});
23712
23713 // Deletion
23714 assert_highlighted_edits(
23715 "Hello, world!",
23716 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
23717 true,
23718 cx,
23719 |highlighted_edits, cx| {
23720 assert_eq!(highlighted_edits.text, "Hello, world!");
23721 assert_eq!(highlighted_edits.highlights.len(), 1);
23722 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
23723 assert_eq!(
23724 highlighted_edits.highlights[0].1.background_color,
23725 Some(cx.theme().status().deleted_background)
23726 );
23727 },
23728 )
23729 .await;
23730
23731 // Insertion
23732 assert_highlighted_edits(
23733 "Hello, world!",
23734 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
23735 true,
23736 cx,
23737 |highlighted_edits, cx| {
23738 assert_eq!(highlighted_edits.highlights.len(), 1);
23739 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
23740 assert_eq!(
23741 highlighted_edits.highlights[0].1.background_color,
23742 Some(cx.theme().status().created_background)
23743 );
23744 },
23745 )
23746 .await;
23747}
23748
23749async fn assert_highlighted_edits(
23750 text: &str,
23751 edits: Vec<(Range<Point>, String)>,
23752 include_deletions: bool,
23753 cx: &mut TestAppContext,
23754 assertion_fn: impl Fn(HighlightedText, &App),
23755) {
23756 let window = cx.add_window(|window, cx| {
23757 let buffer = MultiBuffer::build_simple(text, cx);
23758 Editor::new(EditorMode::full(), buffer, None, window, cx)
23759 });
23760 let cx = &mut VisualTestContext::from_window(*window, cx);
23761
23762 let (buffer, snapshot) = window
23763 .update(cx, |editor, _window, cx| {
23764 (
23765 editor.buffer().clone(),
23766 editor.buffer().read(cx).snapshot(cx),
23767 )
23768 })
23769 .unwrap();
23770
23771 let edits = edits
23772 .into_iter()
23773 .map(|(range, edit)| {
23774 (
23775 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
23776 edit,
23777 )
23778 })
23779 .collect::<Vec<_>>();
23780
23781 let text_anchor_edits = edits
23782 .clone()
23783 .into_iter()
23784 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
23785 .collect::<Vec<_>>();
23786
23787 let edit_preview = window
23788 .update(cx, |_, _window, cx| {
23789 buffer
23790 .read(cx)
23791 .as_singleton()
23792 .unwrap()
23793 .read(cx)
23794 .preview_edits(text_anchor_edits.into(), cx)
23795 })
23796 .unwrap()
23797 .await;
23798
23799 cx.update(|_window, cx| {
23800 let highlighted_edits = edit_prediction_edit_text(
23801 snapshot.as_singleton().unwrap().2,
23802 &edits,
23803 &edit_preview,
23804 include_deletions,
23805 cx,
23806 );
23807 assertion_fn(highlighted_edits, cx)
23808 });
23809}
23810
23811#[track_caller]
23812fn assert_breakpoint(
23813 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
23814 path: &Arc<Path>,
23815 expected: Vec<(u32, Breakpoint)>,
23816) {
23817 if expected.is_empty() {
23818 assert!(!breakpoints.contains_key(path), "{}", path.display());
23819 } else {
23820 let mut breakpoint = breakpoints
23821 .get(path)
23822 .unwrap()
23823 .iter()
23824 .map(|breakpoint| {
23825 (
23826 breakpoint.row,
23827 Breakpoint {
23828 message: breakpoint.message.clone(),
23829 state: breakpoint.state,
23830 condition: breakpoint.condition.clone(),
23831 hit_condition: breakpoint.hit_condition.clone(),
23832 },
23833 )
23834 })
23835 .collect::<Vec<_>>();
23836
23837 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
23838
23839 assert_eq!(expected, breakpoint);
23840 }
23841}
23842
23843fn add_log_breakpoint_at_cursor(
23844 editor: &mut Editor,
23845 log_message: &str,
23846 window: &mut Window,
23847 cx: &mut Context<Editor>,
23848) {
23849 let (anchor, bp) = editor
23850 .breakpoints_at_cursors(window, cx)
23851 .first()
23852 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
23853 .unwrap_or_else(|| {
23854 let snapshot = editor.snapshot(window, cx);
23855 let cursor_position: Point =
23856 editor.selections.newest(&snapshot.display_snapshot).head();
23857
23858 let breakpoint_position = snapshot
23859 .buffer_snapshot()
23860 .anchor_before(Point::new(cursor_position.row, 0));
23861
23862 (breakpoint_position, Breakpoint::new_log(log_message))
23863 });
23864
23865 editor.edit_breakpoint_at_anchor(
23866 anchor,
23867 bp,
23868 BreakpointEditAction::EditLogMessage(log_message.into()),
23869 cx,
23870 );
23871}
23872
23873#[gpui::test]
23874async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23875 init_test(cx, |_| {});
23876
23877 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23878 let fs = FakeFs::new(cx.executor());
23879 fs.insert_tree(
23880 path!("/a"),
23881 json!({
23882 "main.rs": sample_text,
23883 }),
23884 )
23885 .await;
23886 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23887 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23888 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23889
23890 let fs = FakeFs::new(cx.executor());
23891 fs.insert_tree(
23892 path!("/a"),
23893 json!({
23894 "main.rs": sample_text,
23895 }),
23896 )
23897 .await;
23898 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23899 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23900 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23901 let worktree_id = workspace
23902 .update(cx, |workspace, _window, cx| {
23903 workspace.project().update(cx, |project, cx| {
23904 project.worktrees(cx).next().unwrap().read(cx).id()
23905 })
23906 })
23907 .unwrap();
23908
23909 let buffer = project
23910 .update(cx, |project, cx| {
23911 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23912 })
23913 .await
23914 .unwrap();
23915
23916 let (editor, cx) = cx.add_window_view(|window, cx| {
23917 Editor::new(
23918 EditorMode::full(),
23919 MultiBuffer::build_from_buffer(buffer, cx),
23920 Some(project.clone()),
23921 window,
23922 cx,
23923 )
23924 });
23925
23926 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23927 let abs_path = project.read_with(cx, |project, cx| {
23928 project
23929 .absolute_path(&project_path, cx)
23930 .map(Arc::from)
23931 .unwrap()
23932 });
23933
23934 // assert we can add breakpoint on the first line
23935 editor.update_in(cx, |editor, window, cx| {
23936 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23937 editor.move_to_end(&MoveToEnd, window, cx);
23938 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23939 });
23940
23941 let breakpoints = editor.update(cx, |editor, cx| {
23942 editor
23943 .breakpoint_store()
23944 .as_ref()
23945 .unwrap()
23946 .read(cx)
23947 .all_source_breakpoints(cx)
23948 });
23949
23950 assert_eq!(1, breakpoints.len());
23951 assert_breakpoint(
23952 &breakpoints,
23953 &abs_path,
23954 vec![
23955 (0, Breakpoint::new_standard()),
23956 (3, Breakpoint::new_standard()),
23957 ],
23958 );
23959
23960 editor.update_in(cx, |editor, window, cx| {
23961 editor.move_to_beginning(&MoveToBeginning, window, cx);
23962 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23963 });
23964
23965 let breakpoints = editor.update(cx, |editor, cx| {
23966 editor
23967 .breakpoint_store()
23968 .as_ref()
23969 .unwrap()
23970 .read(cx)
23971 .all_source_breakpoints(cx)
23972 });
23973
23974 assert_eq!(1, breakpoints.len());
23975 assert_breakpoint(
23976 &breakpoints,
23977 &abs_path,
23978 vec![(3, Breakpoint::new_standard())],
23979 );
23980
23981 editor.update_in(cx, |editor, window, cx| {
23982 editor.move_to_end(&MoveToEnd, window, cx);
23983 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23984 });
23985
23986 let breakpoints = editor.update(cx, |editor, cx| {
23987 editor
23988 .breakpoint_store()
23989 .as_ref()
23990 .unwrap()
23991 .read(cx)
23992 .all_source_breakpoints(cx)
23993 });
23994
23995 assert_eq!(0, breakpoints.len());
23996 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23997}
23998
23999#[gpui::test]
24000async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
24001 init_test(cx, |_| {});
24002
24003 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24004
24005 let fs = FakeFs::new(cx.executor());
24006 fs.insert_tree(
24007 path!("/a"),
24008 json!({
24009 "main.rs": sample_text,
24010 }),
24011 )
24012 .await;
24013 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24014 let (workspace, cx) =
24015 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24016
24017 let worktree_id = workspace.update(cx, |workspace, cx| {
24018 workspace.project().update(cx, |project, cx| {
24019 project.worktrees(cx).next().unwrap().read(cx).id()
24020 })
24021 });
24022
24023 let buffer = project
24024 .update(cx, |project, cx| {
24025 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24026 })
24027 .await
24028 .unwrap();
24029
24030 let (editor, cx) = cx.add_window_view(|window, cx| {
24031 Editor::new(
24032 EditorMode::full(),
24033 MultiBuffer::build_from_buffer(buffer, cx),
24034 Some(project.clone()),
24035 window,
24036 cx,
24037 )
24038 });
24039
24040 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24041 let abs_path = project.read_with(cx, |project, cx| {
24042 project
24043 .absolute_path(&project_path, cx)
24044 .map(Arc::from)
24045 .unwrap()
24046 });
24047
24048 editor.update_in(cx, |editor, window, cx| {
24049 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24050 });
24051
24052 let breakpoints = editor.update(cx, |editor, cx| {
24053 editor
24054 .breakpoint_store()
24055 .as_ref()
24056 .unwrap()
24057 .read(cx)
24058 .all_source_breakpoints(cx)
24059 });
24060
24061 assert_breakpoint(
24062 &breakpoints,
24063 &abs_path,
24064 vec![(0, Breakpoint::new_log("hello world"))],
24065 );
24066
24067 // Removing a log message from a log breakpoint should remove it
24068 editor.update_in(cx, |editor, window, cx| {
24069 add_log_breakpoint_at_cursor(editor, "", window, cx);
24070 });
24071
24072 let breakpoints = editor.update(cx, |editor, cx| {
24073 editor
24074 .breakpoint_store()
24075 .as_ref()
24076 .unwrap()
24077 .read(cx)
24078 .all_source_breakpoints(cx)
24079 });
24080
24081 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24082
24083 editor.update_in(cx, |editor, window, cx| {
24084 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24085 editor.move_to_end(&MoveToEnd, window, cx);
24086 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24087 // Not adding a log message to a standard breakpoint shouldn't remove it
24088 add_log_breakpoint_at_cursor(editor, "", window, cx);
24089 });
24090
24091 let breakpoints = editor.update(cx, |editor, cx| {
24092 editor
24093 .breakpoint_store()
24094 .as_ref()
24095 .unwrap()
24096 .read(cx)
24097 .all_source_breakpoints(cx)
24098 });
24099
24100 assert_breakpoint(
24101 &breakpoints,
24102 &abs_path,
24103 vec![
24104 (0, Breakpoint::new_standard()),
24105 (3, Breakpoint::new_standard()),
24106 ],
24107 );
24108
24109 editor.update_in(cx, |editor, window, cx| {
24110 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24111 });
24112
24113 let breakpoints = editor.update(cx, |editor, cx| {
24114 editor
24115 .breakpoint_store()
24116 .as_ref()
24117 .unwrap()
24118 .read(cx)
24119 .all_source_breakpoints(cx)
24120 });
24121
24122 assert_breakpoint(
24123 &breakpoints,
24124 &abs_path,
24125 vec![
24126 (0, Breakpoint::new_standard()),
24127 (3, Breakpoint::new_log("hello world")),
24128 ],
24129 );
24130
24131 editor.update_in(cx, |editor, window, cx| {
24132 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
24133 });
24134
24135 let breakpoints = editor.update(cx, |editor, cx| {
24136 editor
24137 .breakpoint_store()
24138 .as_ref()
24139 .unwrap()
24140 .read(cx)
24141 .all_source_breakpoints(cx)
24142 });
24143
24144 assert_breakpoint(
24145 &breakpoints,
24146 &abs_path,
24147 vec![
24148 (0, Breakpoint::new_standard()),
24149 (3, Breakpoint::new_log("hello Earth!!")),
24150 ],
24151 );
24152}
24153
24154/// This also tests that Editor::breakpoint_at_cursor_head is working properly
24155/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
24156/// or when breakpoints were placed out of order. This tests for a regression too
24157#[gpui::test]
24158async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
24159 init_test(cx, |_| {});
24160
24161 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24162 let fs = FakeFs::new(cx.executor());
24163 fs.insert_tree(
24164 path!("/a"),
24165 json!({
24166 "main.rs": sample_text,
24167 }),
24168 )
24169 .await;
24170 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24171 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24172 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24173
24174 let fs = FakeFs::new(cx.executor());
24175 fs.insert_tree(
24176 path!("/a"),
24177 json!({
24178 "main.rs": sample_text,
24179 }),
24180 )
24181 .await;
24182 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24183 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24184 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24185 let worktree_id = workspace
24186 .update(cx, |workspace, _window, cx| {
24187 workspace.project().update(cx, |project, cx| {
24188 project.worktrees(cx).next().unwrap().read(cx).id()
24189 })
24190 })
24191 .unwrap();
24192
24193 let buffer = project
24194 .update(cx, |project, cx| {
24195 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24196 })
24197 .await
24198 .unwrap();
24199
24200 let (editor, cx) = cx.add_window_view(|window, cx| {
24201 Editor::new(
24202 EditorMode::full(),
24203 MultiBuffer::build_from_buffer(buffer, cx),
24204 Some(project.clone()),
24205 window,
24206 cx,
24207 )
24208 });
24209
24210 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24211 let abs_path = project.read_with(cx, |project, cx| {
24212 project
24213 .absolute_path(&project_path, cx)
24214 .map(Arc::from)
24215 .unwrap()
24216 });
24217
24218 // assert we can add breakpoint on the first line
24219 editor.update_in(cx, |editor, window, cx| {
24220 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24221 editor.move_to_end(&MoveToEnd, window, cx);
24222 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24223 editor.move_up(&MoveUp, window, cx);
24224 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24225 });
24226
24227 let breakpoints = editor.update(cx, |editor, cx| {
24228 editor
24229 .breakpoint_store()
24230 .as_ref()
24231 .unwrap()
24232 .read(cx)
24233 .all_source_breakpoints(cx)
24234 });
24235
24236 assert_eq!(1, breakpoints.len());
24237 assert_breakpoint(
24238 &breakpoints,
24239 &abs_path,
24240 vec![
24241 (0, Breakpoint::new_standard()),
24242 (2, Breakpoint::new_standard()),
24243 (3, Breakpoint::new_standard()),
24244 ],
24245 );
24246
24247 editor.update_in(cx, |editor, window, cx| {
24248 editor.move_to_beginning(&MoveToBeginning, window, cx);
24249 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24250 editor.move_to_end(&MoveToEnd, window, cx);
24251 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24252 // Disabling a breakpoint that doesn't exist should do nothing
24253 editor.move_up(&MoveUp, window, cx);
24254 editor.move_up(&MoveUp, window, cx);
24255 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24256 });
24257
24258 let breakpoints = editor.update(cx, |editor, cx| {
24259 editor
24260 .breakpoint_store()
24261 .as_ref()
24262 .unwrap()
24263 .read(cx)
24264 .all_source_breakpoints(cx)
24265 });
24266
24267 let disable_breakpoint = {
24268 let mut bp = Breakpoint::new_standard();
24269 bp.state = BreakpointState::Disabled;
24270 bp
24271 };
24272
24273 assert_eq!(1, breakpoints.len());
24274 assert_breakpoint(
24275 &breakpoints,
24276 &abs_path,
24277 vec![
24278 (0, disable_breakpoint.clone()),
24279 (2, Breakpoint::new_standard()),
24280 (3, disable_breakpoint.clone()),
24281 ],
24282 );
24283
24284 editor.update_in(cx, |editor, window, cx| {
24285 editor.move_to_beginning(&MoveToBeginning, window, cx);
24286 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24287 editor.move_to_end(&MoveToEnd, window, cx);
24288 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24289 editor.move_up(&MoveUp, window, cx);
24290 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24291 });
24292
24293 let breakpoints = editor.update(cx, |editor, cx| {
24294 editor
24295 .breakpoint_store()
24296 .as_ref()
24297 .unwrap()
24298 .read(cx)
24299 .all_source_breakpoints(cx)
24300 });
24301
24302 assert_eq!(1, breakpoints.len());
24303 assert_breakpoint(
24304 &breakpoints,
24305 &abs_path,
24306 vec![
24307 (0, Breakpoint::new_standard()),
24308 (2, disable_breakpoint),
24309 (3, Breakpoint::new_standard()),
24310 ],
24311 );
24312}
24313
24314#[gpui::test]
24315async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
24316 init_test(cx, |_| {});
24317 let capabilities = lsp::ServerCapabilities {
24318 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
24319 prepare_provider: Some(true),
24320 work_done_progress_options: Default::default(),
24321 })),
24322 ..Default::default()
24323 };
24324 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24325
24326 cx.set_state(indoc! {"
24327 struct Fˇoo {}
24328 "});
24329
24330 cx.update_editor(|editor, _, cx| {
24331 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24332 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24333 editor.highlight_background::<DocumentHighlightRead>(
24334 &[highlight_range],
24335 |_, theme| theme.colors().editor_document_highlight_read_background,
24336 cx,
24337 );
24338 });
24339
24340 let mut prepare_rename_handler = cx
24341 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
24342 move |_, _, _| async move {
24343 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
24344 start: lsp::Position {
24345 line: 0,
24346 character: 7,
24347 },
24348 end: lsp::Position {
24349 line: 0,
24350 character: 10,
24351 },
24352 })))
24353 },
24354 );
24355 let prepare_rename_task = cx
24356 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24357 .expect("Prepare rename was not started");
24358 prepare_rename_handler.next().await.unwrap();
24359 prepare_rename_task.await.expect("Prepare rename failed");
24360
24361 let mut rename_handler =
24362 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24363 let edit = lsp::TextEdit {
24364 range: lsp::Range {
24365 start: lsp::Position {
24366 line: 0,
24367 character: 7,
24368 },
24369 end: lsp::Position {
24370 line: 0,
24371 character: 10,
24372 },
24373 },
24374 new_text: "FooRenamed".to_string(),
24375 };
24376 Ok(Some(lsp::WorkspaceEdit::new(
24377 // Specify the same edit twice
24378 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
24379 )))
24380 });
24381 let rename_task = cx
24382 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24383 .expect("Confirm rename was not started");
24384 rename_handler.next().await.unwrap();
24385 rename_task.await.expect("Confirm rename failed");
24386 cx.run_until_parked();
24387
24388 // Despite two edits, only one is actually applied as those are identical
24389 cx.assert_editor_state(indoc! {"
24390 struct FooRenamedˇ {}
24391 "});
24392}
24393
24394#[gpui::test]
24395async fn test_rename_without_prepare(cx: &mut TestAppContext) {
24396 init_test(cx, |_| {});
24397 // These capabilities indicate that the server does not support prepare rename.
24398 let capabilities = lsp::ServerCapabilities {
24399 rename_provider: Some(lsp::OneOf::Left(true)),
24400 ..Default::default()
24401 };
24402 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24403
24404 cx.set_state(indoc! {"
24405 struct Fˇoo {}
24406 "});
24407
24408 cx.update_editor(|editor, _window, cx| {
24409 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24410 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24411 editor.highlight_background::<DocumentHighlightRead>(
24412 &[highlight_range],
24413 |_, theme| theme.colors().editor_document_highlight_read_background,
24414 cx,
24415 );
24416 });
24417
24418 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24419 .expect("Prepare rename was not started")
24420 .await
24421 .expect("Prepare rename failed");
24422
24423 let mut rename_handler =
24424 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24425 let edit = lsp::TextEdit {
24426 range: lsp::Range {
24427 start: lsp::Position {
24428 line: 0,
24429 character: 7,
24430 },
24431 end: lsp::Position {
24432 line: 0,
24433 character: 10,
24434 },
24435 },
24436 new_text: "FooRenamed".to_string(),
24437 };
24438 Ok(Some(lsp::WorkspaceEdit::new(
24439 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
24440 )))
24441 });
24442 let rename_task = cx
24443 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24444 .expect("Confirm rename was not started");
24445 rename_handler.next().await.unwrap();
24446 rename_task.await.expect("Confirm rename failed");
24447 cx.run_until_parked();
24448
24449 // Correct range is renamed, as `surrounding_word` is used to find it.
24450 cx.assert_editor_state(indoc! {"
24451 struct FooRenamedˇ {}
24452 "});
24453}
24454
24455#[gpui::test]
24456async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
24457 init_test(cx, |_| {});
24458 let mut cx = EditorTestContext::new(cx).await;
24459
24460 let language = Arc::new(
24461 Language::new(
24462 LanguageConfig::default(),
24463 Some(tree_sitter_html::LANGUAGE.into()),
24464 )
24465 .with_brackets_query(
24466 r#"
24467 ("<" @open "/>" @close)
24468 ("</" @open ">" @close)
24469 ("<" @open ">" @close)
24470 ("\"" @open "\"" @close)
24471 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
24472 "#,
24473 )
24474 .unwrap(),
24475 );
24476 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24477
24478 cx.set_state(indoc! {"
24479 <span>ˇ</span>
24480 "});
24481 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24482 cx.assert_editor_state(indoc! {"
24483 <span>
24484 ˇ
24485 </span>
24486 "});
24487
24488 cx.set_state(indoc! {"
24489 <span><span></span>ˇ</span>
24490 "});
24491 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24492 cx.assert_editor_state(indoc! {"
24493 <span><span></span>
24494 ˇ</span>
24495 "});
24496
24497 cx.set_state(indoc! {"
24498 <span>ˇ
24499 </span>
24500 "});
24501 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24502 cx.assert_editor_state(indoc! {"
24503 <span>
24504 ˇ
24505 </span>
24506 "});
24507}
24508
24509#[gpui::test(iterations = 10)]
24510async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
24511 init_test(cx, |_| {});
24512
24513 let fs = FakeFs::new(cx.executor());
24514 fs.insert_tree(
24515 path!("/dir"),
24516 json!({
24517 "a.ts": "a",
24518 }),
24519 )
24520 .await;
24521
24522 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
24523 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24524 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24525
24526 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24527 language_registry.add(Arc::new(Language::new(
24528 LanguageConfig {
24529 name: "TypeScript".into(),
24530 matcher: LanguageMatcher {
24531 path_suffixes: vec!["ts".to_string()],
24532 ..Default::default()
24533 },
24534 ..Default::default()
24535 },
24536 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24537 )));
24538 let mut fake_language_servers = language_registry.register_fake_lsp(
24539 "TypeScript",
24540 FakeLspAdapter {
24541 capabilities: lsp::ServerCapabilities {
24542 code_lens_provider: Some(lsp::CodeLensOptions {
24543 resolve_provider: Some(true),
24544 }),
24545 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24546 commands: vec!["_the/command".to_string()],
24547 ..lsp::ExecuteCommandOptions::default()
24548 }),
24549 ..lsp::ServerCapabilities::default()
24550 },
24551 ..FakeLspAdapter::default()
24552 },
24553 );
24554
24555 let editor = workspace
24556 .update(cx, |workspace, window, cx| {
24557 workspace.open_abs_path(
24558 PathBuf::from(path!("/dir/a.ts")),
24559 OpenOptions::default(),
24560 window,
24561 cx,
24562 )
24563 })
24564 .unwrap()
24565 .await
24566 .unwrap()
24567 .downcast::<Editor>()
24568 .unwrap();
24569 cx.executor().run_until_parked();
24570
24571 let fake_server = fake_language_servers.next().await.unwrap();
24572
24573 let buffer = editor.update(cx, |editor, cx| {
24574 editor
24575 .buffer()
24576 .read(cx)
24577 .as_singleton()
24578 .expect("have opened a single file by path")
24579 });
24580
24581 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24582 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24583 drop(buffer_snapshot);
24584 let actions = cx
24585 .update_window(*workspace, |_, window, cx| {
24586 project.code_actions(&buffer, anchor..anchor, window, cx)
24587 })
24588 .unwrap();
24589
24590 fake_server
24591 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24592 Ok(Some(vec![
24593 lsp::CodeLens {
24594 range: lsp::Range::default(),
24595 command: Some(lsp::Command {
24596 title: "Code lens command".to_owned(),
24597 command: "_the/command".to_owned(),
24598 arguments: None,
24599 }),
24600 data: None,
24601 },
24602 lsp::CodeLens {
24603 range: lsp::Range::default(),
24604 command: Some(lsp::Command {
24605 title: "Command not in capabilities".to_owned(),
24606 command: "not in capabilities".to_owned(),
24607 arguments: None,
24608 }),
24609 data: None,
24610 },
24611 lsp::CodeLens {
24612 range: lsp::Range {
24613 start: lsp::Position {
24614 line: 1,
24615 character: 1,
24616 },
24617 end: lsp::Position {
24618 line: 1,
24619 character: 1,
24620 },
24621 },
24622 command: Some(lsp::Command {
24623 title: "Command not in range".to_owned(),
24624 command: "_the/command".to_owned(),
24625 arguments: None,
24626 }),
24627 data: None,
24628 },
24629 ]))
24630 })
24631 .next()
24632 .await;
24633
24634 let actions = actions.await.unwrap();
24635 assert_eq!(
24636 actions.len(),
24637 1,
24638 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24639 );
24640 let action = actions[0].clone();
24641 let apply = project.update(cx, |project, cx| {
24642 project.apply_code_action(buffer.clone(), action, true, cx)
24643 });
24644
24645 // Resolving the code action does not populate its edits. In absence of
24646 // edits, we must execute the given command.
24647 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24648 |mut lens, _| async move {
24649 let lens_command = lens.command.as_mut().expect("should have a command");
24650 assert_eq!(lens_command.title, "Code lens command");
24651 lens_command.arguments = Some(vec![json!("the-argument")]);
24652 Ok(lens)
24653 },
24654 );
24655
24656 // While executing the command, the language server sends the editor
24657 // a `workspaceEdit` request.
24658 fake_server
24659 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24660 let fake = fake_server.clone();
24661 move |params, _| {
24662 assert_eq!(params.command, "_the/command");
24663 let fake = fake.clone();
24664 async move {
24665 fake.server
24666 .request::<lsp::request::ApplyWorkspaceEdit>(
24667 lsp::ApplyWorkspaceEditParams {
24668 label: None,
24669 edit: lsp::WorkspaceEdit {
24670 changes: Some(
24671 [(
24672 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
24673 vec![lsp::TextEdit {
24674 range: lsp::Range::new(
24675 lsp::Position::new(0, 0),
24676 lsp::Position::new(0, 0),
24677 ),
24678 new_text: "X".into(),
24679 }],
24680 )]
24681 .into_iter()
24682 .collect(),
24683 ),
24684 ..lsp::WorkspaceEdit::default()
24685 },
24686 },
24687 )
24688 .await
24689 .into_response()
24690 .unwrap();
24691 Ok(Some(json!(null)))
24692 }
24693 }
24694 })
24695 .next()
24696 .await;
24697
24698 // Applying the code lens command returns a project transaction containing the edits
24699 // sent by the language server in its `workspaceEdit` request.
24700 let transaction = apply.await.unwrap();
24701 assert!(transaction.0.contains_key(&buffer));
24702 buffer.update(cx, |buffer, cx| {
24703 assert_eq!(buffer.text(), "Xa");
24704 buffer.undo(cx);
24705 assert_eq!(buffer.text(), "a");
24706 });
24707
24708 let actions_after_edits = cx
24709 .update_window(*workspace, |_, window, cx| {
24710 project.code_actions(&buffer, anchor..anchor, window, cx)
24711 })
24712 .unwrap()
24713 .await
24714 .unwrap();
24715 assert_eq!(
24716 actions, actions_after_edits,
24717 "For the same selection, same code lens actions should be returned"
24718 );
24719
24720 let _responses =
24721 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24722 panic!("No more code lens requests are expected");
24723 });
24724 editor.update_in(cx, |editor, window, cx| {
24725 editor.select_all(&SelectAll, window, cx);
24726 });
24727 cx.executor().run_until_parked();
24728 let new_actions = cx
24729 .update_window(*workspace, |_, window, cx| {
24730 project.code_actions(&buffer, anchor..anchor, window, cx)
24731 })
24732 .unwrap()
24733 .await
24734 .unwrap();
24735 assert_eq!(
24736 actions, new_actions,
24737 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
24738 );
24739}
24740
24741#[gpui::test]
24742async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
24743 init_test(cx, |_| {});
24744
24745 let fs = FakeFs::new(cx.executor());
24746 let main_text = r#"fn main() {
24747println!("1");
24748println!("2");
24749println!("3");
24750println!("4");
24751println!("5");
24752}"#;
24753 let lib_text = "mod foo {}";
24754 fs.insert_tree(
24755 path!("/a"),
24756 json!({
24757 "lib.rs": lib_text,
24758 "main.rs": main_text,
24759 }),
24760 )
24761 .await;
24762
24763 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24764 let (workspace, cx) =
24765 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24766 let worktree_id = workspace.update(cx, |workspace, cx| {
24767 workspace.project().update(cx, |project, cx| {
24768 project.worktrees(cx).next().unwrap().read(cx).id()
24769 })
24770 });
24771
24772 let expected_ranges = vec![
24773 Point::new(0, 0)..Point::new(0, 0),
24774 Point::new(1, 0)..Point::new(1, 1),
24775 Point::new(2, 0)..Point::new(2, 2),
24776 Point::new(3, 0)..Point::new(3, 3),
24777 ];
24778
24779 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24780 let editor_1 = workspace
24781 .update_in(cx, |workspace, window, cx| {
24782 workspace.open_path(
24783 (worktree_id, rel_path("main.rs")),
24784 Some(pane_1.downgrade()),
24785 true,
24786 window,
24787 cx,
24788 )
24789 })
24790 .unwrap()
24791 .await
24792 .downcast::<Editor>()
24793 .unwrap();
24794 pane_1.update(cx, |pane, cx| {
24795 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24796 open_editor.update(cx, |editor, cx| {
24797 assert_eq!(
24798 editor.display_text(cx),
24799 main_text,
24800 "Original main.rs text on initial open",
24801 );
24802 assert_eq!(
24803 editor
24804 .selections
24805 .all::<Point>(&editor.display_snapshot(cx))
24806 .into_iter()
24807 .map(|s| s.range())
24808 .collect::<Vec<_>>(),
24809 vec![Point::zero()..Point::zero()],
24810 "Default selections on initial open",
24811 );
24812 })
24813 });
24814 editor_1.update_in(cx, |editor, window, cx| {
24815 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24816 s.select_ranges(expected_ranges.clone());
24817 });
24818 });
24819
24820 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
24821 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
24822 });
24823 let editor_2 = workspace
24824 .update_in(cx, |workspace, window, cx| {
24825 workspace.open_path(
24826 (worktree_id, rel_path("main.rs")),
24827 Some(pane_2.downgrade()),
24828 true,
24829 window,
24830 cx,
24831 )
24832 })
24833 .unwrap()
24834 .await
24835 .downcast::<Editor>()
24836 .unwrap();
24837 pane_2.update(cx, |pane, cx| {
24838 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24839 open_editor.update(cx, |editor, cx| {
24840 assert_eq!(
24841 editor.display_text(cx),
24842 main_text,
24843 "Original main.rs text on initial open in another panel",
24844 );
24845 assert_eq!(
24846 editor
24847 .selections
24848 .all::<Point>(&editor.display_snapshot(cx))
24849 .into_iter()
24850 .map(|s| s.range())
24851 .collect::<Vec<_>>(),
24852 vec![Point::zero()..Point::zero()],
24853 "Default selections on initial open in another panel",
24854 );
24855 })
24856 });
24857
24858 editor_2.update_in(cx, |editor, window, cx| {
24859 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
24860 });
24861
24862 let _other_editor_1 = workspace
24863 .update_in(cx, |workspace, window, cx| {
24864 workspace.open_path(
24865 (worktree_id, rel_path("lib.rs")),
24866 Some(pane_1.downgrade()),
24867 true,
24868 window,
24869 cx,
24870 )
24871 })
24872 .unwrap()
24873 .await
24874 .downcast::<Editor>()
24875 .unwrap();
24876 pane_1
24877 .update_in(cx, |pane, window, cx| {
24878 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24879 })
24880 .await
24881 .unwrap();
24882 drop(editor_1);
24883 pane_1.update(cx, |pane, cx| {
24884 pane.active_item()
24885 .unwrap()
24886 .downcast::<Editor>()
24887 .unwrap()
24888 .update(cx, |editor, cx| {
24889 assert_eq!(
24890 editor.display_text(cx),
24891 lib_text,
24892 "Other file should be open and active",
24893 );
24894 });
24895 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24896 });
24897
24898 let _other_editor_2 = workspace
24899 .update_in(cx, |workspace, window, cx| {
24900 workspace.open_path(
24901 (worktree_id, rel_path("lib.rs")),
24902 Some(pane_2.downgrade()),
24903 true,
24904 window,
24905 cx,
24906 )
24907 })
24908 .unwrap()
24909 .await
24910 .downcast::<Editor>()
24911 .unwrap();
24912 pane_2
24913 .update_in(cx, |pane, window, cx| {
24914 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24915 })
24916 .await
24917 .unwrap();
24918 drop(editor_2);
24919 pane_2.update(cx, |pane, cx| {
24920 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24921 open_editor.update(cx, |editor, cx| {
24922 assert_eq!(
24923 editor.display_text(cx),
24924 lib_text,
24925 "Other file should be open and active in another panel too",
24926 );
24927 });
24928 assert_eq!(
24929 pane.items().count(),
24930 1,
24931 "No other editors should be open in another pane",
24932 );
24933 });
24934
24935 let _editor_1_reopened = workspace
24936 .update_in(cx, |workspace, window, cx| {
24937 workspace.open_path(
24938 (worktree_id, rel_path("main.rs")),
24939 Some(pane_1.downgrade()),
24940 true,
24941 window,
24942 cx,
24943 )
24944 })
24945 .unwrap()
24946 .await
24947 .downcast::<Editor>()
24948 .unwrap();
24949 let _editor_2_reopened = workspace
24950 .update_in(cx, |workspace, window, cx| {
24951 workspace.open_path(
24952 (worktree_id, rel_path("main.rs")),
24953 Some(pane_2.downgrade()),
24954 true,
24955 window,
24956 cx,
24957 )
24958 })
24959 .unwrap()
24960 .await
24961 .downcast::<Editor>()
24962 .unwrap();
24963 pane_1.update(cx, |pane, cx| {
24964 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24965 open_editor.update(cx, |editor, cx| {
24966 assert_eq!(
24967 editor.display_text(cx),
24968 main_text,
24969 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24970 );
24971 assert_eq!(
24972 editor
24973 .selections
24974 .all::<Point>(&editor.display_snapshot(cx))
24975 .into_iter()
24976 .map(|s| s.range())
24977 .collect::<Vec<_>>(),
24978 expected_ranges,
24979 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24980 );
24981 })
24982 });
24983 pane_2.update(cx, |pane, cx| {
24984 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24985 open_editor.update(cx, |editor, cx| {
24986 assert_eq!(
24987 editor.display_text(cx),
24988 r#"fn main() {
24989⋯rintln!("1");
24990⋯intln!("2");
24991⋯ntln!("3");
24992println!("4");
24993println!("5");
24994}"#,
24995 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24996 );
24997 assert_eq!(
24998 editor
24999 .selections
25000 .all::<Point>(&editor.display_snapshot(cx))
25001 .into_iter()
25002 .map(|s| s.range())
25003 .collect::<Vec<_>>(),
25004 vec![Point::zero()..Point::zero()],
25005 "Previous editor in the 2nd pane had no selections changed hence should restore none",
25006 );
25007 })
25008 });
25009}
25010
25011#[gpui::test]
25012async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
25013 init_test(cx, |_| {});
25014
25015 let fs = FakeFs::new(cx.executor());
25016 let main_text = r#"fn main() {
25017println!("1");
25018println!("2");
25019println!("3");
25020println!("4");
25021println!("5");
25022}"#;
25023 let lib_text = "mod foo {}";
25024 fs.insert_tree(
25025 path!("/a"),
25026 json!({
25027 "lib.rs": lib_text,
25028 "main.rs": main_text,
25029 }),
25030 )
25031 .await;
25032
25033 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25034 let (workspace, cx) =
25035 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25036 let worktree_id = workspace.update(cx, |workspace, cx| {
25037 workspace.project().update(cx, |project, cx| {
25038 project.worktrees(cx).next().unwrap().read(cx).id()
25039 })
25040 });
25041
25042 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25043 let editor = workspace
25044 .update_in(cx, |workspace, window, cx| {
25045 workspace.open_path(
25046 (worktree_id, rel_path("main.rs")),
25047 Some(pane.downgrade()),
25048 true,
25049 window,
25050 cx,
25051 )
25052 })
25053 .unwrap()
25054 .await
25055 .downcast::<Editor>()
25056 .unwrap();
25057 pane.update(cx, |pane, cx| {
25058 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25059 open_editor.update(cx, |editor, cx| {
25060 assert_eq!(
25061 editor.display_text(cx),
25062 main_text,
25063 "Original main.rs text on initial open",
25064 );
25065 })
25066 });
25067 editor.update_in(cx, |editor, window, cx| {
25068 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
25069 });
25070
25071 cx.update_global(|store: &mut SettingsStore, cx| {
25072 store.update_user_settings(cx, |s| {
25073 s.workspace.restore_on_file_reopen = Some(false);
25074 });
25075 });
25076 editor.update_in(cx, |editor, window, cx| {
25077 editor.fold_ranges(
25078 vec![
25079 Point::new(1, 0)..Point::new(1, 1),
25080 Point::new(2, 0)..Point::new(2, 2),
25081 Point::new(3, 0)..Point::new(3, 3),
25082 ],
25083 false,
25084 window,
25085 cx,
25086 );
25087 });
25088 pane.update_in(cx, |pane, window, cx| {
25089 pane.close_all_items(&CloseAllItems::default(), window, cx)
25090 })
25091 .await
25092 .unwrap();
25093 pane.update(cx, |pane, _| {
25094 assert!(pane.active_item().is_none());
25095 });
25096 cx.update_global(|store: &mut SettingsStore, cx| {
25097 store.update_user_settings(cx, |s| {
25098 s.workspace.restore_on_file_reopen = Some(true);
25099 });
25100 });
25101
25102 let _editor_reopened = workspace
25103 .update_in(cx, |workspace, window, cx| {
25104 workspace.open_path(
25105 (worktree_id, rel_path("main.rs")),
25106 Some(pane.downgrade()),
25107 true,
25108 window,
25109 cx,
25110 )
25111 })
25112 .unwrap()
25113 .await
25114 .downcast::<Editor>()
25115 .unwrap();
25116 pane.update(cx, |pane, cx| {
25117 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25118 open_editor.update(cx, |editor, cx| {
25119 assert_eq!(
25120 editor.display_text(cx),
25121 main_text,
25122 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
25123 );
25124 })
25125 });
25126}
25127
25128#[gpui::test]
25129async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
25130 struct EmptyModalView {
25131 focus_handle: gpui::FocusHandle,
25132 }
25133 impl EventEmitter<DismissEvent> for EmptyModalView {}
25134 impl Render for EmptyModalView {
25135 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
25136 div()
25137 }
25138 }
25139 impl Focusable for EmptyModalView {
25140 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
25141 self.focus_handle.clone()
25142 }
25143 }
25144 impl workspace::ModalView for EmptyModalView {}
25145 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
25146 EmptyModalView {
25147 focus_handle: cx.focus_handle(),
25148 }
25149 }
25150
25151 init_test(cx, |_| {});
25152
25153 let fs = FakeFs::new(cx.executor());
25154 let project = Project::test(fs, [], cx).await;
25155 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25156 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
25157 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
25158 let editor = cx.new_window_entity(|window, cx| {
25159 Editor::new(
25160 EditorMode::full(),
25161 buffer,
25162 Some(project.clone()),
25163 window,
25164 cx,
25165 )
25166 });
25167 workspace
25168 .update(cx, |workspace, window, cx| {
25169 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
25170 })
25171 .unwrap();
25172 editor.update_in(cx, |editor, window, cx| {
25173 editor.open_context_menu(&OpenContextMenu, window, cx);
25174 assert!(editor.mouse_context_menu.is_some());
25175 });
25176 workspace
25177 .update(cx, |workspace, window, cx| {
25178 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
25179 })
25180 .unwrap();
25181 cx.read(|cx| {
25182 assert!(editor.read(cx).mouse_context_menu.is_none());
25183 });
25184}
25185
25186fn set_linked_edit_ranges(
25187 opening: (Point, Point),
25188 closing: (Point, Point),
25189 editor: &mut Editor,
25190 cx: &mut Context<Editor>,
25191) {
25192 let Some((buffer, _)) = editor
25193 .buffer
25194 .read(cx)
25195 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
25196 else {
25197 panic!("Failed to get buffer for selection position");
25198 };
25199 let buffer = buffer.read(cx);
25200 let buffer_id = buffer.remote_id();
25201 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
25202 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
25203 let mut linked_ranges = HashMap::default();
25204 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
25205 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
25206}
25207
25208#[gpui::test]
25209async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
25210 init_test(cx, |_| {});
25211
25212 let fs = FakeFs::new(cx.executor());
25213 fs.insert_file(path!("/file.html"), Default::default())
25214 .await;
25215
25216 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
25217
25218 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25219 let html_language = Arc::new(Language::new(
25220 LanguageConfig {
25221 name: "HTML".into(),
25222 matcher: LanguageMatcher {
25223 path_suffixes: vec!["html".to_string()],
25224 ..LanguageMatcher::default()
25225 },
25226 brackets: BracketPairConfig {
25227 pairs: vec![BracketPair {
25228 start: "<".into(),
25229 end: ">".into(),
25230 close: true,
25231 ..Default::default()
25232 }],
25233 ..Default::default()
25234 },
25235 ..Default::default()
25236 },
25237 Some(tree_sitter_html::LANGUAGE.into()),
25238 ));
25239 language_registry.add(html_language);
25240 let mut fake_servers = language_registry.register_fake_lsp(
25241 "HTML",
25242 FakeLspAdapter {
25243 capabilities: lsp::ServerCapabilities {
25244 completion_provider: Some(lsp::CompletionOptions {
25245 resolve_provider: Some(true),
25246 ..Default::default()
25247 }),
25248 ..Default::default()
25249 },
25250 ..Default::default()
25251 },
25252 );
25253
25254 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25255 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25256
25257 let worktree_id = workspace
25258 .update(cx, |workspace, _window, cx| {
25259 workspace.project().update(cx, |project, cx| {
25260 project.worktrees(cx).next().unwrap().read(cx).id()
25261 })
25262 })
25263 .unwrap();
25264 project
25265 .update(cx, |project, cx| {
25266 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
25267 })
25268 .await
25269 .unwrap();
25270 let editor = workspace
25271 .update(cx, |workspace, window, cx| {
25272 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
25273 })
25274 .unwrap()
25275 .await
25276 .unwrap()
25277 .downcast::<Editor>()
25278 .unwrap();
25279
25280 let fake_server = fake_servers.next().await.unwrap();
25281 editor.update_in(cx, |editor, window, cx| {
25282 editor.set_text("<ad></ad>", window, cx);
25283 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
25284 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
25285 });
25286 set_linked_edit_ranges(
25287 (Point::new(0, 1), Point::new(0, 3)),
25288 (Point::new(0, 6), Point::new(0, 8)),
25289 editor,
25290 cx,
25291 );
25292 });
25293 let mut completion_handle =
25294 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25295 Ok(Some(lsp::CompletionResponse::Array(vec![
25296 lsp::CompletionItem {
25297 label: "head".to_string(),
25298 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25299 lsp::InsertReplaceEdit {
25300 new_text: "head".to_string(),
25301 insert: lsp::Range::new(
25302 lsp::Position::new(0, 1),
25303 lsp::Position::new(0, 3),
25304 ),
25305 replace: lsp::Range::new(
25306 lsp::Position::new(0, 1),
25307 lsp::Position::new(0, 3),
25308 ),
25309 },
25310 )),
25311 ..Default::default()
25312 },
25313 ])))
25314 });
25315 editor.update_in(cx, |editor, window, cx| {
25316 editor.show_completions(&ShowCompletions, window, cx);
25317 });
25318 cx.run_until_parked();
25319 completion_handle.next().await.unwrap();
25320 editor.update(cx, |editor, _| {
25321 assert!(
25322 editor.context_menu_visible(),
25323 "Completion menu should be visible"
25324 );
25325 });
25326 editor.update_in(cx, |editor, window, cx| {
25327 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
25328 });
25329 cx.executor().run_until_parked();
25330 editor.update(cx, |editor, cx| {
25331 assert_eq!(editor.text(cx), "<head></head>");
25332 });
25333}
25334
25335#[gpui::test]
25336async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
25337 init_test(cx, |_| {});
25338
25339 let mut cx = EditorTestContext::new(cx).await;
25340 let language = Arc::new(Language::new(
25341 LanguageConfig {
25342 name: "TSX".into(),
25343 matcher: LanguageMatcher {
25344 path_suffixes: vec!["tsx".to_string()],
25345 ..LanguageMatcher::default()
25346 },
25347 brackets: BracketPairConfig {
25348 pairs: vec![BracketPair {
25349 start: "<".into(),
25350 end: ">".into(),
25351 close: true,
25352 ..Default::default()
25353 }],
25354 ..Default::default()
25355 },
25356 linked_edit_characters: HashSet::from_iter(['.']),
25357 ..Default::default()
25358 },
25359 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
25360 ));
25361 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25362
25363 // Test typing > does not extend linked pair
25364 cx.set_state("<divˇ<div></div>");
25365 cx.update_editor(|editor, _, cx| {
25366 set_linked_edit_ranges(
25367 (Point::new(0, 1), Point::new(0, 4)),
25368 (Point::new(0, 11), Point::new(0, 14)),
25369 editor,
25370 cx,
25371 );
25372 });
25373 cx.update_editor(|editor, window, cx| {
25374 editor.handle_input(">", window, cx);
25375 });
25376 cx.assert_editor_state("<div>ˇ<div></div>");
25377
25378 // Test typing . do extend linked pair
25379 cx.set_state("<Animatedˇ></Animated>");
25380 cx.update_editor(|editor, _, cx| {
25381 set_linked_edit_ranges(
25382 (Point::new(0, 1), Point::new(0, 9)),
25383 (Point::new(0, 12), Point::new(0, 20)),
25384 editor,
25385 cx,
25386 );
25387 });
25388 cx.update_editor(|editor, window, cx| {
25389 editor.handle_input(".", window, cx);
25390 });
25391 cx.assert_editor_state("<Animated.ˇ></Animated.>");
25392 cx.update_editor(|editor, _, cx| {
25393 set_linked_edit_ranges(
25394 (Point::new(0, 1), Point::new(0, 10)),
25395 (Point::new(0, 13), Point::new(0, 21)),
25396 editor,
25397 cx,
25398 );
25399 });
25400 cx.update_editor(|editor, window, cx| {
25401 editor.handle_input("V", window, cx);
25402 });
25403 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
25404}
25405
25406#[gpui::test]
25407async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
25408 init_test(cx, |_| {});
25409
25410 let fs = FakeFs::new(cx.executor());
25411 fs.insert_tree(
25412 path!("/root"),
25413 json!({
25414 "a": {
25415 "main.rs": "fn main() {}",
25416 },
25417 "foo": {
25418 "bar": {
25419 "external_file.rs": "pub mod external {}",
25420 }
25421 }
25422 }),
25423 )
25424 .await;
25425
25426 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
25427 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25428 language_registry.add(rust_lang());
25429 let _fake_servers = language_registry.register_fake_lsp(
25430 "Rust",
25431 FakeLspAdapter {
25432 ..FakeLspAdapter::default()
25433 },
25434 );
25435 let (workspace, cx) =
25436 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25437 let worktree_id = workspace.update(cx, |workspace, cx| {
25438 workspace.project().update(cx, |project, cx| {
25439 project.worktrees(cx).next().unwrap().read(cx).id()
25440 })
25441 });
25442
25443 let assert_language_servers_count =
25444 |expected: usize, context: &str, cx: &mut VisualTestContext| {
25445 project.update(cx, |project, cx| {
25446 let current = project
25447 .lsp_store()
25448 .read(cx)
25449 .as_local()
25450 .unwrap()
25451 .language_servers
25452 .len();
25453 assert_eq!(expected, current, "{context}");
25454 });
25455 };
25456
25457 assert_language_servers_count(
25458 0,
25459 "No servers should be running before any file is open",
25460 cx,
25461 );
25462 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25463 let main_editor = workspace
25464 .update_in(cx, |workspace, window, cx| {
25465 workspace.open_path(
25466 (worktree_id, rel_path("main.rs")),
25467 Some(pane.downgrade()),
25468 true,
25469 window,
25470 cx,
25471 )
25472 })
25473 .unwrap()
25474 .await
25475 .downcast::<Editor>()
25476 .unwrap();
25477 pane.update(cx, |pane, cx| {
25478 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25479 open_editor.update(cx, |editor, cx| {
25480 assert_eq!(
25481 editor.display_text(cx),
25482 "fn main() {}",
25483 "Original main.rs text on initial open",
25484 );
25485 });
25486 assert_eq!(open_editor, main_editor);
25487 });
25488 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
25489
25490 let external_editor = workspace
25491 .update_in(cx, |workspace, window, cx| {
25492 workspace.open_abs_path(
25493 PathBuf::from("/root/foo/bar/external_file.rs"),
25494 OpenOptions::default(),
25495 window,
25496 cx,
25497 )
25498 })
25499 .await
25500 .expect("opening external file")
25501 .downcast::<Editor>()
25502 .expect("downcasted external file's open element to editor");
25503 pane.update(cx, |pane, cx| {
25504 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25505 open_editor.update(cx, |editor, cx| {
25506 assert_eq!(
25507 editor.display_text(cx),
25508 "pub mod external {}",
25509 "External file is open now",
25510 );
25511 });
25512 assert_eq!(open_editor, external_editor);
25513 });
25514 assert_language_servers_count(
25515 1,
25516 "Second, external, *.rs file should join the existing server",
25517 cx,
25518 );
25519
25520 pane.update_in(cx, |pane, window, cx| {
25521 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25522 })
25523 .await
25524 .unwrap();
25525 pane.update_in(cx, |pane, window, cx| {
25526 pane.navigate_backward(&Default::default(), window, cx);
25527 });
25528 cx.run_until_parked();
25529 pane.update(cx, |pane, cx| {
25530 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25531 open_editor.update(cx, |editor, cx| {
25532 assert_eq!(
25533 editor.display_text(cx),
25534 "pub mod external {}",
25535 "External file is open now",
25536 );
25537 });
25538 });
25539 assert_language_servers_count(
25540 1,
25541 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25542 cx,
25543 );
25544
25545 cx.update(|_, cx| {
25546 workspace::reload(cx);
25547 });
25548 assert_language_servers_count(
25549 1,
25550 "After reloading the worktree with local and external files opened, only one project should be started",
25551 cx,
25552 );
25553}
25554
25555#[gpui::test]
25556async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25557 init_test(cx, |_| {});
25558
25559 let mut cx = EditorTestContext::new(cx).await;
25560 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25561 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25562
25563 // test cursor move to start of each line on tab
25564 // for `if`, `elif`, `else`, `while`, `with` and `for`
25565 cx.set_state(indoc! {"
25566 def main():
25567 ˇ for item in items:
25568 ˇ while item.active:
25569 ˇ if item.value > 10:
25570 ˇ continue
25571 ˇ elif item.value < 0:
25572 ˇ break
25573 ˇ else:
25574 ˇ with item.context() as ctx:
25575 ˇ yield count
25576 ˇ else:
25577 ˇ log('while else')
25578 ˇ else:
25579 ˇ log('for else')
25580 "});
25581 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25582 cx.wait_for_autoindent_applied().await;
25583 cx.assert_editor_state(indoc! {"
25584 def main():
25585 ˇfor item in items:
25586 ˇwhile item.active:
25587 ˇif item.value > 10:
25588 ˇcontinue
25589 ˇelif item.value < 0:
25590 ˇbreak
25591 ˇelse:
25592 ˇwith item.context() as ctx:
25593 ˇyield count
25594 ˇelse:
25595 ˇlog('while else')
25596 ˇelse:
25597 ˇlog('for else')
25598 "});
25599 // test relative indent is preserved when tab
25600 // for `if`, `elif`, `else`, `while`, `with` and `for`
25601 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25602 cx.wait_for_autoindent_applied().await;
25603 cx.assert_editor_state(indoc! {"
25604 def main():
25605 ˇfor item in items:
25606 ˇwhile item.active:
25607 ˇif item.value > 10:
25608 ˇcontinue
25609 ˇelif item.value < 0:
25610 ˇbreak
25611 ˇelse:
25612 ˇwith item.context() as ctx:
25613 ˇyield count
25614 ˇelse:
25615 ˇlog('while else')
25616 ˇelse:
25617 ˇlog('for else')
25618 "});
25619
25620 // test cursor move to start of each line on tab
25621 // for `try`, `except`, `else`, `finally`, `match` and `def`
25622 cx.set_state(indoc! {"
25623 def main():
25624 ˇ try:
25625 ˇ fetch()
25626 ˇ except ValueError:
25627 ˇ handle_error()
25628 ˇ else:
25629 ˇ match value:
25630 ˇ case _:
25631 ˇ finally:
25632 ˇ def status():
25633 ˇ return 0
25634 "});
25635 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25636 cx.wait_for_autoindent_applied().await;
25637 cx.assert_editor_state(indoc! {"
25638 def main():
25639 ˇtry:
25640 ˇfetch()
25641 ˇexcept ValueError:
25642 ˇhandle_error()
25643 ˇelse:
25644 ˇmatch value:
25645 ˇcase _:
25646 ˇfinally:
25647 ˇdef status():
25648 ˇreturn 0
25649 "});
25650 // test relative indent is preserved when tab
25651 // for `try`, `except`, `else`, `finally`, `match` and `def`
25652 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25653 cx.wait_for_autoindent_applied().await;
25654 cx.assert_editor_state(indoc! {"
25655 def main():
25656 ˇtry:
25657 ˇfetch()
25658 ˇexcept ValueError:
25659 ˇhandle_error()
25660 ˇelse:
25661 ˇmatch value:
25662 ˇcase _:
25663 ˇfinally:
25664 ˇdef status():
25665 ˇreturn 0
25666 "});
25667}
25668
25669#[gpui::test]
25670async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
25671 init_test(cx, |_| {});
25672
25673 let mut cx = EditorTestContext::new(cx).await;
25674 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25675 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25676
25677 // test `else` auto outdents when typed inside `if` block
25678 cx.set_state(indoc! {"
25679 def main():
25680 if i == 2:
25681 return
25682 ˇ
25683 "});
25684 cx.update_editor(|editor, window, cx| {
25685 editor.handle_input("else:", window, cx);
25686 });
25687 cx.wait_for_autoindent_applied().await;
25688 cx.assert_editor_state(indoc! {"
25689 def main():
25690 if i == 2:
25691 return
25692 else:ˇ
25693 "});
25694
25695 // test `except` auto outdents when typed inside `try` block
25696 cx.set_state(indoc! {"
25697 def main():
25698 try:
25699 i = 2
25700 ˇ
25701 "});
25702 cx.update_editor(|editor, window, cx| {
25703 editor.handle_input("except:", window, cx);
25704 });
25705 cx.wait_for_autoindent_applied().await;
25706 cx.assert_editor_state(indoc! {"
25707 def main():
25708 try:
25709 i = 2
25710 except:ˇ
25711 "});
25712
25713 // test `else` auto outdents when typed inside `except` block
25714 cx.set_state(indoc! {"
25715 def main():
25716 try:
25717 i = 2
25718 except:
25719 j = 2
25720 ˇ
25721 "});
25722 cx.update_editor(|editor, window, cx| {
25723 editor.handle_input("else:", window, cx);
25724 });
25725 cx.wait_for_autoindent_applied().await;
25726 cx.assert_editor_state(indoc! {"
25727 def main():
25728 try:
25729 i = 2
25730 except:
25731 j = 2
25732 else:ˇ
25733 "});
25734
25735 // test `finally` auto outdents when typed inside `else` block
25736 cx.set_state(indoc! {"
25737 def main():
25738 try:
25739 i = 2
25740 except:
25741 j = 2
25742 else:
25743 k = 2
25744 ˇ
25745 "});
25746 cx.update_editor(|editor, window, cx| {
25747 editor.handle_input("finally:", window, cx);
25748 });
25749 cx.wait_for_autoindent_applied().await;
25750 cx.assert_editor_state(indoc! {"
25751 def main():
25752 try:
25753 i = 2
25754 except:
25755 j = 2
25756 else:
25757 k = 2
25758 finally:ˇ
25759 "});
25760
25761 // test `else` does not outdents when typed inside `except` block right after for block
25762 cx.set_state(indoc! {"
25763 def main():
25764 try:
25765 i = 2
25766 except:
25767 for i in range(n):
25768 pass
25769 ˇ
25770 "});
25771 cx.update_editor(|editor, window, cx| {
25772 editor.handle_input("else:", window, cx);
25773 });
25774 cx.wait_for_autoindent_applied().await;
25775 cx.assert_editor_state(indoc! {"
25776 def main():
25777 try:
25778 i = 2
25779 except:
25780 for i in range(n):
25781 pass
25782 else:ˇ
25783 "});
25784
25785 // test `finally` auto outdents when typed inside `else` block right after for block
25786 cx.set_state(indoc! {"
25787 def main():
25788 try:
25789 i = 2
25790 except:
25791 j = 2
25792 else:
25793 for i in range(n):
25794 pass
25795 ˇ
25796 "});
25797 cx.update_editor(|editor, window, cx| {
25798 editor.handle_input("finally:", window, cx);
25799 });
25800 cx.wait_for_autoindent_applied().await;
25801 cx.assert_editor_state(indoc! {"
25802 def main():
25803 try:
25804 i = 2
25805 except:
25806 j = 2
25807 else:
25808 for i in range(n):
25809 pass
25810 finally:ˇ
25811 "});
25812
25813 // test `except` outdents to inner "try" block
25814 cx.set_state(indoc! {"
25815 def main():
25816 try:
25817 i = 2
25818 if i == 2:
25819 try:
25820 i = 3
25821 ˇ
25822 "});
25823 cx.update_editor(|editor, window, cx| {
25824 editor.handle_input("except:", window, cx);
25825 });
25826 cx.wait_for_autoindent_applied().await;
25827 cx.assert_editor_state(indoc! {"
25828 def main():
25829 try:
25830 i = 2
25831 if i == 2:
25832 try:
25833 i = 3
25834 except:ˇ
25835 "});
25836
25837 // test `except` outdents to outer "try" block
25838 cx.set_state(indoc! {"
25839 def main():
25840 try:
25841 i = 2
25842 if i == 2:
25843 try:
25844 i = 3
25845 ˇ
25846 "});
25847 cx.update_editor(|editor, window, cx| {
25848 editor.handle_input("except:", window, cx);
25849 });
25850 cx.wait_for_autoindent_applied().await;
25851 cx.assert_editor_state(indoc! {"
25852 def main():
25853 try:
25854 i = 2
25855 if i == 2:
25856 try:
25857 i = 3
25858 except:ˇ
25859 "});
25860
25861 // test `else` stays at correct indent when typed after `for` block
25862 cx.set_state(indoc! {"
25863 def main():
25864 for i in range(10):
25865 if i == 3:
25866 break
25867 ˇ
25868 "});
25869 cx.update_editor(|editor, window, cx| {
25870 editor.handle_input("else:", window, cx);
25871 });
25872 cx.wait_for_autoindent_applied().await;
25873 cx.assert_editor_state(indoc! {"
25874 def main():
25875 for i in range(10):
25876 if i == 3:
25877 break
25878 else:ˇ
25879 "});
25880
25881 // test does not outdent on typing after line with square brackets
25882 cx.set_state(indoc! {"
25883 def f() -> list[str]:
25884 ˇ
25885 "});
25886 cx.update_editor(|editor, window, cx| {
25887 editor.handle_input("a", window, cx);
25888 });
25889 cx.wait_for_autoindent_applied().await;
25890 cx.assert_editor_state(indoc! {"
25891 def f() -> list[str]:
25892 aˇ
25893 "});
25894
25895 // test does not outdent on typing : after case keyword
25896 cx.set_state(indoc! {"
25897 match 1:
25898 caseˇ
25899 "});
25900 cx.update_editor(|editor, window, cx| {
25901 editor.handle_input(":", window, cx);
25902 });
25903 cx.wait_for_autoindent_applied().await;
25904 cx.assert_editor_state(indoc! {"
25905 match 1:
25906 case:ˇ
25907 "});
25908}
25909
25910#[gpui::test]
25911async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
25912 init_test(cx, |_| {});
25913 update_test_language_settings(cx, |settings| {
25914 settings.defaults.extend_comment_on_newline = Some(false);
25915 });
25916 let mut cx = EditorTestContext::new(cx).await;
25917 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25918 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25919
25920 // test correct indent after newline on comment
25921 cx.set_state(indoc! {"
25922 # COMMENT:ˇ
25923 "});
25924 cx.update_editor(|editor, window, cx| {
25925 editor.newline(&Newline, window, cx);
25926 });
25927 cx.wait_for_autoindent_applied().await;
25928 cx.assert_editor_state(indoc! {"
25929 # COMMENT:
25930 ˇ
25931 "});
25932
25933 // test correct indent after newline in brackets
25934 cx.set_state(indoc! {"
25935 {ˇ}
25936 "});
25937 cx.update_editor(|editor, window, cx| {
25938 editor.newline(&Newline, window, cx);
25939 });
25940 cx.wait_for_autoindent_applied().await;
25941 cx.assert_editor_state(indoc! {"
25942 {
25943 ˇ
25944 }
25945 "});
25946
25947 cx.set_state(indoc! {"
25948 (ˇ)
25949 "});
25950 cx.update_editor(|editor, window, cx| {
25951 editor.newline(&Newline, window, cx);
25952 });
25953 cx.run_until_parked();
25954 cx.assert_editor_state(indoc! {"
25955 (
25956 ˇ
25957 )
25958 "});
25959
25960 // do not indent after empty lists or dictionaries
25961 cx.set_state(indoc! {"
25962 a = []ˇ
25963 "});
25964 cx.update_editor(|editor, window, cx| {
25965 editor.newline(&Newline, window, cx);
25966 });
25967 cx.run_until_parked();
25968 cx.assert_editor_state(indoc! {"
25969 a = []
25970 ˇ
25971 "});
25972}
25973
25974#[gpui::test]
25975async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
25976 init_test(cx, |_| {});
25977
25978 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
25979 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
25980 language_registry.add(markdown_lang());
25981 language_registry.add(python_lang);
25982
25983 let mut cx = EditorTestContext::new(cx).await;
25984 cx.update_buffer(|buffer, cx| {
25985 buffer.set_language_registry(language_registry);
25986 buffer.set_language(Some(markdown_lang()), cx);
25987 });
25988
25989 // Test that `else:` correctly outdents to match `if:` inside the Python code block
25990 cx.set_state(indoc! {"
25991 # Heading
25992
25993 ```python
25994 def main():
25995 if condition:
25996 pass
25997 ˇ
25998 ```
25999 "});
26000 cx.update_editor(|editor, window, cx| {
26001 editor.handle_input("else:", window, cx);
26002 });
26003 cx.run_until_parked();
26004 cx.assert_editor_state(indoc! {"
26005 # Heading
26006
26007 ```python
26008 def main():
26009 if condition:
26010 pass
26011 else:ˇ
26012 ```
26013 "});
26014}
26015
26016#[gpui::test]
26017async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
26018 init_test(cx, |_| {});
26019
26020 let mut cx = EditorTestContext::new(cx).await;
26021 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26022 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26023
26024 // test cursor move to start of each line on tab
26025 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
26026 cx.set_state(indoc! {"
26027 function main() {
26028 ˇ for item in $items; do
26029 ˇ while [ -n \"$item\" ]; do
26030 ˇ if [ \"$value\" -gt 10 ]; then
26031 ˇ continue
26032 ˇ elif [ \"$value\" -lt 0 ]; then
26033 ˇ break
26034 ˇ else
26035 ˇ echo \"$item\"
26036 ˇ fi
26037 ˇ done
26038 ˇ done
26039 ˇ}
26040 "});
26041 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26042 cx.wait_for_autoindent_applied().await;
26043 cx.assert_editor_state(indoc! {"
26044 function main() {
26045 ˇfor item in $items; do
26046 ˇwhile [ -n \"$item\" ]; do
26047 ˇif [ \"$value\" -gt 10 ]; then
26048 ˇcontinue
26049 ˇelif [ \"$value\" -lt 0 ]; then
26050 ˇbreak
26051 ˇelse
26052 ˇecho \"$item\"
26053 ˇfi
26054 ˇdone
26055 ˇdone
26056 ˇ}
26057 "});
26058 // test relative indent is preserved when tab
26059 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26060 cx.wait_for_autoindent_applied().await;
26061 cx.assert_editor_state(indoc! {"
26062 function main() {
26063 ˇfor item in $items; do
26064 ˇwhile [ -n \"$item\" ]; do
26065 ˇif [ \"$value\" -gt 10 ]; then
26066 ˇcontinue
26067 ˇelif [ \"$value\" -lt 0 ]; then
26068 ˇbreak
26069 ˇelse
26070 ˇecho \"$item\"
26071 ˇfi
26072 ˇdone
26073 ˇdone
26074 ˇ}
26075 "});
26076
26077 // test cursor move to start of each line on tab
26078 // for `case` statement with patterns
26079 cx.set_state(indoc! {"
26080 function handle() {
26081 ˇ case \"$1\" in
26082 ˇ start)
26083 ˇ echo \"a\"
26084 ˇ ;;
26085 ˇ stop)
26086 ˇ echo \"b\"
26087 ˇ ;;
26088 ˇ *)
26089 ˇ echo \"c\"
26090 ˇ ;;
26091 ˇ esac
26092 ˇ}
26093 "});
26094 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26095 cx.wait_for_autoindent_applied().await;
26096 cx.assert_editor_state(indoc! {"
26097 function handle() {
26098 ˇcase \"$1\" in
26099 ˇstart)
26100 ˇecho \"a\"
26101 ˇ;;
26102 ˇstop)
26103 ˇecho \"b\"
26104 ˇ;;
26105 ˇ*)
26106 ˇecho \"c\"
26107 ˇ;;
26108 ˇesac
26109 ˇ}
26110 "});
26111}
26112
26113#[gpui::test]
26114async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
26115 init_test(cx, |_| {});
26116
26117 let mut cx = EditorTestContext::new(cx).await;
26118 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26119 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26120
26121 // test indents on comment insert
26122 cx.set_state(indoc! {"
26123 function main() {
26124 ˇ for item in $items; do
26125 ˇ while [ -n \"$item\" ]; do
26126 ˇ if [ \"$value\" -gt 10 ]; then
26127 ˇ continue
26128 ˇ elif [ \"$value\" -lt 0 ]; then
26129 ˇ break
26130 ˇ else
26131 ˇ echo \"$item\"
26132 ˇ fi
26133 ˇ done
26134 ˇ done
26135 ˇ}
26136 "});
26137 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
26138 cx.wait_for_autoindent_applied().await;
26139 cx.assert_editor_state(indoc! {"
26140 function main() {
26141 #ˇ for item in $items; do
26142 #ˇ while [ -n \"$item\" ]; do
26143 #ˇ if [ \"$value\" -gt 10 ]; then
26144 #ˇ continue
26145 #ˇ elif [ \"$value\" -lt 0 ]; then
26146 #ˇ break
26147 #ˇ else
26148 #ˇ echo \"$item\"
26149 #ˇ fi
26150 #ˇ done
26151 #ˇ done
26152 #ˇ}
26153 "});
26154}
26155
26156#[gpui::test]
26157async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
26158 init_test(cx, |_| {});
26159
26160 let mut cx = EditorTestContext::new(cx).await;
26161 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26162 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26163
26164 // test `else` auto outdents when typed inside `if` block
26165 cx.set_state(indoc! {"
26166 if [ \"$1\" = \"test\" ]; then
26167 echo \"foo bar\"
26168 ˇ
26169 "});
26170 cx.update_editor(|editor, window, cx| {
26171 editor.handle_input("else", window, cx);
26172 });
26173 cx.wait_for_autoindent_applied().await;
26174 cx.assert_editor_state(indoc! {"
26175 if [ \"$1\" = \"test\" ]; then
26176 echo \"foo bar\"
26177 elseˇ
26178 "});
26179
26180 // test `elif` auto outdents when typed inside `if` block
26181 cx.set_state(indoc! {"
26182 if [ \"$1\" = \"test\" ]; then
26183 echo \"foo bar\"
26184 ˇ
26185 "});
26186 cx.update_editor(|editor, window, cx| {
26187 editor.handle_input("elif", window, cx);
26188 });
26189 cx.wait_for_autoindent_applied().await;
26190 cx.assert_editor_state(indoc! {"
26191 if [ \"$1\" = \"test\" ]; then
26192 echo \"foo bar\"
26193 elifˇ
26194 "});
26195
26196 // test `fi` auto outdents when typed inside `else` block
26197 cx.set_state(indoc! {"
26198 if [ \"$1\" = \"test\" ]; then
26199 echo \"foo bar\"
26200 else
26201 echo \"bar baz\"
26202 ˇ
26203 "});
26204 cx.update_editor(|editor, window, cx| {
26205 editor.handle_input("fi", window, cx);
26206 });
26207 cx.wait_for_autoindent_applied().await;
26208 cx.assert_editor_state(indoc! {"
26209 if [ \"$1\" = \"test\" ]; then
26210 echo \"foo bar\"
26211 else
26212 echo \"bar baz\"
26213 fiˇ
26214 "});
26215
26216 // test `done` auto outdents when typed inside `while` block
26217 cx.set_state(indoc! {"
26218 while read line; do
26219 echo \"$line\"
26220 ˇ
26221 "});
26222 cx.update_editor(|editor, window, cx| {
26223 editor.handle_input("done", window, cx);
26224 });
26225 cx.wait_for_autoindent_applied().await;
26226 cx.assert_editor_state(indoc! {"
26227 while read line; do
26228 echo \"$line\"
26229 doneˇ
26230 "});
26231
26232 // test `done` auto outdents when typed inside `for` block
26233 cx.set_state(indoc! {"
26234 for file in *.txt; do
26235 cat \"$file\"
26236 ˇ
26237 "});
26238 cx.update_editor(|editor, window, cx| {
26239 editor.handle_input("done", window, cx);
26240 });
26241 cx.wait_for_autoindent_applied().await;
26242 cx.assert_editor_state(indoc! {"
26243 for file in *.txt; do
26244 cat \"$file\"
26245 doneˇ
26246 "});
26247
26248 // test `esac` auto outdents when typed inside `case` block
26249 cx.set_state(indoc! {"
26250 case \"$1\" in
26251 start)
26252 echo \"foo bar\"
26253 ;;
26254 stop)
26255 echo \"bar baz\"
26256 ;;
26257 ˇ
26258 "});
26259 cx.update_editor(|editor, window, cx| {
26260 editor.handle_input("esac", window, cx);
26261 });
26262 cx.wait_for_autoindent_applied().await;
26263 cx.assert_editor_state(indoc! {"
26264 case \"$1\" in
26265 start)
26266 echo \"foo bar\"
26267 ;;
26268 stop)
26269 echo \"bar baz\"
26270 ;;
26271 esacˇ
26272 "});
26273
26274 // test `*)` auto outdents when typed inside `case` block
26275 cx.set_state(indoc! {"
26276 case \"$1\" in
26277 start)
26278 echo \"foo bar\"
26279 ;;
26280 ˇ
26281 "});
26282 cx.update_editor(|editor, window, cx| {
26283 editor.handle_input("*)", window, cx);
26284 });
26285 cx.wait_for_autoindent_applied().await;
26286 cx.assert_editor_state(indoc! {"
26287 case \"$1\" in
26288 start)
26289 echo \"foo bar\"
26290 ;;
26291 *)ˇ
26292 "});
26293
26294 // test `fi` outdents to correct level with nested if blocks
26295 cx.set_state(indoc! {"
26296 if [ \"$1\" = \"test\" ]; then
26297 echo \"outer if\"
26298 if [ \"$2\" = \"debug\" ]; then
26299 echo \"inner if\"
26300 ˇ
26301 "});
26302 cx.update_editor(|editor, window, cx| {
26303 editor.handle_input("fi", window, cx);
26304 });
26305 cx.wait_for_autoindent_applied().await;
26306 cx.assert_editor_state(indoc! {"
26307 if [ \"$1\" = \"test\" ]; then
26308 echo \"outer if\"
26309 if [ \"$2\" = \"debug\" ]; then
26310 echo \"inner if\"
26311 fiˇ
26312 "});
26313}
26314
26315#[gpui::test]
26316async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
26317 init_test(cx, |_| {});
26318 update_test_language_settings(cx, |settings| {
26319 settings.defaults.extend_comment_on_newline = Some(false);
26320 });
26321 let mut cx = EditorTestContext::new(cx).await;
26322 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26323 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26324
26325 // test correct indent after newline on comment
26326 cx.set_state(indoc! {"
26327 # COMMENT:ˇ
26328 "});
26329 cx.update_editor(|editor, window, cx| {
26330 editor.newline(&Newline, window, cx);
26331 });
26332 cx.wait_for_autoindent_applied().await;
26333 cx.assert_editor_state(indoc! {"
26334 # COMMENT:
26335 ˇ
26336 "});
26337
26338 // test correct indent after newline after `then`
26339 cx.set_state(indoc! {"
26340
26341 if [ \"$1\" = \"test\" ]; thenˇ
26342 "});
26343 cx.update_editor(|editor, window, cx| {
26344 editor.newline(&Newline, window, cx);
26345 });
26346 cx.wait_for_autoindent_applied().await;
26347 cx.assert_editor_state(indoc! {"
26348
26349 if [ \"$1\" = \"test\" ]; then
26350 ˇ
26351 "});
26352
26353 // test correct indent after newline after `else`
26354 cx.set_state(indoc! {"
26355 if [ \"$1\" = \"test\" ]; then
26356 elseˇ
26357 "});
26358 cx.update_editor(|editor, window, cx| {
26359 editor.newline(&Newline, window, cx);
26360 });
26361 cx.wait_for_autoindent_applied().await;
26362 cx.assert_editor_state(indoc! {"
26363 if [ \"$1\" = \"test\" ]; then
26364 else
26365 ˇ
26366 "});
26367
26368 // test correct indent after newline after `elif`
26369 cx.set_state(indoc! {"
26370 if [ \"$1\" = \"test\" ]; then
26371 elifˇ
26372 "});
26373 cx.update_editor(|editor, window, cx| {
26374 editor.newline(&Newline, window, cx);
26375 });
26376 cx.wait_for_autoindent_applied().await;
26377 cx.assert_editor_state(indoc! {"
26378 if [ \"$1\" = \"test\" ]; then
26379 elif
26380 ˇ
26381 "});
26382
26383 // test correct indent after newline after `do`
26384 cx.set_state(indoc! {"
26385 for file in *.txt; doˇ
26386 "});
26387 cx.update_editor(|editor, window, cx| {
26388 editor.newline(&Newline, window, cx);
26389 });
26390 cx.wait_for_autoindent_applied().await;
26391 cx.assert_editor_state(indoc! {"
26392 for file in *.txt; do
26393 ˇ
26394 "});
26395
26396 // test correct indent after newline after case pattern
26397 cx.set_state(indoc! {"
26398 case \"$1\" in
26399 start)ˇ
26400 "});
26401 cx.update_editor(|editor, window, cx| {
26402 editor.newline(&Newline, window, cx);
26403 });
26404 cx.wait_for_autoindent_applied().await;
26405 cx.assert_editor_state(indoc! {"
26406 case \"$1\" in
26407 start)
26408 ˇ
26409 "});
26410
26411 // test correct indent after newline after case pattern
26412 cx.set_state(indoc! {"
26413 case \"$1\" in
26414 start)
26415 ;;
26416 *)ˇ
26417 "});
26418 cx.update_editor(|editor, window, cx| {
26419 editor.newline(&Newline, window, cx);
26420 });
26421 cx.wait_for_autoindent_applied().await;
26422 cx.assert_editor_state(indoc! {"
26423 case \"$1\" in
26424 start)
26425 ;;
26426 *)
26427 ˇ
26428 "});
26429
26430 // test correct indent after newline after function opening brace
26431 cx.set_state(indoc! {"
26432 function test() {ˇ}
26433 "});
26434 cx.update_editor(|editor, window, cx| {
26435 editor.newline(&Newline, window, cx);
26436 });
26437 cx.wait_for_autoindent_applied().await;
26438 cx.assert_editor_state(indoc! {"
26439 function test() {
26440 ˇ
26441 }
26442 "});
26443
26444 // test no extra indent after semicolon on same line
26445 cx.set_state(indoc! {"
26446 echo \"test\";ˇ
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 echo \"test\";
26454 ˇ
26455 "});
26456}
26457
26458fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
26459 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
26460 point..point
26461}
26462
26463#[track_caller]
26464fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
26465 let (text, ranges) = marked_text_ranges(marked_text, true);
26466 assert_eq!(editor.text(cx), text);
26467 assert_eq!(
26468 editor.selections.ranges(&editor.display_snapshot(cx)),
26469 ranges
26470 .iter()
26471 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
26472 .collect::<Vec<_>>(),
26473 "Assert selections are {}",
26474 marked_text
26475 );
26476}
26477
26478pub fn handle_signature_help_request(
26479 cx: &mut EditorLspTestContext,
26480 mocked_response: lsp::SignatureHelp,
26481) -> impl Future<Output = ()> + use<> {
26482 let mut request =
26483 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
26484 let mocked_response = mocked_response.clone();
26485 async move { Ok(Some(mocked_response)) }
26486 });
26487
26488 async move {
26489 request.next().await;
26490 }
26491}
26492
26493#[track_caller]
26494pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
26495 cx.update_editor(|editor, _, _| {
26496 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
26497 let entries = menu.entries.borrow();
26498 let entries = entries
26499 .iter()
26500 .map(|entry| entry.string.as_str())
26501 .collect::<Vec<_>>();
26502 assert_eq!(entries, expected);
26503 } else {
26504 panic!("Expected completions menu");
26505 }
26506 });
26507}
26508
26509#[gpui::test]
26510async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
26511 init_test(cx, |_| {});
26512 let mut cx = EditorLspTestContext::new_rust(
26513 lsp::ServerCapabilities {
26514 completion_provider: Some(lsp::CompletionOptions {
26515 ..Default::default()
26516 }),
26517 ..Default::default()
26518 },
26519 cx,
26520 )
26521 .await;
26522 cx.lsp
26523 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
26524 Ok(Some(lsp::CompletionResponse::Array(vec![
26525 lsp::CompletionItem {
26526 label: "unsafe".into(),
26527 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26528 range: lsp::Range {
26529 start: lsp::Position {
26530 line: 0,
26531 character: 9,
26532 },
26533 end: lsp::Position {
26534 line: 0,
26535 character: 11,
26536 },
26537 },
26538 new_text: "unsafe".to_string(),
26539 })),
26540 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
26541 ..Default::default()
26542 },
26543 ])))
26544 });
26545
26546 cx.update_editor(|editor, _, cx| {
26547 editor.project().unwrap().update(cx, |project, cx| {
26548 project.snippets().update(cx, |snippets, _cx| {
26549 snippets.add_snippet_for_test(
26550 None,
26551 PathBuf::from("test_snippets.json"),
26552 vec![
26553 Arc::new(project::snippet_provider::Snippet {
26554 prefix: vec![
26555 "unlimited word count".to_string(),
26556 "unlimit word count".to_string(),
26557 "unlimited unknown".to_string(),
26558 ],
26559 body: "this is many words".to_string(),
26560 description: Some("description".to_string()),
26561 name: "multi-word snippet test".to_string(),
26562 }),
26563 Arc::new(project::snippet_provider::Snippet {
26564 prefix: vec!["unsnip".to_string(), "@few".to_string()],
26565 body: "fewer words".to_string(),
26566 description: Some("alt description".to_string()),
26567 name: "other name".to_string(),
26568 }),
26569 Arc::new(project::snippet_provider::Snippet {
26570 prefix: vec!["ab aa".to_string()],
26571 body: "abcd".to_string(),
26572 description: None,
26573 name: "alphabet".to_string(),
26574 }),
26575 ],
26576 );
26577 });
26578 })
26579 });
26580
26581 let get_completions = |cx: &mut EditorLspTestContext| {
26582 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
26583 Some(CodeContextMenu::Completions(context_menu)) => {
26584 let entries = context_menu.entries.borrow();
26585 entries
26586 .iter()
26587 .map(|entry| entry.string.clone())
26588 .collect_vec()
26589 }
26590 _ => vec![],
26591 })
26592 };
26593
26594 // snippets:
26595 // @foo
26596 // foo bar
26597 //
26598 // when typing:
26599 //
26600 // when typing:
26601 // - if I type a symbol "open the completions with snippets only"
26602 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
26603 //
26604 // stuff we need:
26605 // - filtering logic change?
26606 // - remember how far back the completion started.
26607
26608 let test_cases: &[(&str, &[&str])] = &[
26609 (
26610 "un",
26611 &[
26612 "unsafe",
26613 "unlimit word count",
26614 "unlimited unknown",
26615 "unlimited word count",
26616 "unsnip",
26617 ],
26618 ),
26619 (
26620 "u ",
26621 &[
26622 "unlimit word count",
26623 "unlimited unknown",
26624 "unlimited word count",
26625 ],
26626 ),
26627 ("u a", &["ab aa", "unsafe"]), // unsAfe
26628 (
26629 "u u",
26630 &[
26631 "unsafe",
26632 "unlimit word count",
26633 "unlimited unknown", // ranked highest among snippets
26634 "unlimited word count",
26635 "unsnip",
26636 ],
26637 ),
26638 ("uw c", &["unlimit word count", "unlimited word count"]),
26639 (
26640 "u w",
26641 &[
26642 "unlimit word count",
26643 "unlimited word count",
26644 "unlimited unknown",
26645 ],
26646 ),
26647 ("u w ", &["unlimit word count", "unlimited word count"]),
26648 (
26649 "u ",
26650 &[
26651 "unlimit word count",
26652 "unlimited unknown",
26653 "unlimited word count",
26654 ],
26655 ),
26656 ("wor", &[]),
26657 ("uf", &["unsafe"]),
26658 ("af", &["unsafe"]),
26659 ("afu", &[]),
26660 (
26661 "ue",
26662 &["unsafe", "unlimited unknown", "unlimited word count"],
26663 ),
26664 ("@", &["@few"]),
26665 ("@few", &["@few"]),
26666 ("@ ", &[]),
26667 ("a@", &["@few"]),
26668 ("a@f", &["@few", "unsafe"]),
26669 ("a@fw", &["@few"]),
26670 ("a", &["ab aa", "unsafe"]),
26671 ("aa", &["ab aa"]),
26672 ("aaa", &["ab aa"]),
26673 ("ab", &["ab aa"]),
26674 ("ab ", &["ab aa"]),
26675 ("ab a", &["ab aa", "unsafe"]),
26676 ("ab ab", &["ab aa"]),
26677 ("ab ab aa", &["ab aa"]),
26678 ];
26679
26680 for &(input_to_simulate, expected_completions) in test_cases {
26681 cx.set_state("fn a() { ˇ }\n");
26682 for c in input_to_simulate.split("") {
26683 cx.simulate_input(c);
26684 cx.run_until_parked();
26685 }
26686 let expected_completions = expected_completions
26687 .iter()
26688 .map(|s| s.to_string())
26689 .collect_vec();
26690 assert_eq!(
26691 get_completions(&mut cx),
26692 expected_completions,
26693 "< actual / expected >, input = {input_to_simulate:?}",
26694 );
26695 }
26696}
26697
26698/// Handle completion request passing a marked string specifying where the completion
26699/// should be triggered from using '|' character, what range should be replaced, and what completions
26700/// should be returned using '<' and '>' to delimit the range.
26701///
26702/// Also see `handle_completion_request_with_insert_and_replace`.
26703#[track_caller]
26704pub fn handle_completion_request(
26705 marked_string: &str,
26706 completions: Vec<&'static str>,
26707 is_incomplete: bool,
26708 counter: Arc<AtomicUsize>,
26709 cx: &mut EditorLspTestContext,
26710) -> impl Future<Output = ()> {
26711 let complete_from_marker: TextRangeMarker = '|'.into();
26712 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26713 let (_, mut marked_ranges) = marked_text_ranges_by(
26714 marked_string,
26715 vec![complete_from_marker.clone(), replace_range_marker.clone()],
26716 );
26717
26718 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26719 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26720 ));
26721 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26722 let replace_range =
26723 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26724
26725 let mut request =
26726 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26727 let completions = completions.clone();
26728 counter.fetch_add(1, atomic::Ordering::Release);
26729 async move {
26730 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26731 assert_eq!(
26732 params.text_document_position.position,
26733 complete_from_position
26734 );
26735 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
26736 is_incomplete,
26737 item_defaults: None,
26738 items: completions
26739 .iter()
26740 .map(|completion_text| lsp::CompletionItem {
26741 label: completion_text.to_string(),
26742 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26743 range: replace_range,
26744 new_text: completion_text.to_string(),
26745 })),
26746 ..Default::default()
26747 })
26748 .collect(),
26749 })))
26750 }
26751 });
26752
26753 async move {
26754 request.next().await;
26755 }
26756}
26757
26758/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
26759/// given instead, which also contains an `insert` range.
26760///
26761/// This function uses markers to define ranges:
26762/// - `|` marks the cursor position
26763/// - `<>` marks the replace range
26764/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
26765pub fn handle_completion_request_with_insert_and_replace(
26766 cx: &mut EditorLspTestContext,
26767 marked_string: &str,
26768 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
26769 counter: Arc<AtomicUsize>,
26770) -> impl Future<Output = ()> {
26771 let complete_from_marker: TextRangeMarker = '|'.into();
26772 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26773 let insert_range_marker: TextRangeMarker = ('{', '}').into();
26774
26775 let (_, mut marked_ranges) = marked_text_ranges_by(
26776 marked_string,
26777 vec![
26778 complete_from_marker.clone(),
26779 replace_range_marker.clone(),
26780 insert_range_marker.clone(),
26781 ],
26782 );
26783
26784 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26785 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26786 ));
26787 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26788 let replace_range =
26789 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26790
26791 let insert_range = match marked_ranges.remove(&insert_range_marker) {
26792 Some(ranges) if !ranges.is_empty() => {
26793 let range1 = ranges[0].clone();
26794 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
26795 }
26796 _ => lsp::Range {
26797 start: replace_range.start,
26798 end: complete_from_position,
26799 },
26800 };
26801
26802 let mut request =
26803 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26804 let completions = completions.clone();
26805 counter.fetch_add(1, atomic::Ordering::Release);
26806 async move {
26807 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26808 assert_eq!(
26809 params.text_document_position.position, complete_from_position,
26810 "marker `|` position doesn't match",
26811 );
26812 Ok(Some(lsp::CompletionResponse::Array(
26813 completions
26814 .iter()
26815 .map(|(label, new_text)| lsp::CompletionItem {
26816 label: label.to_string(),
26817 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
26818 lsp::InsertReplaceEdit {
26819 insert: insert_range,
26820 replace: replace_range,
26821 new_text: new_text.to_string(),
26822 },
26823 )),
26824 ..Default::default()
26825 })
26826 .collect(),
26827 )))
26828 }
26829 });
26830
26831 async move {
26832 request.next().await;
26833 }
26834}
26835
26836fn handle_resolve_completion_request(
26837 cx: &mut EditorLspTestContext,
26838 edits: Option<Vec<(&'static str, &'static str)>>,
26839) -> impl Future<Output = ()> {
26840 let edits = edits.map(|edits| {
26841 edits
26842 .iter()
26843 .map(|(marked_string, new_text)| {
26844 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
26845 let replace_range = cx.to_lsp_range(
26846 MultiBufferOffset(marked_ranges[0].start)
26847 ..MultiBufferOffset(marked_ranges[0].end),
26848 );
26849 lsp::TextEdit::new(replace_range, new_text.to_string())
26850 })
26851 .collect::<Vec<_>>()
26852 });
26853
26854 let mut request =
26855 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
26856 let edits = edits.clone();
26857 async move {
26858 Ok(lsp::CompletionItem {
26859 additional_text_edits: edits,
26860 ..Default::default()
26861 })
26862 }
26863 });
26864
26865 async move {
26866 request.next().await;
26867 }
26868}
26869
26870pub(crate) fn update_test_language_settings(
26871 cx: &mut TestAppContext,
26872 f: impl Fn(&mut AllLanguageSettingsContent),
26873) {
26874 cx.update(|cx| {
26875 SettingsStore::update_global(cx, |store, cx| {
26876 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
26877 });
26878 });
26879}
26880
26881pub(crate) fn update_test_project_settings(
26882 cx: &mut TestAppContext,
26883 f: impl Fn(&mut ProjectSettingsContent),
26884) {
26885 cx.update(|cx| {
26886 SettingsStore::update_global(cx, |store, cx| {
26887 store.update_user_settings(cx, |settings| f(&mut settings.project));
26888 });
26889 });
26890}
26891
26892pub(crate) fn update_test_editor_settings(
26893 cx: &mut TestAppContext,
26894 f: impl Fn(&mut EditorSettingsContent),
26895) {
26896 cx.update(|cx| {
26897 SettingsStore::update_global(cx, |store, cx| {
26898 store.update_user_settings(cx, |settings| f(&mut settings.editor));
26899 })
26900 })
26901}
26902
26903pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
26904 cx.update(|cx| {
26905 assets::Assets.load_test_fonts(cx);
26906 let store = SettingsStore::test(cx);
26907 cx.set_global(store);
26908 theme::init(theme::LoadThemes::JustBase, cx);
26909 release_channel::init(semver::Version::new(0, 0, 0), cx);
26910 crate::init(cx);
26911 });
26912 zlog::init_test();
26913 update_test_language_settings(cx, f);
26914}
26915
26916#[track_caller]
26917fn assert_hunk_revert(
26918 not_reverted_text_with_selections: &str,
26919 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
26920 expected_reverted_text_with_selections: &str,
26921 base_text: &str,
26922 cx: &mut EditorLspTestContext,
26923) {
26924 cx.set_state(not_reverted_text_with_selections);
26925 cx.set_head_text(base_text);
26926 cx.executor().run_until_parked();
26927
26928 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
26929 let snapshot = editor.snapshot(window, cx);
26930 let reverted_hunk_statuses = snapshot
26931 .buffer_snapshot()
26932 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
26933 .map(|hunk| hunk.status().kind)
26934 .collect::<Vec<_>>();
26935
26936 editor.git_restore(&Default::default(), window, cx);
26937 reverted_hunk_statuses
26938 });
26939 cx.executor().run_until_parked();
26940 cx.assert_editor_state(expected_reverted_text_with_selections);
26941 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
26942}
26943
26944#[gpui::test(iterations = 10)]
26945async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
26946 init_test(cx, |_| {});
26947
26948 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
26949 let counter = diagnostic_requests.clone();
26950
26951 let fs = FakeFs::new(cx.executor());
26952 fs.insert_tree(
26953 path!("/a"),
26954 json!({
26955 "first.rs": "fn main() { let a = 5; }",
26956 "second.rs": "// Test file",
26957 }),
26958 )
26959 .await;
26960
26961 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26962 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26963 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26964
26965 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26966 language_registry.add(rust_lang());
26967 let mut fake_servers = language_registry.register_fake_lsp(
26968 "Rust",
26969 FakeLspAdapter {
26970 capabilities: lsp::ServerCapabilities {
26971 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
26972 lsp::DiagnosticOptions {
26973 identifier: None,
26974 inter_file_dependencies: true,
26975 workspace_diagnostics: true,
26976 work_done_progress_options: Default::default(),
26977 },
26978 )),
26979 ..Default::default()
26980 },
26981 ..Default::default()
26982 },
26983 );
26984
26985 let editor = workspace
26986 .update(cx, |workspace, window, cx| {
26987 workspace.open_abs_path(
26988 PathBuf::from(path!("/a/first.rs")),
26989 OpenOptions::default(),
26990 window,
26991 cx,
26992 )
26993 })
26994 .unwrap()
26995 .await
26996 .unwrap()
26997 .downcast::<Editor>()
26998 .unwrap();
26999 let fake_server = fake_servers.next().await.unwrap();
27000 let server_id = fake_server.server.server_id();
27001 let mut first_request = fake_server
27002 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
27003 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
27004 let result_id = Some(new_result_id.to_string());
27005 assert_eq!(
27006 params.text_document.uri,
27007 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27008 );
27009 async move {
27010 Ok(lsp::DocumentDiagnosticReportResult::Report(
27011 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
27012 related_documents: None,
27013 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
27014 items: Vec::new(),
27015 result_id,
27016 },
27017 }),
27018 ))
27019 }
27020 });
27021
27022 let ensure_result_id = |expected: Option<SharedString>, cx: &mut TestAppContext| {
27023 project.update(cx, |project, cx| {
27024 let buffer_id = editor
27025 .read(cx)
27026 .buffer()
27027 .read(cx)
27028 .as_singleton()
27029 .expect("created a singleton buffer")
27030 .read(cx)
27031 .remote_id();
27032 let buffer_result_id = project
27033 .lsp_store()
27034 .read(cx)
27035 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
27036 assert_eq!(expected, buffer_result_id);
27037 });
27038 };
27039
27040 ensure_result_id(None, cx);
27041 cx.executor().advance_clock(Duration::from_millis(60));
27042 cx.executor().run_until_parked();
27043 assert_eq!(
27044 diagnostic_requests.load(atomic::Ordering::Acquire),
27045 1,
27046 "Opening file should trigger diagnostic request"
27047 );
27048 first_request
27049 .next()
27050 .await
27051 .expect("should have sent the first diagnostics pull request");
27052 ensure_result_id(Some(SharedString::new("1")), cx);
27053
27054 // Editing should trigger diagnostics
27055 editor.update_in(cx, |editor, window, cx| {
27056 editor.handle_input("2", window, cx)
27057 });
27058 cx.executor().advance_clock(Duration::from_millis(60));
27059 cx.executor().run_until_parked();
27060 assert_eq!(
27061 diagnostic_requests.load(atomic::Ordering::Acquire),
27062 2,
27063 "Editing should trigger diagnostic request"
27064 );
27065 ensure_result_id(Some(SharedString::new("2")), cx);
27066
27067 // Moving cursor should not trigger diagnostic request
27068 editor.update_in(cx, |editor, window, cx| {
27069 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27070 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
27071 });
27072 });
27073 cx.executor().advance_clock(Duration::from_millis(60));
27074 cx.executor().run_until_parked();
27075 assert_eq!(
27076 diagnostic_requests.load(atomic::Ordering::Acquire),
27077 2,
27078 "Cursor movement should not trigger diagnostic request"
27079 );
27080 ensure_result_id(Some(SharedString::new("2")), cx);
27081 // Multiple rapid edits should be debounced
27082 for _ in 0..5 {
27083 editor.update_in(cx, |editor, window, cx| {
27084 editor.handle_input("x", window, cx)
27085 });
27086 }
27087 cx.executor().advance_clock(Duration::from_millis(60));
27088 cx.executor().run_until_parked();
27089
27090 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
27091 assert!(
27092 final_requests <= 4,
27093 "Multiple rapid edits should be debounced (got {final_requests} requests)",
27094 );
27095 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
27096}
27097
27098#[gpui::test]
27099async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
27100 // Regression test for issue #11671
27101 // Previously, adding a cursor after moving multiple cursors would reset
27102 // the cursor count instead of adding to the existing cursors.
27103 init_test(cx, |_| {});
27104 let mut cx = EditorTestContext::new(cx).await;
27105
27106 // Create a simple buffer with cursor at start
27107 cx.set_state(indoc! {"
27108 ˇaaaa
27109 bbbb
27110 cccc
27111 dddd
27112 eeee
27113 ffff
27114 gggg
27115 hhhh"});
27116
27117 // Add 2 cursors below (so we have 3 total)
27118 cx.update_editor(|editor, window, cx| {
27119 editor.add_selection_below(&Default::default(), window, cx);
27120 editor.add_selection_below(&Default::default(), window, cx);
27121 });
27122
27123 // Verify we have 3 cursors
27124 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
27125 assert_eq!(
27126 initial_count, 3,
27127 "Should have 3 cursors after adding 2 below"
27128 );
27129
27130 // Move down one line
27131 cx.update_editor(|editor, window, cx| {
27132 editor.move_down(&MoveDown, window, cx);
27133 });
27134
27135 // Add another cursor below
27136 cx.update_editor(|editor, window, cx| {
27137 editor.add_selection_below(&Default::default(), window, cx);
27138 });
27139
27140 // Should now have 4 cursors (3 original + 1 new)
27141 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
27142 assert_eq!(
27143 final_count, 4,
27144 "Should have 4 cursors after moving and adding another"
27145 );
27146}
27147
27148#[gpui::test]
27149async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
27150 init_test(cx, |_| {});
27151
27152 let mut cx = EditorTestContext::new(cx).await;
27153
27154 cx.set_state(indoc!(
27155 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
27156 Second line here"#
27157 ));
27158
27159 cx.update_editor(|editor, window, cx| {
27160 // Enable soft wrapping with a narrow width to force soft wrapping and
27161 // confirm that more than 2 rows are being displayed.
27162 editor.set_wrap_width(Some(100.0.into()), cx);
27163 assert!(editor.display_text(cx).lines().count() > 2);
27164
27165 editor.add_selection_below(
27166 &AddSelectionBelow {
27167 skip_soft_wrap: true,
27168 },
27169 window,
27170 cx,
27171 );
27172
27173 assert_eq!(
27174 display_ranges(editor, cx),
27175 &[
27176 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27177 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
27178 ]
27179 );
27180
27181 editor.add_selection_above(
27182 &AddSelectionAbove {
27183 skip_soft_wrap: true,
27184 },
27185 window,
27186 cx,
27187 );
27188
27189 assert_eq!(
27190 display_ranges(editor, cx),
27191 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27192 );
27193
27194 editor.add_selection_below(
27195 &AddSelectionBelow {
27196 skip_soft_wrap: false,
27197 },
27198 window,
27199 cx,
27200 );
27201
27202 assert_eq!(
27203 display_ranges(editor, cx),
27204 &[
27205 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27206 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
27207 ]
27208 );
27209
27210 editor.add_selection_above(
27211 &AddSelectionAbove {
27212 skip_soft_wrap: false,
27213 },
27214 window,
27215 cx,
27216 );
27217
27218 assert_eq!(
27219 display_ranges(editor, cx),
27220 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27221 );
27222 });
27223}
27224
27225#[gpui::test]
27226async fn test_insert_snippet(cx: &mut TestAppContext) {
27227 init_test(cx, |_| {});
27228 let mut cx = EditorTestContext::new(cx).await;
27229
27230 cx.update_editor(|editor, _, cx| {
27231 editor.project().unwrap().update(cx, |project, cx| {
27232 project.snippets().update(cx, |snippets, _cx| {
27233 let snippet = project::snippet_provider::Snippet {
27234 prefix: vec![], // no prefix needed!
27235 body: "an Unspecified".to_string(),
27236 description: Some("shhhh it's a secret".to_string()),
27237 name: "super secret snippet".to_string(),
27238 };
27239 snippets.add_snippet_for_test(
27240 None,
27241 PathBuf::from("test_snippets.json"),
27242 vec![Arc::new(snippet)],
27243 );
27244
27245 let snippet = project::snippet_provider::Snippet {
27246 prefix: vec![], // no prefix needed!
27247 body: " Location".to_string(),
27248 description: Some("the word 'location'".to_string()),
27249 name: "location word".to_string(),
27250 };
27251 snippets.add_snippet_for_test(
27252 Some("Markdown".to_string()),
27253 PathBuf::from("test_snippets.json"),
27254 vec![Arc::new(snippet)],
27255 );
27256 });
27257 })
27258 });
27259
27260 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
27261
27262 cx.update_editor(|editor, window, cx| {
27263 editor.insert_snippet_at_selections(
27264 &InsertSnippet {
27265 language: None,
27266 name: Some("super secret snippet".to_string()),
27267 snippet: None,
27268 },
27269 window,
27270 cx,
27271 );
27272
27273 // Language is specified in the action,
27274 // so the buffer language does not need to match
27275 editor.insert_snippet_at_selections(
27276 &InsertSnippet {
27277 language: Some("Markdown".to_string()),
27278 name: Some("location word".to_string()),
27279 snippet: None,
27280 },
27281 window,
27282 cx,
27283 );
27284
27285 editor.insert_snippet_at_selections(
27286 &InsertSnippet {
27287 language: None,
27288 name: None,
27289 snippet: Some("$0 after".to_string()),
27290 },
27291 window,
27292 cx,
27293 );
27294 });
27295
27296 cx.assert_editor_state(
27297 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
27298 );
27299}
27300
27301#[gpui::test(iterations = 10)]
27302async fn test_document_colors(cx: &mut TestAppContext) {
27303 let expected_color = Rgba {
27304 r: 0.33,
27305 g: 0.33,
27306 b: 0.33,
27307 a: 0.33,
27308 };
27309
27310 init_test(cx, |_| {});
27311
27312 let fs = FakeFs::new(cx.executor());
27313 fs.insert_tree(
27314 path!("/a"),
27315 json!({
27316 "first.rs": "fn main() { let a = 5; }",
27317 }),
27318 )
27319 .await;
27320
27321 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27322 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27323 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27324
27325 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27326 language_registry.add(rust_lang());
27327 let mut fake_servers = language_registry.register_fake_lsp(
27328 "Rust",
27329 FakeLspAdapter {
27330 capabilities: lsp::ServerCapabilities {
27331 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
27332 ..lsp::ServerCapabilities::default()
27333 },
27334 name: "rust-analyzer",
27335 ..FakeLspAdapter::default()
27336 },
27337 );
27338 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
27339 "Rust",
27340 FakeLspAdapter {
27341 capabilities: lsp::ServerCapabilities {
27342 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
27343 ..lsp::ServerCapabilities::default()
27344 },
27345 name: "not-rust-analyzer",
27346 ..FakeLspAdapter::default()
27347 },
27348 );
27349
27350 let editor = workspace
27351 .update(cx, |workspace, window, cx| {
27352 workspace.open_abs_path(
27353 PathBuf::from(path!("/a/first.rs")),
27354 OpenOptions::default(),
27355 window,
27356 cx,
27357 )
27358 })
27359 .unwrap()
27360 .await
27361 .unwrap()
27362 .downcast::<Editor>()
27363 .unwrap();
27364 let fake_language_server = fake_servers.next().await.unwrap();
27365 let fake_language_server_without_capabilities =
27366 fake_servers_without_capabilities.next().await.unwrap();
27367 let requests_made = Arc::new(AtomicUsize::new(0));
27368 let closure_requests_made = Arc::clone(&requests_made);
27369 let mut color_request_handle = fake_language_server
27370 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27371 let requests_made = Arc::clone(&closure_requests_made);
27372 async move {
27373 assert_eq!(
27374 params.text_document.uri,
27375 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27376 );
27377 requests_made.fetch_add(1, atomic::Ordering::Release);
27378 Ok(vec![
27379 lsp::ColorInformation {
27380 range: lsp::Range {
27381 start: lsp::Position {
27382 line: 0,
27383 character: 0,
27384 },
27385 end: lsp::Position {
27386 line: 0,
27387 character: 1,
27388 },
27389 },
27390 color: lsp::Color {
27391 red: 0.33,
27392 green: 0.33,
27393 blue: 0.33,
27394 alpha: 0.33,
27395 },
27396 },
27397 lsp::ColorInformation {
27398 range: lsp::Range {
27399 start: lsp::Position {
27400 line: 0,
27401 character: 0,
27402 },
27403 end: lsp::Position {
27404 line: 0,
27405 character: 1,
27406 },
27407 },
27408 color: lsp::Color {
27409 red: 0.33,
27410 green: 0.33,
27411 blue: 0.33,
27412 alpha: 0.33,
27413 },
27414 },
27415 ])
27416 }
27417 });
27418
27419 let _handle = fake_language_server_without_capabilities
27420 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
27421 panic!("Should not be called");
27422 });
27423 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27424 color_request_handle.next().await.unwrap();
27425 cx.run_until_parked();
27426 assert_eq!(
27427 1,
27428 requests_made.load(atomic::Ordering::Acquire),
27429 "Should query for colors once per editor open"
27430 );
27431 editor.update_in(cx, |editor, _, cx| {
27432 assert_eq!(
27433 vec![expected_color],
27434 extract_color_inlays(editor, cx),
27435 "Should have an initial inlay"
27436 );
27437 });
27438
27439 // opening another file in a split should not influence the LSP query counter
27440 workspace
27441 .update(cx, |workspace, window, cx| {
27442 assert_eq!(
27443 workspace.panes().len(),
27444 1,
27445 "Should have one pane with one editor"
27446 );
27447 workspace.move_item_to_pane_in_direction(
27448 &MoveItemToPaneInDirection {
27449 direction: SplitDirection::Right,
27450 focus: false,
27451 clone: true,
27452 },
27453 window,
27454 cx,
27455 );
27456 })
27457 .unwrap();
27458 cx.run_until_parked();
27459 workspace
27460 .update(cx, |workspace, _, cx| {
27461 let panes = workspace.panes();
27462 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
27463 for pane in panes {
27464 let editor = pane
27465 .read(cx)
27466 .active_item()
27467 .and_then(|item| item.downcast::<Editor>())
27468 .expect("Should have opened an editor in each split");
27469 let editor_file = editor
27470 .read(cx)
27471 .buffer()
27472 .read(cx)
27473 .as_singleton()
27474 .expect("test deals with singleton buffers")
27475 .read(cx)
27476 .file()
27477 .expect("test buffese should have a file")
27478 .path();
27479 assert_eq!(
27480 editor_file.as_ref(),
27481 rel_path("first.rs"),
27482 "Both editors should be opened for the same file"
27483 )
27484 }
27485 })
27486 .unwrap();
27487
27488 cx.executor().advance_clock(Duration::from_millis(500));
27489 let save = editor.update_in(cx, |editor, window, cx| {
27490 editor.move_to_end(&MoveToEnd, window, cx);
27491 editor.handle_input("dirty", window, cx);
27492 editor.save(
27493 SaveOptions {
27494 format: true,
27495 autosave: true,
27496 },
27497 project.clone(),
27498 window,
27499 cx,
27500 )
27501 });
27502 save.await.unwrap();
27503
27504 color_request_handle.next().await.unwrap();
27505 cx.run_until_parked();
27506 assert_eq!(
27507 2,
27508 requests_made.load(atomic::Ordering::Acquire),
27509 "Should query for colors once per save (deduplicated) and once per formatting after save"
27510 );
27511
27512 drop(editor);
27513 let close = workspace
27514 .update(cx, |workspace, window, cx| {
27515 workspace.active_pane().update(cx, |pane, cx| {
27516 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27517 })
27518 })
27519 .unwrap();
27520 close.await.unwrap();
27521 let close = workspace
27522 .update(cx, |workspace, window, cx| {
27523 workspace.active_pane().update(cx, |pane, cx| {
27524 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27525 })
27526 })
27527 .unwrap();
27528 close.await.unwrap();
27529 assert_eq!(
27530 2,
27531 requests_made.load(atomic::Ordering::Acquire),
27532 "After saving and closing all editors, no extra requests should be made"
27533 );
27534 workspace
27535 .update(cx, |workspace, _, cx| {
27536 assert!(
27537 workspace.active_item(cx).is_none(),
27538 "Should close all editors"
27539 )
27540 })
27541 .unwrap();
27542
27543 workspace
27544 .update(cx, |workspace, window, cx| {
27545 workspace.active_pane().update(cx, |pane, cx| {
27546 pane.navigate_backward(&workspace::GoBack, window, cx);
27547 })
27548 })
27549 .unwrap();
27550 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27551 cx.run_until_parked();
27552 let editor = workspace
27553 .update(cx, |workspace, _, cx| {
27554 workspace
27555 .active_item(cx)
27556 .expect("Should have reopened the editor again after navigating back")
27557 .downcast::<Editor>()
27558 .expect("Should be an editor")
27559 })
27560 .unwrap();
27561
27562 assert_eq!(
27563 2,
27564 requests_made.load(atomic::Ordering::Acquire),
27565 "Cache should be reused on buffer close and reopen"
27566 );
27567 editor.update(cx, |editor, cx| {
27568 assert_eq!(
27569 vec![expected_color],
27570 extract_color_inlays(editor, cx),
27571 "Should have an initial inlay"
27572 );
27573 });
27574
27575 drop(color_request_handle);
27576 let closure_requests_made = Arc::clone(&requests_made);
27577 let mut empty_color_request_handle = fake_language_server
27578 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27579 let requests_made = Arc::clone(&closure_requests_made);
27580 async move {
27581 assert_eq!(
27582 params.text_document.uri,
27583 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27584 );
27585 requests_made.fetch_add(1, atomic::Ordering::Release);
27586 Ok(Vec::new())
27587 }
27588 });
27589 let save = editor.update_in(cx, |editor, window, cx| {
27590 editor.move_to_end(&MoveToEnd, window, cx);
27591 editor.handle_input("dirty_again", window, cx);
27592 editor.save(
27593 SaveOptions {
27594 format: false,
27595 autosave: true,
27596 },
27597 project.clone(),
27598 window,
27599 cx,
27600 )
27601 });
27602 save.await.unwrap();
27603
27604 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27605 empty_color_request_handle.next().await.unwrap();
27606 cx.run_until_parked();
27607 assert_eq!(
27608 3,
27609 requests_made.load(atomic::Ordering::Acquire),
27610 "Should query for colors once per save only, as formatting was not requested"
27611 );
27612 editor.update(cx, |editor, cx| {
27613 assert_eq!(
27614 Vec::<Rgba>::new(),
27615 extract_color_inlays(editor, cx),
27616 "Should clear all colors when the server returns an empty response"
27617 );
27618 });
27619}
27620
27621#[gpui::test]
27622async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
27623 init_test(cx, |_| {});
27624 let (editor, cx) = cx.add_window_view(Editor::single_line);
27625 editor.update_in(cx, |editor, window, cx| {
27626 editor.set_text("oops\n\nwow\n", window, cx)
27627 });
27628 cx.run_until_parked();
27629 editor.update(cx, |editor, cx| {
27630 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
27631 });
27632 editor.update(cx, |editor, cx| {
27633 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
27634 });
27635 cx.run_until_parked();
27636 editor.update(cx, |editor, cx| {
27637 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
27638 });
27639}
27640
27641#[gpui::test]
27642async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
27643 init_test(cx, |_| {});
27644
27645 cx.update(|cx| {
27646 register_project_item::<Editor>(cx);
27647 });
27648
27649 let fs = FakeFs::new(cx.executor());
27650 fs.insert_tree("/root1", json!({})).await;
27651 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
27652 .await;
27653
27654 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
27655 let (workspace, cx) =
27656 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
27657
27658 let worktree_id = project.update(cx, |project, cx| {
27659 project.worktrees(cx).next().unwrap().read(cx).id()
27660 });
27661
27662 let handle = workspace
27663 .update_in(cx, |workspace, window, cx| {
27664 let project_path = (worktree_id, rel_path("one.pdf"));
27665 workspace.open_path(project_path, None, true, window, cx)
27666 })
27667 .await
27668 .unwrap();
27669 // The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
27670 // Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
27671 // With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
27672 assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
27673}
27674
27675#[gpui::test]
27676async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
27677 init_test(cx, |_| {});
27678
27679 let language = Arc::new(Language::new(
27680 LanguageConfig::default(),
27681 Some(tree_sitter_rust::LANGUAGE.into()),
27682 ));
27683
27684 // Test hierarchical sibling navigation
27685 let text = r#"
27686 fn outer() {
27687 if condition {
27688 let a = 1;
27689 }
27690 let b = 2;
27691 }
27692
27693 fn another() {
27694 let c = 3;
27695 }
27696 "#;
27697
27698 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
27699 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
27700 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
27701
27702 // Wait for parsing to complete
27703 editor
27704 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
27705 .await;
27706
27707 editor.update_in(cx, |editor, window, cx| {
27708 // Start by selecting "let a = 1;" inside the if block
27709 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27710 s.select_display_ranges([
27711 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
27712 ]);
27713 });
27714
27715 let initial_selection = editor
27716 .selections
27717 .display_ranges(&editor.display_snapshot(cx));
27718 assert_eq!(initial_selection.len(), 1, "Should have one selection");
27719
27720 // Test select next sibling - should move up levels to find the next sibling
27721 // Since "let a = 1;" has no siblings in the if block, it should move up
27722 // to find "let b = 2;" which is a sibling of the if block
27723 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27724 let next_selection = editor
27725 .selections
27726 .display_ranges(&editor.display_snapshot(cx));
27727
27728 // Should have a selection and it should be different from the initial
27729 assert_eq!(
27730 next_selection.len(),
27731 1,
27732 "Should have one selection after next"
27733 );
27734 assert_ne!(
27735 next_selection[0], initial_selection[0],
27736 "Next sibling selection should be different"
27737 );
27738
27739 // Test hierarchical navigation by going to the end of the current function
27740 // and trying to navigate to the next function
27741 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27742 s.select_display_ranges([
27743 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
27744 ]);
27745 });
27746
27747 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27748 let function_next_selection = editor
27749 .selections
27750 .display_ranges(&editor.display_snapshot(cx));
27751
27752 // Should move to the next function
27753 assert_eq!(
27754 function_next_selection.len(),
27755 1,
27756 "Should have one selection after function next"
27757 );
27758
27759 // Test select previous sibling navigation
27760 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
27761 let prev_selection = editor
27762 .selections
27763 .display_ranges(&editor.display_snapshot(cx));
27764
27765 // Should have a selection and it should be different
27766 assert_eq!(
27767 prev_selection.len(),
27768 1,
27769 "Should have one selection after prev"
27770 );
27771 assert_ne!(
27772 prev_selection[0], function_next_selection[0],
27773 "Previous sibling selection should be different from next"
27774 );
27775 });
27776}
27777
27778#[gpui::test]
27779async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
27780 init_test(cx, |_| {});
27781
27782 let mut cx = EditorTestContext::new(cx).await;
27783 cx.set_state(
27784 "let ˇvariable = 42;
27785let another = variable + 1;
27786let result = variable * 2;",
27787 );
27788
27789 // Set up document highlights manually (simulating LSP response)
27790 cx.update_editor(|editor, _window, cx| {
27791 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
27792
27793 // Create highlights for "variable" occurrences
27794 let highlight_ranges = [
27795 Point::new(0, 4)..Point::new(0, 12), // First "variable"
27796 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
27797 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
27798 ];
27799
27800 let anchor_ranges: Vec<_> = highlight_ranges
27801 .iter()
27802 .map(|range| range.clone().to_anchors(&buffer_snapshot))
27803 .collect();
27804
27805 editor.highlight_background::<DocumentHighlightRead>(
27806 &anchor_ranges,
27807 |_, theme| theme.colors().editor_document_highlight_read_background,
27808 cx,
27809 );
27810 });
27811
27812 // Go to next highlight - should move to second "variable"
27813 cx.update_editor(|editor, window, cx| {
27814 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27815 });
27816 cx.assert_editor_state(
27817 "let variable = 42;
27818let another = ˇvariable + 1;
27819let result = variable * 2;",
27820 );
27821
27822 // Go to next highlight - should move to third "variable"
27823 cx.update_editor(|editor, window, cx| {
27824 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27825 });
27826 cx.assert_editor_state(
27827 "let variable = 42;
27828let another = variable + 1;
27829let result = ˇvariable * 2;",
27830 );
27831
27832 // Go to next highlight - should stay at third "variable" (no wrap-around)
27833 cx.update_editor(|editor, window, cx| {
27834 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27835 });
27836 cx.assert_editor_state(
27837 "let variable = 42;
27838let another = variable + 1;
27839let result = ˇvariable * 2;",
27840 );
27841
27842 // Now test going backwards from third position
27843 cx.update_editor(|editor, window, cx| {
27844 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27845 });
27846 cx.assert_editor_state(
27847 "let variable = 42;
27848let another = ˇvariable + 1;
27849let result = variable * 2;",
27850 );
27851
27852 // Go to previous highlight - should move to first "variable"
27853 cx.update_editor(|editor, window, cx| {
27854 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27855 });
27856 cx.assert_editor_state(
27857 "let ˇvariable = 42;
27858let another = variable + 1;
27859let result = variable * 2;",
27860 );
27861
27862 // Go to previous highlight - should stay on first "variable"
27863 cx.update_editor(|editor, window, cx| {
27864 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27865 });
27866 cx.assert_editor_state(
27867 "let ˇvariable = 42;
27868let another = variable + 1;
27869let result = variable * 2;",
27870 );
27871}
27872
27873#[gpui::test]
27874async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
27875 cx: &mut gpui::TestAppContext,
27876) {
27877 init_test(cx, |_| {});
27878
27879 let url = "https://zed.dev";
27880
27881 let markdown_language = Arc::new(Language::new(
27882 LanguageConfig {
27883 name: "Markdown".into(),
27884 ..LanguageConfig::default()
27885 },
27886 None,
27887 ));
27888
27889 let mut cx = EditorTestContext::new(cx).await;
27890 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27891 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
27892
27893 cx.update_editor(|editor, window, cx| {
27894 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27895 editor.paste(&Paste, window, cx);
27896 });
27897
27898 cx.assert_editor_state(&format!(
27899 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
27900 ));
27901}
27902
27903#[gpui::test]
27904async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
27905 init_test(cx, |_| {});
27906
27907 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
27908 let mut cx = EditorTestContext::new(cx).await;
27909
27910 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27911
27912 // Case 1: Test if adding a character with multi cursors preserves nested list indents
27913 cx.set_state(&indoc! {"
27914 - [ ] Item 1
27915 - [ ] Item 1.a
27916 - [ˇ] Item 2
27917 - [ˇ] Item 2.a
27918 - [ˇ] Item 2.b
27919 "
27920 });
27921 cx.update_editor(|editor, window, cx| {
27922 editor.handle_input("x", window, cx);
27923 });
27924 cx.run_until_parked();
27925 cx.assert_editor_state(indoc! {"
27926 - [ ] Item 1
27927 - [ ] Item 1.a
27928 - [xˇ] Item 2
27929 - [xˇ] Item 2.a
27930 - [xˇ] Item 2.b
27931 "
27932 });
27933
27934 // Case 2: Test adding new line after nested list preserves indent of previous line
27935 cx.set_state(&indoc! {"
27936 - [ ] Item 1
27937 - [ ] Item 1.a
27938 - [x] Item 2
27939 - [x] Item 2.a
27940 - [x] Item 2.bˇ"
27941 });
27942 cx.update_editor(|editor, window, cx| {
27943 editor.newline(&Newline, window, cx);
27944 });
27945 cx.assert_editor_state(indoc! {"
27946 - [ ] Item 1
27947 - [ ] Item 1.a
27948 - [x] Item 2
27949 - [x] Item 2.a
27950 - [x] Item 2.b
27951 ˇ"
27952 });
27953
27954 // Case 3: Test adding a new nested list item preserves indent
27955 cx.set_state(&indoc! {"
27956 - [ ] Item 1
27957 - [ ] Item 1.a
27958 - [x] Item 2
27959 - [x] Item 2.a
27960 - [x] Item 2.b
27961 ˇ"
27962 });
27963 cx.update_editor(|editor, window, cx| {
27964 editor.handle_input("-", window, cx);
27965 });
27966 cx.run_until_parked();
27967 cx.assert_editor_state(indoc! {"
27968 - [ ] Item 1
27969 - [ ] Item 1.a
27970 - [x] Item 2
27971 - [x] Item 2.a
27972 - [x] Item 2.b
27973 -ˇ"
27974 });
27975 cx.update_editor(|editor, window, cx| {
27976 editor.handle_input(" [x] Item 2.c", window, cx);
27977 });
27978 cx.run_until_parked();
27979 cx.assert_editor_state(indoc! {"
27980 - [ ] Item 1
27981 - [ ] Item 1.a
27982 - [x] Item 2
27983 - [x] Item 2.a
27984 - [x] Item 2.b
27985 - [x] Item 2.cˇ"
27986 });
27987
27988 // Case 4: Test adding new line after nested ordered list preserves indent of previous line
27989 cx.set_state(indoc! {"
27990 1. Item 1
27991 1. Item 1.a
27992 2. Item 2
27993 1. Item 2.a
27994 2. Item 2.bˇ"
27995 });
27996 cx.update_editor(|editor, window, cx| {
27997 editor.newline(&Newline, window, cx);
27998 });
27999 cx.assert_editor_state(indoc! {"
28000 1. Item 1
28001 1. Item 1.a
28002 2. Item 2
28003 1. Item 2.a
28004 2. Item 2.b
28005 ˇ"
28006 });
28007
28008 // Case 5: Adding new ordered list item preserves indent
28009 cx.set_state(indoc! {"
28010 1. Item 1
28011 1. Item 1.a
28012 2. Item 2
28013 1. Item 2.a
28014 2. Item 2.b
28015 ˇ"
28016 });
28017 cx.update_editor(|editor, window, cx| {
28018 editor.handle_input("3", window, cx);
28019 });
28020 cx.run_until_parked();
28021 cx.assert_editor_state(indoc! {"
28022 1. Item 1
28023 1. Item 1.a
28024 2. Item 2
28025 1. Item 2.a
28026 2. Item 2.b
28027 3ˇ"
28028 });
28029 cx.update_editor(|editor, window, cx| {
28030 editor.handle_input(".", window, cx);
28031 });
28032 cx.run_until_parked();
28033 cx.assert_editor_state(indoc! {"
28034 1. Item 1
28035 1. Item 1.a
28036 2. Item 2
28037 1. Item 2.a
28038 2. Item 2.b
28039 3.ˇ"
28040 });
28041 cx.update_editor(|editor, window, cx| {
28042 editor.handle_input(" Item 2.c", window, cx);
28043 });
28044 cx.run_until_parked();
28045 cx.assert_editor_state(indoc! {"
28046 1. Item 1
28047 1. Item 1.a
28048 2. Item 2
28049 1. Item 2.a
28050 2. Item 2.b
28051 3. Item 2.cˇ"
28052 });
28053
28054 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
28055 cx.set_state(indoc! {"
28056 - Item 1
28057 - Item 1.a
28058 - Item 1.a
28059 ˇ"});
28060 cx.update_editor(|editor, window, cx| {
28061 editor.handle_input("-", window, cx);
28062 });
28063 cx.run_until_parked();
28064 cx.assert_editor_state(indoc! {"
28065 - Item 1
28066 - Item 1.a
28067 - Item 1.a
28068 -ˇ"});
28069
28070 // Case 7: Test blockquote newline preserves something
28071 cx.set_state(indoc! {"
28072 > Item 1ˇ"
28073 });
28074 cx.update_editor(|editor, window, cx| {
28075 editor.newline(&Newline, window, cx);
28076 });
28077 cx.assert_editor_state(indoc! {"
28078 > Item 1
28079 ˇ"
28080 });
28081}
28082
28083#[gpui::test]
28084async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
28085 cx: &mut gpui::TestAppContext,
28086) {
28087 init_test(cx, |_| {});
28088
28089 let url = "https://zed.dev";
28090
28091 let markdown_language = Arc::new(Language::new(
28092 LanguageConfig {
28093 name: "Markdown".into(),
28094 ..LanguageConfig::default()
28095 },
28096 None,
28097 ));
28098
28099 let mut cx = EditorTestContext::new(cx).await;
28100 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28101 cx.set_state(&format!(
28102 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
28103 ));
28104
28105 cx.update_editor(|editor, window, cx| {
28106 editor.copy(&Copy, window, cx);
28107 });
28108
28109 cx.set_state(&format!(
28110 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
28111 ));
28112
28113 cx.update_editor(|editor, window, cx| {
28114 editor.paste(&Paste, window, cx);
28115 });
28116
28117 cx.assert_editor_state(&format!(
28118 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
28119 ));
28120}
28121
28122#[gpui::test]
28123async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
28124 cx: &mut gpui::TestAppContext,
28125) {
28126 init_test(cx, |_| {});
28127
28128 let url = "https://zed.dev";
28129
28130 let markdown_language = Arc::new(Language::new(
28131 LanguageConfig {
28132 name: "Markdown".into(),
28133 ..LanguageConfig::default()
28134 },
28135 None,
28136 ));
28137
28138 let mut cx = EditorTestContext::new(cx).await;
28139 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28140 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
28141
28142 cx.update_editor(|editor, window, cx| {
28143 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28144 editor.paste(&Paste, window, cx);
28145 });
28146
28147 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
28148}
28149
28150#[gpui::test]
28151async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
28152 cx: &mut gpui::TestAppContext,
28153) {
28154 init_test(cx, |_| {});
28155
28156 let text = "Awesome";
28157
28158 let markdown_language = Arc::new(Language::new(
28159 LanguageConfig {
28160 name: "Markdown".into(),
28161 ..LanguageConfig::default()
28162 },
28163 None,
28164 ));
28165
28166 let mut cx = EditorTestContext::new(cx).await;
28167 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28168 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
28169
28170 cx.update_editor(|editor, window, cx| {
28171 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
28172 editor.paste(&Paste, window, cx);
28173 });
28174
28175 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
28176}
28177
28178#[gpui::test]
28179async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
28180 cx: &mut gpui::TestAppContext,
28181) {
28182 init_test(cx, |_| {});
28183
28184 let url = "https://zed.dev";
28185
28186 let markdown_language = Arc::new(Language::new(
28187 LanguageConfig {
28188 name: "Rust".into(),
28189 ..LanguageConfig::default()
28190 },
28191 None,
28192 ));
28193
28194 let mut cx = EditorTestContext::new(cx).await;
28195 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28196 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
28197
28198 cx.update_editor(|editor, window, cx| {
28199 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28200 editor.paste(&Paste, window, cx);
28201 });
28202
28203 cx.assert_editor_state(&format!(
28204 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
28205 ));
28206}
28207
28208#[gpui::test]
28209async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
28210 cx: &mut TestAppContext,
28211) {
28212 init_test(cx, |_| {});
28213
28214 let url = "https://zed.dev";
28215
28216 let markdown_language = Arc::new(Language::new(
28217 LanguageConfig {
28218 name: "Markdown".into(),
28219 ..LanguageConfig::default()
28220 },
28221 None,
28222 ));
28223
28224 let (editor, cx) = cx.add_window_view(|window, cx| {
28225 let multi_buffer = MultiBuffer::build_multi(
28226 [
28227 ("this will embed -> link", vec![Point::row_range(0..1)]),
28228 ("this will replace -> link", vec![Point::row_range(0..1)]),
28229 ],
28230 cx,
28231 );
28232 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
28233 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28234 s.select_ranges(vec![
28235 Point::new(0, 19)..Point::new(0, 23),
28236 Point::new(1, 21)..Point::new(1, 25),
28237 ])
28238 });
28239 let first_buffer_id = multi_buffer
28240 .read(cx)
28241 .excerpt_buffer_ids()
28242 .into_iter()
28243 .next()
28244 .unwrap();
28245 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
28246 first_buffer.update(cx, |buffer, cx| {
28247 buffer.set_language(Some(markdown_language.clone()), cx);
28248 });
28249
28250 editor
28251 });
28252 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28253
28254 cx.update_editor(|editor, window, cx| {
28255 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28256 editor.paste(&Paste, window, cx);
28257 });
28258
28259 cx.assert_editor_state(&format!(
28260 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
28261 ));
28262}
28263
28264#[gpui::test]
28265async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
28266 init_test(cx, |_| {});
28267
28268 let fs = FakeFs::new(cx.executor());
28269 fs.insert_tree(
28270 path!("/project"),
28271 json!({
28272 "first.rs": "# First Document\nSome content here.",
28273 "second.rs": "Plain text content for second file.",
28274 }),
28275 )
28276 .await;
28277
28278 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
28279 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
28280 let cx = &mut VisualTestContext::from_window(*workspace, cx);
28281
28282 let language = rust_lang();
28283 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28284 language_registry.add(language.clone());
28285 let mut fake_servers = language_registry.register_fake_lsp(
28286 "Rust",
28287 FakeLspAdapter {
28288 ..FakeLspAdapter::default()
28289 },
28290 );
28291
28292 let buffer1 = project
28293 .update(cx, |project, cx| {
28294 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
28295 })
28296 .await
28297 .unwrap();
28298 let buffer2 = project
28299 .update(cx, |project, cx| {
28300 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
28301 })
28302 .await
28303 .unwrap();
28304
28305 let multi_buffer = cx.new(|cx| {
28306 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
28307 multi_buffer.set_excerpts_for_path(
28308 PathKey::for_buffer(&buffer1, cx),
28309 buffer1.clone(),
28310 [Point::zero()..buffer1.read(cx).max_point()],
28311 3,
28312 cx,
28313 );
28314 multi_buffer.set_excerpts_for_path(
28315 PathKey::for_buffer(&buffer2, cx),
28316 buffer2.clone(),
28317 [Point::zero()..buffer1.read(cx).max_point()],
28318 3,
28319 cx,
28320 );
28321 multi_buffer
28322 });
28323
28324 let (editor, cx) = cx.add_window_view(|window, cx| {
28325 Editor::new(
28326 EditorMode::full(),
28327 multi_buffer,
28328 Some(project.clone()),
28329 window,
28330 cx,
28331 )
28332 });
28333
28334 let fake_language_server = fake_servers.next().await.unwrap();
28335
28336 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
28337
28338 let save = editor.update_in(cx, |editor, window, cx| {
28339 assert!(editor.is_dirty(cx));
28340
28341 editor.save(
28342 SaveOptions {
28343 format: true,
28344 autosave: true,
28345 },
28346 project,
28347 window,
28348 cx,
28349 )
28350 });
28351 let (start_edit_tx, start_edit_rx) = oneshot::channel();
28352 let (done_edit_tx, done_edit_rx) = oneshot::channel();
28353 let mut done_edit_rx = Some(done_edit_rx);
28354 let mut start_edit_tx = Some(start_edit_tx);
28355
28356 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
28357 start_edit_tx.take().unwrap().send(()).unwrap();
28358 let done_edit_rx = done_edit_rx.take().unwrap();
28359 async move {
28360 done_edit_rx.await.unwrap();
28361 Ok(None)
28362 }
28363 });
28364
28365 start_edit_rx.await.unwrap();
28366 buffer2
28367 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
28368 .unwrap();
28369
28370 done_edit_tx.send(()).unwrap();
28371
28372 save.await.unwrap();
28373 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
28374}
28375
28376#[track_caller]
28377fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
28378 editor
28379 .all_inlays(cx)
28380 .into_iter()
28381 .filter_map(|inlay| inlay.get_color())
28382 .map(Rgba::from)
28383 .collect()
28384}
28385
28386#[gpui::test]
28387fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
28388 init_test(cx, |_| {});
28389
28390 let editor = cx.add_window(|window, cx| {
28391 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
28392 build_editor(buffer, window, cx)
28393 });
28394
28395 editor
28396 .update(cx, |editor, window, cx| {
28397 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28398 s.select_display_ranges([
28399 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
28400 ])
28401 });
28402
28403 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
28404
28405 assert_eq!(
28406 editor.display_text(cx),
28407 "line1\nline2\nline2",
28408 "Duplicating last line upward should create duplicate above, not on same line"
28409 );
28410
28411 assert_eq!(
28412 editor
28413 .selections
28414 .display_ranges(&editor.display_snapshot(cx)),
28415 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
28416 "Selection should move to the duplicated line"
28417 );
28418 })
28419 .unwrap();
28420}
28421
28422#[gpui::test]
28423async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
28424 init_test(cx, |_| {});
28425
28426 let mut cx = EditorTestContext::new(cx).await;
28427
28428 cx.set_state("line1\nline2ˇ");
28429
28430 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28431
28432 let clipboard_text = cx
28433 .read_from_clipboard()
28434 .and_then(|item| item.text().as_deref().map(str::to_string));
28435
28436 assert_eq!(
28437 clipboard_text,
28438 Some("line2\n".to_string()),
28439 "Copying a line without trailing newline should include a newline"
28440 );
28441
28442 cx.set_state("line1\nˇ");
28443
28444 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28445
28446 cx.assert_editor_state("line1\nline2\nˇ");
28447}
28448
28449#[gpui::test]
28450async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28451 init_test(cx, |_| {});
28452
28453 let mut cx = EditorTestContext::new(cx).await;
28454
28455 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28456
28457 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28458
28459 let clipboard_text = cx
28460 .read_from_clipboard()
28461 .and_then(|item| item.text().as_deref().map(str::to_string));
28462
28463 assert_eq!(
28464 clipboard_text,
28465 Some("line1\nline2\nline3\n".to_string()),
28466 "Copying multiple lines should include a single newline between lines"
28467 );
28468
28469 cx.set_state("lineA\nˇ");
28470
28471 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28472
28473 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28474}
28475
28476#[gpui::test]
28477async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28478 init_test(cx, |_| {});
28479
28480 let mut cx = EditorTestContext::new(cx).await;
28481
28482 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28483
28484 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
28485
28486 let clipboard_text = cx
28487 .read_from_clipboard()
28488 .and_then(|item| item.text().as_deref().map(str::to_string));
28489
28490 assert_eq!(
28491 clipboard_text,
28492 Some("line1\nline2\nline3\n".to_string()),
28493 "Copying multiple lines should include a single newline between lines"
28494 );
28495
28496 cx.set_state("lineA\nˇ");
28497
28498 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28499
28500 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28501}
28502
28503#[gpui::test]
28504async fn test_end_of_editor_context(cx: &mut TestAppContext) {
28505 init_test(cx, |_| {});
28506
28507 let mut cx = EditorTestContext::new(cx).await;
28508
28509 cx.set_state("line1\nline2ˇ");
28510 cx.update_editor(|e, window, cx| {
28511 e.set_mode(EditorMode::SingleLine);
28512 assert!(e.key_context(window, cx).contains("end_of_input"));
28513 });
28514 cx.set_state("ˇline1\nline2");
28515 cx.update_editor(|e, window, cx| {
28516 assert!(!e.key_context(window, cx).contains("end_of_input"));
28517 });
28518 cx.set_state("line1ˇ\nline2");
28519 cx.update_editor(|e, window, cx| {
28520 assert!(!e.key_context(window, cx).contains("end_of_input"));
28521 });
28522}
28523
28524#[gpui::test]
28525async fn test_sticky_scroll(cx: &mut TestAppContext) {
28526 init_test(cx, |_| {});
28527 let mut cx = EditorTestContext::new(cx).await;
28528
28529 let buffer = indoc! {"
28530 ˇfn foo() {
28531 let abc = 123;
28532 }
28533 struct Bar;
28534 impl Bar {
28535 fn new() -> Self {
28536 Self
28537 }
28538 }
28539 fn baz() {
28540 }
28541 "};
28542 cx.set_state(&buffer);
28543
28544 cx.update_editor(|e, _, cx| {
28545 e.buffer()
28546 .read(cx)
28547 .as_singleton()
28548 .unwrap()
28549 .update(cx, |buffer, cx| {
28550 buffer.set_language(Some(rust_lang()), cx);
28551 })
28552 });
28553
28554 let mut sticky_headers = |offset: ScrollOffset| {
28555 cx.update_editor(|e, window, cx| {
28556 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
28557 let style = e.style(cx).clone();
28558 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), &style, cx)
28559 .into_iter()
28560 .map(
28561 |StickyHeader {
28562 start_point,
28563 offset,
28564 ..
28565 }| { (start_point, offset) },
28566 )
28567 .collect::<Vec<_>>()
28568 })
28569 };
28570
28571 let fn_foo = Point { row: 0, column: 0 };
28572 let impl_bar = Point { row: 4, column: 0 };
28573 let fn_new = Point { row: 5, column: 4 };
28574
28575 assert_eq!(sticky_headers(0.0), vec![]);
28576 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
28577 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
28578 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
28579 assert_eq!(sticky_headers(2.0), vec![]);
28580 assert_eq!(sticky_headers(2.5), vec![]);
28581 assert_eq!(sticky_headers(3.0), vec![]);
28582 assert_eq!(sticky_headers(3.5), vec![]);
28583 assert_eq!(sticky_headers(4.0), vec![]);
28584 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28585 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28586 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
28587 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
28588 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
28589 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
28590 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
28591 assert_eq!(sticky_headers(8.0), vec![]);
28592 assert_eq!(sticky_headers(8.5), vec![]);
28593 assert_eq!(sticky_headers(9.0), vec![]);
28594 assert_eq!(sticky_headers(9.5), vec![]);
28595 assert_eq!(sticky_headers(10.0), vec![]);
28596}
28597
28598#[gpui::test]
28599async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
28600 init_test(cx, |_| {});
28601 cx.update(|cx| {
28602 SettingsStore::update_global(cx, |store, cx| {
28603 store.update_user_settings(cx, |settings| {
28604 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
28605 enabled: Some(true),
28606 })
28607 });
28608 });
28609 });
28610 let mut cx = EditorTestContext::new(cx).await;
28611
28612 let line_height = cx.update_editor(|editor, window, cx| {
28613 editor
28614 .style(cx)
28615 .text
28616 .line_height_in_pixels(window.rem_size())
28617 });
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 fn_foo = || empty_range(0, 0);
28645 let impl_bar = || empty_range(4, 0);
28646 let fn_new = || empty_range(5, 4);
28647
28648 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
28649 cx.update_editor(|e, window, cx| {
28650 e.scroll(
28651 gpui::Point {
28652 x: 0.,
28653 y: scroll_offset,
28654 },
28655 None,
28656 window,
28657 cx,
28658 );
28659 });
28660 cx.simulate_click(
28661 gpui::Point {
28662 x: px(0.),
28663 y: click_offset as f32 * line_height,
28664 },
28665 Modifiers::none(),
28666 );
28667 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
28668 };
28669
28670 assert_eq!(
28671 scroll_and_click(
28672 4.5, // impl Bar is halfway off the screen
28673 0.0 // click top of screen
28674 ),
28675 // scrolled to impl Bar
28676 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28677 );
28678
28679 assert_eq!(
28680 scroll_and_click(
28681 4.5, // impl Bar is halfway off the screen
28682 0.25 // click middle of impl Bar
28683 ),
28684 // scrolled to impl Bar
28685 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28686 );
28687
28688 assert_eq!(
28689 scroll_and_click(
28690 4.5, // impl Bar is halfway off the screen
28691 1.5 // click below impl Bar (e.g. fn new())
28692 ),
28693 // scrolled to fn new() - this is below the impl Bar header which has persisted
28694 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28695 );
28696
28697 assert_eq!(
28698 scroll_and_click(
28699 5.5, // fn new is halfway underneath impl Bar
28700 0.75 // click on the overlap of impl Bar and fn new()
28701 ),
28702 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28703 );
28704
28705 assert_eq!(
28706 scroll_and_click(
28707 5.5, // fn new is halfway underneath impl Bar
28708 1.25 // click on the visible part of fn new()
28709 ),
28710 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28711 );
28712
28713 assert_eq!(
28714 scroll_and_click(
28715 1.5, // fn foo is halfway off the screen
28716 0.0 // click top of screen
28717 ),
28718 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
28719 );
28720
28721 assert_eq!(
28722 scroll_and_click(
28723 1.5, // fn foo is halfway off the screen
28724 0.75 // click visible part of let abc...
28725 )
28726 .0,
28727 // no change in scroll
28728 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
28729 (gpui::Point { x: 0., y: 1.5 })
28730 );
28731}
28732
28733#[gpui::test]
28734async fn test_next_prev_reference(cx: &mut TestAppContext) {
28735 const CYCLE_POSITIONS: &[&'static str] = &[
28736 indoc! {"
28737 fn foo() {
28738 let ˇabc = 123;
28739 let x = abc + 1;
28740 let y = abc + 2;
28741 let z = abc + 2;
28742 }
28743 "},
28744 indoc! {"
28745 fn foo() {
28746 let abc = 123;
28747 let x = ˇabc + 1;
28748 let y = abc + 2;
28749 let z = abc + 2;
28750 }
28751 "},
28752 indoc! {"
28753 fn foo() {
28754 let abc = 123;
28755 let x = abc + 1;
28756 let y = ˇabc + 2;
28757 let z = abc + 2;
28758 }
28759 "},
28760 indoc! {"
28761 fn foo() {
28762 let abc = 123;
28763 let x = abc + 1;
28764 let y = abc + 2;
28765 let z = ˇabc + 2;
28766 }
28767 "},
28768 ];
28769
28770 init_test(cx, |_| {});
28771
28772 let mut cx = EditorLspTestContext::new_rust(
28773 lsp::ServerCapabilities {
28774 references_provider: Some(lsp::OneOf::Left(true)),
28775 ..Default::default()
28776 },
28777 cx,
28778 )
28779 .await;
28780
28781 // importantly, the cursor is in the middle
28782 cx.set_state(indoc! {"
28783 fn foo() {
28784 let aˇbc = 123;
28785 let x = abc + 1;
28786 let y = abc + 2;
28787 let z = abc + 2;
28788 }
28789 "});
28790
28791 let reference_ranges = [
28792 lsp::Position::new(1, 8),
28793 lsp::Position::new(2, 12),
28794 lsp::Position::new(3, 12),
28795 lsp::Position::new(4, 12),
28796 ]
28797 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
28798
28799 cx.lsp
28800 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
28801 Ok(Some(
28802 reference_ranges
28803 .map(|range| lsp::Location {
28804 uri: params.text_document_position.text_document.uri.clone(),
28805 range,
28806 })
28807 .to_vec(),
28808 ))
28809 });
28810
28811 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
28812 cx.update_editor(|editor, window, cx| {
28813 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
28814 })
28815 .unwrap()
28816 .await
28817 .unwrap()
28818 };
28819
28820 _move(Direction::Next, 1, &mut cx).await;
28821 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28822
28823 _move(Direction::Next, 1, &mut cx).await;
28824 cx.assert_editor_state(CYCLE_POSITIONS[2]);
28825
28826 _move(Direction::Next, 1, &mut cx).await;
28827 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28828
28829 // loops back to the start
28830 _move(Direction::Next, 1, &mut cx).await;
28831 cx.assert_editor_state(CYCLE_POSITIONS[0]);
28832
28833 // loops back to the end
28834 _move(Direction::Prev, 1, &mut cx).await;
28835 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28836
28837 _move(Direction::Prev, 1, &mut cx).await;
28838 cx.assert_editor_state(CYCLE_POSITIONS[2]);
28839
28840 _move(Direction::Prev, 1, &mut cx).await;
28841 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28842
28843 _move(Direction::Prev, 1, &mut cx).await;
28844 cx.assert_editor_state(CYCLE_POSITIONS[0]);
28845
28846 _move(Direction::Next, 3, &mut cx).await;
28847 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28848
28849 _move(Direction::Prev, 2, &mut cx).await;
28850 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28851}
28852
28853#[gpui::test]
28854async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
28855 init_test(cx, |_| {});
28856
28857 let (editor, cx) = cx.add_window_view(|window, cx| {
28858 let multi_buffer = MultiBuffer::build_multi(
28859 [
28860 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28861 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28862 ],
28863 cx,
28864 );
28865 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28866 });
28867
28868 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28869 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
28870
28871 cx.assert_excerpts_with_selections(indoc! {"
28872 [EXCERPT]
28873 ˇ1
28874 2
28875 3
28876 [EXCERPT]
28877 1
28878 2
28879 3
28880 "});
28881
28882 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
28883 cx.update_editor(|editor, window, cx| {
28884 editor.change_selections(None.into(), window, cx, |s| {
28885 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28886 });
28887 });
28888 cx.assert_excerpts_with_selections(indoc! {"
28889 [EXCERPT]
28890 1
28891 2ˇ
28892 3
28893 [EXCERPT]
28894 1
28895 2
28896 3
28897 "});
28898
28899 cx.update_editor(|editor, window, cx| {
28900 editor
28901 .select_all_matches(&SelectAllMatches, window, cx)
28902 .unwrap();
28903 });
28904 cx.assert_excerpts_with_selections(indoc! {"
28905 [EXCERPT]
28906 1
28907 2ˇ
28908 3
28909 [EXCERPT]
28910 1
28911 2ˇ
28912 3
28913 "});
28914
28915 cx.update_editor(|editor, window, cx| {
28916 editor.handle_input("X", window, cx);
28917 });
28918 cx.assert_excerpts_with_selections(indoc! {"
28919 [EXCERPT]
28920 1
28921 Xˇ
28922 3
28923 [EXCERPT]
28924 1
28925 Xˇ
28926 3
28927 "});
28928
28929 // Scenario 2: Select "2", then fold second buffer before insertion
28930 cx.update_multibuffer(|mb, cx| {
28931 for buffer_id in buffer_ids.iter() {
28932 let buffer = mb.buffer(*buffer_id).unwrap();
28933 buffer.update(cx, |buffer, cx| {
28934 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28935 });
28936 }
28937 });
28938
28939 // Select "2" and select all matches
28940 cx.update_editor(|editor, window, cx| {
28941 editor.change_selections(None.into(), window, cx, |s| {
28942 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28943 });
28944 editor
28945 .select_all_matches(&SelectAllMatches, window, cx)
28946 .unwrap();
28947 });
28948
28949 // Fold second buffer - should remove selections from folded buffer
28950 cx.update_editor(|editor, _, cx| {
28951 editor.fold_buffer(buffer_ids[1], cx);
28952 });
28953 cx.assert_excerpts_with_selections(indoc! {"
28954 [EXCERPT]
28955 1
28956 2ˇ
28957 3
28958 [EXCERPT]
28959 [FOLDED]
28960 "});
28961
28962 // Insert text - should only affect first buffer
28963 cx.update_editor(|editor, window, cx| {
28964 editor.handle_input("Y", window, cx);
28965 });
28966 cx.update_editor(|editor, _, cx| {
28967 editor.unfold_buffer(buffer_ids[1], cx);
28968 });
28969 cx.assert_excerpts_with_selections(indoc! {"
28970 [EXCERPT]
28971 1
28972 Yˇ
28973 3
28974 [EXCERPT]
28975 1
28976 2
28977 3
28978 "});
28979
28980 // Scenario 3: Select "2", then fold first buffer before insertion
28981 cx.update_multibuffer(|mb, cx| {
28982 for buffer_id in buffer_ids.iter() {
28983 let buffer = mb.buffer(*buffer_id).unwrap();
28984 buffer.update(cx, |buffer, cx| {
28985 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28986 });
28987 }
28988 });
28989
28990 // Select "2" and select all matches
28991 cx.update_editor(|editor, window, cx| {
28992 editor.change_selections(None.into(), window, cx, |s| {
28993 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28994 });
28995 editor
28996 .select_all_matches(&SelectAllMatches, window, cx)
28997 .unwrap();
28998 });
28999
29000 // Fold first buffer - should remove selections from folded buffer
29001 cx.update_editor(|editor, _, cx| {
29002 editor.fold_buffer(buffer_ids[0], cx);
29003 });
29004 cx.assert_excerpts_with_selections(indoc! {"
29005 [EXCERPT]
29006 [FOLDED]
29007 [EXCERPT]
29008 1
29009 2ˇ
29010 3
29011 "});
29012
29013 // Insert text - should only affect second buffer
29014 cx.update_editor(|editor, window, cx| {
29015 editor.handle_input("Z", window, cx);
29016 });
29017 cx.update_editor(|editor, _, cx| {
29018 editor.unfold_buffer(buffer_ids[0], cx);
29019 });
29020 cx.assert_excerpts_with_selections(indoc! {"
29021 [EXCERPT]
29022 1
29023 2
29024 3
29025 [EXCERPT]
29026 1
29027 Zˇ
29028 3
29029 "});
29030
29031 // Test correct folded header is selected upon fold
29032 cx.update_editor(|editor, _, cx| {
29033 editor.fold_buffer(buffer_ids[0], cx);
29034 editor.fold_buffer(buffer_ids[1], cx);
29035 });
29036 cx.assert_excerpts_with_selections(indoc! {"
29037 [EXCERPT]
29038 [FOLDED]
29039 [EXCERPT]
29040 ˇ[FOLDED]
29041 "});
29042
29043 // Test selection inside folded buffer unfolds it on type
29044 cx.update_editor(|editor, window, cx| {
29045 editor.handle_input("W", window, cx);
29046 });
29047 cx.update_editor(|editor, _, cx| {
29048 editor.unfold_buffer(buffer_ids[0], cx);
29049 });
29050 cx.assert_excerpts_with_selections(indoc! {"
29051 [EXCERPT]
29052 1
29053 2
29054 3
29055 [EXCERPT]
29056 Wˇ1
29057 Z
29058 3
29059 "});
29060}
29061
29062#[gpui::test]
29063async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) {
29064 init_test(cx, |_| {});
29065 let mut leader_cx = EditorTestContext::new(cx).await;
29066
29067 let diff_base = indoc!(
29068 r#"
29069 one
29070 two
29071 three
29072 four
29073 five
29074 six
29075 "#
29076 );
29077
29078 let initial_state = indoc!(
29079 r#"
29080 ˇone
29081 two
29082 THREE
29083 four
29084 five
29085 six
29086 "#
29087 );
29088
29089 leader_cx.set_state(initial_state);
29090
29091 leader_cx.set_head_text(&diff_base);
29092 leader_cx.run_until_parked();
29093
29094 let follower = leader_cx.update_multibuffer(|leader, cx| {
29095 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
29096 leader.set_all_diff_hunks_expanded(cx);
29097 leader.get_or_create_follower(cx)
29098 });
29099 follower.update(cx, |follower, cx| {
29100 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
29101 follower.set_all_diff_hunks_expanded(cx);
29102 });
29103
29104 let follower_editor =
29105 leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx));
29106 // leader_cx.window.focus(&follower_editor.focus_handle(cx));
29107
29108 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await;
29109 cx.run_until_parked();
29110
29111 leader_cx.assert_editor_state(initial_state);
29112 follower_cx.assert_editor_state(indoc! {
29113 r#"
29114 ˇone
29115 two
29116 three
29117 four
29118 five
29119 six
29120 "#
29121 });
29122
29123 follower_cx.editor(|editor, _window, cx| {
29124 assert!(editor.read_only(cx));
29125 });
29126
29127 leader_cx.update_editor(|editor, _window, cx| {
29128 editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx);
29129 });
29130 cx.run_until_parked();
29131
29132 leader_cx.assert_editor_state(indoc! {
29133 r#"
29134 ˇone
29135 two
29136 THREE
29137 four
29138 FIVE
29139 six
29140 "#
29141 });
29142
29143 follower_cx.assert_editor_state(indoc! {
29144 r#"
29145 ˇone
29146 two
29147 three
29148 four
29149 five
29150 six
29151 "#
29152 });
29153
29154 leader_cx.update_editor(|editor, _window, cx| {
29155 editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx);
29156 });
29157 cx.run_until_parked();
29158
29159 leader_cx.assert_editor_state(indoc! {
29160 r#"
29161 ˇone
29162 two
29163 THREE
29164 four
29165 FIVE
29166 six
29167 SEVEN"#
29168 });
29169
29170 follower_cx.assert_editor_state(indoc! {
29171 r#"
29172 ˇone
29173 two
29174 three
29175 four
29176 five
29177 six
29178 "#
29179 });
29180
29181 leader_cx.update_editor(|editor, window, cx| {
29182 editor.move_down(&MoveDown, window, cx);
29183 editor.refresh_selected_text_highlights(true, window, cx);
29184 });
29185 leader_cx.run_until_parked();
29186}
29187
29188#[gpui::test]
29189async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) {
29190 init_test(cx, |_| {});
29191 let base_text = "base\n";
29192 let buffer_text = "buffer\n";
29193
29194 let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx));
29195 let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx));
29196
29197 let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx));
29198 let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx));
29199 let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx));
29200 let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx));
29201
29202 let leader = cx.new(|cx| {
29203 let mut leader = MultiBuffer::new(Capability::ReadWrite);
29204 leader.set_all_diff_hunks_expanded(cx);
29205 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
29206 leader
29207 });
29208 let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx));
29209 follower.update(cx, |follower, _| {
29210 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
29211 });
29212
29213 leader.update(cx, |leader, cx| {
29214 leader.insert_excerpts_after(
29215 ExcerptId::min(),
29216 extra_buffer_2.clone(),
29217 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29218 cx,
29219 );
29220 leader.add_diff(extra_diff_2.clone(), cx);
29221
29222 leader.insert_excerpts_after(
29223 ExcerptId::min(),
29224 extra_buffer_1.clone(),
29225 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29226 cx,
29227 );
29228 leader.add_diff(extra_diff_1.clone(), cx);
29229
29230 leader.insert_excerpts_after(
29231 ExcerptId::min(),
29232 buffer1.clone(),
29233 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29234 cx,
29235 );
29236 leader.add_diff(diff1.clone(), cx);
29237 });
29238
29239 cx.run_until_parked();
29240 let mut cx = cx.add_empty_window();
29241
29242 let leader_editor = cx
29243 .new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx));
29244 let follower_editor = cx.new_window_entity(|window, cx| {
29245 Editor::for_multibuffer(follower.clone(), None, window, cx)
29246 });
29247
29248 let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await;
29249 leader_cx.assert_editor_state(indoc! {"
29250 ˇbuffer
29251
29252 dummy text 1
29253
29254 dummy text 2
29255 "});
29256 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await;
29257 follower_cx.assert_editor_state(indoc! {"
29258 ˇbase
29259
29260
29261 "});
29262}
29263
29264#[gpui::test]
29265async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
29266 init_test(cx, |_| {});
29267
29268 let (editor, cx) = cx.add_window_view(|window, cx| {
29269 let multi_buffer = MultiBuffer::build_multi(
29270 [
29271 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29272 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
29273 ],
29274 cx,
29275 );
29276 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29277 });
29278
29279 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29280
29281 cx.assert_excerpts_with_selections(indoc! {"
29282 [EXCERPT]
29283 ˇ1
29284 2
29285 3
29286 [EXCERPT]
29287 1
29288 2
29289 3
29290 4
29291 5
29292 6
29293 7
29294 8
29295 9
29296 "});
29297
29298 cx.update_editor(|editor, window, cx| {
29299 editor.change_selections(None.into(), window, cx, |s| {
29300 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
29301 });
29302 });
29303
29304 cx.assert_excerpts_with_selections(indoc! {"
29305 [EXCERPT]
29306 1
29307 2
29308 3
29309 [EXCERPT]
29310 1
29311 2
29312 3
29313 4
29314 5
29315 6
29316 ˇ7
29317 8
29318 9
29319 "});
29320
29321 cx.update_editor(|editor, _window, cx| {
29322 editor.set_vertical_scroll_margin(0, cx);
29323 });
29324
29325 cx.update_editor(|editor, window, cx| {
29326 assert_eq!(editor.vertical_scroll_margin(), 0);
29327 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29328 assert_eq!(
29329 editor.snapshot(window, cx).scroll_position(),
29330 gpui::Point::new(0., 12.0)
29331 );
29332 });
29333
29334 cx.update_editor(|editor, _window, cx| {
29335 editor.set_vertical_scroll_margin(3, cx);
29336 });
29337
29338 cx.update_editor(|editor, window, cx| {
29339 assert_eq!(editor.vertical_scroll_margin(), 3);
29340 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29341 assert_eq!(
29342 editor.snapshot(window, cx).scroll_position(),
29343 gpui::Point::new(0., 9.0)
29344 );
29345 });
29346}
29347
29348#[gpui::test]
29349async fn test_find_references_single_case(cx: &mut TestAppContext) {
29350 init_test(cx, |_| {});
29351 let mut cx = EditorLspTestContext::new_rust(
29352 lsp::ServerCapabilities {
29353 references_provider: Some(lsp::OneOf::Left(true)),
29354 ..lsp::ServerCapabilities::default()
29355 },
29356 cx,
29357 )
29358 .await;
29359
29360 let before = indoc!(
29361 r#"
29362 fn main() {
29363 let aˇbc = 123;
29364 let xyz = abc;
29365 }
29366 "#
29367 );
29368 let after = indoc!(
29369 r#"
29370 fn main() {
29371 let abc = 123;
29372 let xyz = ˇabc;
29373 }
29374 "#
29375 );
29376
29377 cx.lsp
29378 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
29379 Ok(Some(vec![
29380 lsp::Location {
29381 uri: params.text_document_position.text_document.uri.clone(),
29382 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
29383 },
29384 lsp::Location {
29385 uri: params.text_document_position.text_document.uri,
29386 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
29387 },
29388 ]))
29389 });
29390
29391 cx.set_state(before);
29392
29393 let action = FindAllReferences {
29394 always_open_multibuffer: false,
29395 };
29396
29397 let navigated = cx
29398 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
29399 .expect("should have spawned a task")
29400 .await
29401 .unwrap();
29402
29403 assert_eq!(navigated, Navigated::No);
29404
29405 cx.run_until_parked();
29406
29407 cx.assert_editor_state(after);
29408}
29409
29410#[gpui::test]
29411async fn test_local_worktree_trust(cx: &mut TestAppContext) {
29412 init_test(cx, |_| {});
29413 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), None, None, cx));
29414
29415 cx.update(|cx| {
29416 SettingsStore::update_global(cx, |store, cx| {
29417 store.update_user_settings(cx, |settings| {
29418 settings.project.all_languages.defaults.inlay_hints =
29419 Some(InlayHintSettingsContent {
29420 enabled: Some(true),
29421 ..InlayHintSettingsContent::default()
29422 });
29423 });
29424 });
29425 });
29426
29427 let fs = FakeFs::new(cx.executor());
29428 fs.insert_tree(
29429 path!("/project"),
29430 json!({
29431 ".zed": {
29432 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
29433 },
29434 "main.rs": "fn main() {}"
29435 }),
29436 )
29437 .await;
29438
29439 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
29440 let server_name = "override-rust-analyzer";
29441 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
29442
29443 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
29444 language_registry.add(rust_lang());
29445
29446 let capabilities = lsp::ServerCapabilities {
29447 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
29448 ..lsp::ServerCapabilities::default()
29449 };
29450 let mut fake_language_servers = language_registry.register_fake_lsp(
29451 "Rust",
29452 FakeLspAdapter {
29453 name: server_name,
29454 capabilities,
29455 initializer: Some(Box::new({
29456 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
29457 move |fake_server| {
29458 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
29459 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
29460 move |_params, _| {
29461 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
29462 async move {
29463 Ok(Some(vec![lsp::InlayHint {
29464 position: lsp::Position::new(0, 0),
29465 label: lsp::InlayHintLabel::String("hint".to_string()),
29466 kind: None,
29467 text_edits: None,
29468 tooltip: None,
29469 padding_left: None,
29470 padding_right: None,
29471 data: None,
29472 }]))
29473 }
29474 },
29475 );
29476 }
29477 })),
29478 ..FakeLspAdapter::default()
29479 },
29480 );
29481
29482 cx.run_until_parked();
29483
29484 let worktree_id = project.read_with(cx, |project, cx| {
29485 project
29486 .worktrees(cx)
29487 .next()
29488 .map(|wt| wt.read(cx).id())
29489 .expect("should have a worktree")
29490 });
29491
29492 let trusted_worktrees =
29493 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
29494
29495 let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
29496 assert!(!can_trust, "worktree should be restricted initially");
29497
29498 let buffer_before_approval = project
29499 .update(cx, |project, cx| {
29500 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
29501 })
29502 .await
29503 .unwrap();
29504
29505 let (editor, cx) = cx.add_window_view(|window, cx| {
29506 Editor::new(
29507 EditorMode::full(),
29508 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
29509 Some(project.clone()),
29510 window,
29511 cx,
29512 )
29513 });
29514 cx.run_until_parked();
29515 let fake_language_server = fake_language_servers.next();
29516
29517 cx.read(|cx| {
29518 let file = buffer_before_approval.read(cx).file();
29519 assert_eq!(
29520 language::language_settings::language_settings(Some("Rust".into()), file, cx)
29521 .language_servers,
29522 ["...".to_string()],
29523 "local .zed/settings.json must not apply before trust approval"
29524 )
29525 });
29526
29527 editor.update_in(cx, |editor, window, cx| {
29528 editor.handle_input("1", window, cx);
29529 });
29530 cx.run_until_parked();
29531 cx.executor()
29532 .advance_clock(std::time::Duration::from_secs(1));
29533 assert_eq!(
29534 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
29535 0,
29536 "inlay hints must not be queried before trust approval"
29537 );
29538
29539 trusted_worktrees.update(cx, |store, cx| {
29540 store.trust(
29541 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
29542 None,
29543 cx,
29544 );
29545 });
29546 cx.run_until_parked();
29547
29548 cx.read(|cx| {
29549 let file = buffer_before_approval.read(cx).file();
29550 assert_eq!(
29551 language::language_settings::language_settings(Some("Rust".into()), file, cx)
29552 .language_servers,
29553 ["override-rust-analyzer".to_string()],
29554 "local .zed/settings.json should apply after trust approval"
29555 )
29556 });
29557 let _fake_language_server = fake_language_server.await.unwrap();
29558 editor.update_in(cx, |editor, window, cx| {
29559 editor.handle_input("1", window, cx);
29560 });
29561 cx.run_until_parked();
29562 cx.executor()
29563 .advance_clock(std::time::Duration::from_secs(1));
29564 assert!(
29565 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
29566 "inlay hints should be queried after trust approval"
29567 );
29568
29569 let can_trust_after =
29570 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
29571 assert!(can_trust_after, "worktree should be trusted after trust()");
29572}
29573
29574#[gpui::test]
29575fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
29576 // This test reproduces a bug where drawing an editor at a position above the viewport
29577 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
29578 // causes an infinite loop in blocks_in_range.
29579 //
29580 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
29581 // the content mask intersection produces visible_bounds with origin at the viewport top.
29582 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
29583 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
29584 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
29585 init_test(cx, |_| {});
29586
29587 let window = cx.add_window(|_, _| gpui::Empty);
29588 let mut cx = VisualTestContext::from_window(*window, cx);
29589
29590 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
29591 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
29592
29593 // Simulate a small viewport (500x500 pixels at origin 0,0)
29594 cx.simulate_resize(gpui::size(px(500.), px(500.)));
29595
29596 // Draw the editor at a very negative Y position, simulating an editor that's been
29597 // scrolled way above the visible viewport (like in a List that has scrolled past it).
29598 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
29599 // This should NOT hang - it should just render nothing.
29600 cx.draw(
29601 gpui::point(px(0.), px(-10000.)),
29602 gpui::size(px(500.), px(3000.)),
29603 |_, _| editor.clone(),
29604 );
29605
29606 // If we get here without hanging, the test passes
29607}