1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionDelegate,
6 element::StickyHeader,
7 linked_editing_ranges::LinkedEditingRanges,
8 scroll::scroll_amount::ScrollAmount,
9 test::{
10 assert_text_with_selections, build_editor,
11 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
12 editor_test_context::EditorTestContext,
13 select_ranges,
14 },
15};
16use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
17use collections::HashMap;
18use futures::{StreamExt, channel::oneshot};
19use gpui::{
20 BackgroundExecutor, DismissEvent, Rgba, TestAppContext, UpdateGlobal, VisualTestContext,
21 WindowBounds, WindowOptions, div,
22};
23use indoc::indoc;
24use language::{
25 BracketPairConfig,
26 Capability::ReadWrite,
27 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
28 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
29 language_settings::{
30 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use languages::markdown_lang;
36use languages::rust_lang;
37use lsp::CompletionParams;
38use multi_buffer::{
39 ExcerptRange, IndentGuide, MultiBuffer, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey,
40};
41use parking_lot::Mutex;
42use pretty_assertions::{assert_eq, assert_ne};
43use project::{
44 FakeFs, Project,
45 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
46 project_settings::LspSettings,
47 trusted_worktrees::{PathTrust, TrustedWorktrees},
48};
49use serde_json::{self, json};
50use settings::{
51 AllLanguageSettingsContent, DelayMs, EditorSettingsContent, IndentGuideBackgroundColoring,
52 IndentGuideColoring, InlayHintSettingsContent, ProjectSettingsContent, SearchSettingsContent,
53 SettingsStore,
54};
55use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
56use std::{
57 iter,
58 sync::atomic::{self, AtomicUsize},
59};
60use test::build_editor_with_project;
61use text::ToPoint as _;
62use unindent::Unindent;
63use util::{
64 assert_set_eq, path,
65 rel_path::rel_path,
66 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
67 uri,
68};
69use workspace::{
70 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
71 OpenOptions, ViewId,
72 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
73 register_project_item,
74};
75
76fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
77 editor
78 .selections
79 .display_ranges(&editor.display_snapshot(cx))
80}
81
82#[gpui::test]
83fn test_edit_events(cx: &mut TestAppContext) {
84 init_test(cx, |_| {});
85
86 let buffer = cx.new(|cx| {
87 let mut buffer = language::Buffer::local("123456", cx);
88 buffer.set_group_interval(Duration::from_secs(1));
89 buffer
90 });
91
92 let events = Rc::new(RefCell::new(Vec::new()));
93 let editor1 = cx.add_window({
94 let events = events.clone();
95 |window, cx| {
96 let entity = cx.entity();
97 cx.subscribe_in(
98 &entity,
99 window,
100 move |_, _, event: &EditorEvent, _, _| match event {
101 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
102 EditorEvent::BufferEdited => {
103 events.borrow_mut().push(("editor1", "buffer edited"))
104 }
105 _ => {}
106 },
107 )
108 .detach();
109 Editor::for_buffer(buffer.clone(), None, window, cx)
110 }
111 });
112
113 let editor2 = cx.add_window({
114 let events = events.clone();
115 |window, cx| {
116 cx.subscribe_in(
117 &cx.entity(),
118 window,
119 move |_, _, event: &EditorEvent, _, _| match event {
120 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
121 EditorEvent::BufferEdited => {
122 events.borrow_mut().push(("editor2", "buffer edited"))
123 }
124 _ => {}
125 },
126 )
127 .detach();
128 Editor::for_buffer(buffer.clone(), None, window, cx)
129 }
130 });
131
132 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
133
134 // Mutating editor 1 will emit an `Edited` event only for that editor.
135 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
136 assert_eq!(
137 mem::take(&mut *events.borrow_mut()),
138 [
139 ("editor1", "edited"),
140 ("editor1", "buffer edited"),
141 ("editor2", "buffer edited"),
142 ]
143 );
144
145 // Mutating editor 2 will emit an `Edited` event only for that editor.
146 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
147 assert_eq!(
148 mem::take(&mut *events.borrow_mut()),
149 [
150 ("editor2", "edited"),
151 ("editor1", "buffer edited"),
152 ("editor2", "buffer edited"),
153 ]
154 );
155
156 // Undoing on editor 1 will emit an `Edited` event only for that editor.
157 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
158 assert_eq!(
159 mem::take(&mut *events.borrow_mut()),
160 [
161 ("editor1", "edited"),
162 ("editor1", "buffer edited"),
163 ("editor2", "buffer edited"),
164 ]
165 );
166
167 // Redoing on editor 1 will emit an `Edited` event only for that editor.
168 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
169 assert_eq!(
170 mem::take(&mut *events.borrow_mut()),
171 [
172 ("editor1", "edited"),
173 ("editor1", "buffer edited"),
174 ("editor2", "buffer edited"),
175 ]
176 );
177
178 // Undoing on editor 2 will emit an `Edited` event only for that editor.
179 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
180 assert_eq!(
181 mem::take(&mut *events.borrow_mut()),
182 [
183 ("editor2", "edited"),
184 ("editor1", "buffer edited"),
185 ("editor2", "buffer edited"),
186 ]
187 );
188
189 // Redoing on editor 2 will emit an `Edited` event only for that editor.
190 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
191 assert_eq!(
192 mem::take(&mut *events.borrow_mut()),
193 [
194 ("editor2", "edited"),
195 ("editor1", "buffer edited"),
196 ("editor2", "buffer edited"),
197 ]
198 );
199
200 // No event is emitted when the mutation is a no-op.
201 _ = editor2.update(cx, |editor, window, cx| {
202 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
203 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
204 });
205
206 editor.backspace(&Backspace, window, cx);
207 });
208 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
209}
210
211#[gpui::test]
212fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
213 init_test(cx, |_| {});
214
215 let mut now = Instant::now();
216 let group_interval = Duration::from_millis(1);
217 let buffer = cx.new(|cx| {
218 let mut buf = language::Buffer::local("123456", cx);
219 buf.set_group_interval(group_interval);
220 buf
221 });
222 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
223 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
224
225 _ = editor.update(cx, |editor, window, cx| {
226 editor.start_transaction_at(now, window, cx);
227 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
228 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(4)])
229 });
230
231 editor.insert("cd", window, cx);
232 editor.end_transaction_at(now, cx);
233 assert_eq!(editor.text(cx), "12cd56");
234 assert_eq!(
235 editor.selections.ranges(&editor.display_snapshot(cx)),
236 vec![MultiBufferOffset(4)..MultiBufferOffset(4)]
237 );
238
239 editor.start_transaction_at(now, window, cx);
240 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
241 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(5)])
242 });
243 editor.insert("e", window, cx);
244 editor.end_transaction_at(now, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(
247 editor.selections.ranges(&editor.display_snapshot(cx)),
248 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
249 );
250
251 now += group_interval + Duration::from_millis(1);
252 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
253 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)])
254 });
255
256 // Simulate an edit in another editor
257 buffer.update(cx, |buffer, cx| {
258 buffer.start_transaction_at(now, cx);
259 buffer.edit(
260 [(MultiBufferOffset(0)..MultiBufferOffset(1), "a")],
261 None,
262 cx,
263 );
264 buffer.edit(
265 [(MultiBufferOffset(1)..MultiBufferOffset(1), "b")],
266 None,
267 cx,
268 );
269 buffer.end_transaction_at(now, cx);
270 });
271
272 assert_eq!(editor.text(cx), "ab2cde6");
273 assert_eq!(
274 editor.selections.ranges(&editor.display_snapshot(cx)),
275 vec![MultiBufferOffset(3)..MultiBufferOffset(3)]
276 );
277
278 // Last transaction happened past the group interval in a different editor.
279 // Undo it individually and don't restore selections.
280 editor.undo(&Undo, window, cx);
281 assert_eq!(editor.text(cx), "12cde6");
282 assert_eq!(
283 editor.selections.ranges(&editor.display_snapshot(cx)),
284 vec![MultiBufferOffset(2)..MultiBufferOffset(2)]
285 );
286
287 // First two transactions happened within the group interval in this editor.
288 // Undo them together and restore selections.
289 editor.undo(&Undo, window, cx);
290 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
291 assert_eq!(editor.text(cx), "123456");
292 assert_eq!(
293 editor.selections.ranges(&editor.display_snapshot(cx)),
294 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
295 );
296
297 // Redo the first two transactions together.
298 editor.redo(&Redo, window, cx);
299 assert_eq!(editor.text(cx), "12cde6");
300 assert_eq!(
301 editor.selections.ranges(&editor.display_snapshot(cx)),
302 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
303 );
304
305 // Redo the last transaction on its own.
306 editor.redo(&Redo, window, cx);
307 assert_eq!(editor.text(cx), "ab2cde6");
308 assert_eq!(
309 editor.selections.ranges(&editor.display_snapshot(cx)),
310 vec![MultiBufferOffset(6)..MultiBufferOffset(6)]
311 );
312
313 // Test empty transactions.
314 editor.start_transaction_at(now, window, cx);
315 editor.end_transaction_at(now, cx);
316 editor.undo(&Undo, window, cx);
317 assert_eq!(editor.text(cx), "12cde6");
318 });
319}
320
321#[gpui::test]
322fn test_ime_composition(cx: &mut TestAppContext) {
323 init_test(cx, |_| {});
324
325 let buffer = cx.new(|cx| {
326 let mut buffer = language::Buffer::local("abcde", cx);
327 // Ensure automatic grouping doesn't occur.
328 buffer.set_group_interval(Duration::ZERO);
329 buffer
330 });
331
332 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
333 cx.add_window(|window, cx| {
334 let mut editor = build_editor(buffer.clone(), window, cx);
335
336 // Start a new IME composition.
337 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
338 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
339 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
340 assert_eq!(editor.text(cx), "äbcde");
341 assert_eq!(
342 editor.marked_text_ranges(cx),
343 Some(vec![
344 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
345 ])
346 );
347
348 // Finalize IME composition.
349 editor.replace_text_in_range(None, "ā", window, cx);
350 assert_eq!(editor.text(cx), "ābcde");
351 assert_eq!(editor.marked_text_ranges(cx), None);
352
353 // IME composition edits are grouped and are undone/redone at once.
354 editor.undo(&Default::default(), window, cx);
355 assert_eq!(editor.text(cx), "abcde");
356 assert_eq!(editor.marked_text_ranges(cx), None);
357 editor.redo(&Default::default(), window, cx);
358 assert_eq!(editor.text(cx), "ābcde");
359 assert_eq!(editor.marked_text_ranges(cx), None);
360
361 // Start a new IME composition.
362 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
363 assert_eq!(
364 editor.marked_text_ranges(cx),
365 Some(vec![
366 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
367 ])
368 );
369
370 // Undoing during an IME composition cancels it.
371 editor.undo(&Default::default(), window, cx);
372 assert_eq!(editor.text(cx), "ābcde");
373 assert_eq!(editor.marked_text_ranges(cx), None);
374
375 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
376 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
377 assert_eq!(editor.text(cx), "ābcdè");
378 assert_eq!(
379 editor.marked_text_ranges(cx),
380 Some(vec![
381 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(5))
382 ])
383 );
384
385 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
386 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
387 assert_eq!(editor.text(cx), "ābcdę");
388 assert_eq!(editor.marked_text_ranges(cx), None);
389
390 // Start a new IME composition with multiple cursors.
391 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
392 s.select_ranges([
393 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(1)),
394 MultiBufferOffsetUtf16(OffsetUtf16(3))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
395 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(5)),
396 ])
397 });
398 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
399 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
400 assert_eq!(
401 editor.marked_text_ranges(cx),
402 Some(vec![
403 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
404 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(7)),
405 MultiBufferOffsetUtf16(OffsetUtf16(8))..MultiBufferOffsetUtf16(OffsetUtf16(11))
406 ])
407 );
408
409 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
410 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
411 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
412 assert_eq!(
413 editor.marked_text_ranges(cx),
414 Some(vec![
415 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(2)),
416 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(6)),
417 MultiBufferOffsetUtf16(OffsetUtf16(9))..MultiBufferOffsetUtf16(OffsetUtf16(10))
418 ])
419 );
420
421 // Finalize IME composition with multiple cursors.
422 editor.replace_text_in_range(Some(9..10), "2", window, cx);
423 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
424 assert_eq!(editor.marked_text_ranges(cx), None);
425
426 editor
427 });
428}
429
430#[gpui::test]
431fn test_selection_with_mouse(cx: &mut TestAppContext) {
432 init_test(cx, |_| {});
433
434 let editor = cx.add_window(|window, cx| {
435 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
436 build_editor(buffer, window, cx)
437 });
438
439 _ = editor.update(cx, |editor, window, cx| {
440 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
441 });
442 assert_eq!(
443 editor
444 .update(cx, |editor, _, cx| display_ranges(editor, cx))
445 .unwrap(),
446 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
447 );
448
449 _ = editor.update(cx, |editor, window, cx| {
450 editor.update_selection(
451 DisplayPoint::new(DisplayRow(3), 3),
452 0,
453 gpui::Point::<f32>::default(),
454 window,
455 cx,
456 );
457 });
458
459 assert_eq!(
460 editor
461 .update(cx, |editor, _, cx| display_ranges(editor, cx))
462 .unwrap(),
463 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
464 );
465
466 _ = editor.update(cx, |editor, window, cx| {
467 editor.update_selection(
468 DisplayPoint::new(DisplayRow(1), 1),
469 0,
470 gpui::Point::<f32>::default(),
471 window,
472 cx,
473 );
474 });
475
476 assert_eq!(
477 editor
478 .update(cx, |editor, _, cx| display_ranges(editor, cx))
479 .unwrap(),
480 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
481 );
482
483 _ = editor.update(cx, |editor, window, cx| {
484 editor.end_selection(window, cx);
485 editor.update_selection(
486 DisplayPoint::new(DisplayRow(3), 3),
487 0,
488 gpui::Point::<f32>::default(),
489 window,
490 cx,
491 );
492 });
493
494 assert_eq!(
495 editor
496 .update(cx, |editor, _, cx| display_ranges(editor, cx))
497 .unwrap(),
498 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
499 );
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
503 editor.update_selection(
504 DisplayPoint::new(DisplayRow(0), 0),
505 0,
506 gpui::Point::<f32>::default(),
507 window,
508 cx,
509 );
510 });
511
512 assert_eq!(
513 editor
514 .update(cx, |editor, _, cx| display_ranges(editor, cx))
515 .unwrap(),
516 [
517 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
518 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
519 ]
520 );
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.end_selection(window, cx);
524 });
525
526 assert_eq!(
527 editor
528 .update(cx, |editor, _, cx| display_ranges(editor, cx))
529 .unwrap(),
530 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
531 );
532}
533
534#[gpui::test]
535fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
536 init_test(cx, |_| {});
537
538 let editor = cx.add_window(|window, cx| {
539 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
540 build_editor(buffer, window, cx)
541 });
542
543 _ = editor.update(cx, |editor, window, cx| {
544 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
545 });
546
547 _ = editor.update(cx, |editor, window, cx| {
548 editor.end_selection(window, cx);
549 });
550
551 _ = editor.update(cx, |editor, window, cx| {
552 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
553 });
554
555 _ = editor.update(cx, |editor, window, cx| {
556 editor.end_selection(window, cx);
557 });
558
559 assert_eq!(
560 editor
561 .update(cx, |editor, _, cx| display_ranges(editor, cx))
562 .unwrap(),
563 [
564 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
565 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
566 ]
567 );
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
571 });
572
573 _ = editor.update(cx, |editor, window, cx| {
574 editor.end_selection(window, cx);
575 });
576
577 assert_eq!(
578 editor
579 .update(cx, |editor, _, cx| display_ranges(editor, cx))
580 .unwrap(),
581 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
582 );
583}
584
585#[gpui::test]
586fn test_canceling_pending_selection(cx: &mut TestAppContext) {
587 init_test(cx, |_| {});
588
589 let editor = cx.add_window(|window, cx| {
590 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
591 build_editor(buffer, window, cx)
592 });
593
594 _ = editor.update(cx, |editor, window, cx| {
595 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
596 assert_eq!(
597 display_ranges(editor, cx),
598 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
599 );
600 });
601
602 _ = editor.update(cx, |editor, window, cx| {
603 editor.update_selection(
604 DisplayPoint::new(DisplayRow(3), 3),
605 0,
606 gpui::Point::<f32>::default(),
607 window,
608 cx,
609 );
610 assert_eq!(
611 display_ranges(editor, cx),
612 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
613 );
614 });
615
616 _ = editor.update(cx, |editor, window, cx| {
617 editor.cancel(&Cancel, window, cx);
618 editor.update_selection(
619 DisplayPoint::new(DisplayRow(1), 1),
620 0,
621 gpui::Point::<f32>::default(),
622 window,
623 cx,
624 );
625 assert_eq!(
626 display_ranges(editor, cx),
627 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
628 );
629 });
630}
631
632#[gpui::test]
633fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
634 init_test(cx, |_| {});
635
636 let editor = cx.add_window(|window, cx| {
637 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
638 build_editor(buffer, window, cx)
639 });
640
641 _ = editor.update(cx, |editor, window, cx| {
642 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
643 assert_eq!(
644 display_ranges(editor, cx),
645 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
646 );
647
648 editor.move_down(&Default::default(), window, cx);
649 assert_eq!(
650 display_ranges(editor, cx),
651 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
652 );
653
654 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
655 assert_eq!(
656 display_ranges(editor, cx),
657 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
658 );
659
660 editor.move_up(&Default::default(), window, cx);
661 assert_eq!(
662 display_ranges(editor, cx),
663 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
664 );
665 });
666}
667
668#[gpui::test]
669fn test_extending_selection(cx: &mut TestAppContext) {
670 init_test(cx, |_| {});
671
672 let editor = cx.add_window(|window, cx| {
673 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
674 build_editor(buffer, window, cx)
675 });
676
677 _ = editor.update(cx, |editor, window, cx| {
678 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
679 editor.end_selection(window, cx);
680 assert_eq!(
681 display_ranges(editor, cx),
682 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
683 );
684
685 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
686 editor.end_selection(window, cx);
687 assert_eq!(
688 display_ranges(editor, cx),
689 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
690 );
691
692 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
693 editor.end_selection(window, cx);
694 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
695 assert_eq!(
696 display_ranges(editor, cx),
697 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
698 );
699
700 editor.update_selection(
701 DisplayPoint::new(DisplayRow(0), 1),
702 0,
703 gpui::Point::<f32>::default(),
704 window,
705 cx,
706 );
707 editor.end_selection(window, cx);
708 assert_eq!(
709 display_ranges(editor, cx),
710 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
711 );
712
713 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
714 editor.end_selection(window, cx);
715 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
716 editor.end_selection(window, cx);
717 assert_eq!(
718 display_ranges(editor, cx),
719 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
720 );
721
722 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
723 assert_eq!(
724 display_ranges(editor, cx),
725 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
726 );
727
728 editor.update_selection(
729 DisplayPoint::new(DisplayRow(0), 6),
730 0,
731 gpui::Point::<f32>::default(),
732 window,
733 cx,
734 );
735 assert_eq!(
736 display_ranges(editor, cx),
737 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
738 );
739
740 editor.update_selection(
741 DisplayPoint::new(DisplayRow(0), 1),
742 0,
743 gpui::Point::<f32>::default(),
744 window,
745 cx,
746 );
747 editor.end_selection(window, cx);
748 assert_eq!(
749 display_ranges(editor, cx),
750 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
751 );
752 });
753}
754
755#[gpui::test]
756fn test_clone(cx: &mut TestAppContext) {
757 init_test(cx, |_| {});
758
759 let (text, selection_ranges) = marked_text_ranges(
760 indoc! {"
761 one
762 two
763 threeˇ
764 four
765 fiveˇ
766 "},
767 true,
768 );
769
770 let editor = cx.add_window(|window, cx| {
771 let buffer = MultiBuffer::build_simple(&text, cx);
772 build_editor(buffer, window, cx)
773 });
774
775 _ = editor.update(cx, |editor, window, cx| {
776 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
777 s.select_ranges(
778 selection_ranges
779 .iter()
780 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
781 )
782 });
783 editor.fold_creases(
784 vec![
785 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
786 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
787 ],
788 true,
789 window,
790 cx,
791 );
792 });
793
794 let cloned_editor = editor
795 .update(cx, |editor, _, cx| {
796 cx.open_window(Default::default(), |window, cx| {
797 cx.new(|cx| editor.clone(window, cx))
798 })
799 })
800 .unwrap()
801 .unwrap();
802
803 let snapshot = editor
804 .update(cx, |e, window, cx| e.snapshot(window, cx))
805 .unwrap();
806 let cloned_snapshot = cloned_editor
807 .update(cx, |e, window, cx| e.snapshot(window, cx))
808 .unwrap();
809
810 assert_eq!(
811 cloned_editor
812 .update(cx, |e, _, cx| e.display_text(cx))
813 .unwrap(),
814 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
815 );
816 assert_eq!(
817 cloned_snapshot
818 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
819 .collect::<Vec<_>>(),
820 snapshot
821 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
822 .collect::<Vec<_>>(),
823 );
824 assert_set_eq!(
825 cloned_editor
826 .update(cx, |editor, _, cx| editor
827 .selections
828 .ranges::<Point>(&editor.display_snapshot(cx)))
829 .unwrap(),
830 editor
831 .update(cx, |editor, _, cx| editor
832 .selections
833 .ranges(&editor.display_snapshot(cx)))
834 .unwrap()
835 );
836 assert_set_eq!(
837 cloned_editor
838 .update(cx, |e, _window, cx| e
839 .selections
840 .display_ranges(&e.display_snapshot(cx)))
841 .unwrap(),
842 editor
843 .update(cx, |e, _, cx| e
844 .selections
845 .display_ranges(&e.display_snapshot(cx)))
846 .unwrap()
847 );
848}
849
850#[gpui::test]
851async fn test_navigation_history(cx: &mut TestAppContext) {
852 init_test(cx, |_| {});
853
854 use workspace::item::Item;
855
856 let fs = FakeFs::new(cx.executor());
857 let project = Project::test(fs, [], cx).await;
858 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
859 let pane = workspace
860 .update(cx, |workspace, _, _| workspace.active_pane().clone())
861 .unwrap();
862
863 _ = workspace.update(cx, |_v, window, cx| {
864 cx.new(|cx| {
865 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
866 let mut editor = build_editor(buffer, window, cx);
867 let handle = cx.entity();
868 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
869
870 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
871 editor.nav_history.as_mut().unwrap().pop_backward(cx)
872 }
873
874 // Move the cursor a small distance.
875 // Nothing is added to the navigation history.
876 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
877 s.select_display_ranges([
878 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
879 ])
880 });
881 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
882 s.select_display_ranges([
883 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
884 ])
885 });
886 assert!(pop_history(&mut editor, cx).is_none());
887
888 // Move the cursor a large distance.
889 // The history can jump back to the previous position.
890 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
891 s.select_display_ranges([
892 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
893 ])
894 });
895 let nav_entry = pop_history(&mut editor, cx).unwrap();
896 editor.navigate(nav_entry.data.unwrap(), window, cx);
897 assert_eq!(nav_entry.item.id(), cx.entity_id());
898 assert_eq!(
899 editor
900 .selections
901 .display_ranges(&editor.display_snapshot(cx)),
902 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
903 );
904 assert!(pop_history(&mut editor, cx).is_none());
905
906 // Move the cursor a small distance via the mouse.
907 // Nothing is added to the navigation history.
908 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
909 editor.end_selection(window, cx);
910 assert_eq!(
911 editor
912 .selections
913 .display_ranges(&editor.display_snapshot(cx)),
914 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
915 );
916 assert!(pop_history(&mut editor, cx).is_none());
917
918 // Move the cursor a large distance via the mouse.
919 // The history can jump back to the previous position.
920 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
921 editor.end_selection(window, cx);
922 assert_eq!(
923 editor
924 .selections
925 .display_ranges(&editor.display_snapshot(cx)),
926 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
927 );
928 let nav_entry = pop_history(&mut editor, cx).unwrap();
929 editor.navigate(nav_entry.data.unwrap(), window, cx);
930 assert_eq!(nav_entry.item.id(), cx.entity_id());
931 assert_eq!(
932 editor
933 .selections
934 .display_ranges(&editor.display_snapshot(cx)),
935 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
936 );
937 assert!(pop_history(&mut editor, cx).is_none());
938
939 // Set scroll position to check later
940 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
941 let original_scroll_position = editor.scroll_manager.anchor();
942
943 // Jump to the end of the document and adjust scroll
944 editor.move_to_end(&MoveToEnd, window, cx);
945 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
946 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
947
948 let nav_entry = pop_history(&mut editor, cx).unwrap();
949 editor.navigate(nav_entry.data.unwrap(), window, cx);
950 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
951
952 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
953 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
954 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
955 let invalid_point = Point::new(9999, 0);
956 editor.navigate(
957 Arc::new(NavigationData {
958 cursor_anchor: invalid_anchor,
959 cursor_position: invalid_point,
960 scroll_anchor: ScrollAnchor {
961 anchor: invalid_anchor,
962 offset: Default::default(),
963 },
964 scroll_top_row: invalid_point.row,
965 }),
966 window,
967 cx,
968 );
969 assert_eq!(
970 editor
971 .selections
972 .display_ranges(&editor.display_snapshot(cx)),
973 &[editor.max_point(cx)..editor.max_point(cx)]
974 );
975 assert_eq!(
976 editor.scroll_position(cx),
977 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
978 );
979
980 editor
981 })
982 });
983}
984
985#[gpui::test]
986fn test_cancel(cx: &mut TestAppContext) {
987 init_test(cx, |_| {});
988
989 let editor = cx.add_window(|window, cx| {
990 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
991 build_editor(buffer, window, cx)
992 });
993
994 _ = editor.update(cx, |editor, window, cx| {
995 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
996 editor.update_selection(
997 DisplayPoint::new(DisplayRow(1), 1),
998 0,
999 gpui::Point::<f32>::default(),
1000 window,
1001 cx,
1002 );
1003 editor.end_selection(window, cx);
1004
1005 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
1006 editor.update_selection(
1007 DisplayPoint::new(DisplayRow(0), 3),
1008 0,
1009 gpui::Point::<f32>::default(),
1010 window,
1011 cx,
1012 );
1013 editor.end_selection(window, cx);
1014 assert_eq!(
1015 display_ranges(editor, cx),
1016 [
1017 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
1018 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
1019 ]
1020 );
1021 });
1022
1023 _ = editor.update(cx, |editor, window, cx| {
1024 editor.cancel(&Cancel, window, cx);
1025 assert_eq!(
1026 display_ranges(editor, cx),
1027 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1028 );
1029 });
1030
1031 _ = editor.update(cx, |editor, window, cx| {
1032 editor.cancel(&Cancel, window, cx);
1033 assert_eq!(
1034 display_ranges(editor, cx),
1035 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1036 );
1037 });
1038}
1039
1040#[gpui::test]
1041fn test_fold_action(cx: &mut TestAppContext) {
1042 init_test(cx, |_| {});
1043
1044 let editor = cx.add_window(|window, cx| {
1045 let buffer = MultiBuffer::build_simple(
1046 &"
1047 impl Foo {
1048 // Hello!
1049
1050 fn a() {
1051 1
1052 }
1053
1054 fn b() {
1055 2
1056 }
1057
1058 fn c() {
1059 3
1060 }
1061 }
1062 "
1063 .unindent(),
1064 cx,
1065 );
1066 build_editor(buffer, window, cx)
1067 });
1068
1069 _ = editor.update(cx, |editor, window, cx| {
1070 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1071 s.select_display_ranges([
1072 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1073 ]);
1074 });
1075 editor.fold(&Fold, window, cx);
1076 assert_eq!(
1077 editor.display_text(cx),
1078 "
1079 impl Foo {
1080 // Hello!
1081
1082 fn a() {
1083 1
1084 }
1085
1086 fn b() {⋯
1087 }
1088
1089 fn c() {⋯
1090 }
1091 }
1092 "
1093 .unindent(),
1094 );
1095
1096 editor.fold(&Fold, window, cx);
1097 assert_eq!(
1098 editor.display_text(cx),
1099 "
1100 impl Foo {⋯
1101 }
1102 "
1103 .unindent(),
1104 );
1105
1106 editor.unfold_lines(&UnfoldLines, window, cx);
1107 assert_eq!(
1108 editor.display_text(cx),
1109 "
1110 impl Foo {
1111 // Hello!
1112
1113 fn a() {
1114 1
1115 }
1116
1117 fn b() {⋯
1118 }
1119
1120 fn c() {⋯
1121 }
1122 }
1123 "
1124 .unindent(),
1125 );
1126
1127 editor.unfold_lines(&UnfoldLines, window, cx);
1128 assert_eq!(
1129 editor.display_text(cx),
1130 editor.buffer.read(cx).read(cx).text()
1131 );
1132 });
1133}
1134
1135#[gpui::test]
1136fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1137 init_test(cx, |_| {});
1138
1139 let editor = cx.add_window(|window, cx| {
1140 let buffer = MultiBuffer::build_simple(
1141 &"
1142 class Foo:
1143 # Hello!
1144
1145 def a():
1146 print(1)
1147
1148 def b():
1149 print(2)
1150
1151 def c():
1152 print(3)
1153 "
1154 .unindent(),
1155 cx,
1156 );
1157 build_editor(buffer, window, cx)
1158 });
1159
1160 _ = editor.update(cx, |editor, window, cx| {
1161 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1162 s.select_display_ranges([
1163 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1164 ]);
1165 });
1166 editor.fold(&Fold, window, cx);
1167 assert_eq!(
1168 editor.display_text(cx),
1169 "
1170 class Foo:
1171 # Hello!
1172
1173 def a():
1174 print(1)
1175
1176 def b():⋯
1177
1178 def c():⋯
1179 "
1180 .unindent(),
1181 );
1182
1183 editor.fold(&Fold, window, cx);
1184 assert_eq!(
1185 editor.display_text(cx),
1186 "
1187 class Foo:⋯
1188 "
1189 .unindent(),
1190 );
1191
1192 editor.unfold_lines(&UnfoldLines, window, cx);
1193 assert_eq!(
1194 editor.display_text(cx),
1195 "
1196 class Foo:
1197 # Hello!
1198
1199 def a():
1200 print(1)
1201
1202 def b():⋯
1203
1204 def c():⋯
1205 "
1206 .unindent(),
1207 );
1208
1209 editor.unfold_lines(&UnfoldLines, window, cx);
1210 assert_eq!(
1211 editor.display_text(cx),
1212 editor.buffer.read(cx).read(cx).text()
1213 );
1214 });
1215}
1216
1217#[gpui::test]
1218fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1219 init_test(cx, |_| {});
1220
1221 let editor = cx.add_window(|window, cx| {
1222 let buffer = MultiBuffer::build_simple(
1223 &"
1224 class Foo:
1225 # Hello!
1226
1227 def a():
1228 print(1)
1229
1230 def b():
1231 print(2)
1232
1233
1234 def c():
1235 print(3)
1236
1237
1238 "
1239 .unindent(),
1240 cx,
1241 );
1242 build_editor(buffer, window, cx)
1243 });
1244
1245 _ = editor.update(cx, |editor, window, cx| {
1246 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1247 s.select_display_ranges([
1248 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1249 ]);
1250 });
1251 editor.fold(&Fold, window, cx);
1252 assert_eq!(
1253 editor.display_text(cx),
1254 "
1255 class Foo:
1256 # Hello!
1257
1258 def a():
1259 print(1)
1260
1261 def b():⋯
1262
1263
1264 def c():⋯
1265
1266
1267 "
1268 .unindent(),
1269 );
1270
1271 editor.fold(&Fold, window, cx);
1272 assert_eq!(
1273 editor.display_text(cx),
1274 "
1275 class Foo:⋯
1276
1277
1278 "
1279 .unindent(),
1280 );
1281
1282 editor.unfold_lines(&UnfoldLines, window, cx);
1283 assert_eq!(
1284 editor.display_text(cx),
1285 "
1286 class Foo:
1287 # Hello!
1288
1289 def a():
1290 print(1)
1291
1292 def b():⋯
1293
1294
1295 def c():⋯
1296
1297
1298 "
1299 .unindent(),
1300 );
1301
1302 editor.unfold_lines(&UnfoldLines, window, cx);
1303 assert_eq!(
1304 editor.display_text(cx),
1305 editor.buffer.read(cx).read(cx).text()
1306 );
1307 });
1308}
1309
1310#[gpui::test]
1311fn test_fold_at_level(cx: &mut TestAppContext) {
1312 init_test(cx, |_| {});
1313
1314 let editor = cx.add_window(|window, cx| {
1315 let buffer = MultiBuffer::build_simple(
1316 &"
1317 class Foo:
1318 # Hello!
1319
1320 def a():
1321 print(1)
1322
1323 def b():
1324 print(2)
1325
1326
1327 class Bar:
1328 # World!
1329
1330 def a():
1331 print(1)
1332
1333 def b():
1334 print(2)
1335
1336
1337 "
1338 .unindent(),
1339 cx,
1340 );
1341 build_editor(buffer, window, cx)
1342 });
1343
1344 _ = editor.update(cx, |editor, window, cx| {
1345 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1346 assert_eq!(
1347 editor.display_text(cx),
1348 "
1349 class Foo:
1350 # Hello!
1351
1352 def a():⋯
1353
1354 def b():⋯
1355
1356
1357 class Bar:
1358 # World!
1359
1360 def a():⋯
1361
1362 def b():⋯
1363
1364
1365 "
1366 .unindent(),
1367 );
1368
1369 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1370 assert_eq!(
1371 editor.display_text(cx),
1372 "
1373 class Foo:⋯
1374
1375
1376 class Bar:⋯
1377
1378
1379 "
1380 .unindent(),
1381 );
1382
1383 editor.unfold_all(&UnfoldAll, window, cx);
1384 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1385 assert_eq!(
1386 editor.display_text(cx),
1387 "
1388 class Foo:
1389 # Hello!
1390
1391 def a():
1392 print(1)
1393
1394 def b():
1395 print(2)
1396
1397
1398 class Bar:
1399 # World!
1400
1401 def a():
1402 print(1)
1403
1404 def b():
1405 print(2)
1406
1407
1408 "
1409 .unindent(),
1410 );
1411
1412 assert_eq!(
1413 editor.display_text(cx),
1414 editor.buffer.read(cx).read(cx).text()
1415 );
1416 let (_, positions) = marked_text_ranges(
1417 &"
1418 class Foo:
1419 # Hello!
1420
1421 def a():
1422 print(1)
1423
1424 def b():
1425 p«riˇ»nt(2)
1426
1427
1428 class Bar:
1429 # World!
1430
1431 def a():
1432 «ˇprint(1)
1433
1434 def b():
1435 print(2)»
1436
1437
1438 "
1439 .unindent(),
1440 true,
1441 );
1442
1443 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1444 s.select_ranges(
1445 positions
1446 .iter()
1447 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
1448 )
1449 });
1450
1451 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1452 assert_eq!(
1453 editor.display_text(cx),
1454 "
1455 class Foo:
1456 # Hello!
1457
1458 def a():⋯
1459
1460 def b():
1461 print(2)
1462
1463
1464 class Bar:
1465 # World!
1466
1467 def a():
1468 print(1)
1469
1470 def b():
1471 print(2)
1472
1473
1474 "
1475 .unindent(),
1476 );
1477 });
1478}
1479
1480#[gpui::test]
1481fn test_move_cursor(cx: &mut TestAppContext) {
1482 init_test(cx, |_| {});
1483
1484 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1485 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1486
1487 buffer.update(cx, |buffer, cx| {
1488 buffer.edit(
1489 vec![
1490 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1491 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1492 ],
1493 None,
1494 cx,
1495 );
1496 });
1497 _ = editor.update(cx, |editor, window, cx| {
1498 assert_eq!(
1499 display_ranges(editor, cx),
1500 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1501 );
1502
1503 editor.move_down(&MoveDown, window, cx);
1504 assert_eq!(
1505 display_ranges(editor, cx),
1506 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1507 );
1508
1509 editor.move_right(&MoveRight, window, cx);
1510 assert_eq!(
1511 display_ranges(editor, cx),
1512 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1513 );
1514
1515 editor.move_left(&MoveLeft, window, cx);
1516 assert_eq!(
1517 display_ranges(editor, cx),
1518 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1519 );
1520
1521 editor.move_up(&MoveUp, window, cx);
1522 assert_eq!(
1523 display_ranges(editor, cx),
1524 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1525 );
1526
1527 editor.move_to_end(&MoveToEnd, window, cx);
1528 assert_eq!(
1529 display_ranges(editor, cx),
1530 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1531 );
1532
1533 editor.move_to_beginning(&MoveToBeginning, window, cx);
1534 assert_eq!(
1535 display_ranges(editor, cx),
1536 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1537 );
1538
1539 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1540 s.select_display_ranges([
1541 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1542 ]);
1543 });
1544 editor.select_to_beginning(&SelectToBeginning, window, cx);
1545 assert_eq!(
1546 display_ranges(editor, cx),
1547 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1548 );
1549
1550 editor.select_to_end(&SelectToEnd, window, cx);
1551 assert_eq!(
1552 display_ranges(editor, cx),
1553 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1554 );
1555 });
1556}
1557
1558#[gpui::test]
1559fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1560 init_test(cx, |_| {});
1561
1562 let editor = cx.add_window(|window, cx| {
1563 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1564 build_editor(buffer, window, cx)
1565 });
1566
1567 assert_eq!('🟥'.len_utf8(), 4);
1568 assert_eq!('α'.len_utf8(), 2);
1569
1570 _ = editor.update(cx, |editor, window, cx| {
1571 editor.fold_creases(
1572 vec![
1573 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1574 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1575 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1576 ],
1577 true,
1578 window,
1579 cx,
1580 );
1581 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1582
1583 editor.move_right(&MoveRight, window, cx);
1584 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1585 editor.move_right(&MoveRight, window, cx);
1586 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1587 editor.move_right(&MoveRight, window, cx);
1588 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1589
1590 editor.move_down(&MoveDown, window, cx);
1591 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1592 editor.move_left(&MoveLeft, window, cx);
1593 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".len())]);
1594 editor.move_left(&MoveLeft, window, cx);
1595 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab".len())]);
1596 editor.move_left(&MoveLeft, window, cx);
1597 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1598
1599 editor.move_down(&MoveDown, window, cx);
1600 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1601 editor.move_right(&MoveRight, window, cx);
1602 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ".len())]);
1603 editor.move_right(&MoveRight, window, cx);
1604 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯".len())]);
1605 editor.move_right(&MoveRight, window, cx);
1606 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1607
1608 editor.move_up(&MoveUp, window, cx);
1609 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1610 editor.move_down(&MoveDown, window, cx);
1611 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1612 editor.move_up(&MoveUp, window, cx);
1613 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1614
1615 editor.move_up(&MoveUp, window, cx);
1616 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1617 editor.move_left(&MoveLeft, window, cx);
1618 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1619 editor.move_left(&MoveLeft, window, cx);
1620 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1621 });
1622}
1623
1624#[gpui::test]
1625fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1626 init_test(cx, |_| {});
1627
1628 let editor = cx.add_window(|window, cx| {
1629 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1630 build_editor(buffer, window, cx)
1631 });
1632 _ = editor.update(cx, |editor, window, cx| {
1633 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1634 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1635 });
1636
1637 // moving above start of document should move selection to start of document,
1638 // but the next move down should still be at the original goal_x
1639 editor.move_up(&MoveUp, window, cx);
1640 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1641
1642 editor.move_down(&MoveDown, window, cx);
1643 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1644
1645 editor.move_down(&MoveDown, window, cx);
1646 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1647
1648 editor.move_down(&MoveDown, window, cx);
1649 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1650
1651 editor.move_down(&MoveDown, window, cx);
1652 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1653
1654 // moving past end of document should not change goal_x
1655 editor.move_down(&MoveDown, window, cx);
1656 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1657
1658 editor.move_down(&MoveDown, window, cx);
1659 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1660
1661 editor.move_up(&MoveUp, window, cx);
1662 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1663
1664 editor.move_up(&MoveUp, window, cx);
1665 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1666
1667 editor.move_up(&MoveUp, window, cx);
1668 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1669 });
1670}
1671
1672#[gpui::test]
1673fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1674 init_test(cx, |_| {});
1675 let move_to_beg = MoveToBeginningOfLine {
1676 stop_at_soft_wraps: true,
1677 stop_at_indent: true,
1678 };
1679
1680 let delete_to_beg = DeleteToBeginningOfLine {
1681 stop_at_indent: false,
1682 };
1683
1684 let move_to_end = MoveToEndOfLine {
1685 stop_at_soft_wraps: true,
1686 };
1687
1688 let editor = cx.add_window(|window, cx| {
1689 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1690 build_editor(buffer, window, cx)
1691 });
1692 _ = editor.update(cx, |editor, window, cx| {
1693 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1694 s.select_display_ranges([
1695 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1696 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1697 ]);
1698 });
1699 });
1700
1701 _ = editor.update(cx, |editor, window, cx| {
1702 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1703 assert_eq!(
1704 display_ranges(editor, cx),
1705 &[
1706 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1707 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1708 ]
1709 );
1710 });
1711
1712 _ = editor.update(cx, |editor, window, cx| {
1713 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1714 assert_eq!(
1715 display_ranges(editor, cx),
1716 &[
1717 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1718 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1719 ]
1720 );
1721 });
1722
1723 _ = editor.update(cx, |editor, window, cx| {
1724 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1725 assert_eq!(
1726 display_ranges(editor, cx),
1727 &[
1728 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1729 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1730 ]
1731 );
1732 });
1733
1734 _ = editor.update(cx, |editor, window, cx| {
1735 editor.move_to_end_of_line(&move_to_end, window, cx);
1736 assert_eq!(
1737 display_ranges(editor, cx),
1738 &[
1739 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1740 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1741 ]
1742 );
1743 });
1744
1745 // Moving to the end of line again is a no-op.
1746 _ = editor.update(cx, |editor, window, cx| {
1747 editor.move_to_end_of_line(&move_to_end, window, cx);
1748 assert_eq!(
1749 display_ranges(editor, cx),
1750 &[
1751 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1752 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1753 ]
1754 );
1755 });
1756
1757 _ = editor.update(cx, |editor, window, cx| {
1758 editor.move_left(&MoveLeft, window, cx);
1759 editor.select_to_beginning_of_line(
1760 &SelectToBeginningOfLine {
1761 stop_at_soft_wraps: true,
1762 stop_at_indent: true,
1763 },
1764 window,
1765 cx,
1766 );
1767 assert_eq!(
1768 display_ranges(editor, cx),
1769 &[
1770 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1771 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1772 ]
1773 );
1774 });
1775
1776 _ = editor.update(cx, |editor, window, cx| {
1777 editor.select_to_beginning_of_line(
1778 &SelectToBeginningOfLine {
1779 stop_at_soft_wraps: true,
1780 stop_at_indent: true,
1781 },
1782 window,
1783 cx,
1784 );
1785 assert_eq!(
1786 display_ranges(editor, cx),
1787 &[
1788 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1789 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1790 ]
1791 );
1792 });
1793
1794 _ = editor.update(cx, |editor, window, cx| {
1795 editor.select_to_beginning_of_line(
1796 &SelectToBeginningOfLine {
1797 stop_at_soft_wraps: true,
1798 stop_at_indent: true,
1799 },
1800 window,
1801 cx,
1802 );
1803 assert_eq!(
1804 display_ranges(editor, cx),
1805 &[
1806 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1807 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1808 ]
1809 );
1810 });
1811
1812 _ = editor.update(cx, |editor, window, cx| {
1813 editor.select_to_end_of_line(
1814 &SelectToEndOfLine {
1815 stop_at_soft_wraps: true,
1816 },
1817 window,
1818 cx,
1819 );
1820 assert_eq!(
1821 display_ranges(editor, cx),
1822 &[
1823 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1824 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1825 ]
1826 );
1827 });
1828
1829 _ = editor.update(cx, |editor, window, cx| {
1830 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1831 assert_eq!(editor.display_text(cx), "ab\n de");
1832 assert_eq!(
1833 display_ranges(editor, cx),
1834 &[
1835 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1836 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1837 ]
1838 );
1839 });
1840
1841 _ = editor.update(cx, |editor, window, cx| {
1842 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1843 assert_eq!(editor.display_text(cx), "\n");
1844 assert_eq!(
1845 display_ranges(editor, cx),
1846 &[
1847 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1848 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1849 ]
1850 );
1851 });
1852}
1853
1854#[gpui::test]
1855fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1856 init_test(cx, |_| {});
1857 let move_to_beg = MoveToBeginningOfLine {
1858 stop_at_soft_wraps: false,
1859 stop_at_indent: false,
1860 };
1861
1862 let move_to_end = MoveToEndOfLine {
1863 stop_at_soft_wraps: false,
1864 };
1865
1866 let editor = cx.add_window(|window, cx| {
1867 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1868 build_editor(buffer, window, cx)
1869 });
1870
1871 _ = editor.update(cx, |editor, window, cx| {
1872 editor.set_wrap_width(Some(140.0.into()), cx);
1873
1874 // We expect the following lines after wrapping
1875 // ```
1876 // thequickbrownfox
1877 // jumpedoverthelazydo
1878 // gs
1879 // ```
1880 // The final `gs` was soft-wrapped onto a new line.
1881 assert_eq!(
1882 "thequickbrownfox\njumpedoverthelaz\nydogs",
1883 editor.display_text(cx),
1884 );
1885
1886 // First, let's assert behavior on the first line, that was not soft-wrapped.
1887 // Start the cursor at the `k` on the first line
1888 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1889 s.select_display_ranges([
1890 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1891 ]);
1892 });
1893
1894 // Moving to the beginning of the line should put us at the beginning of the line.
1895 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1896 assert_eq!(
1897 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1898 display_ranges(editor, cx)
1899 );
1900
1901 // Moving to the end of the line should put us at the end of the line.
1902 editor.move_to_end_of_line(&move_to_end, window, cx);
1903 assert_eq!(
1904 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1905 display_ranges(editor, cx)
1906 );
1907
1908 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1909 // Start the cursor at the last line (`y` that was wrapped to a new line)
1910 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1911 s.select_display_ranges([
1912 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1913 ]);
1914 });
1915
1916 // Moving to the beginning of the line should put us at the start of the second line of
1917 // display text, i.e., the `j`.
1918 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1919 assert_eq!(
1920 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1921 display_ranges(editor, cx)
1922 );
1923
1924 // Moving to the beginning of the line again should be a no-op.
1925 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1926 assert_eq!(
1927 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1928 display_ranges(editor, cx)
1929 );
1930
1931 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1932 // next display line.
1933 editor.move_to_end_of_line(&move_to_end, window, cx);
1934 assert_eq!(
1935 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1936 display_ranges(editor, cx)
1937 );
1938
1939 // Moving to the end of the line again should be a no-op.
1940 editor.move_to_end_of_line(&move_to_end, window, cx);
1941 assert_eq!(
1942 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1943 display_ranges(editor, cx)
1944 );
1945 });
1946}
1947
1948#[gpui::test]
1949fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1950 init_test(cx, |_| {});
1951
1952 let move_to_beg = MoveToBeginningOfLine {
1953 stop_at_soft_wraps: true,
1954 stop_at_indent: true,
1955 };
1956
1957 let select_to_beg = SelectToBeginningOfLine {
1958 stop_at_soft_wraps: true,
1959 stop_at_indent: true,
1960 };
1961
1962 let delete_to_beg = DeleteToBeginningOfLine {
1963 stop_at_indent: true,
1964 };
1965
1966 let move_to_end = MoveToEndOfLine {
1967 stop_at_soft_wraps: false,
1968 };
1969
1970 let editor = cx.add_window(|window, cx| {
1971 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1972 build_editor(buffer, window, cx)
1973 });
1974
1975 _ = editor.update(cx, |editor, window, cx| {
1976 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1977 s.select_display_ranges([
1978 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1979 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1980 ]);
1981 });
1982
1983 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1984 // and the second cursor at the first non-whitespace character in the line.
1985 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1986 assert_eq!(
1987 display_ranges(editor, cx),
1988 &[
1989 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1990 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1991 ]
1992 );
1993
1994 // Moving to the beginning of the line again should be a no-op for the first cursor,
1995 // and should move the second cursor to the beginning of the line.
1996 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1997 assert_eq!(
1998 display_ranges(editor, cx),
1999 &[
2000 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2001 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2002 ]
2003 );
2004
2005 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2006 // and should move the second cursor back to the first non-whitespace character in the line.
2007 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2008 assert_eq!(
2009 display_ranges(editor, cx),
2010 &[
2011 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2012 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2013 ]
2014 );
2015
2016 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2017 // and to the first non-whitespace character in the line for the second cursor.
2018 editor.move_to_end_of_line(&move_to_end, window, cx);
2019 editor.move_left(&MoveLeft, window, cx);
2020 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2021 assert_eq!(
2022 display_ranges(editor, cx),
2023 &[
2024 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2025 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2026 ]
2027 );
2028
2029 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2030 // and should select to the beginning of the line for the second cursor.
2031 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2032 assert_eq!(
2033 display_ranges(editor, cx),
2034 &[
2035 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2036 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2037 ]
2038 );
2039
2040 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2041 // and should delete to the first non-whitespace character in the line for the second cursor.
2042 editor.move_to_end_of_line(&move_to_end, window, cx);
2043 editor.move_left(&MoveLeft, window, cx);
2044 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2045 assert_eq!(editor.text(cx), "c\n f");
2046 });
2047}
2048
2049#[gpui::test]
2050fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2051 init_test(cx, |_| {});
2052
2053 let move_to_beg = MoveToBeginningOfLine {
2054 stop_at_soft_wraps: true,
2055 stop_at_indent: true,
2056 };
2057
2058 let editor = cx.add_window(|window, cx| {
2059 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2060 build_editor(buffer, window, cx)
2061 });
2062
2063 _ = editor.update(cx, |editor, window, cx| {
2064 // test cursor between line_start and indent_start
2065 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2066 s.select_display_ranges([
2067 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2068 ]);
2069 });
2070
2071 // cursor should move to line_start
2072 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2073 assert_eq!(
2074 display_ranges(editor, cx),
2075 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2076 );
2077
2078 // cursor should move to indent_start
2079 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2080 assert_eq!(
2081 display_ranges(editor, cx),
2082 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2083 );
2084
2085 // cursor should move to back to line_start
2086 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2087 assert_eq!(
2088 display_ranges(editor, cx),
2089 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2090 );
2091 });
2092}
2093
2094#[gpui::test]
2095fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2096 init_test(cx, |_| {});
2097
2098 let editor = cx.add_window(|window, cx| {
2099 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2100 build_editor(buffer, window, cx)
2101 });
2102 _ = editor.update(cx, |editor, window, cx| {
2103 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2104 s.select_display_ranges([
2105 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2106 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2107 ])
2108 });
2109 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2110 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2111
2112 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2113 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2114
2115 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2116 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2117
2118 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2119 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2120
2121 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2122 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2123
2124 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2125 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2126
2127 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2128 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2129
2130 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2131 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2132
2133 editor.move_right(&MoveRight, window, cx);
2134 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2135 assert_selection_ranges(
2136 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2137 editor,
2138 cx,
2139 );
2140
2141 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2142 assert_selection_ranges(
2143 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2144 editor,
2145 cx,
2146 );
2147
2148 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2149 assert_selection_ranges(
2150 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2151 editor,
2152 cx,
2153 );
2154 });
2155}
2156
2157#[gpui::test]
2158fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2159 init_test(cx, |_| {});
2160
2161 let editor = cx.add_window(|window, cx| {
2162 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2163 build_editor(buffer, window, cx)
2164 });
2165
2166 _ = editor.update(cx, |editor, window, cx| {
2167 editor.set_wrap_width(Some(140.0.into()), cx);
2168 assert_eq!(
2169 editor.display_text(cx),
2170 "use one::{\n two::three::\n four::five\n};"
2171 );
2172
2173 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2174 s.select_display_ranges([
2175 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2176 ]);
2177 });
2178
2179 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2180 assert_eq!(
2181 display_ranges(editor, cx),
2182 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2183 );
2184
2185 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2186 assert_eq!(
2187 display_ranges(editor, cx),
2188 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2189 );
2190
2191 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2192 assert_eq!(
2193 display_ranges(editor, cx),
2194 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2195 );
2196
2197 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2198 assert_eq!(
2199 display_ranges(editor, cx),
2200 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2201 );
2202
2203 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2204 assert_eq!(
2205 display_ranges(editor, cx),
2206 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2207 );
2208
2209 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2210 assert_eq!(
2211 display_ranges(editor, cx),
2212 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2213 );
2214 });
2215}
2216
2217#[gpui::test]
2218async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2219 init_test(cx, |_| {});
2220 let mut cx = EditorTestContext::new(cx).await;
2221
2222 let line_height = cx.update_editor(|editor, window, cx| {
2223 editor
2224 .style(cx)
2225 .text
2226 .line_height_in_pixels(window.rem_size())
2227 });
2228 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2229
2230 cx.set_state(
2231 &r#"ˇone
2232 two
2233
2234 three
2235 fourˇ
2236 five
2237
2238 six"#
2239 .unindent(),
2240 );
2241
2242 cx.update_editor(|editor, window, cx| {
2243 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2244 });
2245 cx.assert_editor_state(
2246 &r#"one
2247 two
2248 ˇ
2249 three
2250 four
2251 five
2252 ˇ
2253 six"#
2254 .unindent(),
2255 );
2256
2257 cx.update_editor(|editor, window, cx| {
2258 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2259 });
2260 cx.assert_editor_state(
2261 &r#"one
2262 two
2263
2264 three
2265 four
2266 five
2267 ˇ
2268 sixˇ"#
2269 .unindent(),
2270 );
2271
2272 cx.update_editor(|editor, window, cx| {
2273 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2274 });
2275 cx.assert_editor_state(
2276 &r#"one
2277 two
2278
2279 three
2280 four
2281 five
2282
2283 sixˇ"#
2284 .unindent(),
2285 );
2286
2287 cx.update_editor(|editor, window, cx| {
2288 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2289 });
2290 cx.assert_editor_state(
2291 &r#"one
2292 two
2293
2294 three
2295 four
2296 five
2297 ˇ
2298 six"#
2299 .unindent(),
2300 );
2301
2302 cx.update_editor(|editor, window, cx| {
2303 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2304 });
2305 cx.assert_editor_state(
2306 &r#"one
2307 two
2308 ˇ
2309 three
2310 four
2311 five
2312
2313 six"#
2314 .unindent(),
2315 );
2316
2317 cx.update_editor(|editor, window, cx| {
2318 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2319 });
2320 cx.assert_editor_state(
2321 &r#"ˇone
2322 two
2323
2324 three
2325 four
2326 five
2327
2328 six"#
2329 .unindent(),
2330 );
2331}
2332
2333#[gpui::test]
2334async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2335 init_test(cx, |_| {});
2336 let mut cx = EditorTestContext::new(cx).await;
2337 let line_height = cx.update_editor(|editor, window, cx| {
2338 editor
2339 .style(cx)
2340 .text
2341 .line_height_in_pixels(window.rem_size())
2342 });
2343 let window = cx.window;
2344 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2345
2346 cx.set_state(
2347 r#"ˇone
2348 two
2349 three
2350 four
2351 five
2352 six
2353 seven
2354 eight
2355 nine
2356 ten
2357 "#,
2358 );
2359
2360 cx.update_editor(|editor, window, cx| {
2361 assert_eq!(
2362 editor.snapshot(window, cx).scroll_position(),
2363 gpui::Point::new(0., 0.)
2364 );
2365 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2366 assert_eq!(
2367 editor.snapshot(window, cx).scroll_position(),
2368 gpui::Point::new(0., 3.)
2369 );
2370 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2371 assert_eq!(
2372 editor.snapshot(window, cx).scroll_position(),
2373 gpui::Point::new(0., 6.)
2374 );
2375 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2376 assert_eq!(
2377 editor.snapshot(window, cx).scroll_position(),
2378 gpui::Point::new(0., 3.)
2379 );
2380
2381 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2382 assert_eq!(
2383 editor.snapshot(window, cx).scroll_position(),
2384 gpui::Point::new(0., 1.)
2385 );
2386 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2387 assert_eq!(
2388 editor.snapshot(window, cx).scroll_position(),
2389 gpui::Point::new(0., 3.)
2390 );
2391 });
2392}
2393
2394#[gpui::test]
2395async fn test_autoscroll(cx: &mut TestAppContext) {
2396 init_test(cx, |_| {});
2397 let mut cx = EditorTestContext::new(cx).await;
2398
2399 let line_height = cx.update_editor(|editor, window, cx| {
2400 editor.set_vertical_scroll_margin(2, cx);
2401 editor
2402 .style(cx)
2403 .text
2404 .line_height_in_pixels(window.rem_size())
2405 });
2406 let window = cx.window;
2407 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2408
2409 cx.set_state(
2410 r#"ˇone
2411 two
2412 three
2413 four
2414 five
2415 six
2416 seven
2417 eight
2418 nine
2419 ten
2420 "#,
2421 );
2422 cx.update_editor(|editor, window, cx| {
2423 assert_eq!(
2424 editor.snapshot(window, cx).scroll_position(),
2425 gpui::Point::new(0., 0.0)
2426 );
2427 });
2428
2429 // Add a cursor below the visible area. Since both cursors cannot fit
2430 // on screen, the editor autoscrolls to reveal the newest cursor, and
2431 // allows the vertical scroll margin below that cursor.
2432 cx.update_editor(|editor, window, cx| {
2433 editor.change_selections(Default::default(), window, cx, |selections| {
2434 selections.select_ranges([
2435 Point::new(0, 0)..Point::new(0, 0),
2436 Point::new(6, 0)..Point::new(6, 0),
2437 ]);
2438 })
2439 });
2440 cx.update_editor(|editor, window, cx| {
2441 assert_eq!(
2442 editor.snapshot(window, cx).scroll_position(),
2443 gpui::Point::new(0., 3.0)
2444 );
2445 });
2446
2447 // Move down. The editor cursor scrolls down to track the newest cursor.
2448 cx.update_editor(|editor, window, cx| {
2449 editor.move_down(&Default::default(), window, cx);
2450 });
2451 cx.update_editor(|editor, window, cx| {
2452 assert_eq!(
2453 editor.snapshot(window, cx).scroll_position(),
2454 gpui::Point::new(0., 4.0)
2455 );
2456 });
2457
2458 // Add a cursor above the visible area. Since both cursors fit on screen,
2459 // the editor scrolls to show both.
2460 cx.update_editor(|editor, window, cx| {
2461 editor.change_selections(Default::default(), window, cx, |selections| {
2462 selections.select_ranges([
2463 Point::new(1, 0)..Point::new(1, 0),
2464 Point::new(6, 0)..Point::new(6, 0),
2465 ]);
2466 })
2467 });
2468 cx.update_editor(|editor, window, cx| {
2469 assert_eq!(
2470 editor.snapshot(window, cx).scroll_position(),
2471 gpui::Point::new(0., 1.0)
2472 );
2473 });
2474}
2475
2476#[gpui::test]
2477async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2478 init_test(cx, |_| {});
2479 let mut cx = EditorTestContext::new(cx).await;
2480
2481 let line_height = cx.update_editor(|editor, window, cx| {
2482 editor
2483 .style(cx)
2484 .text
2485 .line_height_in_pixels(window.rem_size())
2486 });
2487 let window = cx.window;
2488 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2489 cx.set_state(
2490 &r#"
2491 ˇone
2492 two
2493 threeˇ
2494 four
2495 five
2496 six
2497 seven
2498 eight
2499 nine
2500 ten
2501 "#
2502 .unindent(),
2503 );
2504
2505 cx.update_editor(|editor, window, cx| {
2506 editor.move_page_down(&MovePageDown::default(), window, cx)
2507 });
2508 cx.assert_editor_state(
2509 &r#"
2510 one
2511 two
2512 three
2513 ˇfour
2514 five
2515 sixˇ
2516 seven
2517 eight
2518 nine
2519 ten
2520 "#
2521 .unindent(),
2522 );
2523
2524 cx.update_editor(|editor, window, cx| {
2525 editor.move_page_down(&MovePageDown::default(), window, cx)
2526 });
2527 cx.assert_editor_state(
2528 &r#"
2529 one
2530 two
2531 three
2532 four
2533 five
2534 six
2535 ˇseven
2536 eight
2537 nineˇ
2538 ten
2539 "#
2540 .unindent(),
2541 );
2542
2543 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2544 cx.assert_editor_state(
2545 &r#"
2546 one
2547 two
2548 three
2549 ˇfour
2550 five
2551 sixˇ
2552 seven
2553 eight
2554 nine
2555 ten
2556 "#
2557 .unindent(),
2558 );
2559
2560 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2561 cx.assert_editor_state(
2562 &r#"
2563 ˇone
2564 two
2565 threeˇ
2566 four
2567 five
2568 six
2569 seven
2570 eight
2571 nine
2572 ten
2573 "#
2574 .unindent(),
2575 );
2576
2577 // Test select collapsing
2578 cx.update_editor(|editor, window, cx| {
2579 editor.move_page_down(&MovePageDown::default(), window, cx);
2580 editor.move_page_down(&MovePageDown::default(), window, cx);
2581 editor.move_page_down(&MovePageDown::default(), window, cx);
2582 });
2583 cx.assert_editor_state(
2584 &r#"
2585 one
2586 two
2587 three
2588 four
2589 five
2590 six
2591 seven
2592 eight
2593 nine
2594 ˇten
2595 ˇ"#
2596 .unindent(),
2597 );
2598}
2599
2600#[gpui::test]
2601async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2602 init_test(cx, |_| {});
2603 let mut cx = EditorTestContext::new(cx).await;
2604 cx.set_state("one «two threeˇ» four");
2605 cx.update_editor(|editor, window, cx| {
2606 editor.delete_to_beginning_of_line(
2607 &DeleteToBeginningOfLine {
2608 stop_at_indent: false,
2609 },
2610 window,
2611 cx,
2612 );
2613 assert_eq!(editor.text(cx), " four");
2614 });
2615}
2616
2617#[gpui::test]
2618async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2619 init_test(cx, |_| {});
2620
2621 let mut cx = EditorTestContext::new(cx).await;
2622
2623 // For an empty selection, the preceding word fragment is deleted.
2624 // For non-empty selections, only selected characters are deleted.
2625 cx.set_state("onˇe two t«hreˇ»e four");
2626 cx.update_editor(|editor, window, cx| {
2627 editor.delete_to_previous_word_start(
2628 &DeleteToPreviousWordStart {
2629 ignore_newlines: false,
2630 ignore_brackets: false,
2631 },
2632 window,
2633 cx,
2634 );
2635 });
2636 cx.assert_editor_state("ˇe two tˇe four");
2637
2638 cx.set_state("e tˇwo te «fˇ»our");
2639 cx.update_editor(|editor, window, cx| {
2640 editor.delete_to_next_word_end(
2641 &DeleteToNextWordEnd {
2642 ignore_newlines: false,
2643 ignore_brackets: false,
2644 },
2645 window,
2646 cx,
2647 );
2648 });
2649 cx.assert_editor_state("e tˇ te ˇour");
2650}
2651
2652#[gpui::test]
2653async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2654 init_test(cx, |_| {});
2655
2656 let mut cx = EditorTestContext::new(cx).await;
2657
2658 cx.set_state("here is some text ˇwith a space");
2659 cx.update_editor(|editor, window, cx| {
2660 editor.delete_to_previous_word_start(
2661 &DeleteToPreviousWordStart {
2662 ignore_newlines: false,
2663 ignore_brackets: true,
2664 },
2665 window,
2666 cx,
2667 );
2668 });
2669 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2670 cx.assert_editor_state("here is some textˇwith a space");
2671
2672 cx.set_state("here is some text ˇwith a space");
2673 cx.update_editor(|editor, window, cx| {
2674 editor.delete_to_previous_word_start(
2675 &DeleteToPreviousWordStart {
2676 ignore_newlines: false,
2677 ignore_brackets: false,
2678 },
2679 window,
2680 cx,
2681 );
2682 });
2683 cx.assert_editor_state("here is some textˇwith a space");
2684
2685 cx.set_state("here is some textˇ with a space");
2686 cx.update_editor(|editor, window, cx| {
2687 editor.delete_to_next_word_end(
2688 &DeleteToNextWordEnd {
2689 ignore_newlines: false,
2690 ignore_brackets: true,
2691 },
2692 window,
2693 cx,
2694 );
2695 });
2696 // Same happens in the other direction.
2697 cx.assert_editor_state("here is some textˇwith a space");
2698
2699 cx.set_state("here is some textˇ with a space");
2700 cx.update_editor(|editor, window, cx| {
2701 editor.delete_to_next_word_end(
2702 &DeleteToNextWordEnd {
2703 ignore_newlines: false,
2704 ignore_brackets: false,
2705 },
2706 window,
2707 cx,
2708 );
2709 });
2710 cx.assert_editor_state("here is some textˇwith a space");
2711
2712 cx.set_state("here is some textˇ with a space");
2713 cx.update_editor(|editor, window, cx| {
2714 editor.delete_to_next_word_end(
2715 &DeleteToNextWordEnd {
2716 ignore_newlines: true,
2717 ignore_brackets: false,
2718 },
2719 window,
2720 cx,
2721 );
2722 });
2723 cx.assert_editor_state("here is some textˇwith a space");
2724 cx.update_editor(|editor, window, cx| {
2725 editor.delete_to_previous_word_start(
2726 &DeleteToPreviousWordStart {
2727 ignore_newlines: true,
2728 ignore_brackets: false,
2729 },
2730 window,
2731 cx,
2732 );
2733 });
2734 cx.assert_editor_state("here is some ˇwith a space");
2735 cx.update_editor(|editor, window, cx| {
2736 editor.delete_to_previous_word_start(
2737 &DeleteToPreviousWordStart {
2738 ignore_newlines: true,
2739 ignore_brackets: false,
2740 },
2741 window,
2742 cx,
2743 );
2744 });
2745 // Single whitespaces are removed with the word behind them.
2746 cx.assert_editor_state("here is ˇwith a space");
2747 cx.update_editor(|editor, window, cx| {
2748 editor.delete_to_previous_word_start(
2749 &DeleteToPreviousWordStart {
2750 ignore_newlines: true,
2751 ignore_brackets: false,
2752 },
2753 window,
2754 cx,
2755 );
2756 });
2757 cx.assert_editor_state("here ˇwith a space");
2758 cx.update_editor(|editor, window, cx| {
2759 editor.delete_to_previous_word_start(
2760 &DeleteToPreviousWordStart {
2761 ignore_newlines: true,
2762 ignore_brackets: false,
2763 },
2764 window,
2765 cx,
2766 );
2767 });
2768 cx.assert_editor_state("ˇwith a space");
2769 cx.update_editor(|editor, window, cx| {
2770 editor.delete_to_previous_word_start(
2771 &DeleteToPreviousWordStart {
2772 ignore_newlines: true,
2773 ignore_brackets: false,
2774 },
2775 window,
2776 cx,
2777 );
2778 });
2779 cx.assert_editor_state("ˇwith a space");
2780 cx.update_editor(|editor, window, cx| {
2781 editor.delete_to_next_word_end(
2782 &DeleteToNextWordEnd {
2783 ignore_newlines: true,
2784 ignore_brackets: false,
2785 },
2786 window,
2787 cx,
2788 );
2789 });
2790 // Same happens in the other direction.
2791 cx.assert_editor_state("ˇ a space");
2792 cx.update_editor(|editor, window, cx| {
2793 editor.delete_to_next_word_end(
2794 &DeleteToNextWordEnd {
2795 ignore_newlines: true,
2796 ignore_brackets: false,
2797 },
2798 window,
2799 cx,
2800 );
2801 });
2802 cx.assert_editor_state("ˇ space");
2803 cx.update_editor(|editor, window, cx| {
2804 editor.delete_to_next_word_end(
2805 &DeleteToNextWordEnd {
2806 ignore_newlines: true,
2807 ignore_brackets: false,
2808 },
2809 window,
2810 cx,
2811 );
2812 });
2813 cx.assert_editor_state("ˇ");
2814 cx.update_editor(|editor, window, cx| {
2815 editor.delete_to_next_word_end(
2816 &DeleteToNextWordEnd {
2817 ignore_newlines: true,
2818 ignore_brackets: false,
2819 },
2820 window,
2821 cx,
2822 );
2823 });
2824 cx.assert_editor_state("ˇ");
2825 cx.update_editor(|editor, window, cx| {
2826 editor.delete_to_previous_word_start(
2827 &DeleteToPreviousWordStart {
2828 ignore_newlines: true,
2829 ignore_brackets: false,
2830 },
2831 window,
2832 cx,
2833 );
2834 });
2835 cx.assert_editor_state("ˇ");
2836}
2837
2838#[gpui::test]
2839async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2840 init_test(cx, |_| {});
2841
2842 let language = Arc::new(
2843 Language::new(
2844 LanguageConfig {
2845 brackets: BracketPairConfig {
2846 pairs: vec![
2847 BracketPair {
2848 start: "\"".to_string(),
2849 end: "\"".to_string(),
2850 close: true,
2851 surround: true,
2852 newline: false,
2853 },
2854 BracketPair {
2855 start: "(".to_string(),
2856 end: ")".to_string(),
2857 close: true,
2858 surround: true,
2859 newline: true,
2860 },
2861 ],
2862 ..BracketPairConfig::default()
2863 },
2864 ..LanguageConfig::default()
2865 },
2866 Some(tree_sitter_rust::LANGUAGE.into()),
2867 )
2868 .with_brackets_query(
2869 r#"
2870 ("(" @open ")" @close)
2871 ("\"" @open "\"" @close)
2872 "#,
2873 )
2874 .unwrap(),
2875 );
2876
2877 let mut cx = EditorTestContext::new(cx).await;
2878 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2879
2880 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2881 cx.update_editor(|editor, window, cx| {
2882 editor.delete_to_previous_word_start(
2883 &DeleteToPreviousWordStart {
2884 ignore_newlines: true,
2885 ignore_brackets: false,
2886 },
2887 window,
2888 cx,
2889 );
2890 });
2891 // Deletion stops before brackets if asked to not ignore them.
2892 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2893 cx.update_editor(|editor, window, cx| {
2894 editor.delete_to_previous_word_start(
2895 &DeleteToPreviousWordStart {
2896 ignore_newlines: true,
2897 ignore_brackets: false,
2898 },
2899 window,
2900 cx,
2901 );
2902 });
2903 // Deletion has to remove a single bracket and then stop again.
2904 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2905
2906 cx.update_editor(|editor, window, cx| {
2907 editor.delete_to_previous_word_start(
2908 &DeleteToPreviousWordStart {
2909 ignore_newlines: true,
2910 ignore_brackets: false,
2911 },
2912 window,
2913 cx,
2914 );
2915 });
2916 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2917
2918 cx.update_editor(|editor, window, cx| {
2919 editor.delete_to_previous_word_start(
2920 &DeleteToPreviousWordStart {
2921 ignore_newlines: true,
2922 ignore_brackets: false,
2923 },
2924 window,
2925 cx,
2926 );
2927 });
2928 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2929
2930 cx.update_editor(|editor, window, cx| {
2931 editor.delete_to_previous_word_start(
2932 &DeleteToPreviousWordStart {
2933 ignore_newlines: true,
2934 ignore_brackets: false,
2935 },
2936 window,
2937 cx,
2938 );
2939 });
2940 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2941
2942 cx.update_editor(|editor, window, cx| {
2943 editor.delete_to_next_word_end(
2944 &DeleteToNextWordEnd {
2945 ignore_newlines: true,
2946 ignore_brackets: false,
2947 },
2948 window,
2949 cx,
2950 );
2951 });
2952 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2953 cx.assert_editor_state(r#"ˇ");"#);
2954
2955 cx.update_editor(|editor, window, cx| {
2956 editor.delete_to_next_word_end(
2957 &DeleteToNextWordEnd {
2958 ignore_newlines: true,
2959 ignore_brackets: false,
2960 },
2961 window,
2962 cx,
2963 );
2964 });
2965 cx.assert_editor_state(r#"ˇ"#);
2966
2967 cx.update_editor(|editor, window, cx| {
2968 editor.delete_to_next_word_end(
2969 &DeleteToNextWordEnd {
2970 ignore_newlines: true,
2971 ignore_brackets: false,
2972 },
2973 window,
2974 cx,
2975 );
2976 });
2977 cx.assert_editor_state(r#"ˇ"#);
2978
2979 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2980 cx.update_editor(|editor, window, cx| {
2981 editor.delete_to_previous_word_start(
2982 &DeleteToPreviousWordStart {
2983 ignore_newlines: true,
2984 ignore_brackets: true,
2985 },
2986 window,
2987 cx,
2988 );
2989 });
2990 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2991}
2992
2993#[gpui::test]
2994fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2995 init_test(cx, |_| {});
2996
2997 let editor = cx.add_window(|window, cx| {
2998 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2999 build_editor(buffer, window, cx)
3000 });
3001 let del_to_prev_word_start = DeleteToPreviousWordStart {
3002 ignore_newlines: false,
3003 ignore_brackets: false,
3004 };
3005 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3006 ignore_newlines: true,
3007 ignore_brackets: false,
3008 };
3009
3010 _ = editor.update(cx, |editor, window, cx| {
3011 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3012 s.select_display_ranges([
3013 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3014 ])
3015 });
3016 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3017 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3018 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3019 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3020 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3021 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3022 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3023 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3024 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3025 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3026 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3027 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3028 });
3029}
3030
3031#[gpui::test]
3032fn test_delete_to_previous_subword_start_or_newline(cx: &mut TestAppContext) {
3033 init_test(cx, |_| {});
3034
3035 let editor = cx.add_window(|window, cx| {
3036 let buffer = MultiBuffer::build_simple("fooBar\n\nbazQux", cx);
3037 build_editor(buffer, window, cx)
3038 });
3039 let del_to_prev_sub_word_start = DeleteToPreviousSubwordStart {
3040 ignore_newlines: false,
3041 ignore_brackets: false,
3042 };
3043 let del_to_prev_sub_word_start_ignore_newlines = DeleteToPreviousSubwordStart {
3044 ignore_newlines: true,
3045 ignore_brackets: false,
3046 };
3047
3048 _ = editor.update(cx, |editor, window, cx| {
3049 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3050 s.select_display_ranges([
3051 DisplayPoint::new(DisplayRow(2), 6)..DisplayPoint::new(DisplayRow(2), 6)
3052 ])
3053 });
3054 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3055 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n\nbaz");
3056 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3057 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n\n");
3058 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3059 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n");
3060 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3061 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar");
3062 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3063 assert_eq!(editor.buffer.read(cx).read(cx).text(), "foo");
3064 editor.delete_to_previous_subword_start(
3065 &del_to_prev_sub_word_start_ignore_newlines,
3066 window,
3067 cx,
3068 );
3069 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3070 });
3071}
3072
3073#[gpui::test]
3074fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3075 init_test(cx, |_| {});
3076
3077 let editor = cx.add_window(|window, cx| {
3078 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3079 build_editor(buffer, window, cx)
3080 });
3081 let del_to_next_word_end = DeleteToNextWordEnd {
3082 ignore_newlines: false,
3083 ignore_brackets: false,
3084 };
3085 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3086 ignore_newlines: true,
3087 ignore_brackets: false,
3088 };
3089
3090 _ = editor.update(cx, |editor, window, cx| {
3091 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3092 s.select_display_ranges([
3093 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3094 ])
3095 });
3096 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3097 assert_eq!(
3098 editor.buffer.read(cx).read(cx).text(),
3099 "one\n two\nthree\n four"
3100 );
3101 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3102 assert_eq!(
3103 editor.buffer.read(cx).read(cx).text(),
3104 "\n two\nthree\n four"
3105 );
3106 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3107 assert_eq!(
3108 editor.buffer.read(cx).read(cx).text(),
3109 "two\nthree\n four"
3110 );
3111 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3112 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3113 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3114 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3115 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3116 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3117 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3118 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3119 });
3120}
3121
3122#[gpui::test]
3123fn test_delete_to_next_subword_end_or_newline(cx: &mut TestAppContext) {
3124 init_test(cx, |_| {});
3125
3126 let editor = cx.add_window(|window, cx| {
3127 let buffer = MultiBuffer::build_simple("\nfooBar\n bazQux", cx);
3128 build_editor(buffer, window, cx)
3129 });
3130 let del_to_next_subword_end = DeleteToNextSubwordEnd {
3131 ignore_newlines: false,
3132 ignore_brackets: false,
3133 };
3134 let del_to_next_subword_end_ignore_newlines = DeleteToNextSubwordEnd {
3135 ignore_newlines: true,
3136 ignore_brackets: false,
3137 };
3138
3139 _ = editor.update(cx, |editor, window, cx| {
3140 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3141 s.select_display_ranges([
3142 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3143 ])
3144 });
3145 // Delete "\n" (empty line)
3146 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3147 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n bazQux");
3148 // Delete "foo" (subword boundary)
3149 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3150 assert_eq!(editor.buffer.read(cx).read(cx).text(), "Bar\n bazQux");
3151 // Delete "Bar"
3152 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3153 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n bazQux");
3154 // Delete "\n " (newline + leading whitespace)
3155 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3156 assert_eq!(editor.buffer.read(cx).read(cx).text(), "bazQux");
3157 // Delete "baz" (subword boundary)
3158 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3159 assert_eq!(editor.buffer.read(cx).read(cx).text(), "Qux");
3160 // With ignore_newlines, delete "Qux"
3161 editor.delete_to_next_subword_end(&del_to_next_subword_end_ignore_newlines, window, cx);
3162 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3163 });
3164}
3165
3166#[gpui::test]
3167fn test_newline(cx: &mut TestAppContext) {
3168 init_test(cx, |_| {});
3169
3170 let editor = cx.add_window(|window, cx| {
3171 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3172 build_editor(buffer, window, cx)
3173 });
3174
3175 _ = editor.update(cx, |editor, window, cx| {
3176 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3177 s.select_display_ranges([
3178 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3179 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3180 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3181 ])
3182 });
3183
3184 editor.newline(&Newline, window, cx);
3185 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3186 });
3187}
3188
3189#[gpui::test]
3190async fn test_newline_yaml(cx: &mut TestAppContext) {
3191 init_test(cx, |_| {});
3192
3193 let mut cx = EditorTestContext::new(cx).await;
3194 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3195 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3196
3197 // Object (between 2 fields)
3198 cx.set_state(indoc! {"
3199 test:ˇ
3200 hello: bye"});
3201 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3202 cx.assert_editor_state(indoc! {"
3203 test:
3204 ˇ
3205 hello: bye"});
3206
3207 // Object (first and single line)
3208 cx.set_state(indoc! {"
3209 test:ˇ"});
3210 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3211 cx.assert_editor_state(indoc! {"
3212 test:
3213 ˇ"});
3214
3215 // Array with objects (after first element)
3216 cx.set_state(indoc! {"
3217 test:
3218 - foo: barˇ"});
3219 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3220 cx.assert_editor_state(indoc! {"
3221 test:
3222 - foo: bar
3223 ˇ"});
3224
3225 // Array with objects and comment
3226 cx.set_state(indoc! {"
3227 test:
3228 - foo: bar
3229 - bar: # testˇ"});
3230 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3231 cx.assert_editor_state(indoc! {"
3232 test:
3233 - foo: bar
3234 - bar: # test
3235 ˇ"});
3236
3237 // Array with objects (after second element)
3238 cx.set_state(indoc! {"
3239 test:
3240 - foo: bar
3241 - bar: fooˇ"});
3242 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3243 cx.assert_editor_state(indoc! {"
3244 test:
3245 - foo: bar
3246 - bar: foo
3247 ˇ"});
3248
3249 // Array with strings (after first element)
3250 cx.set_state(indoc! {"
3251 test:
3252 - fooˇ"});
3253 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3254 cx.assert_editor_state(indoc! {"
3255 test:
3256 - foo
3257 ˇ"});
3258}
3259
3260#[gpui::test]
3261fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3262 init_test(cx, |_| {});
3263
3264 let editor = cx.add_window(|window, cx| {
3265 let buffer = MultiBuffer::build_simple(
3266 "
3267 a
3268 b(
3269 X
3270 )
3271 c(
3272 X
3273 )
3274 "
3275 .unindent()
3276 .as_str(),
3277 cx,
3278 );
3279 let mut editor = build_editor(buffer, window, cx);
3280 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3281 s.select_ranges([
3282 Point::new(2, 4)..Point::new(2, 5),
3283 Point::new(5, 4)..Point::new(5, 5),
3284 ])
3285 });
3286 editor
3287 });
3288
3289 _ = editor.update(cx, |editor, window, cx| {
3290 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3291 editor.buffer.update(cx, |buffer, cx| {
3292 buffer.edit(
3293 [
3294 (Point::new(1, 2)..Point::new(3, 0), ""),
3295 (Point::new(4, 2)..Point::new(6, 0), ""),
3296 ],
3297 None,
3298 cx,
3299 );
3300 assert_eq!(
3301 buffer.read(cx).text(),
3302 "
3303 a
3304 b()
3305 c()
3306 "
3307 .unindent()
3308 );
3309 });
3310 assert_eq!(
3311 editor.selections.ranges(&editor.display_snapshot(cx)),
3312 &[
3313 Point::new(1, 2)..Point::new(1, 2),
3314 Point::new(2, 2)..Point::new(2, 2),
3315 ],
3316 );
3317
3318 editor.newline(&Newline, window, cx);
3319 assert_eq!(
3320 editor.text(cx),
3321 "
3322 a
3323 b(
3324 )
3325 c(
3326 )
3327 "
3328 .unindent()
3329 );
3330
3331 // The selections are moved after the inserted newlines
3332 assert_eq!(
3333 editor.selections.ranges(&editor.display_snapshot(cx)),
3334 &[
3335 Point::new(2, 0)..Point::new(2, 0),
3336 Point::new(4, 0)..Point::new(4, 0),
3337 ],
3338 );
3339 });
3340}
3341
3342#[gpui::test]
3343async fn test_newline_above(cx: &mut TestAppContext) {
3344 init_test(cx, |settings| {
3345 settings.defaults.tab_size = NonZeroU32::new(4)
3346 });
3347
3348 let language = Arc::new(
3349 Language::new(
3350 LanguageConfig::default(),
3351 Some(tree_sitter_rust::LANGUAGE.into()),
3352 )
3353 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3354 .unwrap(),
3355 );
3356
3357 let mut cx = EditorTestContext::new(cx).await;
3358 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3359 cx.set_state(indoc! {"
3360 const a: ˇA = (
3361 (ˇ
3362 «const_functionˇ»(ˇ),
3363 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3364 )ˇ
3365 ˇ);ˇ
3366 "});
3367
3368 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3369 cx.assert_editor_state(indoc! {"
3370 ˇ
3371 const a: A = (
3372 ˇ
3373 (
3374 ˇ
3375 ˇ
3376 const_function(),
3377 ˇ
3378 ˇ
3379 ˇ
3380 ˇ
3381 something_else,
3382 ˇ
3383 )
3384 ˇ
3385 ˇ
3386 );
3387 "});
3388}
3389
3390#[gpui::test]
3391async fn test_newline_below(cx: &mut TestAppContext) {
3392 init_test(cx, |settings| {
3393 settings.defaults.tab_size = NonZeroU32::new(4)
3394 });
3395
3396 let language = Arc::new(
3397 Language::new(
3398 LanguageConfig::default(),
3399 Some(tree_sitter_rust::LANGUAGE.into()),
3400 )
3401 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3402 .unwrap(),
3403 );
3404
3405 let mut cx = EditorTestContext::new(cx).await;
3406 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3407 cx.set_state(indoc! {"
3408 const a: ˇA = (
3409 (ˇ
3410 «const_functionˇ»(ˇ),
3411 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3412 )ˇ
3413 ˇ);ˇ
3414 "});
3415
3416 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3417 cx.assert_editor_state(indoc! {"
3418 const a: A = (
3419 ˇ
3420 (
3421 ˇ
3422 const_function(),
3423 ˇ
3424 ˇ
3425 something_else,
3426 ˇ
3427 ˇ
3428 ˇ
3429 ˇ
3430 )
3431 ˇ
3432 );
3433 ˇ
3434 ˇ
3435 "});
3436}
3437
3438#[gpui::test]
3439async fn test_newline_comments(cx: &mut TestAppContext) {
3440 init_test(cx, |settings| {
3441 settings.defaults.tab_size = NonZeroU32::new(4)
3442 });
3443
3444 let language = Arc::new(Language::new(
3445 LanguageConfig {
3446 line_comments: vec!["// ".into()],
3447 ..LanguageConfig::default()
3448 },
3449 None,
3450 ));
3451 {
3452 let mut cx = EditorTestContext::new(cx).await;
3453 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3454 cx.set_state(indoc! {"
3455 // Fooˇ
3456 "});
3457
3458 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3459 cx.assert_editor_state(indoc! {"
3460 // Foo
3461 // ˇ
3462 "});
3463 // Ensure that we add comment prefix when existing line contains space
3464 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3465 cx.assert_editor_state(
3466 indoc! {"
3467 // Foo
3468 //s
3469 // ˇ
3470 "}
3471 .replace("s", " ") // s is used as space placeholder to prevent format on save
3472 .as_str(),
3473 );
3474 // Ensure that we add comment prefix when existing line does not contain space
3475 cx.set_state(indoc! {"
3476 // Foo
3477 //ˇ
3478 "});
3479 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3480 cx.assert_editor_state(indoc! {"
3481 // Foo
3482 //
3483 // ˇ
3484 "});
3485 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3486 cx.set_state(indoc! {"
3487 ˇ// Foo
3488 "});
3489 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3490 cx.assert_editor_state(indoc! {"
3491
3492 ˇ// Foo
3493 "});
3494 }
3495 // Ensure that comment continuations can be disabled.
3496 update_test_language_settings(cx, |settings| {
3497 settings.defaults.extend_comment_on_newline = Some(false);
3498 });
3499 let mut cx = EditorTestContext::new(cx).await;
3500 cx.set_state(indoc! {"
3501 // Fooˇ
3502 "});
3503 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3504 cx.assert_editor_state(indoc! {"
3505 // Foo
3506 ˇ
3507 "});
3508}
3509
3510#[gpui::test]
3511async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3512 init_test(cx, |settings| {
3513 settings.defaults.tab_size = NonZeroU32::new(4)
3514 });
3515
3516 let language = Arc::new(Language::new(
3517 LanguageConfig {
3518 line_comments: vec!["// ".into(), "/// ".into()],
3519 ..LanguageConfig::default()
3520 },
3521 None,
3522 ));
3523 {
3524 let mut cx = EditorTestContext::new(cx).await;
3525 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3526 cx.set_state(indoc! {"
3527 //ˇ
3528 "});
3529 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3530 cx.assert_editor_state(indoc! {"
3531 //
3532 // ˇ
3533 "});
3534
3535 cx.set_state(indoc! {"
3536 ///ˇ
3537 "});
3538 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3539 cx.assert_editor_state(indoc! {"
3540 ///
3541 /// ˇ
3542 "});
3543 }
3544}
3545
3546#[gpui::test]
3547async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3548 init_test(cx, |settings| {
3549 settings.defaults.tab_size = NonZeroU32::new(4)
3550 });
3551
3552 let language = Arc::new(
3553 Language::new(
3554 LanguageConfig {
3555 documentation_comment: Some(language::BlockCommentConfig {
3556 start: "/**".into(),
3557 end: "*/".into(),
3558 prefix: "* ".into(),
3559 tab_size: 1,
3560 }),
3561
3562 ..LanguageConfig::default()
3563 },
3564 Some(tree_sitter_rust::LANGUAGE.into()),
3565 )
3566 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3567 .unwrap(),
3568 );
3569
3570 {
3571 let mut cx = EditorTestContext::new(cx).await;
3572 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3573 cx.set_state(indoc! {"
3574 /**ˇ
3575 "});
3576
3577 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3578 cx.assert_editor_state(indoc! {"
3579 /**
3580 * ˇ
3581 "});
3582 // Ensure that if cursor is before the comment start,
3583 // we do not actually insert a comment prefix.
3584 cx.set_state(indoc! {"
3585 ˇ/**
3586 "});
3587 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3588 cx.assert_editor_state(indoc! {"
3589
3590 ˇ/**
3591 "});
3592 // Ensure that if cursor is between it doesn't add comment prefix.
3593 cx.set_state(indoc! {"
3594 /*ˇ*
3595 "});
3596 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3597 cx.assert_editor_state(indoc! {"
3598 /*
3599 ˇ*
3600 "});
3601 // Ensure that if suffix exists on same line after cursor it adds new line.
3602 cx.set_state(indoc! {"
3603 /**ˇ*/
3604 "});
3605 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3606 cx.assert_editor_state(indoc! {"
3607 /**
3608 * ˇ
3609 */
3610 "});
3611 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3612 cx.set_state(indoc! {"
3613 /**ˇ */
3614 "});
3615 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3616 cx.assert_editor_state(indoc! {"
3617 /**
3618 * ˇ
3619 */
3620 "});
3621 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3622 cx.set_state(indoc! {"
3623 /** ˇ*/
3624 "});
3625 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3626 cx.assert_editor_state(
3627 indoc! {"
3628 /**s
3629 * ˇ
3630 */
3631 "}
3632 .replace("s", " ") // s is used as space placeholder to prevent format on save
3633 .as_str(),
3634 );
3635 // Ensure that delimiter space is preserved when newline on already
3636 // spaced delimiter.
3637 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3638 cx.assert_editor_state(
3639 indoc! {"
3640 /**s
3641 *s
3642 * ˇ
3643 */
3644 "}
3645 .replace("s", " ") // s is used as space placeholder to prevent format on save
3646 .as_str(),
3647 );
3648 // Ensure that delimiter space is preserved when space is not
3649 // on existing delimiter.
3650 cx.set_state(indoc! {"
3651 /**
3652 *ˇ
3653 */
3654 "});
3655 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3656 cx.assert_editor_state(indoc! {"
3657 /**
3658 *
3659 * ˇ
3660 */
3661 "});
3662 // Ensure that if suffix exists on same line after cursor it
3663 // doesn't add extra new line if prefix is not on same line.
3664 cx.set_state(indoc! {"
3665 /**
3666 ˇ*/
3667 "});
3668 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3669 cx.assert_editor_state(indoc! {"
3670 /**
3671
3672 ˇ*/
3673 "});
3674 // Ensure that it detects suffix after existing prefix.
3675 cx.set_state(indoc! {"
3676 /**ˇ/
3677 "});
3678 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3679 cx.assert_editor_state(indoc! {"
3680 /**
3681 ˇ/
3682 "});
3683 // Ensure that if suffix exists on same line before
3684 // cursor it does not add comment prefix.
3685 cx.set_state(indoc! {"
3686 /** */ˇ
3687 "});
3688 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3689 cx.assert_editor_state(indoc! {"
3690 /** */
3691 ˇ
3692 "});
3693 // Ensure that if suffix exists on same line before
3694 // cursor it does not add comment prefix.
3695 cx.set_state(indoc! {"
3696 /**
3697 *
3698 */ˇ
3699 "});
3700 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3701 cx.assert_editor_state(indoc! {"
3702 /**
3703 *
3704 */
3705 ˇ
3706 "});
3707
3708 // Ensure that inline comment followed by code
3709 // doesn't add comment prefix on newline
3710 cx.set_state(indoc! {"
3711 /** */ textˇ
3712 "});
3713 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3714 cx.assert_editor_state(indoc! {"
3715 /** */ text
3716 ˇ
3717 "});
3718
3719 // Ensure that text after comment end tag
3720 // doesn't add comment prefix on newline
3721 cx.set_state(indoc! {"
3722 /**
3723 *
3724 */ˇtext
3725 "});
3726 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3727 cx.assert_editor_state(indoc! {"
3728 /**
3729 *
3730 */
3731 ˇtext
3732 "});
3733
3734 // Ensure if not comment block it doesn't
3735 // add comment prefix on newline
3736 cx.set_state(indoc! {"
3737 * textˇ
3738 "});
3739 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3740 cx.assert_editor_state(indoc! {"
3741 * text
3742 ˇ
3743 "});
3744 }
3745 // Ensure that comment continuations can be disabled.
3746 update_test_language_settings(cx, |settings| {
3747 settings.defaults.extend_comment_on_newline = Some(false);
3748 });
3749 let mut cx = EditorTestContext::new(cx).await;
3750 cx.set_state(indoc! {"
3751 /**ˇ
3752 "});
3753 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3754 cx.assert_editor_state(indoc! {"
3755 /**
3756 ˇ
3757 "});
3758}
3759
3760#[gpui::test]
3761async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3762 init_test(cx, |settings| {
3763 settings.defaults.tab_size = NonZeroU32::new(4)
3764 });
3765
3766 let lua_language = Arc::new(Language::new(
3767 LanguageConfig {
3768 line_comments: vec!["--".into()],
3769 block_comment: Some(language::BlockCommentConfig {
3770 start: "--[[".into(),
3771 prefix: "".into(),
3772 end: "]]".into(),
3773 tab_size: 0,
3774 }),
3775 ..LanguageConfig::default()
3776 },
3777 None,
3778 ));
3779
3780 let mut cx = EditorTestContext::new(cx).await;
3781 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3782
3783 // Line with line comment should extend
3784 cx.set_state(indoc! {"
3785 --ˇ
3786 "});
3787 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3788 cx.assert_editor_state(indoc! {"
3789 --
3790 --ˇ
3791 "});
3792
3793 // Line with block comment that matches line comment should not extend
3794 cx.set_state(indoc! {"
3795 --[[ˇ
3796 "});
3797 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3798 cx.assert_editor_state(indoc! {"
3799 --[[
3800 ˇ
3801 "});
3802}
3803
3804#[gpui::test]
3805fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3806 init_test(cx, |_| {});
3807
3808 let editor = cx.add_window(|window, cx| {
3809 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3810 let mut editor = build_editor(buffer, window, cx);
3811 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3812 s.select_ranges([
3813 MultiBufferOffset(3)..MultiBufferOffset(4),
3814 MultiBufferOffset(11)..MultiBufferOffset(12),
3815 MultiBufferOffset(19)..MultiBufferOffset(20),
3816 ])
3817 });
3818 editor
3819 });
3820
3821 _ = editor.update(cx, |editor, window, cx| {
3822 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3823 editor.buffer.update(cx, |buffer, cx| {
3824 buffer.edit(
3825 [
3826 (MultiBufferOffset(2)..MultiBufferOffset(5), ""),
3827 (MultiBufferOffset(10)..MultiBufferOffset(13), ""),
3828 (MultiBufferOffset(18)..MultiBufferOffset(21), ""),
3829 ],
3830 None,
3831 cx,
3832 );
3833 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3834 });
3835 assert_eq!(
3836 editor.selections.ranges(&editor.display_snapshot(cx)),
3837 &[
3838 MultiBufferOffset(2)..MultiBufferOffset(2),
3839 MultiBufferOffset(7)..MultiBufferOffset(7),
3840 MultiBufferOffset(12)..MultiBufferOffset(12)
3841 ],
3842 );
3843
3844 editor.insert("Z", window, cx);
3845 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3846
3847 // The selections are moved after the inserted characters
3848 assert_eq!(
3849 editor.selections.ranges(&editor.display_snapshot(cx)),
3850 &[
3851 MultiBufferOffset(3)..MultiBufferOffset(3),
3852 MultiBufferOffset(9)..MultiBufferOffset(9),
3853 MultiBufferOffset(15)..MultiBufferOffset(15)
3854 ],
3855 );
3856 });
3857}
3858
3859#[gpui::test]
3860async fn test_tab(cx: &mut TestAppContext) {
3861 init_test(cx, |settings| {
3862 settings.defaults.tab_size = NonZeroU32::new(3)
3863 });
3864
3865 let mut cx = EditorTestContext::new(cx).await;
3866 cx.set_state(indoc! {"
3867 ˇabˇc
3868 ˇ🏀ˇ🏀ˇefg
3869 dˇ
3870 "});
3871 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3872 cx.assert_editor_state(indoc! {"
3873 ˇab ˇc
3874 ˇ🏀 ˇ🏀 ˇefg
3875 d ˇ
3876 "});
3877
3878 cx.set_state(indoc! {"
3879 a
3880 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3881 "});
3882 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3883 cx.assert_editor_state(indoc! {"
3884 a
3885 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3886 "});
3887}
3888
3889#[gpui::test]
3890async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3891 init_test(cx, |_| {});
3892
3893 let mut cx = EditorTestContext::new(cx).await;
3894 let language = Arc::new(
3895 Language::new(
3896 LanguageConfig::default(),
3897 Some(tree_sitter_rust::LANGUAGE.into()),
3898 )
3899 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3900 .unwrap(),
3901 );
3902 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3903
3904 // test when all cursors are not at suggested indent
3905 // then simply move to their suggested indent location
3906 cx.set_state(indoc! {"
3907 const a: B = (
3908 c(
3909 ˇ
3910 ˇ )
3911 );
3912 "});
3913 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3914 cx.assert_editor_state(indoc! {"
3915 const a: B = (
3916 c(
3917 ˇ
3918 ˇ)
3919 );
3920 "});
3921
3922 // test cursor already at suggested indent not moving when
3923 // other cursors are yet to reach their suggested indents
3924 cx.set_state(indoc! {"
3925 ˇ
3926 const a: B = (
3927 c(
3928 d(
3929 ˇ
3930 )
3931 ˇ
3932 ˇ )
3933 );
3934 "});
3935 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3936 cx.assert_editor_state(indoc! {"
3937 ˇ
3938 const a: B = (
3939 c(
3940 d(
3941 ˇ
3942 )
3943 ˇ
3944 ˇ)
3945 );
3946 "});
3947 // test when all cursors are at suggested indent then tab is inserted
3948 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3949 cx.assert_editor_state(indoc! {"
3950 ˇ
3951 const a: B = (
3952 c(
3953 d(
3954 ˇ
3955 )
3956 ˇ
3957 ˇ)
3958 );
3959 "});
3960
3961 // test when current indent is less than suggested indent,
3962 // we adjust line to match suggested indent and move cursor to it
3963 //
3964 // when no other cursor is at word boundary, all of them should move
3965 cx.set_state(indoc! {"
3966 const a: B = (
3967 c(
3968 d(
3969 ˇ
3970 ˇ )
3971 ˇ )
3972 );
3973 "});
3974 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3975 cx.assert_editor_state(indoc! {"
3976 const a: B = (
3977 c(
3978 d(
3979 ˇ
3980 ˇ)
3981 ˇ)
3982 );
3983 "});
3984
3985 // test when current indent is less than suggested indent,
3986 // we adjust line to match suggested indent and move cursor to it
3987 //
3988 // when some other cursor is at word boundary, it should not move
3989 cx.set_state(indoc! {"
3990 const a: B = (
3991 c(
3992 d(
3993 ˇ
3994 ˇ )
3995 ˇ)
3996 );
3997 "});
3998 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3999 cx.assert_editor_state(indoc! {"
4000 const a: B = (
4001 c(
4002 d(
4003 ˇ
4004 ˇ)
4005 ˇ)
4006 );
4007 "});
4008
4009 // test when current indent is more than suggested indent,
4010 // we just move cursor to current indent instead of suggested indent
4011 //
4012 // when no other cursor is at word boundary, all of them should move
4013 cx.set_state(indoc! {"
4014 const a: B = (
4015 c(
4016 d(
4017 ˇ
4018 ˇ )
4019 ˇ )
4020 );
4021 "});
4022 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4023 cx.assert_editor_state(indoc! {"
4024 const a: B = (
4025 c(
4026 d(
4027 ˇ
4028 ˇ)
4029 ˇ)
4030 );
4031 "});
4032 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4033 cx.assert_editor_state(indoc! {"
4034 const a: B = (
4035 c(
4036 d(
4037 ˇ
4038 ˇ)
4039 ˇ)
4040 );
4041 "});
4042
4043 // test when current indent is more than suggested indent,
4044 // we just move cursor to current indent instead of suggested indent
4045 //
4046 // when some other cursor is at word boundary, it doesn't move
4047 cx.set_state(indoc! {"
4048 const a: B = (
4049 c(
4050 d(
4051 ˇ
4052 ˇ )
4053 ˇ)
4054 );
4055 "});
4056 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4057 cx.assert_editor_state(indoc! {"
4058 const a: B = (
4059 c(
4060 d(
4061 ˇ
4062 ˇ)
4063 ˇ)
4064 );
4065 "});
4066
4067 // handle auto-indent when there are multiple cursors on the same line
4068 cx.set_state(indoc! {"
4069 const a: B = (
4070 c(
4071 ˇ ˇ
4072 ˇ )
4073 );
4074 "});
4075 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4076 cx.assert_editor_state(indoc! {"
4077 const a: B = (
4078 c(
4079 ˇ
4080 ˇ)
4081 );
4082 "});
4083}
4084
4085#[gpui::test]
4086async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
4087 init_test(cx, |settings| {
4088 settings.defaults.tab_size = NonZeroU32::new(3)
4089 });
4090
4091 let mut cx = EditorTestContext::new(cx).await;
4092 cx.set_state(indoc! {"
4093 ˇ
4094 \t ˇ
4095 \t ˇ
4096 \t ˇ
4097 \t \t\t \t \t\t \t\t \t \t ˇ
4098 "});
4099
4100 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4101 cx.assert_editor_state(indoc! {"
4102 ˇ
4103 \t ˇ
4104 \t ˇ
4105 \t ˇ
4106 \t \t\t \t \t\t \t\t \t \t ˇ
4107 "});
4108}
4109
4110#[gpui::test]
4111async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
4112 init_test(cx, |settings| {
4113 settings.defaults.tab_size = NonZeroU32::new(4)
4114 });
4115
4116 let language = Arc::new(
4117 Language::new(
4118 LanguageConfig::default(),
4119 Some(tree_sitter_rust::LANGUAGE.into()),
4120 )
4121 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
4122 .unwrap(),
4123 );
4124
4125 let mut cx = EditorTestContext::new(cx).await;
4126 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4127 cx.set_state(indoc! {"
4128 fn a() {
4129 if b {
4130 \t ˇc
4131 }
4132 }
4133 "});
4134
4135 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4136 cx.assert_editor_state(indoc! {"
4137 fn a() {
4138 if b {
4139 ˇc
4140 }
4141 }
4142 "});
4143}
4144
4145#[gpui::test]
4146async fn test_indent_outdent(cx: &mut TestAppContext) {
4147 init_test(cx, |settings| {
4148 settings.defaults.tab_size = NonZeroU32::new(4);
4149 });
4150
4151 let mut cx = EditorTestContext::new(cx).await;
4152
4153 cx.set_state(indoc! {"
4154 «oneˇ» «twoˇ»
4155 three
4156 four
4157 "});
4158 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4159 cx.assert_editor_state(indoc! {"
4160 «oneˇ» «twoˇ»
4161 three
4162 four
4163 "});
4164
4165 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4166 cx.assert_editor_state(indoc! {"
4167 «oneˇ» «twoˇ»
4168 three
4169 four
4170 "});
4171
4172 // select across line ending
4173 cx.set_state(indoc! {"
4174 one two
4175 t«hree
4176 ˇ» four
4177 "});
4178 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4179 cx.assert_editor_state(indoc! {"
4180 one two
4181 t«hree
4182 ˇ» four
4183 "});
4184
4185 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4186 cx.assert_editor_state(indoc! {"
4187 one two
4188 t«hree
4189 ˇ» four
4190 "});
4191
4192 // Ensure that indenting/outdenting works when the cursor is at column 0.
4193 cx.set_state(indoc! {"
4194 one two
4195 ˇthree
4196 four
4197 "});
4198 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4199 cx.assert_editor_state(indoc! {"
4200 one two
4201 ˇthree
4202 four
4203 "});
4204
4205 cx.set_state(indoc! {"
4206 one two
4207 ˇ three
4208 four
4209 "});
4210 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4211 cx.assert_editor_state(indoc! {"
4212 one two
4213 ˇthree
4214 four
4215 "});
4216}
4217
4218#[gpui::test]
4219async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4220 // This is a regression test for issue #33761
4221 init_test(cx, |_| {});
4222
4223 let mut cx = EditorTestContext::new(cx).await;
4224 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4225 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4226
4227 cx.set_state(
4228 r#"ˇ# ingress:
4229ˇ# api:
4230ˇ# enabled: false
4231ˇ# pathType: Prefix
4232ˇ# console:
4233ˇ# enabled: false
4234ˇ# pathType: Prefix
4235"#,
4236 );
4237
4238 // Press tab to indent all lines
4239 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4240
4241 cx.assert_editor_state(
4242 r#" ˇ# ingress:
4243 ˇ# api:
4244 ˇ# enabled: false
4245 ˇ# pathType: Prefix
4246 ˇ# console:
4247 ˇ# enabled: false
4248 ˇ# pathType: Prefix
4249"#,
4250 );
4251}
4252
4253#[gpui::test]
4254async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4255 // This is a test to make sure our fix for issue #33761 didn't break anything
4256 init_test(cx, |_| {});
4257
4258 let mut cx = EditorTestContext::new(cx).await;
4259 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4260 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4261
4262 cx.set_state(
4263 r#"ˇingress:
4264ˇ api:
4265ˇ enabled: false
4266ˇ pathType: Prefix
4267"#,
4268 );
4269
4270 // Press tab to indent all lines
4271 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4272
4273 cx.assert_editor_state(
4274 r#"ˇingress:
4275 ˇapi:
4276 ˇenabled: false
4277 ˇpathType: Prefix
4278"#,
4279 );
4280}
4281
4282#[gpui::test]
4283async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4284 init_test(cx, |settings| {
4285 settings.defaults.hard_tabs = Some(true);
4286 });
4287
4288 let mut cx = EditorTestContext::new(cx).await;
4289
4290 // select two ranges on one line
4291 cx.set_state(indoc! {"
4292 «oneˇ» «twoˇ»
4293 three
4294 four
4295 "});
4296 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4297 cx.assert_editor_state(indoc! {"
4298 \t«oneˇ» «twoˇ»
4299 three
4300 four
4301 "});
4302 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4303 cx.assert_editor_state(indoc! {"
4304 \t\t«oneˇ» «twoˇ»
4305 three
4306 four
4307 "});
4308 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4309 cx.assert_editor_state(indoc! {"
4310 \t«oneˇ» «twoˇ»
4311 three
4312 four
4313 "});
4314 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4315 cx.assert_editor_state(indoc! {"
4316 «oneˇ» «twoˇ»
4317 three
4318 four
4319 "});
4320
4321 // select across a line ending
4322 cx.set_state(indoc! {"
4323 one two
4324 t«hree
4325 ˇ»four
4326 "});
4327 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4328 cx.assert_editor_state(indoc! {"
4329 one two
4330 \tt«hree
4331 ˇ»four
4332 "});
4333 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4334 cx.assert_editor_state(indoc! {"
4335 one two
4336 \t\tt«hree
4337 ˇ»four
4338 "});
4339 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4340 cx.assert_editor_state(indoc! {"
4341 one two
4342 \tt«hree
4343 ˇ»four
4344 "});
4345 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4346 cx.assert_editor_state(indoc! {"
4347 one two
4348 t«hree
4349 ˇ»four
4350 "});
4351
4352 // Ensure that indenting/outdenting works when the cursor is at column 0.
4353 cx.set_state(indoc! {"
4354 one two
4355 ˇthree
4356 four
4357 "});
4358 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4359 cx.assert_editor_state(indoc! {"
4360 one two
4361 ˇthree
4362 four
4363 "});
4364 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4365 cx.assert_editor_state(indoc! {"
4366 one two
4367 \tˇthree
4368 four
4369 "});
4370 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4371 cx.assert_editor_state(indoc! {"
4372 one two
4373 ˇthree
4374 four
4375 "});
4376}
4377
4378#[gpui::test]
4379fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4380 init_test(cx, |settings| {
4381 settings.languages.0.extend([
4382 (
4383 "TOML".into(),
4384 LanguageSettingsContent {
4385 tab_size: NonZeroU32::new(2),
4386 ..Default::default()
4387 },
4388 ),
4389 (
4390 "Rust".into(),
4391 LanguageSettingsContent {
4392 tab_size: NonZeroU32::new(4),
4393 ..Default::default()
4394 },
4395 ),
4396 ]);
4397 });
4398
4399 let toml_language = Arc::new(Language::new(
4400 LanguageConfig {
4401 name: "TOML".into(),
4402 ..Default::default()
4403 },
4404 None,
4405 ));
4406 let rust_language = Arc::new(Language::new(
4407 LanguageConfig {
4408 name: "Rust".into(),
4409 ..Default::default()
4410 },
4411 None,
4412 ));
4413
4414 let toml_buffer =
4415 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4416 let rust_buffer =
4417 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4418 let multibuffer = cx.new(|cx| {
4419 let mut multibuffer = MultiBuffer::new(ReadWrite);
4420 multibuffer.push_excerpts(
4421 toml_buffer.clone(),
4422 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4423 cx,
4424 );
4425 multibuffer.push_excerpts(
4426 rust_buffer.clone(),
4427 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4428 cx,
4429 );
4430 multibuffer
4431 });
4432
4433 cx.add_window(|window, cx| {
4434 let mut editor = build_editor(multibuffer, window, cx);
4435
4436 assert_eq!(
4437 editor.text(cx),
4438 indoc! {"
4439 a = 1
4440 b = 2
4441
4442 const c: usize = 3;
4443 "}
4444 );
4445
4446 select_ranges(
4447 &mut editor,
4448 indoc! {"
4449 «aˇ» = 1
4450 b = 2
4451
4452 «const c:ˇ» usize = 3;
4453 "},
4454 window,
4455 cx,
4456 );
4457
4458 editor.tab(&Tab, window, cx);
4459 assert_text_with_selections(
4460 &mut editor,
4461 indoc! {"
4462 «aˇ» = 1
4463 b = 2
4464
4465 «const c:ˇ» usize = 3;
4466 "},
4467 cx,
4468 );
4469 editor.backtab(&Backtab, window, cx);
4470 assert_text_with_selections(
4471 &mut editor,
4472 indoc! {"
4473 «aˇ» = 1
4474 b = 2
4475
4476 «const c:ˇ» usize = 3;
4477 "},
4478 cx,
4479 );
4480
4481 editor
4482 });
4483}
4484
4485#[gpui::test]
4486async fn test_backspace(cx: &mut TestAppContext) {
4487 init_test(cx, |_| {});
4488
4489 let mut cx = EditorTestContext::new(cx).await;
4490
4491 // Basic backspace
4492 cx.set_state(indoc! {"
4493 onˇe two three
4494 fou«rˇ» five six
4495 seven «ˇeight nine
4496 »ten
4497 "});
4498 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4499 cx.assert_editor_state(indoc! {"
4500 oˇe two three
4501 fouˇ five six
4502 seven ˇten
4503 "});
4504
4505 // Test backspace inside and around indents
4506 cx.set_state(indoc! {"
4507 zero
4508 ˇone
4509 ˇtwo
4510 ˇ ˇ ˇ three
4511 ˇ ˇ four
4512 "});
4513 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4514 cx.assert_editor_state(indoc! {"
4515 zero
4516 ˇone
4517 ˇtwo
4518 ˇ threeˇ four
4519 "});
4520}
4521
4522#[gpui::test]
4523async fn test_delete(cx: &mut TestAppContext) {
4524 init_test(cx, |_| {});
4525
4526 let mut cx = EditorTestContext::new(cx).await;
4527 cx.set_state(indoc! {"
4528 onˇe two three
4529 fou«rˇ» five six
4530 seven «ˇeight nine
4531 »ten
4532 "});
4533 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4534 cx.assert_editor_state(indoc! {"
4535 onˇ two three
4536 fouˇ five six
4537 seven ˇten
4538 "});
4539}
4540
4541#[gpui::test]
4542fn test_delete_line(cx: &mut TestAppContext) {
4543 init_test(cx, |_| {});
4544
4545 let editor = cx.add_window(|window, cx| {
4546 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4547 build_editor(buffer, window, cx)
4548 });
4549 _ = editor.update(cx, |editor, window, cx| {
4550 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4551 s.select_display_ranges([
4552 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4553 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4554 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4555 ])
4556 });
4557 editor.delete_line(&DeleteLine, window, cx);
4558 assert_eq!(editor.display_text(cx), "ghi");
4559 assert_eq!(
4560 display_ranges(editor, cx),
4561 vec![
4562 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4563 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4564 ]
4565 );
4566 });
4567
4568 let editor = cx.add_window(|window, cx| {
4569 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4570 build_editor(buffer, window, cx)
4571 });
4572 _ = editor.update(cx, |editor, window, cx| {
4573 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4574 s.select_display_ranges([
4575 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4576 ])
4577 });
4578 editor.delete_line(&DeleteLine, window, cx);
4579 assert_eq!(editor.display_text(cx), "ghi\n");
4580 assert_eq!(
4581 display_ranges(editor, cx),
4582 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4583 );
4584 });
4585
4586 let editor = cx.add_window(|window, cx| {
4587 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4588 build_editor(buffer, window, cx)
4589 });
4590 _ = editor.update(cx, |editor, window, cx| {
4591 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4592 s.select_display_ranges([
4593 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4594 ])
4595 });
4596 editor.delete_line(&DeleteLine, window, cx);
4597 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4598 assert_eq!(
4599 display_ranges(editor, cx),
4600 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4601 );
4602 });
4603}
4604
4605#[gpui::test]
4606fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4607 init_test(cx, |_| {});
4608
4609 cx.add_window(|window, cx| {
4610 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4611 let mut editor = build_editor(buffer.clone(), window, cx);
4612 let buffer = buffer.read(cx).as_singleton().unwrap();
4613
4614 assert_eq!(
4615 editor
4616 .selections
4617 .ranges::<Point>(&editor.display_snapshot(cx)),
4618 &[Point::new(0, 0)..Point::new(0, 0)]
4619 );
4620
4621 // When on single line, replace newline at end by space
4622 editor.join_lines(&JoinLines, window, cx);
4623 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4624 assert_eq!(
4625 editor
4626 .selections
4627 .ranges::<Point>(&editor.display_snapshot(cx)),
4628 &[Point::new(0, 3)..Point::new(0, 3)]
4629 );
4630
4631 // When multiple lines are selected, remove newlines that are spanned by the selection
4632 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4633 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4634 });
4635 editor.join_lines(&JoinLines, window, cx);
4636 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4637 assert_eq!(
4638 editor
4639 .selections
4640 .ranges::<Point>(&editor.display_snapshot(cx)),
4641 &[Point::new(0, 11)..Point::new(0, 11)]
4642 );
4643
4644 // Undo should be transactional
4645 editor.undo(&Undo, window, cx);
4646 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4647 assert_eq!(
4648 editor
4649 .selections
4650 .ranges::<Point>(&editor.display_snapshot(cx)),
4651 &[Point::new(0, 5)..Point::new(2, 2)]
4652 );
4653
4654 // When joining an empty line don't insert a space
4655 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4656 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4657 });
4658 editor.join_lines(&JoinLines, window, cx);
4659 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4660 assert_eq!(
4661 editor
4662 .selections
4663 .ranges::<Point>(&editor.display_snapshot(cx)),
4664 [Point::new(2, 3)..Point::new(2, 3)]
4665 );
4666
4667 // We can remove trailing newlines
4668 editor.join_lines(&JoinLines, window, cx);
4669 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4670 assert_eq!(
4671 editor
4672 .selections
4673 .ranges::<Point>(&editor.display_snapshot(cx)),
4674 [Point::new(2, 3)..Point::new(2, 3)]
4675 );
4676
4677 // We don't blow up on the last line
4678 editor.join_lines(&JoinLines, window, cx);
4679 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4680 assert_eq!(
4681 editor
4682 .selections
4683 .ranges::<Point>(&editor.display_snapshot(cx)),
4684 [Point::new(2, 3)..Point::new(2, 3)]
4685 );
4686
4687 // reset to test indentation
4688 editor.buffer.update(cx, |buffer, cx| {
4689 buffer.edit(
4690 [
4691 (Point::new(1, 0)..Point::new(1, 2), " "),
4692 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4693 ],
4694 None,
4695 cx,
4696 )
4697 });
4698
4699 // We remove any leading spaces
4700 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4701 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4702 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4703 });
4704 editor.join_lines(&JoinLines, window, cx);
4705 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4706
4707 // We don't insert a space for a line containing only spaces
4708 editor.join_lines(&JoinLines, window, cx);
4709 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4710
4711 // We ignore any leading tabs
4712 editor.join_lines(&JoinLines, window, cx);
4713 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4714
4715 editor
4716 });
4717}
4718
4719#[gpui::test]
4720fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4721 init_test(cx, |_| {});
4722
4723 cx.add_window(|window, cx| {
4724 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4725 let mut editor = build_editor(buffer.clone(), window, cx);
4726 let buffer = buffer.read(cx).as_singleton().unwrap();
4727
4728 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4729 s.select_ranges([
4730 Point::new(0, 2)..Point::new(1, 1),
4731 Point::new(1, 2)..Point::new(1, 2),
4732 Point::new(3, 1)..Point::new(3, 2),
4733 ])
4734 });
4735
4736 editor.join_lines(&JoinLines, window, cx);
4737 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4738
4739 assert_eq!(
4740 editor
4741 .selections
4742 .ranges::<Point>(&editor.display_snapshot(cx)),
4743 [
4744 Point::new(0, 7)..Point::new(0, 7),
4745 Point::new(1, 3)..Point::new(1, 3)
4746 ]
4747 );
4748 editor
4749 });
4750}
4751
4752#[gpui::test]
4753async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4754 init_test(cx, |_| {});
4755
4756 let mut cx = EditorTestContext::new(cx).await;
4757
4758 let diff_base = r#"
4759 Line 0
4760 Line 1
4761 Line 2
4762 Line 3
4763 "#
4764 .unindent();
4765
4766 cx.set_state(
4767 &r#"
4768 ˇLine 0
4769 Line 1
4770 Line 2
4771 Line 3
4772 "#
4773 .unindent(),
4774 );
4775
4776 cx.set_head_text(&diff_base);
4777 executor.run_until_parked();
4778
4779 // Join lines
4780 cx.update_editor(|editor, window, cx| {
4781 editor.join_lines(&JoinLines, window, cx);
4782 });
4783 executor.run_until_parked();
4784
4785 cx.assert_editor_state(
4786 &r#"
4787 Line 0ˇ Line 1
4788 Line 2
4789 Line 3
4790 "#
4791 .unindent(),
4792 );
4793 // Join again
4794 cx.update_editor(|editor, window, cx| {
4795 editor.join_lines(&JoinLines, window, cx);
4796 });
4797 executor.run_until_parked();
4798
4799 cx.assert_editor_state(
4800 &r#"
4801 Line 0 Line 1ˇ Line 2
4802 Line 3
4803 "#
4804 .unindent(),
4805 );
4806}
4807
4808#[gpui::test]
4809async fn test_custom_newlines_cause_no_false_positive_diffs(
4810 executor: BackgroundExecutor,
4811 cx: &mut TestAppContext,
4812) {
4813 init_test(cx, |_| {});
4814 let mut cx = EditorTestContext::new(cx).await;
4815 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4816 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4817 executor.run_until_parked();
4818
4819 cx.update_editor(|editor, window, cx| {
4820 let snapshot = editor.snapshot(window, cx);
4821 assert_eq!(
4822 snapshot
4823 .buffer_snapshot()
4824 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
4825 .collect::<Vec<_>>(),
4826 Vec::new(),
4827 "Should not have any diffs for files with custom newlines"
4828 );
4829 });
4830}
4831
4832#[gpui::test]
4833async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4834 init_test(cx, |_| {});
4835
4836 let mut cx = EditorTestContext::new(cx).await;
4837
4838 // Test sort_lines_case_insensitive()
4839 cx.set_state(indoc! {"
4840 «z
4841 y
4842 x
4843 Z
4844 Y
4845 Xˇ»
4846 "});
4847 cx.update_editor(|e, window, cx| {
4848 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4849 });
4850 cx.assert_editor_state(indoc! {"
4851 «x
4852 X
4853 y
4854 Y
4855 z
4856 Zˇ»
4857 "});
4858
4859 // Test sort_lines_by_length()
4860 //
4861 // Demonstrates:
4862 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4863 // - sort is stable
4864 cx.set_state(indoc! {"
4865 «123
4866 æ
4867 12
4868 ∞
4869 1
4870 æˇ»
4871 "});
4872 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4873 cx.assert_editor_state(indoc! {"
4874 «æ
4875 ∞
4876 1
4877 æ
4878 12
4879 123ˇ»
4880 "});
4881
4882 // Test reverse_lines()
4883 cx.set_state(indoc! {"
4884 «5
4885 4
4886 3
4887 2
4888 1ˇ»
4889 "});
4890 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4891 cx.assert_editor_state(indoc! {"
4892 «1
4893 2
4894 3
4895 4
4896 5ˇ»
4897 "});
4898
4899 // Skip testing shuffle_line()
4900
4901 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4902 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4903
4904 // Don't manipulate when cursor is on single line, but expand the selection
4905 cx.set_state(indoc! {"
4906 ddˇdd
4907 ccc
4908 bb
4909 a
4910 "});
4911 cx.update_editor(|e, window, cx| {
4912 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4913 });
4914 cx.assert_editor_state(indoc! {"
4915 «ddddˇ»
4916 ccc
4917 bb
4918 a
4919 "});
4920
4921 // Basic manipulate case
4922 // Start selection moves to column 0
4923 // End of selection shrinks to fit shorter line
4924 cx.set_state(indoc! {"
4925 dd«d
4926 ccc
4927 bb
4928 aaaaaˇ»
4929 "});
4930 cx.update_editor(|e, window, cx| {
4931 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4932 });
4933 cx.assert_editor_state(indoc! {"
4934 «aaaaa
4935 bb
4936 ccc
4937 dddˇ»
4938 "});
4939
4940 // Manipulate case with newlines
4941 cx.set_state(indoc! {"
4942 dd«d
4943 ccc
4944
4945 bb
4946 aaaaa
4947
4948 ˇ»
4949 "});
4950 cx.update_editor(|e, window, cx| {
4951 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4952 });
4953 cx.assert_editor_state(indoc! {"
4954 «
4955
4956 aaaaa
4957 bb
4958 ccc
4959 dddˇ»
4960
4961 "});
4962
4963 // Adding new line
4964 cx.set_state(indoc! {"
4965 aa«a
4966 bbˇ»b
4967 "});
4968 cx.update_editor(|e, window, cx| {
4969 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4970 });
4971 cx.assert_editor_state(indoc! {"
4972 «aaa
4973 bbb
4974 added_lineˇ»
4975 "});
4976
4977 // Removing line
4978 cx.set_state(indoc! {"
4979 aa«a
4980 bbbˇ»
4981 "});
4982 cx.update_editor(|e, window, cx| {
4983 e.manipulate_immutable_lines(window, cx, |lines| {
4984 lines.pop();
4985 })
4986 });
4987 cx.assert_editor_state(indoc! {"
4988 «aaaˇ»
4989 "});
4990
4991 // Removing all lines
4992 cx.set_state(indoc! {"
4993 aa«a
4994 bbbˇ»
4995 "});
4996 cx.update_editor(|e, window, cx| {
4997 e.manipulate_immutable_lines(window, cx, |lines| {
4998 lines.drain(..);
4999 })
5000 });
5001 cx.assert_editor_state(indoc! {"
5002 ˇ
5003 "});
5004}
5005
5006#[gpui::test]
5007async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
5008 init_test(cx, |_| {});
5009
5010 let mut cx = EditorTestContext::new(cx).await;
5011
5012 // Consider continuous selection as single selection
5013 cx.set_state(indoc! {"
5014 Aaa«aa
5015 cˇ»c«c
5016 bb
5017 aaaˇ»aa
5018 "});
5019 cx.update_editor(|e, window, cx| {
5020 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5021 });
5022 cx.assert_editor_state(indoc! {"
5023 «Aaaaa
5024 ccc
5025 bb
5026 aaaaaˇ»
5027 "});
5028
5029 cx.set_state(indoc! {"
5030 Aaa«aa
5031 cˇ»c«c
5032 bb
5033 aaaˇ»aa
5034 "});
5035 cx.update_editor(|e, window, cx| {
5036 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5037 });
5038 cx.assert_editor_state(indoc! {"
5039 «Aaaaa
5040 ccc
5041 bbˇ»
5042 "});
5043
5044 // Consider non continuous selection as distinct dedup operations
5045 cx.set_state(indoc! {"
5046 «aaaaa
5047 bb
5048 aaaaa
5049 aaaaaˇ»
5050
5051 aaa«aaˇ»
5052 "});
5053 cx.update_editor(|e, window, cx| {
5054 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5055 });
5056 cx.assert_editor_state(indoc! {"
5057 «aaaaa
5058 bbˇ»
5059
5060 «aaaaaˇ»
5061 "});
5062}
5063
5064#[gpui::test]
5065async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
5066 init_test(cx, |_| {});
5067
5068 let mut cx = EditorTestContext::new(cx).await;
5069
5070 cx.set_state(indoc! {"
5071 «Aaa
5072 aAa
5073 Aaaˇ»
5074 "});
5075 cx.update_editor(|e, window, cx| {
5076 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5077 });
5078 cx.assert_editor_state(indoc! {"
5079 «Aaa
5080 aAaˇ»
5081 "});
5082
5083 cx.set_state(indoc! {"
5084 «Aaa
5085 aAa
5086 aaAˇ»
5087 "});
5088 cx.update_editor(|e, window, cx| {
5089 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5090 });
5091 cx.assert_editor_state(indoc! {"
5092 «Aaaˇ»
5093 "});
5094}
5095
5096#[gpui::test]
5097async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
5098 init_test(cx, |_| {});
5099
5100 let mut cx = EditorTestContext::new(cx).await;
5101
5102 let js_language = Arc::new(Language::new(
5103 LanguageConfig {
5104 name: "JavaScript".into(),
5105 wrap_characters: Some(language::WrapCharactersConfig {
5106 start_prefix: "<".into(),
5107 start_suffix: ">".into(),
5108 end_prefix: "</".into(),
5109 end_suffix: ">".into(),
5110 }),
5111 ..LanguageConfig::default()
5112 },
5113 None,
5114 ));
5115
5116 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5117
5118 cx.set_state(indoc! {"
5119 «testˇ»
5120 "});
5121 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5122 cx.assert_editor_state(indoc! {"
5123 <«ˇ»>test</«ˇ»>
5124 "});
5125
5126 cx.set_state(indoc! {"
5127 «test
5128 testˇ»
5129 "});
5130 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5131 cx.assert_editor_state(indoc! {"
5132 <«ˇ»>test
5133 test</«ˇ»>
5134 "});
5135
5136 cx.set_state(indoc! {"
5137 teˇst
5138 "});
5139 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5140 cx.assert_editor_state(indoc! {"
5141 te<«ˇ»></«ˇ»>st
5142 "});
5143}
5144
5145#[gpui::test]
5146async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5147 init_test(cx, |_| {});
5148
5149 let mut cx = EditorTestContext::new(cx).await;
5150
5151 let js_language = Arc::new(Language::new(
5152 LanguageConfig {
5153 name: "JavaScript".into(),
5154 wrap_characters: Some(language::WrapCharactersConfig {
5155 start_prefix: "<".into(),
5156 start_suffix: ">".into(),
5157 end_prefix: "</".into(),
5158 end_suffix: ">".into(),
5159 }),
5160 ..LanguageConfig::default()
5161 },
5162 None,
5163 ));
5164
5165 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5166
5167 cx.set_state(indoc! {"
5168 «testˇ»
5169 «testˇ» «testˇ»
5170 «testˇ»
5171 "});
5172 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5173 cx.assert_editor_state(indoc! {"
5174 <«ˇ»>test</«ˇ»>
5175 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5176 <«ˇ»>test</«ˇ»>
5177 "});
5178
5179 cx.set_state(indoc! {"
5180 «test
5181 testˇ»
5182 «test
5183 testˇ»
5184 "});
5185 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5186 cx.assert_editor_state(indoc! {"
5187 <«ˇ»>test
5188 test</«ˇ»>
5189 <«ˇ»>test
5190 test</«ˇ»>
5191 "});
5192}
5193
5194#[gpui::test]
5195async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5196 init_test(cx, |_| {});
5197
5198 let mut cx = EditorTestContext::new(cx).await;
5199
5200 let plaintext_language = Arc::new(Language::new(
5201 LanguageConfig {
5202 name: "Plain Text".into(),
5203 ..LanguageConfig::default()
5204 },
5205 None,
5206 ));
5207
5208 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5209
5210 cx.set_state(indoc! {"
5211 «testˇ»
5212 "});
5213 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5214 cx.assert_editor_state(indoc! {"
5215 «testˇ»
5216 "});
5217}
5218
5219#[gpui::test]
5220async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5221 init_test(cx, |_| {});
5222
5223 let mut cx = EditorTestContext::new(cx).await;
5224
5225 // Manipulate with multiple selections on a single line
5226 cx.set_state(indoc! {"
5227 dd«dd
5228 cˇ»c«c
5229 bb
5230 aaaˇ»aa
5231 "});
5232 cx.update_editor(|e, window, cx| {
5233 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5234 });
5235 cx.assert_editor_state(indoc! {"
5236 «aaaaa
5237 bb
5238 ccc
5239 ddddˇ»
5240 "});
5241
5242 // Manipulate with multiple disjoin selections
5243 cx.set_state(indoc! {"
5244 5«
5245 4
5246 3
5247 2
5248 1ˇ»
5249
5250 dd«dd
5251 ccc
5252 bb
5253 aaaˇ»aa
5254 "});
5255 cx.update_editor(|e, window, cx| {
5256 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5257 });
5258 cx.assert_editor_state(indoc! {"
5259 «1
5260 2
5261 3
5262 4
5263 5ˇ»
5264
5265 «aaaaa
5266 bb
5267 ccc
5268 ddddˇ»
5269 "});
5270
5271 // Adding lines on each selection
5272 cx.set_state(indoc! {"
5273 2«
5274 1ˇ»
5275
5276 bb«bb
5277 aaaˇ»aa
5278 "});
5279 cx.update_editor(|e, window, cx| {
5280 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5281 });
5282 cx.assert_editor_state(indoc! {"
5283 «2
5284 1
5285 added lineˇ»
5286
5287 «bbbb
5288 aaaaa
5289 added lineˇ»
5290 "});
5291
5292 // Removing lines on each selection
5293 cx.set_state(indoc! {"
5294 2«
5295 1ˇ»
5296
5297 bb«bb
5298 aaaˇ»aa
5299 "});
5300 cx.update_editor(|e, window, cx| {
5301 e.manipulate_immutable_lines(window, cx, |lines| {
5302 lines.pop();
5303 })
5304 });
5305 cx.assert_editor_state(indoc! {"
5306 «2ˇ»
5307
5308 «bbbbˇ»
5309 "});
5310}
5311
5312#[gpui::test]
5313async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5314 init_test(cx, |settings| {
5315 settings.defaults.tab_size = NonZeroU32::new(3)
5316 });
5317
5318 let mut cx = EditorTestContext::new(cx).await;
5319
5320 // MULTI SELECTION
5321 // Ln.1 "«" tests empty lines
5322 // Ln.9 tests just leading whitespace
5323 cx.set_state(indoc! {"
5324 «
5325 abc // No indentationˇ»
5326 «\tabc // 1 tabˇ»
5327 \t\tabc « ˇ» // 2 tabs
5328 \t ab«c // Tab followed by space
5329 \tabc // Space followed by tab (3 spaces should be the result)
5330 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5331 abˇ»ˇc ˇ ˇ // Already space indented«
5332 \t
5333 \tabc\tdef // Only the leading tab is manipulatedˇ»
5334 "});
5335 cx.update_editor(|e, window, cx| {
5336 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5337 });
5338 cx.assert_editor_state(
5339 indoc! {"
5340 «
5341 abc // No indentation
5342 abc // 1 tab
5343 abc // 2 tabs
5344 abc // Tab followed by space
5345 abc // Space followed by tab (3 spaces should be the result)
5346 abc // Mixed indentation (tab conversion depends on the column)
5347 abc // Already space indented
5348 ·
5349 abc\tdef // Only the leading tab is manipulatedˇ»
5350 "}
5351 .replace("·", "")
5352 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5353 );
5354
5355 // Test on just a few lines, the others should remain unchanged
5356 // Only lines (3, 5, 10, 11) should change
5357 cx.set_state(
5358 indoc! {"
5359 ·
5360 abc // No indentation
5361 \tabcˇ // 1 tab
5362 \t\tabc // 2 tabs
5363 \t abcˇ // Tab followed by space
5364 \tabc // Space followed by tab (3 spaces should be the result)
5365 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5366 abc // Already space indented
5367 «\t
5368 \tabc\tdef // Only the leading tab is manipulatedˇ»
5369 "}
5370 .replace("·", "")
5371 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5372 );
5373 cx.update_editor(|e, window, cx| {
5374 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5375 });
5376 cx.assert_editor_state(
5377 indoc! {"
5378 ·
5379 abc // No indentation
5380 « abc // 1 tabˇ»
5381 \t\tabc // 2 tabs
5382 « abc // Tab followed by spaceˇ»
5383 \tabc // Space followed by tab (3 spaces should be the result)
5384 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5385 abc // Already space indented
5386 « ·
5387 abc\tdef // Only the leading tab is manipulatedˇ»
5388 "}
5389 .replace("·", "")
5390 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5391 );
5392
5393 // SINGLE SELECTION
5394 // Ln.1 "«" tests empty lines
5395 // Ln.9 tests just leading whitespace
5396 cx.set_state(indoc! {"
5397 «
5398 abc // No indentation
5399 \tabc // 1 tab
5400 \t\tabc // 2 tabs
5401 \t abc // Tab followed by space
5402 \tabc // Space followed by tab (3 spaces should be the result)
5403 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5404 abc // Already space indented
5405 \t
5406 \tabc\tdef // Only the leading tab is manipulatedˇ»
5407 "});
5408 cx.update_editor(|e, window, cx| {
5409 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5410 });
5411 cx.assert_editor_state(
5412 indoc! {"
5413 «
5414 abc // No indentation
5415 abc // 1 tab
5416 abc // 2 tabs
5417 abc // Tab followed by space
5418 abc // Space followed by tab (3 spaces should be the result)
5419 abc // Mixed indentation (tab conversion depends on the column)
5420 abc // Already space indented
5421 ·
5422 abc\tdef // Only the leading tab is manipulatedˇ»
5423 "}
5424 .replace("·", "")
5425 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5426 );
5427}
5428
5429#[gpui::test]
5430async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5431 init_test(cx, |settings| {
5432 settings.defaults.tab_size = NonZeroU32::new(3)
5433 });
5434
5435 let mut cx = EditorTestContext::new(cx).await;
5436
5437 // MULTI SELECTION
5438 // Ln.1 "«" tests empty lines
5439 // Ln.11 tests just leading whitespace
5440 cx.set_state(indoc! {"
5441 «
5442 abˇ»ˇc // No indentation
5443 abc ˇ ˇ // 1 space (< 3 so dont convert)
5444 abc « // 2 spaces (< 3 so dont convert)
5445 abc // 3 spaces (convert)
5446 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5447 «\tˇ»\t«\tˇ»abc // Already tab indented
5448 «\t abc // Tab followed by space
5449 \tabc // Space followed by tab (should be consumed due to tab)
5450 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5451 \tˇ» «\t
5452 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5453 "});
5454 cx.update_editor(|e, window, cx| {
5455 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5456 });
5457 cx.assert_editor_state(indoc! {"
5458 «
5459 abc // No indentation
5460 abc // 1 space (< 3 so dont convert)
5461 abc // 2 spaces (< 3 so dont convert)
5462 \tabc // 3 spaces (convert)
5463 \t abc // 5 spaces (1 tab + 2 spaces)
5464 \t\t\tabc // Already tab indented
5465 \t abc // Tab followed by space
5466 \tabc // Space followed by tab (should be consumed due to tab)
5467 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5468 \t\t\t
5469 \tabc \t // Only the leading spaces should be convertedˇ»
5470 "});
5471
5472 // Test on just a few lines, the other should remain unchanged
5473 // Only lines (4, 8, 11, 12) should change
5474 cx.set_state(
5475 indoc! {"
5476 ·
5477 abc // No indentation
5478 abc // 1 space (< 3 so dont convert)
5479 abc // 2 spaces (< 3 so dont convert)
5480 « abc // 3 spaces (convert)ˇ»
5481 abc // 5 spaces (1 tab + 2 spaces)
5482 \t\t\tabc // Already tab indented
5483 \t abc // Tab followed by space
5484 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5485 \t\t \tabc // Mixed indentation
5486 \t \t \t \tabc // Mixed indentation
5487 \t \tˇ
5488 « abc \t // Only the leading spaces should be convertedˇ»
5489 "}
5490 .replace("·", "")
5491 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5492 );
5493 cx.update_editor(|e, window, cx| {
5494 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5495 });
5496 cx.assert_editor_state(
5497 indoc! {"
5498 ·
5499 abc // No indentation
5500 abc // 1 space (< 3 so dont convert)
5501 abc // 2 spaces (< 3 so dont convert)
5502 «\tabc // 3 spaces (convert)ˇ»
5503 abc // 5 spaces (1 tab + 2 spaces)
5504 \t\t\tabc // Already tab indented
5505 \t abc // Tab followed by space
5506 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5507 \t\t \tabc // Mixed indentation
5508 \t \t \t \tabc // Mixed indentation
5509 «\t\t\t
5510 \tabc \t // Only the leading spaces should be convertedˇ»
5511 "}
5512 .replace("·", "")
5513 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5514 );
5515
5516 // SINGLE SELECTION
5517 // Ln.1 "«" tests empty lines
5518 // Ln.11 tests just leading whitespace
5519 cx.set_state(indoc! {"
5520 «
5521 abc // No indentation
5522 abc // 1 space (< 3 so dont convert)
5523 abc // 2 spaces (< 3 so dont convert)
5524 abc // 3 spaces (convert)
5525 abc // 5 spaces (1 tab + 2 spaces)
5526 \t\t\tabc // Already tab indented
5527 \t abc // Tab followed by space
5528 \tabc // Space followed by tab (should be consumed due to tab)
5529 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5530 \t \t
5531 abc \t // Only the leading spaces should be convertedˇ»
5532 "});
5533 cx.update_editor(|e, window, cx| {
5534 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5535 });
5536 cx.assert_editor_state(indoc! {"
5537 «
5538 abc // No indentation
5539 abc // 1 space (< 3 so dont convert)
5540 abc // 2 spaces (< 3 so dont convert)
5541 \tabc // 3 spaces (convert)
5542 \t abc // 5 spaces (1 tab + 2 spaces)
5543 \t\t\tabc // Already tab indented
5544 \t abc // Tab followed by space
5545 \tabc // Space followed by tab (should be consumed due to tab)
5546 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5547 \t\t\t
5548 \tabc \t // Only the leading spaces should be convertedˇ»
5549 "});
5550}
5551
5552#[gpui::test]
5553async fn test_toggle_case(cx: &mut TestAppContext) {
5554 init_test(cx, |_| {});
5555
5556 let mut cx = EditorTestContext::new(cx).await;
5557
5558 // If all lower case -> upper case
5559 cx.set_state(indoc! {"
5560 «hello worldˇ»
5561 "});
5562 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5563 cx.assert_editor_state(indoc! {"
5564 «HELLO WORLDˇ»
5565 "});
5566
5567 // If all upper case -> lower case
5568 cx.set_state(indoc! {"
5569 «HELLO WORLDˇ»
5570 "});
5571 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5572 cx.assert_editor_state(indoc! {"
5573 «hello worldˇ»
5574 "});
5575
5576 // If any upper case characters are identified -> lower case
5577 // This matches JetBrains IDEs
5578 cx.set_state(indoc! {"
5579 «hEllo worldˇ»
5580 "});
5581 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5582 cx.assert_editor_state(indoc! {"
5583 «hello worldˇ»
5584 "});
5585}
5586
5587#[gpui::test]
5588async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5589 init_test(cx, |_| {});
5590
5591 let mut cx = EditorTestContext::new(cx).await;
5592
5593 cx.set_state(indoc! {"
5594 «implement-windows-supportˇ»
5595 "});
5596 cx.update_editor(|e, window, cx| {
5597 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5598 });
5599 cx.assert_editor_state(indoc! {"
5600 «Implement windows supportˇ»
5601 "});
5602}
5603
5604#[gpui::test]
5605async fn test_manipulate_text(cx: &mut TestAppContext) {
5606 init_test(cx, |_| {});
5607
5608 let mut cx = EditorTestContext::new(cx).await;
5609
5610 // Test convert_to_upper_case()
5611 cx.set_state(indoc! {"
5612 «hello worldˇ»
5613 "});
5614 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5615 cx.assert_editor_state(indoc! {"
5616 «HELLO WORLDˇ»
5617 "});
5618
5619 // Test convert_to_lower_case()
5620 cx.set_state(indoc! {"
5621 «HELLO WORLDˇ»
5622 "});
5623 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5624 cx.assert_editor_state(indoc! {"
5625 «hello worldˇ»
5626 "});
5627
5628 // Test multiple line, single selection case
5629 cx.set_state(indoc! {"
5630 «The quick brown
5631 fox jumps over
5632 the lazy dogˇ»
5633 "});
5634 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5635 cx.assert_editor_state(indoc! {"
5636 «The Quick Brown
5637 Fox Jumps Over
5638 The Lazy Dogˇ»
5639 "});
5640
5641 // Test multiple line, single selection case
5642 cx.set_state(indoc! {"
5643 «The quick brown
5644 fox jumps over
5645 the lazy dogˇ»
5646 "});
5647 cx.update_editor(|e, window, cx| {
5648 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5649 });
5650 cx.assert_editor_state(indoc! {"
5651 «TheQuickBrown
5652 FoxJumpsOver
5653 TheLazyDogˇ»
5654 "});
5655
5656 // From here on out, test more complex cases of manipulate_text()
5657
5658 // Test no selection case - should affect words cursors are in
5659 // Cursor at beginning, middle, and end of word
5660 cx.set_state(indoc! {"
5661 ˇhello big beauˇtiful worldˇ
5662 "});
5663 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5664 cx.assert_editor_state(indoc! {"
5665 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5666 "});
5667
5668 // Test multiple selections on a single line and across multiple lines
5669 cx.set_state(indoc! {"
5670 «Theˇ» quick «brown
5671 foxˇ» jumps «overˇ»
5672 the «lazyˇ» dog
5673 "});
5674 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5675 cx.assert_editor_state(indoc! {"
5676 «THEˇ» quick «BROWN
5677 FOXˇ» jumps «OVERˇ»
5678 the «LAZYˇ» dog
5679 "});
5680
5681 // Test case where text length grows
5682 cx.set_state(indoc! {"
5683 «tschüߡ»
5684 "});
5685 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5686 cx.assert_editor_state(indoc! {"
5687 «TSCHÜSSˇ»
5688 "});
5689
5690 // Test to make sure we don't crash when text shrinks
5691 cx.set_state(indoc! {"
5692 aaa_bbbˇ
5693 "});
5694 cx.update_editor(|e, window, cx| {
5695 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5696 });
5697 cx.assert_editor_state(indoc! {"
5698 «aaaBbbˇ»
5699 "});
5700
5701 // Test to make sure we all aware of the fact that each word can grow and shrink
5702 // Final selections should be aware of this fact
5703 cx.set_state(indoc! {"
5704 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5705 "});
5706 cx.update_editor(|e, window, cx| {
5707 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5708 });
5709 cx.assert_editor_state(indoc! {"
5710 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5711 "});
5712
5713 cx.set_state(indoc! {"
5714 «hElLo, WoRld!ˇ»
5715 "});
5716 cx.update_editor(|e, window, cx| {
5717 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5718 });
5719 cx.assert_editor_state(indoc! {"
5720 «HeLlO, wOrLD!ˇ»
5721 "});
5722
5723 // Test selections with `line_mode() = true`.
5724 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5725 cx.set_state(indoc! {"
5726 «The quick brown
5727 fox jumps over
5728 tˇ»he lazy dog
5729 "});
5730 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5731 cx.assert_editor_state(indoc! {"
5732 «THE QUICK BROWN
5733 FOX JUMPS OVER
5734 THE LAZY DOGˇ»
5735 "});
5736}
5737
5738#[gpui::test]
5739fn test_duplicate_line(cx: &mut TestAppContext) {
5740 init_test(cx, |_| {});
5741
5742 let editor = cx.add_window(|window, cx| {
5743 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5744 build_editor(buffer, window, cx)
5745 });
5746 _ = editor.update(cx, |editor, window, cx| {
5747 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5748 s.select_display_ranges([
5749 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5750 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5751 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5752 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5753 ])
5754 });
5755 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5756 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5757 assert_eq!(
5758 display_ranges(editor, cx),
5759 vec![
5760 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5761 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5762 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5763 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5764 ]
5765 );
5766 });
5767
5768 let editor = cx.add_window(|window, cx| {
5769 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5770 build_editor(buffer, window, cx)
5771 });
5772 _ = editor.update(cx, |editor, window, cx| {
5773 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5774 s.select_display_ranges([
5775 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5776 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5777 ])
5778 });
5779 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5780 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5781 assert_eq!(
5782 display_ranges(editor, cx),
5783 vec![
5784 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5785 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5786 ]
5787 );
5788 });
5789
5790 // With `duplicate_line_up` the selections move to the duplicated lines,
5791 // which are inserted above the original lines
5792 let editor = cx.add_window(|window, cx| {
5793 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5794 build_editor(buffer, window, cx)
5795 });
5796 _ = editor.update(cx, |editor, window, cx| {
5797 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5798 s.select_display_ranges([
5799 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5800 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5801 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5802 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5803 ])
5804 });
5805 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5806 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5807 assert_eq!(
5808 display_ranges(editor, cx),
5809 vec![
5810 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5811 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5812 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5813 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5814 ]
5815 );
5816 });
5817
5818 let editor = cx.add_window(|window, cx| {
5819 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5820 build_editor(buffer, window, cx)
5821 });
5822 _ = editor.update(cx, |editor, window, cx| {
5823 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5824 s.select_display_ranges([
5825 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5826 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5827 ])
5828 });
5829 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5830 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5831 assert_eq!(
5832 display_ranges(editor, cx),
5833 vec![
5834 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5835 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5836 ]
5837 );
5838 });
5839
5840 let editor = cx.add_window(|window, cx| {
5841 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5842 build_editor(buffer, window, cx)
5843 });
5844 _ = editor.update(cx, |editor, window, cx| {
5845 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5846 s.select_display_ranges([
5847 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5848 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5849 ])
5850 });
5851 editor.duplicate_selection(&DuplicateSelection, window, cx);
5852 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5853 assert_eq!(
5854 display_ranges(editor, cx),
5855 vec![
5856 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5857 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5858 ]
5859 );
5860 });
5861}
5862
5863#[gpui::test]
5864async fn test_rotate_selections(cx: &mut TestAppContext) {
5865 init_test(cx, |_| {});
5866
5867 let mut cx = EditorTestContext::new(cx).await;
5868
5869 // Rotate text selections (horizontal)
5870 cx.set_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
5871 cx.update_editor(|e, window, cx| {
5872 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5873 });
5874 cx.assert_editor_state("x=«3ˇ», y=«1ˇ», z=«2ˇ»");
5875 cx.update_editor(|e, window, cx| {
5876 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5877 });
5878 cx.assert_editor_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
5879
5880 // Rotate text selections (vertical)
5881 cx.set_state(indoc! {"
5882 x=«1ˇ»
5883 y=«2ˇ»
5884 z=«3ˇ»
5885 "});
5886 cx.update_editor(|e, window, cx| {
5887 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5888 });
5889 cx.assert_editor_state(indoc! {"
5890 x=«3ˇ»
5891 y=«1ˇ»
5892 z=«2ˇ»
5893 "});
5894 cx.update_editor(|e, window, cx| {
5895 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5896 });
5897 cx.assert_editor_state(indoc! {"
5898 x=«1ˇ»
5899 y=«2ˇ»
5900 z=«3ˇ»
5901 "});
5902
5903 // Rotate text selections (vertical, different lengths)
5904 cx.set_state(indoc! {"
5905 x=\"«ˇ»\"
5906 y=\"«aˇ»\"
5907 z=\"«aaˇ»\"
5908 "});
5909 cx.update_editor(|e, window, cx| {
5910 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5911 });
5912 cx.assert_editor_state(indoc! {"
5913 x=\"«aaˇ»\"
5914 y=\"«ˇ»\"
5915 z=\"«aˇ»\"
5916 "});
5917 cx.update_editor(|e, window, cx| {
5918 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5919 });
5920 cx.assert_editor_state(indoc! {"
5921 x=\"«ˇ»\"
5922 y=\"«aˇ»\"
5923 z=\"«aaˇ»\"
5924 "});
5925
5926 // Rotate whole lines (cursor positions preserved)
5927 cx.set_state(indoc! {"
5928 ˇline123
5929 liˇne23
5930 line3ˇ
5931 "});
5932 cx.update_editor(|e, window, cx| {
5933 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5934 });
5935 cx.assert_editor_state(indoc! {"
5936 line3ˇ
5937 ˇline123
5938 liˇne23
5939 "});
5940 cx.update_editor(|e, window, cx| {
5941 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5942 });
5943 cx.assert_editor_state(indoc! {"
5944 ˇline123
5945 liˇne23
5946 line3ˇ
5947 "});
5948
5949 // Rotate whole lines, multiple cursors per line (positions preserved)
5950 cx.set_state(indoc! {"
5951 ˇliˇne123
5952 ˇline23
5953 ˇline3
5954 "});
5955 cx.update_editor(|e, window, cx| {
5956 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5957 });
5958 cx.assert_editor_state(indoc! {"
5959 ˇline3
5960 ˇliˇne123
5961 ˇline23
5962 "});
5963 cx.update_editor(|e, window, cx| {
5964 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5965 });
5966 cx.assert_editor_state(indoc! {"
5967 ˇliˇne123
5968 ˇline23
5969 ˇline3
5970 "});
5971}
5972
5973#[gpui::test]
5974fn test_move_line_up_down(cx: &mut TestAppContext) {
5975 init_test(cx, |_| {});
5976
5977 let editor = cx.add_window(|window, cx| {
5978 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5979 build_editor(buffer, window, cx)
5980 });
5981 _ = editor.update(cx, |editor, window, cx| {
5982 editor.fold_creases(
5983 vec![
5984 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5985 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5986 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5987 ],
5988 true,
5989 window,
5990 cx,
5991 );
5992 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5993 s.select_display_ranges([
5994 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5995 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5996 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5997 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5998 ])
5999 });
6000 assert_eq!(
6001 editor.display_text(cx),
6002 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
6003 );
6004
6005 editor.move_line_up(&MoveLineUp, window, cx);
6006 assert_eq!(
6007 editor.display_text(cx),
6008 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
6009 );
6010 assert_eq!(
6011 display_ranges(editor, cx),
6012 vec![
6013 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6014 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6015 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
6016 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
6017 ]
6018 );
6019 });
6020
6021 _ = editor.update(cx, |editor, window, cx| {
6022 editor.move_line_down(&MoveLineDown, window, cx);
6023 assert_eq!(
6024 editor.display_text(cx),
6025 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
6026 );
6027 assert_eq!(
6028 display_ranges(editor, cx),
6029 vec![
6030 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6031 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
6032 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
6033 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
6034 ]
6035 );
6036 });
6037
6038 _ = editor.update(cx, |editor, window, cx| {
6039 editor.move_line_down(&MoveLineDown, window, cx);
6040 assert_eq!(
6041 editor.display_text(cx),
6042 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
6043 );
6044 assert_eq!(
6045 display_ranges(editor, cx),
6046 vec![
6047 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6048 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
6049 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
6050 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
6051 ]
6052 );
6053 });
6054
6055 _ = editor.update(cx, |editor, window, cx| {
6056 editor.move_line_up(&MoveLineUp, window, cx);
6057 assert_eq!(
6058 editor.display_text(cx),
6059 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
6060 );
6061 assert_eq!(
6062 display_ranges(editor, cx),
6063 vec![
6064 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6065 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6066 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
6067 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
6068 ]
6069 );
6070 });
6071}
6072
6073#[gpui::test]
6074fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
6075 init_test(cx, |_| {});
6076 let editor = cx.add_window(|window, cx| {
6077 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
6078 build_editor(buffer, window, cx)
6079 });
6080 _ = editor.update(cx, |editor, window, cx| {
6081 editor.fold_creases(
6082 vec![Crease::simple(
6083 Point::new(6, 4)..Point::new(7, 4),
6084 FoldPlaceholder::test(),
6085 )],
6086 true,
6087 window,
6088 cx,
6089 );
6090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6091 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
6092 });
6093 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
6094 editor.move_line_up(&MoveLineUp, window, cx);
6095 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
6096 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
6097 });
6098}
6099
6100#[gpui::test]
6101fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
6102 init_test(cx, |_| {});
6103
6104 let editor = cx.add_window(|window, cx| {
6105 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
6106 build_editor(buffer, window, cx)
6107 });
6108 _ = editor.update(cx, |editor, window, cx| {
6109 let snapshot = editor.buffer.read(cx).snapshot(cx);
6110 editor.insert_blocks(
6111 [BlockProperties {
6112 style: BlockStyle::Fixed,
6113 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
6114 height: Some(1),
6115 render: Arc::new(|_| div().into_any()),
6116 priority: 0,
6117 }],
6118 Some(Autoscroll::fit()),
6119 cx,
6120 );
6121 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6122 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
6123 });
6124 editor.move_line_down(&MoveLineDown, window, cx);
6125 });
6126}
6127
6128#[gpui::test]
6129async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
6130 init_test(cx, |_| {});
6131
6132 let mut cx = EditorTestContext::new(cx).await;
6133 cx.set_state(
6134 &"
6135 ˇzero
6136 one
6137 two
6138 three
6139 four
6140 five
6141 "
6142 .unindent(),
6143 );
6144
6145 // Create a four-line block that replaces three lines of text.
6146 cx.update_editor(|editor, window, cx| {
6147 let snapshot = editor.snapshot(window, cx);
6148 let snapshot = &snapshot.buffer_snapshot();
6149 let placement = BlockPlacement::Replace(
6150 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
6151 );
6152 editor.insert_blocks(
6153 [BlockProperties {
6154 placement,
6155 height: Some(4),
6156 style: BlockStyle::Sticky,
6157 render: Arc::new(|_| gpui::div().into_any_element()),
6158 priority: 0,
6159 }],
6160 None,
6161 cx,
6162 );
6163 });
6164
6165 // Move down so that the cursor touches the block.
6166 cx.update_editor(|editor, window, cx| {
6167 editor.move_down(&Default::default(), window, cx);
6168 });
6169 cx.assert_editor_state(
6170 &"
6171 zero
6172 «one
6173 two
6174 threeˇ»
6175 four
6176 five
6177 "
6178 .unindent(),
6179 );
6180
6181 // Move down past the block.
6182 cx.update_editor(|editor, window, cx| {
6183 editor.move_down(&Default::default(), window, cx);
6184 });
6185 cx.assert_editor_state(
6186 &"
6187 zero
6188 one
6189 two
6190 three
6191 ˇfour
6192 five
6193 "
6194 .unindent(),
6195 );
6196}
6197
6198#[gpui::test]
6199fn test_transpose(cx: &mut TestAppContext) {
6200 init_test(cx, |_| {});
6201
6202 _ = cx.add_window(|window, cx| {
6203 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
6204 editor.set_style(EditorStyle::default(), window, cx);
6205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6206 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
6207 });
6208 editor.transpose(&Default::default(), window, cx);
6209 assert_eq!(editor.text(cx), "bac");
6210 assert_eq!(
6211 editor.selections.ranges(&editor.display_snapshot(cx)),
6212 [MultiBufferOffset(2)..MultiBufferOffset(2)]
6213 );
6214
6215 editor.transpose(&Default::default(), window, cx);
6216 assert_eq!(editor.text(cx), "bca");
6217 assert_eq!(
6218 editor.selections.ranges(&editor.display_snapshot(cx)),
6219 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6220 );
6221
6222 editor.transpose(&Default::default(), window, cx);
6223 assert_eq!(editor.text(cx), "bac");
6224 assert_eq!(
6225 editor.selections.ranges(&editor.display_snapshot(cx)),
6226 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6227 );
6228
6229 editor
6230 });
6231
6232 _ = cx.add_window(|window, cx| {
6233 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6234 editor.set_style(EditorStyle::default(), window, cx);
6235 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6236 s.select_ranges([MultiBufferOffset(3)..MultiBufferOffset(3)])
6237 });
6238 editor.transpose(&Default::default(), window, cx);
6239 assert_eq!(editor.text(cx), "acb\nde");
6240 assert_eq!(
6241 editor.selections.ranges(&editor.display_snapshot(cx)),
6242 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6243 );
6244
6245 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6246 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6247 });
6248 editor.transpose(&Default::default(), window, cx);
6249 assert_eq!(editor.text(cx), "acbd\ne");
6250 assert_eq!(
6251 editor.selections.ranges(&editor.display_snapshot(cx)),
6252 [MultiBufferOffset(5)..MultiBufferOffset(5)]
6253 );
6254
6255 editor.transpose(&Default::default(), window, cx);
6256 assert_eq!(editor.text(cx), "acbde\n");
6257 assert_eq!(
6258 editor.selections.ranges(&editor.display_snapshot(cx)),
6259 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6260 );
6261
6262 editor.transpose(&Default::default(), window, cx);
6263 assert_eq!(editor.text(cx), "acbd\ne");
6264 assert_eq!(
6265 editor.selections.ranges(&editor.display_snapshot(cx)),
6266 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6267 );
6268
6269 editor
6270 });
6271
6272 _ = cx.add_window(|window, cx| {
6273 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6274 editor.set_style(EditorStyle::default(), window, cx);
6275 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6276 s.select_ranges([
6277 MultiBufferOffset(1)..MultiBufferOffset(1),
6278 MultiBufferOffset(2)..MultiBufferOffset(2),
6279 MultiBufferOffset(4)..MultiBufferOffset(4),
6280 ])
6281 });
6282 editor.transpose(&Default::default(), window, cx);
6283 assert_eq!(editor.text(cx), "bacd\ne");
6284 assert_eq!(
6285 editor.selections.ranges(&editor.display_snapshot(cx)),
6286 [
6287 MultiBufferOffset(2)..MultiBufferOffset(2),
6288 MultiBufferOffset(3)..MultiBufferOffset(3),
6289 MultiBufferOffset(5)..MultiBufferOffset(5)
6290 ]
6291 );
6292
6293 editor.transpose(&Default::default(), window, cx);
6294 assert_eq!(editor.text(cx), "bcade\n");
6295 assert_eq!(
6296 editor.selections.ranges(&editor.display_snapshot(cx)),
6297 [
6298 MultiBufferOffset(3)..MultiBufferOffset(3),
6299 MultiBufferOffset(4)..MultiBufferOffset(4),
6300 MultiBufferOffset(6)..MultiBufferOffset(6)
6301 ]
6302 );
6303
6304 editor.transpose(&Default::default(), window, cx);
6305 assert_eq!(editor.text(cx), "bcda\ne");
6306 assert_eq!(
6307 editor.selections.ranges(&editor.display_snapshot(cx)),
6308 [
6309 MultiBufferOffset(4)..MultiBufferOffset(4),
6310 MultiBufferOffset(6)..MultiBufferOffset(6)
6311 ]
6312 );
6313
6314 editor.transpose(&Default::default(), window, cx);
6315 assert_eq!(editor.text(cx), "bcade\n");
6316 assert_eq!(
6317 editor.selections.ranges(&editor.display_snapshot(cx)),
6318 [
6319 MultiBufferOffset(4)..MultiBufferOffset(4),
6320 MultiBufferOffset(6)..MultiBufferOffset(6)
6321 ]
6322 );
6323
6324 editor.transpose(&Default::default(), window, cx);
6325 assert_eq!(editor.text(cx), "bcaed\n");
6326 assert_eq!(
6327 editor.selections.ranges(&editor.display_snapshot(cx)),
6328 [
6329 MultiBufferOffset(5)..MultiBufferOffset(5),
6330 MultiBufferOffset(6)..MultiBufferOffset(6)
6331 ]
6332 );
6333
6334 editor
6335 });
6336
6337 _ = cx.add_window(|window, cx| {
6338 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6339 editor.set_style(EditorStyle::default(), window, cx);
6340 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6341 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6342 });
6343 editor.transpose(&Default::default(), window, cx);
6344 assert_eq!(editor.text(cx), "🏀🍐✋");
6345 assert_eq!(
6346 editor.selections.ranges(&editor.display_snapshot(cx)),
6347 [MultiBufferOffset(8)..MultiBufferOffset(8)]
6348 );
6349
6350 editor.transpose(&Default::default(), window, cx);
6351 assert_eq!(editor.text(cx), "🏀✋🍐");
6352 assert_eq!(
6353 editor.selections.ranges(&editor.display_snapshot(cx)),
6354 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6355 );
6356
6357 editor.transpose(&Default::default(), window, cx);
6358 assert_eq!(editor.text(cx), "🏀🍐✋");
6359 assert_eq!(
6360 editor.selections.ranges(&editor.display_snapshot(cx)),
6361 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6362 );
6363
6364 editor
6365 });
6366}
6367
6368#[gpui::test]
6369async fn test_rewrap(cx: &mut TestAppContext) {
6370 init_test(cx, |settings| {
6371 settings.languages.0.extend([
6372 (
6373 "Markdown".into(),
6374 LanguageSettingsContent {
6375 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6376 preferred_line_length: Some(40),
6377 ..Default::default()
6378 },
6379 ),
6380 (
6381 "Plain Text".into(),
6382 LanguageSettingsContent {
6383 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6384 preferred_line_length: Some(40),
6385 ..Default::default()
6386 },
6387 ),
6388 (
6389 "C++".into(),
6390 LanguageSettingsContent {
6391 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6392 preferred_line_length: Some(40),
6393 ..Default::default()
6394 },
6395 ),
6396 (
6397 "Python".into(),
6398 LanguageSettingsContent {
6399 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6400 preferred_line_length: Some(40),
6401 ..Default::default()
6402 },
6403 ),
6404 (
6405 "Rust".into(),
6406 LanguageSettingsContent {
6407 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6408 preferred_line_length: Some(40),
6409 ..Default::default()
6410 },
6411 ),
6412 ])
6413 });
6414
6415 let mut cx = EditorTestContext::new(cx).await;
6416
6417 let cpp_language = Arc::new(Language::new(
6418 LanguageConfig {
6419 name: "C++".into(),
6420 line_comments: vec!["// ".into()],
6421 ..LanguageConfig::default()
6422 },
6423 None,
6424 ));
6425 let python_language = Arc::new(Language::new(
6426 LanguageConfig {
6427 name: "Python".into(),
6428 line_comments: vec!["# ".into()],
6429 ..LanguageConfig::default()
6430 },
6431 None,
6432 ));
6433 let markdown_language = Arc::new(Language::new(
6434 LanguageConfig {
6435 name: "Markdown".into(),
6436 rewrap_prefixes: vec![
6437 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6438 regex::Regex::new("[-*+]\\s+").unwrap(),
6439 ],
6440 ..LanguageConfig::default()
6441 },
6442 None,
6443 ));
6444 let rust_language = Arc::new(
6445 Language::new(
6446 LanguageConfig {
6447 name: "Rust".into(),
6448 line_comments: vec!["// ".into(), "/// ".into()],
6449 ..LanguageConfig::default()
6450 },
6451 Some(tree_sitter_rust::LANGUAGE.into()),
6452 )
6453 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6454 .unwrap(),
6455 );
6456
6457 let plaintext_language = Arc::new(Language::new(
6458 LanguageConfig {
6459 name: "Plain Text".into(),
6460 ..LanguageConfig::default()
6461 },
6462 None,
6463 ));
6464
6465 // Test basic rewrapping of a long line with a cursor
6466 assert_rewrap(
6467 indoc! {"
6468 // ˇThis is a long comment that needs to be wrapped.
6469 "},
6470 indoc! {"
6471 // ˇThis is a long comment that needs to
6472 // be wrapped.
6473 "},
6474 cpp_language.clone(),
6475 &mut cx,
6476 );
6477
6478 // Test rewrapping a full selection
6479 assert_rewrap(
6480 indoc! {"
6481 «// This selected long comment needs to be wrapped.ˇ»"
6482 },
6483 indoc! {"
6484 «// This selected long comment needs to
6485 // be wrapped.ˇ»"
6486 },
6487 cpp_language.clone(),
6488 &mut cx,
6489 );
6490
6491 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6492 assert_rewrap(
6493 indoc! {"
6494 // ˇThis is the first line.
6495 // Thisˇ is the second line.
6496 // This is the thirdˇ line, all part of one paragraph.
6497 "},
6498 indoc! {"
6499 // ˇThis is the first line. Thisˇ is the
6500 // second line. This is the thirdˇ line,
6501 // all part of one paragraph.
6502 "},
6503 cpp_language.clone(),
6504 &mut cx,
6505 );
6506
6507 // Test multiple cursors in different paragraphs trigger separate rewraps
6508 assert_rewrap(
6509 indoc! {"
6510 // ˇThis is the first paragraph, first line.
6511 // ˇThis is the first paragraph, second line.
6512
6513 // ˇThis is the second paragraph, first line.
6514 // ˇThis is the second paragraph, second line.
6515 "},
6516 indoc! {"
6517 // ˇThis is the first paragraph, first
6518 // line. ˇThis is the first paragraph,
6519 // second line.
6520
6521 // ˇThis is the second paragraph, first
6522 // line. ˇThis is the second paragraph,
6523 // second line.
6524 "},
6525 cpp_language.clone(),
6526 &mut cx,
6527 );
6528
6529 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6530 assert_rewrap(
6531 indoc! {"
6532 «// A regular long long comment to be wrapped.
6533 /// A documentation long comment to be wrapped.ˇ»
6534 "},
6535 indoc! {"
6536 «// A regular long long comment to be
6537 // wrapped.
6538 /// A documentation long comment to be
6539 /// wrapped.ˇ»
6540 "},
6541 rust_language.clone(),
6542 &mut cx,
6543 );
6544
6545 // Test that change in indentation level trigger seperate rewraps
6546 assert_rewrap(
6547 indoc! {"
6548 fn foo() {
6549 «// This is a long comment at the base indent.
6550 // This is a long comment at the next indent.ˇ»
6551 }
6552 "},
6553 indoc! {"
6554 fn foo() {
6555 «// This is a long comment at the
6556 // base indent.
6557 // This is a long comment at the
6558 // next indent.ˇ»
6559 }
6560 "},
6561 rust_language.clone(),
6562 &mut cx,
6563 );
6564
6565 // Test that different comment prefix characters (e.g., '#') are handled correctly
6566 assert_rewrap(
6567 indoc! {"
6568 # ˇThis is a long comment using a pound sign.
6569 "},
6570 indoc! {"
6571 # ˇThis is a long comment using a pound
6572 # sign.
6573 "},
6574 python_language,
6575 &mut cx,
6576 );
6577
6578 // Test rewrapping only affects comments, not code even when selected
6579 assert_rewrap(
6580 indoc! {"
6581 «/// This doc comment is long and should be wrapped.
6582 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6583 "},
6584 indoc! {"
6585 «/// This doc comment is long and should
6586 /// be wrapped.
6587 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6588 "},
6589 rust_language.clone(),
6590 &mut cx,
6591 );
6592
6593 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6594 assert_rewrap(
6595 indoc! {"
6596 # Header
6597
6598 A long long long line of markdown text to wrap.ˇ
6599 "},
6600 indoc! {"
6601 # Header
6602
6603 A long long long line of markdown text
6604 to wrap.ˇ
6605 "},
6606 markdown_language.clone(),
6607 &mut cx,
6608 );
6609
6610 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6611 assert_rewrap(
6612 indoc! {"
6613 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6614 2. This is a numbered list item that is very long and needs to be wrapped properly.
6615 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6616 "},
6617 indoc! {"
6618 «1. This is a numbered list item that is
6619 very long and needs to be wrapped
6620 properly.
6621 2. This is a numbered list item that is
6622 very long and needs to be wrapped
6623 properly.
6624 - This is an unordered list item that is
6625 also very long and should not merge
6626 with the numbered item.ˇ»
6627 "},
6628 markdown_language.clone(),
6629 &mut cx,
6630 );
6631
6632 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6633 assert_rewrap(
6634 indoc! {"
6635 «1. This is a numbered list item that is
6636 very long and needs to be wrapped
6637 properly.
6638 2. This is a numbered list item that is
6639 very long and needs to be wrapped
6640 properly.
6641 - This is an unordered list item that is
6642 also very long and should not merge with
6643 the numbered item.ˇ»
6644 "},
6645 indoc! {"
6646 «1. This is a numbered list item that is
6647 very long and needs to be wrapped
6648 properly.
6649 2. This is a numbered list item that is
6650 very long and needs to be wrapped
6651 properly.
6652 - This is an unordered list item that is
6653 also very long and should not merge
6654 with the numbered item.ˇ»
6655 "},
6656 markdown_language.clone(),
6657 &mut cx,
6658 );
6659
6660 // Test that rewrapping maintain indents even when they already exists.
6661 assert_rewrap(
6662 indoc! {"
6663 «1. This is a numbered list
6664 item that is very long and needs to be wrapped properly.
6665 2. This is a numbered list
6666 item that is very long and needs to be wrapped properly.
6667 - This is an unordered list item that is also very long and
6668 should not merge with the numbered item.ˇ»
6669 "},
6670 indoc! {"
6671 «1. This is a numbered list item that is
6672 very long and needs to be wrapped
6673 properly.
6674 2. This is a numbered list item that is
6675 very long and needs to be wrapped
6676 properly.
6677 - This is an unordered list item that is
6678 also very long and should not merge
6679 with the numbered item.ˇ»
6680 "},
6681 markdown_language,
6682 &mut cx,
6683 );
6684
6685 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6686 assert_rewrap(
6687 indoc! {"
6688 ˇThis is a very long line of plain text that will be wrapped.
6689 "},
6690 indoc! {"
6691 ˇThis is a very long line of plain text
6692 that will be wrapped.
6693 "},
6694 plaintext_language.clone(),
6695 &mut cx,
6696 );
6697
6698 // Test that non-commented code acts as a paragraph boundary within a selection
6699 assert_rewrap(
6700 indoc! {"
6701 «// This is the first long comment block to be wrapped.
6702 fn my_func(a: u32);
6703 // This is the second long comment block to be wrapped.ˇ»
6704 "},
6705 indoc! {"
6706 «// This is the first long comment block
6707 // to be wrapped.
6708 fn my_func(a: u32);
6709 // This is the second long comment block
6710 // to be wrapped.ˇ»
6711 "},
6712 rust_language,
6713 &mut cx,
6714 );
6715
6716 // Test rewrapping multiple selections, including ones with blank lines or tabs
6717 assert_rewrap(
6718 indoc! {"
6719 «ˇThis is a very long line that will be wrapped.
6720
6721 This is another paragraph in the same selection.»
6722
6723 «\tThis is a very long indented line that will be wrapped.ˇ»
6724 "},
6725 indoc! {"
6726 «ˇThis is a very long line that will be
6727 wrapped.
6728
6729 This is another paragraph in the same
6730 selection.»
6731
6732 «\tThis is a very long indented line
6733 \tthat will be wrapped.ˇ»
6734 "},
6735 plaintext_language,
6736 &mut cx,
6737 );
6738
6739 // Test that an empty comment line acts as a paragraph boundary
6740 assert_rewrap(
6741 indoc! {"
6742 // ˇThis is a long comment that will be wrapped.
6743 //
6744 // And this is another long comment that will also be wrapped.ˇ
6745 "},
6746 indoc! {"
6747 // ˇThis is a long comment that will be
6748 // wrapped.
6749 //
6750 // And this is another long comment that
6751 // will also be wrapped.ˇ
6752 "},
6753 cpp_language,
6754 &mut cx,
6755 );
6756
6757 #[track_caller]
6758 fn assert_rewrap(
6759 unwrapped_text: &str,
6760 wrapped_text: &str,
6761 language: Arc<Language>,
6762 cx: &mut EditorTestContext,
6763 ) {
6764 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6765 cx.set_state(unwrapped_text);
6766 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6767 cx.assert_editor_state(wrapped_text);
6768 }
6769}
6770
6771#[gpui::test]
6772async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6773 init_test(cx, |settings| {
6774 settings.languages.0.extend([(
6775 "Rust".into(),
6776 LanguageSettingsContent {
6777 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6778 preferred_line_length: Some(40),
6779 ..Default::default()
6780 },
6781 )])
6782 });
6783
6784 let mut cx = EditorTestContext::new(cx).await;
6785
6786 let rust_lang = Arc::new(
6787 Language::new(
6788 LanguageConfig {
6789 name: "Rust".into(),
6790 line_comments: vec!["// ".into()],
6791 block_comment: Some(BlockCommentConfig {
6792 start: "/*".into(),
6793 end: "*/".into(),
6794 prefix: "* ".into(),
6795 tab_size: 1,
6796 }),
6797 documentation_comment: Some(BlockCommentConfig {
6798 start: "/**".into(),
6799 end: "*/".into(),
6800 prefix: "* ".into(),
6801 tab_size: 1,
6802 }),
6803
6804 ..LanguageConfig::default()
6805 },
6806 Some(tree_sitter_rust::LANGUAGE.into()),
6807 )
6808 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6809 .unwrap(),
6810 );
6811
6812 // regular block comment
6813 assert_rewrap(
6814 indoc! {"
6815 /*
6816 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6817 */
6818 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6819 "},
6820 indoc! {"
6821 /*
6822 *ˇ Lorem ipsum dolor sit amet,
6823 * consectetur adipiscing elit.
6824 */
6825 /*
6826 *ˇ Lorem ipsum dolor sit amet,
6827 * consectetur adipiscing elit.
6828 */
6829 "},
6830 rust_lang.clone(),
6831 &mut cx,
6832 );
6833
6834 // indent is respected
6835 assert_rewrap(
6836 indoc! {"
6837 {}
6838 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6839 "},
6840 indoc! {"
6841 {}
6842 /*
6843 *ˇ Lorem ipsum dolor sit amet,
6844 * consectetur adipiscing elit.
6845 */
6846 "},
6847 rust_lang.clone(),
6848 &mut cx,
6849 );
6850
6851 // short block comments with inline delimiters
6852 assert_rewrap(
6853 indoc! {"
6854 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6855 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6856 */
6857 /*
6858 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6859 "},
6860 indoc! {"
6861 /*
6862 *ˇ Lorem ipsum dolor sit amet,
6863 * consectetur adipiscing elit.
6864 */
6865 /*
6866 *ˇ Lorem ipsum dolor sit amet,
6867 * consectetur adipiscing elit.
6868 */
6869 /*
6870 *ˇ Lorem ipsum dolor sit amet,
6871 * consectetur adipiscing elit.
6872 */
6873 "},
6874 rust_lang.clone(),
6875 &mut cx,
6876 );
6877
6878 // multiline block comment with inline start/end delimiters
6879 assert_rewrap(
6880 indoc! {"
6881 /*ˇ Lorem ipsum dolor sit amet,
6882 * consectetur adipiscing elit. */
6883 "},
6884 indoc! {"
6885 /*
6886 *ˇ Lorem ipsum dolor sit amet,
6887 * consectetur adipiscing elit.
6888 */
6889 "},
6890 rust_lang.clone(),
6891 &mut cx,
6892 );
6893
6894 // block comment rewrap still respects paragraph bounds
6895 assert_rewrap(
6896 indoc! {"
6897 /*
6898 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6899 *
6900 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6901 */
6902 "},
6903 indoc! {"
6904 /*
6905 *ˇ Lorem ipsum dolor sit amet,
6906 * consectetur adipiscing elit.
6907 *
6908 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6909 */
6910 "},
6911 rust_lang.clone(),
6912 &mut cx,
6913 );
6914
6915 // documentation comments
6916 assert_rewrap(
6917 indoc! {"
6918 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6919 /**
6920 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6921 */
6922 "},
6923 indoc! {"
6924 /**
6925 *ˇ Lorem ipsum dolor sit amet,
6926 * consectetur adipiscing elit.
6927 */
6928 /**
6929 *ˇ Lorem ipsum dolor sit amet,
6930 * consectetur adipiscing elit.
6931 */
6932 "},
6933 rust_lang.clone(),
6934 &mut cx,
6935 );
6936
6937 // different, adjacent comments
6938 assert_rewrap(
6939 indoc! {"
6940 /**
6941 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6942 */
6943 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6944 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6945 "},
6946 indoc! {"
6947 /**
6948 *ˇ Lorem ipsum dolor sit amet,
6949 * consectetur adipiscing elit.
6950 */
6951 /*
6952 *ˇ Lorem ipsum dolor sit amet,
6953 * consectetur adipiscing elit.
6954 */
6955 //ˇ Lorem ipsum dolor sit amet,
6956 // consectetur adipiscing elit.
6957 "},
6958 rust_lang.clone(),
6959 &mut cx,
6960 );
6961
6962 // selection w/ single short block comment
6963 assert_rewrap(
6964 indoc! {"
6965 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6966 "},
6967 indoc! {"
6968 «/*
6969 * Lorem ipsum dolor sit amet,
6970 * consectetur adipiscing elit.
6971 */ˇ»
6972 "},
6973 rust_lang.clone(),
6974 &mut cx,
6975 );
6976
6977 // rewrapping a single comment w/ abutting comments
6978 assert_rewrap(
6979 indoc! {"
6980 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6981 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6982 "},
6983 indoc! {"
6984 /*
6985 * ˇLorem ipsum dolor sit amet,
6986 * consectetur adipiscing elit.
6987 */
6988 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6989 "},
6990 rust_lang.clone(),
6991 &mut cx,
6992 );
6993
6994 // selection w/ non-abutting short block comments
6995 assert_rewrap(
6996 indoc! {"
6997 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6998
6999 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7000 "},
7001 indoc! {"
7002 «/*
7003 * Lorem ipsum dolor sit amet,
7004 * consectetur adipiscing elit.
7005 */
7006
7007 /*
7008 * Lorem ipsum dolor sit amet,
7009 * consectetur adipiscing elit.
7010 */ˇ»
7011 "},
7012 rust_lang.clone(),
7013 &mut cx,
7014 );
7015
7016 // selection of multiline block comments
7017 assert_rewrap(
7018 indoc! {"
7019 «/* Lorem ipsum dolor sit amet,
7020 * consectetur adipiscing elit. */ˇ»
7021 "},
7022 indoc! {"
7023 «/*
7024 * Lorem ipsum dolor sit amet,
7025 * consectetur adipiscing elit.
7026 */ˇ»
7027 "},
7028 rust_lang.clone(),
7029 &mut cx,
7030 );
7031
7032 // partial selection of multiline block comments
7033 assert_rewrap(
7034 indoc! {"
7035 «/* Lorem ipsum dolor sit amet,ˇ»
7036 * consectetur adipiscing elit. */
7037 /* Lorem ipsum dolor sit amet,
7038 «* consectetur adipiscing elit. */ˇ»
7039 "},
7040 indoc! {"
7041 «/*
7042 * Lorem ipsum dolor sit amet,ˇ»
7043 * consectetur adipiscing elit. */
7044 /* Lorem ipsum dolor sit amet,
7045 «* consectetur adipiscing elit.
7046 */ˇ»
7047 "},
7048 rust_lang.clone(),
7049 &mut cx,
7050 );
7051
7052 // selection w/ abutting short block comments
7053 // TODO: should not be combined; should rewrap as 2 comments
7054 assert_rewrap(
7055 indoc! {"
7056 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7057 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7058 "},
7059 // desired behavior:
7060 // indoc! {"
7061 // «/*
7062 // * Lorem ipsum dolor sit amet,
7063 // * consectetur adipiscing elit.
7064 // */
7065 // /*
7066 // * Lorem ipsum dolor sit amet,
7067 // * consectetur adipiscing elit.
7068 // */ˇ»
7069 // "},
7070 // actual behaviour:
7071 indoc! {"
7072 «/*
7073 * Lorem ipsum dolor sit amet,
7074 * consectetur adipiscing elit. Lorem
7075 * ipsum dolor sit amet, consectetur
7076 * adipiscing elit.
7077 */ˇ»
7078 "},
7079 rust_lang.clone(),
7080 &mut cx,
7081 );
7082
7083 // TODO: same as above, but with delimiters on separate line
7084 // assert_rewrap(
7085 // indoc! {"
7086 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7087 // */
7088 // /*
7089 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7090 // "},
7091 // // desired:
7092 // // indoc! {"
7093 // // «/*
7094 // // * Lorem ipsum dolor sit amet,
7095 // // * consectetur adipiscing elit.
7096 // // */
7097 // // /*
7098 // // * Lorem ipsum dolor sit amet,
7099 // // * consectetur adipiscing elit.
7100 // // */ˇ»
7101 // // "},
7102 // // actual: (but with trailing w/s on the empty lines)
7103 // indoc! {"
7104 // «/*
7105 // * Lorem ipsum dolor sit amet,
7106 // * consectetur adipiscing elit.
7107 // *
7108 // */
7109 // /*
7110 // *
7111 // * Lorem ipsum dolor sit amet,
7112 // * consectetur adipiscing elit.
7113 // */ˇ»
7114 // "},
7115 // rust_lang.clone(),
7116 // &mut cx,
7117 // );
7118
7119 // TODO these are unhandled edge cases; not correct, just documenting known issues
7120 assert_rewrap(
7121 indoc! {"
7122 /*
7123 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7124 */
7125 /*
7126 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7127 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
7128 "},
7129 // desired:
7130 // indoc! {"
7131 // /*
7132 // *ˇ Lorem ipsum dolor sit amet,
7133 // * consectetur adipiscing elit.
7134 // */
7135 // /*
7136 // *ˇ Lorem ipsum dolor sit amet,
7137 // * consectetur adipiscing elit.
7138 // */
7139 // /*
7140 // *ˇ Lorem ipsum dolor sit amet
7141 // */ /* consectetur adipiscing elit. */
7142 // "},
7143 // actual:
7144 indoc! {"
7145 /*
7146 //ˇ Lorem ipsum dolor sit amet,
7147 // consectetur adipiscing elit.
7148 */
7149 /*
7150 * //ˇ Lorem ipsum dolor sit amet,
7151 * consectetur adipiscing elit.
7152 */
7153 /*
7154 *ˇ Lorem ipsum dolor sit amet */ /*
7155 * consectetur adipiscing elit.
7156 */
7157 "},
7158 rust_lang,
7159 &mut cx,
7160 );
7161
7162 #[track_caller]
7163 fn assert_rewrap(
7164 unwrapped_text: &str,
7165 wrapped_text: &str,
7166 language: Arc<Language>,
7167 cx: &mut EditorTestContext,
7168 ) {
7169 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
7170 cx.set_state(unwrapped_text);
7171 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
7172 cx.assert_editor_state(wrapped_text);
7173 }
7174}
7175
7176#[gpui::test]
7177async fn test_hard_wrap(cx: &mut TestAppContext) {
7178 init_test(cx, |_| {});
7179 let mut cx = EditorTestContext::new(cx).await;
7180
7181 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
7182 cx.update_editor(|editor, _, cx| {
7183 editor.set_hard_wrap(Some(14), cx);
7184 });
7185
7186 cx.set_state(indoc!(
7187 "
7188 one two three ˇ
7189 "
7190 ));
7191 cx.simulate_input("four");
7192 cx.run_until_parked();
7193
7194 cx.assert_editor_state(indoc!(
7195 "
7196 one two three
7197 fourˇ
7198 "
7199 ));
7200
7201 cx.update_editor(|editor, window, cx| {
7202 editor.newline(&Default::default(), window, cx);
7203 });
7204 cx.run_until_parked();
7205 cx.assert_editor_state(indoc!(
7206 "
7207 one two three
7208 four
7209 ˇ
7210 "
7211 ));
7212
7213 cx.simulate_input("five");
7214 cx.run_until_parked();
7215 cx.assert_editor_state(indoc!(
7216 "
7217 one two three
7218 four
7219 fiveˇ
7220 "
7221 ));
7222
7223 cx.update_editor(|editor, window, cx| {
7224 editor.newline(&Default::default(), window, cx);
7225 });
7226 cx.run_until_parked();
7227 cx.simulate_input("# ");
7228 cx.run_until_parked();
7229 cx.assert_editor_state(indoc!(
7230 "
7231 one two three
7232 four
7233 five
7234 # ˇ
7235 "
7236 ));
7237
7238 cx.update_editor(|editor, window, cx| {
7239 editor.newline(&Default::default(), window, cx);
7240 });
7241 cx.run_until_parked();
7242 cx.assert_editor_state(indoc!(
7243 "
7244 one two three
7245 four
7246 five
7247 #\x20
7248 #ˇ
7249 "
7250 ));
7251
7252 cx.simulate_input(" 6");
7253 cx.run_until_parked();
7254 cx.assert_editor_state(indoc!(
7255 "
7256 one two three
7257 four
7258 five
7259 #
7260 # 6ˇ
7261 "
7262 ));
7263}
7264
7265#[gpui::test]
7266async fn test_cut_line_ends(cx: &mut TestAppContext) {
7267 init_test(cx, |_| {});
7268
7269 let mut cx = EditorTestContext::new(cx).await;
7270
7271 cx.set_state(indoc! {"The quick brownˇ"});
7272 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7273 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7274
7275 cx.set_state(indoc! {"The emacs foxˇ"});
7276 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7277 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7278
7279 cx.set_state(indoc! {"
7280 The quick« brownˇ»
7281 fox jumps overˇ
7282 the lazy dog"});
7283 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7284 cx.assert_editor_state(indoc! {"
7285 The quickˇ
7286 ˇthe lazy dog"});
7287
7288 cx.set_state(indoc! {"
7289 The quick« brownˇ»
7290 fox jumps overˇ
7291 the lazy dog"});
7292 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7293 cx.assert_editor_state(indoc! {"
7294 The quickˇ
7295 fox jumps overˇthe lazy dog"});
7296
7297 cx.set_state(indoc! {"
7298 The quick« brownˇ»
7299 fox jumps overˇ
7300 the lazy dog"});
7301 cx.update_editor(|e, window, cx| {
7302 e.cut_to_end_of_line(
7303 &CutToEndOfLine {
7304 stop_at_newlines: true,
7305 },
7306 window,
7307 cx,
7308 )
7309 });
7310 cx.assert_editor_state(indoc! {"
7311 The quickˇ
7312 fox jumps overˇ
7313 the lazy dog"});
7314
7315 cx.set_state(indoc! {"
7316 The quick« brownˇ»
7317 fox jumps overˇ
7318 the lazy dog"});
7319 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7320 cx.assert_editor_state(indoc! {"
7321 The quickˇ
7322 fox jumps overˇthe lazy dog"});
7323}
7324
7325#[gpui::test]
7326async fn test_clipboard(cx: &mut TestAppContext) {
7327 init_test(cx, |_| {});
7328
7329 let mut cx = EditorTestContext::new(cx).await;
7330
7331 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7332 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7333 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7334
7335 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7336 cx.set_state("two ˇfour ˇsix ˇ");
7337 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7338 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7339
7340 // Paste again but with only two cursors. Since the number of cursors doesn't
7341 // match the number of slices in the clipboard, the entire clipboard text
7342 // is pasted at each cursor.
7343 cx.set_state("ˇtwo one✅ four three six five ˇ");
7344 cx.update_editor(|e, window, cx| {
7345 e.handle_input("( ", window, cx);
7346 e.paste(&Paste, window, cx);
7347 e.handle_input(") ", window, cx);
7348 });
7349 cx.assert_editor_state(
7350 &([
7351 "( one✅ ",
7352 "three ",
7353 "five ) ˇtwo one✅ four three six five ( one✅ ",
7354 "three ",
7355 "five ) ˇ",
7356 ]
7357 .join("\n")),
7358 );
7359
7360 // Cut with three selections, one of which is full-line.
7361 cx.set_state(indoc! {"
7362 1«2ˇ»3
7363 4ˇ567
7364 «8ˇ»9"});
7365 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7366 cx.assert_editor_state(indoc! {"
7367 1ˇ3
7368 ˇ9"});
7369
7370 // Paste with three selections, noticing how the copied selection that was full-line
7371 // gets inserted before the second cursor.
7372 cx.set_state(indoc! {"
7373 1ˇ3
7374 9ˇ
7375 «oˇ»ne"});
7376 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7377 cx.assert_editor_state(indoc! {"
7378 12ˇ3
7379 4567
7380 9ˇ
7381 8ˇne"});
7382
7383 // Copy with a single cursor only, which writes the whole line into the clipboard.
7384 cx.set_state(indoc! {"
7385 The quick brown
7386 fox juˇmps over
7387 the lazy dog"});
7388 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7389 assert_eq!(
7390 cx.read_from_clipboard()
7391 .and_then(|item| item.text().as_deref().map(str::to_string)),
7392 Some("fox jumps over\n".to_string())
7393 );
7394
7395 // Paste with three selections, noticing how the copied full-line selection is inserted
7396 // before the empty selections but replaces the selection that is non-empty.
7397 cx.set_state(indoc! {"
7398 Tˇhe quick brown
7399 «foˇ»x jumps over
7400 tˇhe lazy dog"});
7401 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7402 cx.assert_editor_state(indoc! {"
7403 fox jumps over
7404 Tˇhe quick brown
7405 fox jumps over
7406 ˇx jumps over
7407 fox jumps over
7408 tˇhe lazy dog"});
7409}
7410
7411#[gpui::test]
7412async fn test_copy_trim(cx: &mut TestAppContext) {
7413 init_test(cx, |_| {});
7414
7415 let mut cx = EditorTestContext::new(cx).await;
7416 cx.set_state(
7417 r#" «for selection in selections.iter() {
7418 let mut start = selection.start;
7419 let mut end = selection.end;
7420 let is_entire_line = selection.is_empty();
7421 if is_entire_line {
7422 start = Point::new(start.row, 0);ˇ»
7423 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7424 }
7425 "#,
7426 );
7427 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7428 assert_eq!(
7429 cx.read_from_clipboard()
7430 .and_then(|item| item.text().as_deref().map(str::to_string)),
7431 Some(
7432 "for selection in selections.iter() {
7433 let mut start = selection.start;
7434 let mut end = selection.end;
7435 let is_entire_line = selection.is_empty();
7436 if is_entire_line {
7437 start = Point::new(start.row, 0);"
7438 .to_string()
7439 ),
7440 "Regular copying preserves all indentation selected",
7441 );
7442 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7443 assert_eq!(
7444 cx.read_from_clipboard()
7445 .and_then(|item| item.text().as_deref().map(str::to_string)),
7446 Some(
7447 "for selection in selections.iter() {
7448let mut start = selection.start;
7449let mut end = selection.end;
7450let is_entire_line = selection.is_empty();
7451if is_entire_line {
7452 start = Point::new(start.row, 0);"
7453 .to_string()
7454 ),
7455 "Copying with stripping should strip all leading whitespaces"
7456 );
7457
7458 cx.set_state(
7459 r#" « for selection in selections.iter() {
7460 let mut start = selection.start;
7461 let mut end = selection.end;
7462 let is_entire_line = selection.is_empty();
7463 if is_entire_line {
7464 start = Point::new(start.row, 0);ˇ»
7465 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7466 }
7467 "#,
7468 );
7469 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7470 assert_eq!(
7471 cx.read_from_clipboard()
7472 .and_then(|item| item.text().as_deref().map(str::to_string)),
7473 Some(
7474 " for selection in selections.iter() {
7475 let mut start = selection.start;
7476 let mut end = selection.end;
7477 let is_entire_line = selection.is_empty();
7478 if is_entire_line {
7479 start = Point::new(start.row, 0);"
7480 .to_string()
7481 ),
7482 "Regular copying preserves all indentation selected",
7483 );
7484 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7485 assert_eq!(
7486 cx.read_from_clipboard()
7487 .and_then(|item| item.text().as_deref().map(str::to_string)),
7488 Some(
7489 "for selection in selections.iter() {
7490let mut start = selection.start;
7491let mut end = selection.end;
7492let is_entire_line = selection.is_empty();
7493if is_entire_line {
7494 start = Point::new(start.row, 0);"
7495 .to_string()
7496 ),
7497 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7498 );
7499
7500 cx.set_state(
7501 r#" «ˇ for selection in selections.iter() {
7502 let mut start = selection.start;
7503 let mut end = selection.end;
7504 let is_entire_line = selection.is_empty();
7505 if is_entire_line {
7506 start = Point::new(start.row, 0);»
7507 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7508 }
7509 "#,
7510 );
7511 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7512 assert_eq!(
7513 cx.read_from_clipboard()
7514 .and_then(|item| item.text().as_deref().map(str::to_string)),
7515 Some(
7516 " for selection in selections.iter() {
7517 let mut start = selection.start;
7518 let mut end = selection.end;
7519 let is_entire_line = selection.is_empty();
7520 if is_entire_line {
7521 start = Point::new(start.row, 0);"
7522 .to_string()
7523 ),
7524 "Regular copying for reverse selection works the same",
7525 );
7526 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7527 assert_eq!(
7528 cx.read_from_clipboard()
7529 .and_then(|item| item.text().as_deref().map(str::to_string)),
7530 Some(
7531 "for selection in selections.iter() {
7532let mut start = selection.start;
7533let mut end = selection.end;
7534let is_entire_line = selection.is_empty();
7535if is_entire_line {
7536 start = Point::new(start.row, 0);"
7537 .to_string()
7538 ),
7539 "Copying with stripping for reverse selection works the same"
7540 );
7541
7542 cx.set_state(
7543 r#" for selection «in selections.iter() {
7544 let mut start = selection.start;
7545 let mut end = selection.end;
7546 let is_entire_line = selection.is_empty();
7547 if is_entire_line {
7548 start = Point::new(start.row, 0);ˇ»
7549 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7550 }
7551 "#,
7552 );
7553 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7554 assert_eq!(
7555 cx.read_from_clipboard()
7556 .and_then(|item| item.text().as_deref().map(str::to_string)),
7557 Some(
7558 "in selections.iter() {
7559 let mut start = selection.start;
7560 let mut end = selection.end;
7561 let is_entire_line = selection.is_empty();
7562 if is_entire_line {
7563 start = Point::new(start.row, 0);"
7564 .to_string()
7565 ),
7566 "When selecting past the indent, the copying works as usual",
7567 );
7568 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7569 assert_eq!(
7570 cx.read_from_clipboard()
7571 .and_then(|item| item.text().as_deref().map(str::to_string)),
7572 Some(
7573 "in selections.iter() {
7574 let mut start = selection.start;
7575 let mut end = selection.end;
7576 let is_entire_line = selection.is_empty();
7577 if is_entire_line {
7578 start = Point::new(start.row, 0);"
7579 .to_string()
7580 ),
7581 "When selecting past the indent, nothing is trimmed"
7582 );
7583
7584 cx.set_state(
7585 r#" «for selection in selections.iter() {
7586 let mut start = selection.start;
7587
7588 let mut end = selection.end;
7589 let is_entire_line = selection.is_empty();
7590 if is_entire_line {
7591 start = Point::new(start.row, 0);
7592ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7593 }
7594 "#,
7595 );
7596 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7597 assert_eq!(
7598 cx.read_from_clipboard()
7599 .and_then(|item| item.text().as_deref().map(str::to_string)),
7600 Some(
7601 "for selection in selections.iter() {
7602let mut start = selection.start;
7603
7604let mut end = selection.end;
7605let is_entire_line = selection.is_empty();
7606if is_entire_line {
7607 start = Point::new(start.row, 0);
7608"
7609 .to_string()
7610 ),
7611 "Copying with stripping should ignore empty lines"
7612 );
7613}
7614
7615#[gpui::test]
7616async fn test_copy_trim_line_mode(cx: &mut TestAppContext) {
7617 init_test(cx, |_| {});
7618
7619 let mut cx = EditorTestContext::new(cx).await;
7620
7621 cx.set_state(indoc! {"
7622 « a
7623 bˇ»
7624 "});
7625 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
7626 cx.update_editor(|editor, window, cx| editor.copy_and_trim(&CopyAndTrim, window, cx));
7627
7628 assert_eq!(
7629 cx.read_from_clipboard().and_then(|item| item.text()),
7630 Some("a\nb\n".to_string())
7631 );
7632}
7633
7634#[gpui::test]
7635async fn test_clipboard_line_numbers_from_multibuffer(cx: &mut TestAppContext) {
7636 init_test(cx, |_| {});
7637
7638 let fs = FakeFs::new(cx.executor());
7639 fs.insert_file(
7640 path!("/file.txt"),
7641 "first line\nsecond line\nthird line\nfourth line\nfifth line\n".into(),
7642 )
7643 .await;
7644
7645 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
7646
7647 let buffer = project
7648 .update(cx, |project, cx| {
7649 project.open_local_buffer(path!("/file.txt"), cx)
7650 })
7651 .await
7652 .unwrap();
7653
7654 let multibuffer = cx.new(|cx| {
7655 let mut multibuffer = MultiBuffer::new(ReadWrite);
7656 multibuffer.push_excerpts(
7657 buffer.clone(),
7658 [ExcerptRange::new(Point::new(2, 0)..Point::new(5, 0))],
7659 cx,
7660 );
7661 multibuffer
7662 });
7663
7664 let (editor, cx) = cx.add_window_view(|window, cx| {
7665 build_editor_with_project(project.clone(), multibuffer, window, cx)
7666 });
7667
7668 editor.update_in(cx, |editor, window, cx| {
7669 assert_eq!(editor.text(cx), "third line\nfourth line\nfifth line\n");
7670
7671 editor.select_all(&SelectAll, window, cx);
7672 editor.copy(&Copy, window, cx);
7673 });
7674
7675 let clipboard_selections: Option<Vec<ClipboardSelection>> = cx
7676 .read_from_clipboard()
7677 .and_then(|item| item.entries().first().cloned())
7678 .and_then(|entry| match entry {
7679 gpui::ClipboardEntry::String(text) => text.metadata_json(),
7680 _ => None,
7681 });
7682
7683 let selections = clipboard_selections.expect("should have clipboard selections");
7684 assert_eq!(selections.len(), 1);
7685 let selection = &selections[0];
7686 assert_eq!(
7687 selection.line_range,
7688 Some(2..=5),
7689 "line range should be from original file (rows 2-5), not multibuffer rows (0-2)"
7690 );
7691}
7692
7693#[gpui::test]
7694async fn test_paste_multiline(cx: &mut TestAppContext) {
7695 init_test(cx, |_| {});
7696
7697 let mut cx = EditorTestContext::new(cx).await;
7698 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7699
7700 // Cut an indented block, without the leading whitespace.
7701 cx.set_state(indoc! {"
7702 const a: B = (
7703 c(),
7704 «d(
7705 e,
7706 f
7707 )ˇ»
7708 );
7709 "});
7710 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7711 cx.assert_editor_state(indoc! {"
7712 const a: B = (
7713 c(),
7714 ˇ
7715 );
7716 "});
7717
7718 // Paste it at the same position.
7719 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7720 cx.assert_editor_state(indoc! {"
7721 const a: B = (
7722 c(),
7723 d(
7724 e,
7725 f
7726 )ˇ
7727 );
7728 "});
7729
7730 // Paste it at a line with a lower indent level.
7731 cx.set_state(indoc! {"
7732 ˇ
7733 const a: B = (
7734 c(),
7735 );
7736 "});
7737 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7738 cx.assert_editor_state(indoc! {"
7739 d(
7740 e,
7741 f
7742 )ˇ
7743 const a: B = (
7744 c(),
7745 );
7746 "});
7747
7748 // Cut an indented block, with the leading whitespace.
7749 cx.set_state(indoc! {"
7750 const a: B = (
7751 c(),
7752 « d(
7753 e,
7754 f
7755 )
7756 ˇ»);
7757 "});
7758 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7759 cx.assert_editor_state(indoc! {"
7760 const a: B = (
7761 c(),
7762 ˇ);
7763 "});
7764
7765 // Paste it at the same position.
7766 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7767 cx.assert_editor_state(indoc! {"
7768 const a: B = (
7769 c(),
7770 d(
7771 e,
7772 f
7773 )
7774 ˇ);
7775 "});
7776
7777 // Paste it at a line with a higher indent level.
7778 cx.set_state(indoc! {"
7779 const a: B = (
7780 c(),
7781 d(
7782 e,
7783 fˇ
7784 )
7785 );
7786 "});
7787 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7788 cx.assert_editor_state(indoc! {"
7789 const a: B = (
7790 c(),
7791 d(
7792 e,
7793 f d(
7794 e,
7795 f
7796 )
7797 ˇ
7798 )
7799 );
7800 "});
7801
7802 // Copy an indented block, starting mid-line
7803 cx.set_state(indoc! {"
7804 const a: B = (
7805 c(),
7806 somethin«g(
7807 e,
7808 f
7809 )ˇ»
7810 );
7811 "});
7812 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7813
7814 // Paste it on a line with a lower indent level
7815 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7816 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7817 cx.assert_editor_state(indoc! {"
7818 const a: B = (
7819 c(),
7820 something(
7821 e,
7822 f
7823 )
7824 );
7825 g(
7826 e,
7827 f
7828 )ˇ"});
7829}
7830
7831#[gpui::test]
7832async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7833 init_test(cx, |_| {});
7834
7835 cx.write_to_clipboard(ClipboardItem::new_string(
7836 " d(\n e\n );\n".into(),
7837 ));
7838
7839 let mut cx = EditorTestContext::new(cx).await;
7840 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7841
7842 cx.set_state(indoc! {"
7843 fn a() {
7844 b();
7845 if c() {
7846 ˇ
7847 }
7848 }
7849 "});
7850
7851 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7852 cx.assert_editor_state(indoc! {"
7853 fn a() {
7854 b();
7855 if c() {
7856 d(
7857 e
7858 );
7859 ˇ
7860 }
7861 }
7862 "});
7863
7864 cx.set_state(indoc! {"
7865 fn a() {
7866 b();
7867 ˇ
7868 }
7869 "});
7870
7871 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7872 cx.assert_editor_state(indoc! {"
7873 fn a() {
7874 b();
7875 d(
7876 e
7877 );
7878 ˇ
7879 }
7880 "});
7881}
7882
7883#[gpui::test]
7884fn test_select_all(cx: &mut TestAppContext) {
7885 init_test(cx, |_| {});
7886
7887 let editor = cx.add_window(|window, cx| {
7888 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7889 build_editor(buffer, window, cx)
7890 });
7891 _ = editor.update(cx, |editor, window, cx| {
7892 editor.select_all(&SelectAll, window, cx);
7893 assert_eq!(
7894 display_ranges(editor, cx),
7895 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7896 );
7897 });
7898}
7899
7900#[gpui::test]
7901fn test_select_line(cx: &mut TestAppContext) {
7902 init_test(cx, |_| {});
7903
7904 let editor = cx.add_window(|window, cx| {
7905 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7906 build_editor(buffer, window, cx)
7907 });
7908 _ = editor.update(cx, |editor, window, cx| {
7909 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7910 s.select_display_ranges([
7911 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7912 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7913 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7914 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7915 ])
7916 });
7917 editor.select_line(&SelectLine, window, cx);
7918 // Adjacent line selections should NOT merge (only overlapping ones do)
7919 assert_eq!(
7920 display_ranges(editor, cx),
7921 vec![
7922 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0),
7923 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(2), 0),
7924 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7925 ]
7926 );
7927 });
7928
7929 _ = editor.update(cx, |editor, window, cx| {
7930 editor.select_line(&SelectLine, window, cx);
7931 assert_eq!(
7932 display_ranges(editor, cx),
7933 vec![
7934 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7935 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7936 ]
7937 );
7938 });
7939
7940 _ = editor.update(cx, |editor, window, cx| {
7941 editor.select_line(&SelectLine, window, cx);
7942 // Adjacent but not overlapping, so they stay separate
7943 assert_eq!(
7944 display_ranges(editor, cx),
7945 vec![
7946 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(4), 0),
7947 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7948 ]
7949 );
7950 });
7951}
7952
7953#[gpui::test]
7954async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7955 init_test(cx, |_| {});
7956 let mut cx = EditorTestContext::new(cx).await;
7957
7958 #[track_caller]
7959 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7960 cx.set_state(initial_state);
7961 cx.update_editor(|e, window, cx| {
7962 e.split_selection_into_lines(&Default::default(), window, cx)
7963 });
7964 cx.assert_editor_state(expected_state);
7965 }
7966
7967 // Selection starts and ends at the middle of lines, left-to-right
7968 test(
7969 &mut cx,
7970 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7971 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7972 );
7973 // Same thing, right-to-left
7974 test(
7975 &mut cx,
7976 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7977 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7978 );
7979
7980 // Whole buffer, left-to-right, last line *doesn't* end with newline
7981 test(
7982 &mut cx,
7983 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7984 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7985 );
7986 // Same thing, right-to-left
7987 test(
7988 &mut cx,
7989 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7990 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7991 );
7992
7993 // Whole buffer, left-to-right, last line ends with newline
7994 test(
7995 &mut cx,
7996 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7997 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7998 );
7999 // Same thing, right-to-left
8000 test(
8001 &mut cx,
8002 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
8003 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
8004 );
8005
8006 // Starts at the end of a line, ends at the start of another
8007 test(
8008 &mut cx,
8009 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
8010 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
8011 );
8012}
8013
8014#[gpui::test]
8015async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
8016 init_test(cx, |_| {});
8017
8018 let editor = cx.add_window(|window, cx| {
8019 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
8020 build_editor(buffer, window, cx)
8021 });
8022
8023 // setup
8024 _ = editor.update(cx, |editor, window, cx| {
8025 editor.fold_creases(
8026 vec![
8027 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
8028 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
8029 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
8030 ],
8031 true,
8032 window,
8033 cx,
8034 );
8035 assert_eq!(
8036 editor.display_text(cx),
8037 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
8038 );
8039 });
8040
8041 _ = editor.update(cx, |editor, window, cx| {
8042 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8043 s.select_display_ranges([
8044 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8045 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
8046 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
8047 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
8048 ])
8049 });
8050 editor.split_selection_into_lines(&Default::default(), window, cx);
8051 assert_eq!(
8052 editor.display_text(cx),
8053 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
8054 );
8055 });
8056 EditorTestContext::for_editor(editor, cx)
8057 .await
8058 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
8059
8060 _ = editor.update(cx, |editor, window, cx| {
8061 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8062 s.select_display_ranges([
8063 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
8064 ])
8065 });
8066 editor.split_selection_into_lines(&Default::default(), window, cx);
8067 assert_eq!(
8068 editor.display_text(cx),
8069 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
8070 );
8071 assert_eq!(
8072 display_ranges(editor, cx),
8073 [
8074 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
8075 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
8076 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
8077 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
8078 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
8079 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
8080 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
8081 ]
8082 );
8083 });
8084 EditorTestContext::for_editor(editor, cx)
8085 .await
8086 .assert_editor_state(
8087 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
8088 );
8089}
8090
8091#[gpui::test]
8092async fn test_add_selection_above_below(cx: &mut TestAppContext) {
8093 init_test(cx, |_| {});
8094
8095 let mut cx = EditorTestContext::new(cx).await;
8096
8097 cx.set_state(indoc!(
8098 r#"abc
8099 defˇghi
8100
8101 jk
8102 nlmo
8103 "#
8104 ));
8105
8106 cx.update_editor(|editor, window, cx| {
8107 editor.add_selection_above(&Default::default(), window, cx);
8108 });
8109
8110 cx.assert_editor_state(indoc!(
8111 r#"abcˇ
8112 defˇghi
8113
8114 jk
8115 nlmo
8116 "#
8117 ));
8118
8119 cx.update_editor(|editor, window, cx| {
8120 editor.add_selection_above(&Default::default(), window, cx);
8121 });
8122
8123 cx.assert_editor_state(indoc!(
8124 r#"abcˇ
8125 defˇghi
8126
8127 jk
8128 nlmo
8129 "#
8130 ));
8131
8132 cx.update_editor(|editor, window, cx| {
8133 editor.add_selection_below(&Default::default(), window, cx);
8134 });
8135
8136 cx.assert_editor_state(indoc!(
8137 r#"abc
8138 defˇghi
8139
8140 jk
8141 nlmo
8142 "#
8143 ));
8144
8145 cx.update_editor(|editor, window, cx| {
8146 editor.undo_selection(&Default::default(), window, cx);
8147 });
8148
8149 cx.assert_editor_state(indoc!(
8150 r#"abcˇ
8151 defˇghi
8152
8153 jk
8154 nlmo
8155 "#
8156 ));
8157
8158 cx.update_editor(|editor, window, cx| {
8159 editor.redo_selection(&Default::default(), window, cx);
8160 });
8161
8162 cx.assert_editor_state(indoc!(
8163 r#"abc
8164 defˇghi
8165
8166 jk
8167 nlmo
8168 "#
8169 ));
8170
8171 cx.update_editor(|editor, window, cx| {
8172 editor.add_selection_below(&Default::default(), window, cx);
8173 });
8174
8175 cx.assert_editor_state(indoc!(
8176 r#"abc
8177 defˇghi
8178 ˇ
8179 jk
8180 nlmo
8181 "#
8182 ));
8183
8184 cx.update_editor(|editor, window, cx| {
8185 editor.add_selection_below(&Default::default(), window, cx);
8186 });
8187
8188 cx.assert_editor_state(indoc!(
8189 r#"abc
8190 defˇghi
8191 ˇ
8192 jkˇ
8193 nlmo
8194 "#
8195 ));
8196
8197 cx.update_editor(|editor, window, cx| {
8198 editor.add_selection_below(&Default::default(), window, cx);
8199 });
8200
8201 cx.assert_editor_state(indoc!(
8202 r#"abc
8203 defˇghi
8204 ˇ
8205 jkˇ
8206 nlmˇo
8207 "#
8208 ));
8209
8210 cx.update_editor(|editor, window, cx| {
8211 editor.add_selection_below(&Default::default(), window, cx);
8212 });
8213
8214 cx.assert_editor_state(indoc!(
8215 r#"abc
8216 defˇghi
8217 ˇ
8218 jkˇ
8219 nlmˇo
8220 ˇ"#
8221 ));
8222
8223 // change selections
8224 cx.set_state(indoc!(
8225 r#"abc
8226 def«ˇg»hi
8227
8228 jk
8229 nlmo
8230 "#
8231 ));
8232
8233 cx.update_editor(|editor, window, cx| {
8234 editor.add_selection_below(&Default::default(), window, cx);
8235 });
8236
8237 cx.assert_editor_state(indoc!(
8238 r#"abc
8239 def«ˇg»hi
8240
8241 jk
8242 nlm«ˇo»
8243 "#
8244 ));
8245
8246 cx.update_editor(|editor, window, cx| {
8247 editor.add_selection_below(&Default::default(), window, cx);
8248 });
8249
8250 cx.assert_editor_state(indoc!(
8251 r#"abc
8252 def«ˇg»hi
8253
8254 jk
8255 nlm«ˇo»
8256 "#
8257 ));
8258
8259 cx.update_editor(|editor, window, cx| {
8260 editor.add_selection_above(&Default::default(), window, cx);
8261 });
8262
8263 cx.assert_editor_state(indoc!(
8264 r#"abc
8265 def«ˇg»hi
8266
8267 jk
8268 nlmo
8269 "#
8270 ));
8271
8272 cx.update_editor(|editor, window, cx| {
8273 editor.add_selection_above(&Default::default(), window, cx);
8274 });
8275
8276 cx.assert_editor_state(indoc!(
8277 r#"abc
8278 def«ˇg»hi
8279
8280 jk
8281 nlmo
8282 "#
8283 ));
8284
8285 // Change selections again
8286 cx.set_state(indoc!(
8287 r#"a«bc
8288 defgˇ»hi
8289
8290 jk
8291 nlmo
8292 "#
8293 ));
8294
8295 cx.update_editor(|editor, window, cx| {
8296 editor.add_selection_below(&Default::default(), window, cx);
8297 });
8298
8299 cx.assert_editor_state(indoc!(
8300 r#"a«bcˇ»
8301 d«efgˇ»hi
8302
8303 j«kˇ»
8304 nlmo
8305 "#
8306 ));
8307
8308 cx.update_editor(|editor, window, cx| {
8309 editor.add_selection_below(&Default::default(), window, cx);
8310 });
8311 cx.assert_editor_state(indoc!(
8312 r#"a«bcˇ»
8313 d«efgˇ»hi
8314
8315 j«kˇ»
8316 n«lmoˇ»
8317 "#
8318 ));
8319 cx.update_editor(|editor, window, cx| {
8320 editor.add_selection_above(&Default::default(), window, cx);
8321 });
8322
8323 cx.assert_editor_state(indoc!(
8324 r#"a«bcˇ»
8325 d«efgˇ»hi
8326
8327 j«kˇ»
8328 nlmo
8329 "#
8330 ));
8331
8332 // Change selections again
8333 cx.set_state(indoc!(
8334 r#"abc
8335 d«ˇefghi
8336
8337 jk
8338 nlm»o
8339 "#
8340 ));
8341
8342 cx.update_editor(|editor, window, cx| {
8343 editor.add_selection_above(&Default::default(), window, cx);
8344 });
8345
8346 cx.assert_editor_state(indoc!(
8347 r#"a«ˇbc»
8348 d«ˇef»ghi
8349
8350 j«ˇk»
8351 n«ˇlm»o
8352 "#
8353 ));
8354
8355 cx.update_editor(|editor, window, cx| {
8356 editor.add_selection_below(&Default::default(), window, cx);
8357 });
8358
8359 cx.assert_editor_state(indoc!(
8360 r#"abc
8361 d«ˇef»ghi
8362
8363 j«ˇk»
8364 n«ˇlm»o
8365 "#
8366 ));
8367
8368 // Assert that the oldest selection's goal column is used when adding more
8369 // selections, not the most recently added selection's actual column.
8370 cx.set_state(indoc! {"
8371 foo bar bazˇ
8372 foo
8373 foo bar
8374 "});
8375
8376 cx.update_editor(|editor, window, cx| {
8377 editor.add_selection_below(
8378 &AddSelectionBelow {
8379 skip_soft_wrap: true,
8380 },
8381 window,
8382 cx,
8383 );
8384 });
8385
8386 cx.assert_editor_state(indoc! {"
8387 foo bar bazˇ
8388 fooˇ
8389 foo bar
8390 "});
8391
8392 cx.update_editor(|editor, window, cx| {
8393 editor.add_selection_below(
8394 &AddSelectionBelow {
8395 skip_soft_wrap: true,
8396 },
8397 window,
8398 cx,
8399 );
8400 });
8401
8402 cx.assert_editor_state(indoc! {"
8403 foo bar bazˇ
8404 fooˇ
8405 foo barˇ
8406 "});
8407
8408 cx.set_state(indoc! {"
8409 foo bar baz
8410 foo
8411 foo barˇ
8412 "});
8413
8414 cx.update_editor(|editor, window, cx| {
8415 editor.add_selection_above(
8416 &AddSelectionAbove {
8417 skip_soft_wrap: true,
8418 },
8419 window,
8420 cx,
8421 );
8422 });
8423
8424 cx.assert_editor_state(indoc! {"
8425 foo bar baz
8426 fooˇ
8427 foo barˇ
8428 "});
8429
8430 cx.update_editor(|editor, window, cx| {
8431 editor.add_selection_above(
8432 &AddSelectionAbove {
8433 skip_soft_wrap: true,
8434 },
8435 window,
8436 cx,
8437 );
8438 });
8439
8440 cx.assert_editor_state(indoc! {"
8441 foo barˇ baz
8442 fooˇ
8443 foo barˇ
8444 "});
8445}
8446
8447#[gpui::test]
8448async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8449 init_test(cx, |_| {});
8450 let mut cx = EditorTestContext::new(cx).await;
8451
8452 cx.set_state(indoc!(
8453 r#"line onˇe
8454 liˇne two
8455 line three
8456 line four"#
8457 ));
8458
8459 cx.update_editor(|editor, window, cx| {
8460 editor.add_selection_below(&Default::default(), window, cx);
8461 });
8462
8463 // test multiple cursors expand in the same direction
8464 cx.assert_editor_state(indoc!(
8465 r#"line onˇe
8466 liˇne twˇo
8467 liˇne three
8468 line four"#
8469 ));
8470
8471 cx.update_editor(|editor, window, cx| {
8472 editor.add_selection_below(&Default::default(), window, cx);
8473 });
8474
8475 cx.update_editor(|editor, window, cx| {
8476 editor.add_selection_below(&Default::default(), window, cx);
8477 });
8478
8479 // test multiple cursors expand below overflow
8480 cx.assert_editor_state(indoc!(
8481 r#"line onˇe
8482 liˇne twˇo
8483 liˇne thˇree
8484 liˇne foˇur"#
8485 ));
8486
8487 cx.update_editor(|editor, window, cx| {
8488 editor.add_selection_above(&Default::default(), window, cx);
8489 });
8490
8491 // test multiple cursors retrieves back correctly
8492 cx.assert_editor_state(indoc!(
8493 r#"line onˇe
8494 liˇne twˇo
8495 liˇne thˇree
8496 line four"#
8497 ));
8498
8499 cx.update_editor(|editor, window, cx| {
8500 editor.add_selection_above(&Default::default(), window, cx);
8501 });
8502
8503 cx.update_editor(|editor, window, cx| {
8504 editor.add_selection_above(&Default::default(), window, cx);
8505 });
8506
8507 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8508 cx.assert_editor_state(indoc!(
8509 r#"liˇne onˇe
8510 liˇne two
8511 line three
8512 line four"#
8513 ));
8514
8515 cx.update_editor(|editor, window, cx| {
8516 editor.undo_selection(&Default::default(), window, cx);
8517 });
8518
8519 // test undo
8520 cx.assert_editor_state(indoc!(
8521 r#"line onˇe
8522 liˇne twˇo
8523 line three
8524 line four"#
8525 ));
8526
8527 cx.update_editor(|editor, window, cx| {
8528 editor.redo_selection(&Default::default(), window, cx);
8529 });
8530
8531 // test redo
8532 cx.assert_editor_state(indoc!(
8533 r#"liˇne onˇe
8534 liˇne two
8535 line three
8536 line four"#
8537 ));
8538
8539 cx.set_state(indoc!(
8540 r#"abcd
8541 ef«ghˇ»
8542 ijkl
8543 «mˇ»nop"#
8544 ));
8545
8546 cx.update_editor(|editor, window, cx| {
8547 editor.add_selection_above(&Default::default(), window, cx);
8548 });
8549
8550 // test multiple selections expand in the same direction
8551 cx.assert_editor_state(indoc!(
8552 r#"ab«cdˇ»
8553 ef«ghˇ»
8554 «iˇ»jkl
8555 «mˇ»nop"#
8556 ));
8557
8558 cx.update_editor(|editor, window, cx| {
8559 editor.add_selection_above(&Default::default(), window, cx);
8560 });
8561
8562 // test multiple selection upward overflow
8563 cx.assert_editor_state(indoc!(
8564 r#"ab«cdˇ»
8565 «eˇ»f«ghˇ»
8566 «iˇ»jkl
8567 «mˇ»nop"#
8568 ));
8569
8570 cx.update_editor(|editor, window, cx| {
8571 editor.add_selection_below(&Default::default(), window, cx);
8572 });
8573
8574 // test multiple selection retrieves back correctly
8575 cx.assert_editor_state(indoc!(
8576 r#"abcd
8577 ef«ghˇ»
8578 «iˇ»jkl
8579 «mˇ»nop"#
8580 ));
8581
8582 cx.update_editor(|editor, window, cx| {
8583 editor.add_selection_below(&Default::default(), window, cx);
8584 });
8585
8586 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8587 cx.assert_editor_state(indoc!(
8588 r#"abcd
8589 ef«ghˇ»
8590 ij«klˇ»
8591 «mˇ»nop"#
8592 ));
8593
8594 cx.update_editor(|editor, window, cx| {
8595 editor.undo_selection(&Default::default(), window, cx);
8596 });
8597
8598 // test undo
8599 cx.assert_editor_state(indoc!(
8600 r#"abcd
8601 ef«ghˇ»
8602 «iˇ»jkl
8603 «mˇ»nop"#
8604 ));
8605
8606 cx.update_editor(|editor, window, cx| {
8607 editor.redo_selection(&Default::default(), window, cx);
8608 });
8609
8610 // test redo
8611 cx.assert_editor_state(indoc!(
8612 r#"abcd
8613 ef«ghˇ»
8614 ij«klˇ»
8615 «mˇ»nop"#
8616 ));
8617}
8618
8619#[gpui::test]
8620async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8621 init_test(cx, |_| {});
8622 let mut cx = EditorTestContext::new(cx).await;
8623
8624 cx.set_state(indoc!(
8625 r#"line onˇe
8626 liˇne two
8627 line three
8628 line four"#
8629 ));
8630
8631 cx.update_editor(|editor, window, cx| {
8632 editor.add_selection_below(&Default::default(), window, cx);
8633 editor.add_selection_below(&Default::default(), window, cx);
8634 editor.add_selection_below(&Default::default(), window, cx);
8635 });
8636
8637 // initial state with two multi cursor groups
8638 cx.assert_editor_state(indoc!(
8639 r#"line onˇe
8640 liˇne twˇo
8641 liˇne thˇree
8642 liˇne foˇur"#
8643 ));
8644
8645 // add single cursor in middle - simulate opt click
8646 cx.update_editor(|editor, window, cx| {
8647 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8648 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8649 editor.end_selection(window, cx);
8650 });
8651
8652 cx.assert_editor_state(indoc!(
8653 r#"line onˇe
8654 liˇne twˇo
8655 liˇneˇ thˇree
8656 liˇne foˇur"#
8657 ));
8658
8659 cx.update_editor(|editor, window, cx| {
8660 editor.add_selection_above(&Default::default(), window, cx);
8661 });
8662
8663 // test new added selection expands above and existing selection shrinks
8664 cx.assert_editor_state(indoc!(
8665 r#"line onˇe
8666 liˇneˇ twˇo
8667 liˇneˇ thˇree
8668 line four"#
8669 ));
8670
8671 cx.update_editor(|editor, window, cx| {
8672 editor.add_selection_above(&Default::default(), window, cx);
8673 });
8674
8675 // test new added selection expands above and existing selection shrinks
8676 cx.assert_editor_state(indoc!(
8677 r#"lineˇ onˇe
8678 liˇneˇ twˇo
8679 lineˇ three
8680 line four"#
8681 ));
8682
8683 // intial state with two selection groups
8684 cx.set_state(indoc!(
8685 r#"abcd
8686 ef«ghˇ»
8687 ijkl
8688 «mˇ»nop"#
8689 ));
8690
8691 cx.update_editor(|editor, window, cx| {
8692 editor.add_selection_above(&Default::default(), window, cx);
8693 editor.add_selection_above(&Default::default(), window, cx);
8694 });
8695
8696 cx.assert_editor_state(indoc!(
8697 r#"ab«cdˇ»
8698 «eˇ»f«ghˇ»
8699 «iˇ»jkl
8700 «mˇ»nop"#
8701 ));
8702
8703 // add single selection in middle - simulate opt drag
8704 cx.update_editor(|editor, window, cx| {
8705 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8706 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8707 editor.update_selection(
8708 DisplayPoint::new(DisplayRow(2), 4),
8709 0,
8710 gpui::Point::<f32>::default(),
8711 window,
8712 cx,
8713 );
8714 editor.end_selection(window, cx);
8715 });
8716
8717 cx.assert_editor_state(indoc!(
8718 r#"ab«cdˇ»
8719 «eˇ»f«ghˇ»
8720 «iˇ»jk«lˇ»
8721 «mˇ»nop"#
8722 ));
8723
8724 cx.update_editor(|editor, window, cx| {
8725 editor.add_selection_below(&Default::default(), window, cx);
8726 });
8727
8728 // test new added selection expands below, others shrinks from above
8729 cx.assert_editor_state(indoc!(
8730 r#"abcd
8731 ef«ghˇ»
8732 «iˇ»jk«lˇ»
8733 «mˇ»no«pˇ»"#
8734 ));
8735}
8736
8737#[gpui::test]
8738async fn test_select_next(cx: &mut TestAppContext) {
8739 init_test(cx, |_| {});
8740 let mut cx = EditorTestContext::new(cx).await;
8741
8742 // Enable case sensitive search.
8743 update_test_editor_settings(&mut cx, |settings| {
8744 let mut search_settings = SearchSettingsContent::default();
8745 search_settings.case_sensitive = Some(true);
8746 settings.search = Some(search_settings);
8747 });
8748
8749 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8750
8751 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8752 .unwrap();
8753 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8754
8755 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8756 .unwrap();
8757 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8758
8759 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8760 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8761
8762 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8763 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8764
8765 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8766 .unwrap();
8767 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8768
8769 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8770 .unwrap();
8771 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8772
8773 // Test selection direction should be preserved
8774 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8775
8776 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8777 .unwrap();
8778 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8779
8780 // Test case sensitivity
8781 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8782 cx.update_editor(|e, window, cx| {
8783 e.select_next(&SelectNext::default(), window, cx).unwrap();
8784 });
8785 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8786
8787 // Disable case sensitive search.
8788 update_test_editor_settings(&mut cx, |settings| {
8789 let mut search_settings = SearchSettingsContent::default();
8790 search_settings.case_sensitive = Some(false);
8791 settings.search = Some(search_settings);
8792 });
8793
8794 cx.set_state("«ˇfoo»\nFOO\nFoo");
8795 cx.update_editor(|e, window, cx| {
8796 e.select_next(&SelectNext::default(), window, cx).unwrap();
8797 e.select_next(&SelectNext::default(), window, cx).unwrap();
8798 });
8799 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8800}
8801
8802#[gpui::test]
8803async fn test_select_all_matches(cx: &mut TestAppContext) {
8804 init_test(cx, |_| {});
8805 let mut cx = EditorTestContext::new(cx).await;
8806
8807 // Enable case sensitive search.
8808 update_test_editor_settings(&mut cx, |settings| {
8809 let mut search_settings = SearchSettingsContent::default();
8810 search_settings.case_sensitive = Some(true);
8811 settings.search = Some(search_settings);
8812 });
8813
8814 // Test caret-only selections
8815 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8816 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8817 .unwrap();
8818 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8819
8820 // Test left-to-right selections
8821 cx.set_state("abc\n«abcˇ»\nabc");
8822 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8823 .unwrap();
8824 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8825
8826 // Test right-to-left selections
8827 cx.set_state("abc\n«ˇabc»\nabc");
8828 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8829 .unwrap();
8830 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8831
8832 // Test selecting whitespace with caret selection
8833 cx.set_state("abc\nˇ abc\nabc");
8834 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8835 .unwrap();
8836 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8837
8838 // Test selecting whitespace with left-to-right selection
8839 cx.set_state("abc\n«ˇ »abc\nabc");
8840 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8841 .unwrap();
8842 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8843
8844 // Test no matches with right-to-left selection
8845 cx.set_state("abc\n« ˇ»abc\nabc");
8846 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8847 .unwrap();
8848 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8849
8850 // Test with a single word and clip_at_line_ends=true (#29823)
8851 cx.set_state("aˇbc");
8852 cx.update_editor(|e, window, cx| {
8853 e.set_clip_at_line_ends(true, cx);
8854 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8855 e.set_clip_at_line_ends(false, cx);
8856 });
8857 cx.assert_editor_state("«abcˇ»");
8858
8859 // Test case sensitivity
8860 cx.set_state("fˇoo\nFOO\nFoo");
8861 cx.update_editor(|e, window, cx| {
8862 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8863 });
8864 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8865
8866 // Disable case sensitive search.
8867 update_test_editor_settings(&mut cx, |settings| {
8868 let mut search_settings = SearchSettingsContent::default();
8869 search_settings.case_sensitive = Some(false);
8870 settings.search = Some(search_settings);
8871 });
8872
8873 cx.set_state("fˇoo\nFOO\nFoo");
8874 cx.update_editor(|e, window, cx| {
8875 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8876 });
8877 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
8878}
8879
8880#[gpui::test]
8881async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8882 init_test(cx, |_| {});
8883
8884 let mut cx = EditorTestContext::new(cx).await;
8885
8886 let large_body_1 = "\nd".repeat(200);
8887 let large_body_2 = "\ne".repeat(200);
8888
8889 cx.set_state(&format!(
8890 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8891 ));
8892 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8893 let scroll_position = editor.scroll_position(cx);
8894 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8895 scroll_position
8896 });
8897
8898 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8899 .unwrap();
8900 cx.assert_editor_state(&format!(
8901 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8902 ));
8903 let scroll_position_after_selection =
8904 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8905 assert_eq!(
8906 initial_scroll_position, scroll_position_after_selection,
8907 "Scroll position should not change after selecting all matches"
8908 );
8909}
8910
8911#[gpui::test]
8912async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8913 init_test(cx, |_| {});
8914
8915 let mut cx = EditorLspTestContext::new_rust(
8916 lsp::ServerCapabilities {
8917 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8918 ..Default::default()
8919 },
8920 cx,
8921 )
8922 .await;
8923
8924 cx.set_state(indoc! {"
8925 line 1
8926 line 2
8927 linˇe 3
8928 line 4
8929 line 5
8930 "});
8931
8932 // Make an edit
8933 cx.update_editor(|editor, window, cx| {
8934 editor.handle_input("X", window, cx);
8935 });
8936
8937 // Move cursor to a different position
8938 cx.update_editor(|editor, window, cx| {
8939 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8940 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8941 });
8942 });
8943
8944 cx.assert_editor_state(indoc! {"
8945 line 1
8946 line 2
8947 linXe 3
8948 line 4
8949 liˇne 5
8950 "});
8951
8952 cx.lsp
8953 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8954 Ok(Some(vec![lsp::TextEdit::new(
8955 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8956 "PREFIX ".to_string(),
8957 )]))
8958 });
8959
8960 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8961 .unwrap()
8962 .await
8963 .unwrap();
8964
8965 cx.assert_editor_state(indoc! {"
8966 PREFIX line 1
8967 line 2
8968 linXe 3
8969 line 4
8970 liˇne 5
8971 "});
8972
8973 // Undo formatting
8974 cx.update_editor(|editor, window, cx| {
8975 editor.undo(&Default::default(), window, cx);
8976 });
8977
8978 // Verify cursor moved back to position after edit
8979 cx.assert_editor_state(indoc! {"
8980 line 1
8981 line 2
8982 linXˇe 3
8983 line 4
8984 line 5
8985 "});
8986}
8987
8988#[gpui::test]
8989async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8990 init_test(cx, |_| {});
8991
8992 let mut cx = EditorTestContext::new(cx).await;
8993
8994 let provider = cx.new(|_| FakeEditPredictionDelegate::default());
8995 cx.update_editor(|editor, window, cx| {
8996 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8997 });
8998
8999 cx.set_state(indoc! {"
9000 line 1
9001 line 2
9002 linˇe 3
9003 line 4
9004 line 5
9005 line 6
9006 line 7
9007 line 8
9008 line 9
9009 line 10
9010 "});
9011
9012 let snapshot = cx.buffer_snapshot();
9013 let edit_position = snapshot.anchor_after(Point::new(2, 4));
9014
9015 cx.update(|_, cx| {
9016 provider.update(cx, |provider, _| {
9017 provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
9018 id: None,
9019 edits: vec![(edit_position..edit_position, "X".into())],
9020 edit_preview: None,
9021 }))
9022 })
9023 });
9024
9025 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
9026 cx.update_editor(|editor, window, cx| {
9027 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
9028 });
9029
9030 cx.assert_editor_state(indoc! {"
9031 line 1
9032 line 2
9033 lineXˇ 3
9034 line 4
9035 line 5
9036 line 6
9037 line 7
9038 line 8
9039 line 9
9040 line 10
9041 "});
9042
9043 cx.update_editor(|editor, window, cx| {
9044 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9045 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
9046 });
9047 });
9048
9049 cx.assert_editor_state(indoc! {"
9050 line 1
9051 line 2
9052 lineX 3
9053 line 4
9054 line 5
9055 line 6
9056 line 7
9057 line 8
9058 line 9
9059 liˇne 10
9060 "});
9061
9062 cx.update_editor(|editor, window, cx| {
9063 editor.undo(&Default::default(), window, cx);
9064 });
9065
9066 cx.assert_editor_state(indoc! {"
9067 line 1
9068 line 2
9069 lineˇ 3
9070 line 4
9071 line 5
9072 line 6
9073 line 7
9074 line 8
9075 line 9
9076 line 10
9077 "});
9078}
9079
9080#[gpui::test]
9081async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
9082 init_test(cx, |_| {});
9083
9084 let mut cx = EditorTestContext::new(cx).await;
9085 cx.set_state(
9086 r#"let foo = 2;
9087lˇet foo = 2;
9088let fooˇ = 2;
9089let foo = 2;
9090let foo = ˇ2;"#,
9091 );
9092
9093 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9094 .unwrap();
9095 cx.assert_editor_state(
9096 r#"let foo = 2;
9097«letˇ» foo = 2;
9098let «fooˇ» = 2;
9099let foo = 2;
9100let foo = «2ˇ»;"#,
9101 );
9102
9103 // noop for multiple selections with different contents
9104 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9105 .unwrap();
9106 cx.assert_editor_state(
9107 r#"let foo = 2;
9108«letˇ» foo = 2;
9109let «fooˇ» = 2;
9110let foo = 2;
9111let foo = «2ˇ»;"#,
9112 );
9113
9114 // Test last selection direction should be preserved
9115 cx.set_state(
9116 r#"let foo = 2;
9117let foo = 2;
9118let «fooˇ» = 2;
9119let «ˇfoo» = 2;
9120let foo = 2;"#,
9121 );
9122
9123 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9124 .unwrap();
9125 cx.assert_editor_state(
9126 r#"let foo = 2;
9127let foo = 2;
9128let «fooˇ» = 2;
9129let «ˇfoo» = 2;
9130let «ˇfoo» = 2;"#,
9131 );
9132}
9133
9134#[gpui::test]
9135async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
9136 init_test(cx, |_| {});
9137
9138 let mut cx =
9139 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
9140
9141 cx.assert_editor_state(indoc! {"
9142 ˇbbb
9143 ccc
9144
9145 bbb
9146 ccc
9147 "});
9148 cx.dispatch_action(SelectPrevious::default());
9149 cx.assert_editor_state(indoc! {"
9150 «bbbˇ»
9151 ccc
9152
9153 bbb
9154 ccc
9155 "});
9156 cx.dispatch_action(SelectPrevious::default());
9157 cx.assert_editor_state(indoc! {"
9158 «bbbˇ»
9159 ccc
9160
9161 «bbbˇ»
9162 ccc
9163 "});
9164}
9165
9166#[gpui::test]
9167async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
9168 init_test(cx, |_| {});
9169
9170 let mut cx = EditorTestContext::new(cx).await;
9171 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
9172
9173 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9174 .unwrap();
9175 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9176
9177 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9178 .unwrap();
9179 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
9180
9181 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9182 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9183
9184 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9185 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
9186
9187 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9188 .unwrap();
9189 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
9190
9191 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9192 .unwrap();
9193 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
9194}
9195
9196#[gpui::test]
9197async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
9198 init_test(cx, |_| {});
9199
9200 let mut cx = EditorTestContext::new(cx).await;
9201 cx.set_state("aˇ");
9202
9203 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9204 .unwrap();
9205 cx.assert_editor_state("«aˇ»");
9206 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9207 .unwrap();
9208 cx.assert_editor_state("«aˇ»");
9209}
9210
9211#[gpui::test]
9212async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
9213 init_test(cx, |_| {});
9214
9215 let mut cx = EditorTestContext::new(cx).await;
9216 cx.set_state(
9217 r#"let foo = 2;
9218lˇet foo = 2;
9219let fooˇ = 2;
9220let foo = 2;
9221let foo = ˇ2;"#,
9222 );
9223
9224 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9225 .unwrap();
9226 cx.assert_editor_state(
9227 r#"let foo = 2;
9228«letˇ» foo = 2;
9229let «fooˇ» = 2;
9230let foo = 2;
9231let foo = «2ˇ»;"#,
9232 );
9233
9234 // noop for multiple selections with different contents
9235 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9236 .unwrap();
9237 cx.assert_editor_state(
9238 r#"let foo = 2;
9239«letˇ» foo = 2;
9240let «fooˇ» = 2;
9241let foo = 2;
9242let foo = «2ˇ»;"#,
9243 );
9244}
9245
9246#[gpui::test]
9247async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
9248 init_test(cx, |_| {});
9249 let mut cx = EditorTestContext::new(cx).await;
9250
9251 // Enable case sensitive search.
9252 update_test_editor_settings(&mut cx, |settings| {
9253 let mut search_settings = SearchSettingsContent::default();
9254 search_settings.case_sensitive = Some(true);
9255 settings.search = Some(search_settings);
9256 });
9257
9258 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
9259
9260 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9261 .unwrap();
9262 // selection direction is preserved
9263 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9264
9265 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9266 .unwrap();
9267 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9268
9269 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9270 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9271
9272 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9273 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9274
9275 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9276 .unwrap();
9277 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
9278
9279 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9280 .unwrap();
9281 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
9282
9283 // Test case sensitivity
9284 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
9285 cx.update_editor(|e, window, cx| {
9286 e.select_previous(&SelectPrevious::default(), window, cx)
9287 .unwrap();
9288 e.select_previous(&SelectPrevious::default(), window, cx)
9289 .unwrap();
9290 });
9291 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
9292
9293 // Disable case sensitive search.
9294 update_test_editor_settings(&mut cx, |settings| {
9295 let mut search_settings = SearchSettingsContent::default();
9296 search_settings.case_sensitive = Some(false);
9297 settings.search = Some(search_settings);
9298 });
9299
9300 cx.set_state("foo\nFOO\n«ˇFoo»");
9301 cx.update_editor(|e, window, cx| {
9302 e.select_previous(&SelectPrevious::default(), window, cx)
9303 .unwrap();
9304 e.select_previous(&SelectPrevious::default(), window, cx)
9305 .unwrap();
9306 });
9307 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
9308}
9309
9310#[gpui::test]
9311async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
9312 init_test(cx, |_| {});
9313
9314 let language = Arc::new(Language::new(
9315 LanguageConfig::default(),
9316 Some(tree_sitter_rust::LANGUAGE.into()),
9317 ));
9318
9319 let text = r#"
9320 use mod1::mod2::{mod3, mod4};
9321
9322 fn fn_1(param1: bool, param2: &str) {
9323 let var1 = "text";
9324 }
9325 "#
9326 .unindent();
9327
9328 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9329 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9330 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9331
9332 editor
9333 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9334 .await;
9335
9336 editor.update_in(cx, |editor, window, cx| {
9337 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9338 s.select_display_ranges([
9339 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
9340 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
9341 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
9342 ]);
9343 });
9344 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9345 });
9346 editor.update(cx, |editor, cx| {
9347 assert_text_with_selections(
9348 editor,
9349 indoc! {r#"
9350 use mod1::mod2::{mod3, «mod4ˇ»};
9351
9352 fn fn_1«ˇ(param1: bool, param2: &str)» {
9353 let var1 = "«ˇtext»";
9354 }
9355 "#},
9356 cx,
9357 );
9358 });
9359
9360 editor.update_in(cx, |editor, window, cx| {
9361 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9362 });
9363 editor.update(cx, |editor, cx| {
9364 assert_text_with_selections(
9365 editor,
9366 indoc! {r#"
9367 use mod1::mod2::«{mod3, mod4}ˇ»;
9368
9369 «ˇfn fn_1(param1: bool, param2: &str) {
9370 let var1 = "text";
9371 }»
9372 "#},
9373 cx,
9374 );
9375 });
9376
9377 editor.update_in(cx, |editor, window, cx| {
9378 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9379 });
9380 assert_eq!(
9381 editor.update(cx, |editor, cx| editor
9382 .selections
9383 .display_ranges(&editor.display_snapshot(cx))),
9384 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9385 );
9386
9387 // Trying to expand the selected syntax node one more time has no effect.
9388 editor.update_in(cx, |editor, window, cx| {
9389 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9390 });
9391 assert_eq!(
9392 editor.update(cx, |editor, cx| editor
9393 .selections
9394 .display_ranges(&editor.display_snapshot(cx))),
9395 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9396 );
9397
9398 editor.update_in(cx, |editor, window, cx| {
9399 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9400 });
9401 editor.update(cx, |editor, cx| {
9402 assert_text_with_selections(
9403 editor,
9404 indoc! {r#"
9405 use mod1::mod2::«{mod3, mod4}ˇ»;
9406
9407 «ˇfn fn_1(param1: bool, param2: &str) {
9408 let var1 = "text";
9409 }»
9410 "#},
9411 cx,
9412 );
9413 });
9414
9415 editor.update_in(cx, |editor, window, cx| {
9416 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9417 });
9418 editor.update(cx, |editor, cx| {
9419 assert_text_with_selections(
9420 editor,
9421 indoc! {r#"
9422 use mod1::mod2::{mod3, «mod4ˇ»};
9423
9424 fn fn_1«ˇ(param1: bool, param2: &str)» {
9425 let var1 = "«ˇtext»";
9426 }
9427 "#},
9428 cx,
9429 );
9430 });
9431
9432 editor.update_in(cx, |editor, window, cx| {
9433 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9434 });
9435 editor.update(cx, |editor, cx| {
9436 assert_text_with_selections(
9437 editor,
9438 indoc! {r#"
9439 use mod1::mod2::{mod3, moˇd4};
9440
9441 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9442 let var1 = "teˇxt";
9443 }
9444 "#},
9445 cx,
9446 );
9447 });
9448
9449 // Trying to shrink the selected syntax node one more time has no effect.
9450 editor.update_in(cx, |editor, window, cx| {
9451 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9452 });
9453 editor.update_in(cx, |editor, _, cx| {
9454 assert_text_with_selections(
9455 editor,
9456 indoc! {r#"
9457 use mod1::mod2::{mod3, moˇd4};
9458
9459 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9460 let var1 = "teˇxt";
9461 }
9462 "#},
9463 cx,
9464 );
9465 });
9466
9467 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9468 // a fold.
9469 editor.update_in(cx, |editor, window, cx| {
9470 editor.fold_creases(
9471 vec![
9472 Crease::simple(
9473 Point::new(0, 21)..Point::new(0, 24),
9474 FoldPlaceholder::test(),
9475 ),
9476 Crease::simple(
9477 Point::new(3, 20)..Point::new(3, 22),
9478 FoldPlaceholder::test(),
9479 ),
9480 ],
9481 true,
9482 window,
9483 cx,
9484 );
9485 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9486 });
9487 editor.update(cx, |editor, cx| {
9488 assert_text_with_selections(
9489 editor,
9490 indoc! {r#"
9491 use mod1::mod2::«{mod3, mod4}ˇ»;
9492
9493 fn fn_1«ˇ(param1: bool, param2: &str)» {
9494 let var1 = "«ˇtext»";
9495 }
9496 "#},
9497 cx,
9498 );
9499 });
9500}
9501
9502#[gpui::test]
9503async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9504 init_test(cx, |_| {});
9505
9506 let language = Arc::new(Language::new(
9507 LanguageConfig::default(),
9508 Some(tree_sitter_rust::LANGUAGE.into()),
9509 ));
9510
9511 let text = "let a = 2;";
9512
9513 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9514 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9515 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9516
9517 editor
9518 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9519 .await;
9520
9521 // Test case 1: Cursor at end of word
9522 editor.update_in(cx, |editor, window, cx| {
9523 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9524 s.select_display_ranges([
9525 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9526 ]);
9527 });
9528 });
9529 editor.update(cx, |editor, cx| {
9530 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9531 });
9532 editor.update_in(cx, |editor, window, cx| {
9533 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9534 });
9535 editor.update(cx, |editor, cx| {
9536 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9537 });
9538 editor.update_in(cx, |editor, window, cx| {
9539 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9540 });
9541 editor.update(cx, |editor, cx| {
9542 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9543 });
9544
9545 // Test case 2: Cursor at end of statement
9546 editor.update_in(cx, |editor, window, cx| {
9547 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9548 s.select_display_ranges([
9549 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9550 ]);
9551 });
9552 });
9553 editor.update(cx, |editor, cx| {
9554 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9555 });
9556 editor.update_in(cx, |editor, window, cx| {
9557 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9558 });
9559 editor.update(cx, |editor, cx| {
9560 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9561 });
9562}
9563
9564#[gpui::test]
9565async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9566 init_test(cx, |_| {});
9567
9568 let language = Arc::new(Language::new(
9569 LanguageConfig {
9570 name: "JavaScript".into(),
9571 ..Default::default()
9572 },
9573 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9574 ));
9575
9576 let text = r#"
9577 let a = {
9578 key: "value",
9579 };
9580 "#
9581 .unindent();
9582
9583 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9584 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9585 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9586
9587 editor
9588 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9589 .await;
9590
9591 // Test case 1: Cursor after '{'
9592 editor.update_in(cx, |editor, window, cx| {
9593 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9594 s.select_display_ranges([
9595 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9596 ]);
9597 });
9598 });
9599 editor.update(cx, |editor, cx| {
9600 assert_text_with_selections(
9601 editor,
9602 indoc! {r#"
9603 let a = {ˇ
9604 key: "value",
9605 };
9606 "#},
9607 cx,
9608 );
9609 });
9610 editor.update_in(cx, |editor, window, cx| {
9611 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9612 });
9613 editor.update(cx, |editor, cx| {
9614 assert_text_with_selections(
9615 editor,
9616 indoc! {r#"
9617 let a = «ˇ{
9618 key: "value",
9619 }»;
9620 "#},
9621 cx,
9622 );
9623 });
9624
9625 // Test case 2: Cursor after ':'
9626 editor.update_in(cx, |editor, window, cx| {
9627 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9628 s.select_display_ranges([
9629 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9630 ]);
9631 });
9632 });
9633 editor.update(cx, |editor, cx| {
9634 assert_text_with_selections(
9635 editor,
9636 indoc! {r#"
9637 let a = {
9638 key:ˇ "value",
9639 };
9640 "#},
9641 cx,
9642 );
9643 });
9644 editor.update_in(cx, |editor, window, cx| {
9645 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9646 });
9647 editor.update(cx, |editor, cx| {
9648 assert_text_with_selections(
9649 editor,
9650 indoc! {r#"
9651 let a = {
9652 «ˇkey: "value"»,
9653 };
9654 "#},
9655 cx,
9656 );
9657 });
9658 editor.update_in(cx, |editor, window, cx| {
9659 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9660 });
9661 editor.update(cx, |editor, cx| {
9662 assert_text_with_selections(
9663 editor,
9664 indoc! {r#"
9665 let a = «ˇ{
9666 key: "value",
9667 }»;
9668 "#},
9669 cx,
9670 );
9671 });
9672
9673 // Test case 3: Cursor after ','
9674 editor.update_in(cx, |editor, window, cx| {
9675 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9676 s.select_display_ranges([
9677 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9678 ]);
9679 });
9680 });
9681 editor.update(cx, |editor, cx| {
9682 assert_text_with_selections(
9683 editor,
9684 indoc! {r#"
9685 let a = {
9686 key: "value",ˇ
9687 };
9688 "#},
9689 cx,
9690 );
9691 });
9692 editor.update_in(cx, |editor, window, cx| {
9693 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9694 });
9695 editor.update(cx, |editor, cx| {
9696 assert_text_with_selections(
9697 editor,
9698 indoc! {r#"
9699 let a = «ˇ{
9700 key: "value",
9701 }»;
9702 "#},
9703 cx,
9704 );
9705 });
9706
9707 // Test case 4: Cursor after ';'
9708 editor.update_in(cx, |editor, window, cx| {
9709 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9710 s.select_display_ranges([
9711 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9712 ]);
9713 });
9714 });
9715 editor.update(cx, |editor, cx| {
9716 assert_text_with_selections(
9717 editor,
9718 indoc! {r#"
9719 let a = {
9720 key: "value",
9721 };ˇ
9722 "#},
9723 cx,
9724 );
9725 });
9726 editor.update_in(cx, |editor, window, cx| {
9727 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9728 });
9729 editor.update(cx, |editor, cx| {
9730 assert_text_with_selections(
9731 editor,
9732 indoc! {r#"
9733 «ˇlet a = {
9734 key: "value",
9735 };
9736 »"#},
9737 cx,
9738 );
9739 });
9740}
9741
9742#[gpui::test]
9743async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9744 init_test(cx, |_| {});
9745
9746 let language = Arc::new(Language::new(
9747 LanguageConfig::default(),
9748 Some(tree_sitter_rust::LANGUAGE.into()),
9749 ));
9750
9751 let text = r#"
9752 use mod1::mod2::{mod3, mod4};
9753
9754 fn fn_1(param1: bool, param2: &str) {
9755 let var1 = "hello world";
9756 }
9757 "#
9758 .unindent();
9759
9760 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9761 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9762 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9763
9764 editor
9765 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9766 .await;
9767
9768 // Test 1: Cursor on a letter of a string word
9769 editor.update_in(cx, |editor, window, cx| {
9770 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9771 s.select_display_ranges([
9772 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9773 ]);
9774 });
9775 });
9776 editor.update_in(cx, |editor, window, cx| {
9777 assert_text_with_selections(
9778 editor,
9779 indoc! {r#"
9780 use mod1::mod2::{mod3, mod4};
9781
9782 fn fn_1(param1: bool, param2: &str) {
9783 let var1 = "hˇello world";
9784 }
9785 "#},
9786 cx,
9787 );
9788 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9789 assert_text_with_selections(
9790 editor,
9791 indoc! {r#"
9792 use mod1::mod2::{mod3, mod4};
9793
9794 fn fn_1(param1: bool, param2: &str) {
9795 let var1 = "«ˇhello» world";
9796 }
9797 "#},
9798 cx,
9799 );
9800 });
9801
9802 // Test 2: Partial selection within a word
9803 editor.update_in(cx, |editor, window, cx| {
9804 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9805 s.select_display_ranges([
9806 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9807 ]);
9808 });
9809 });
9810 editor.update_in(cx, |editor, window, cx| {
9811 assert_text_with_selections(
9812 editor,
9813 indoc! {r#"
9814 use mod1::mod2::{mod3, mod4};
9815
9816 fn fn_1(param1: bool, param2: &str) {
9817 let var1 = "h«elˇ»lo world";
9818 }
9819 "#},
9820 cx,
9821 );
9822 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9823 assert_text_with_selections(
9824 editor,
9825 indoc! {r#"
9826 use mod1::mod2::{mod3, mod4};
9827
9828 fn fn_1(param1: bool, param2: &str) {
9829 let var1 = "«ˇhello» world";
9830 }
9831 "#},
9832 cx,
9833 );
9834 });
9835
9836 // Test 3: Complete word already selected
9837 editor.update_in(cx, |editor, window, cx| {
9838 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9839 s.select_display_ranges([
9840 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9841 ]);
9842 });
9843 });
9844 editor.update_in(cx, |editor, window, cx| {
9845 assert_text_with_selections(
9846 editor,
9847 indoc! {r#"
9848 use mod1::mod2::{mod3, mod4};
9849
9850 fn fn_1(param1: bool, param2: &str) {
9851 let var1 = "«helloˇ» world";
9852 }
9853 "#},
9854 cx,
9855 );
9856 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9857 assert_text_with_selections(
9858 editor,
9859 indoc! {r#"
9860 use mod1::mod2::{mod3, mod4};
9861
9862 fn fn_1(param1: bool, param2: &str) {
9863 let var1 = "«hello worldˇ»";
9864 }
9865 "#},
9866 cx,
9867 );
9868 });
9869
9870 // Test 4: Selection spanning across words
9871 editor.update_in(cx, |editor, window, cx| {
9872 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9873 s.select_display_ranges([
9874 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9875 ]);
9876 });
9877 });
9878 editor.update_in(cx, |editor, window, cx| {
9879 assert_text_with_selections(
9880 editor,
9881 indoc! {r#"
9882 use mod1::mod2::{mod3, mod4};
9883
9884 fn fn_1(param1: bool, param2: &str) {
9885 let var1 = "hel«lo woˇ»rld";
9886 }
9887 "#},
9888 cx,
9889 );
9890 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9891 assert_text_with_selections(
9892 editor,
9893 indoc! {r#"
9894 use mod1::mod2::{mod3, mod4};
9895
9896 fn fn_1(param1: bool, param2: &str) {
9897 let var1 = "«ˇhello world»";
9898 }
9899 "#},
9900 cx,
9901 );
9902 });
9903
9904 // Test 5: Expansion beyond string
9905 editor.update_in(cx, |editor, window, cx| {
9906 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9907 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9908 assert_text_with_selections(
9909 editor,
9910 indoc! {r#"
9911 use mod1::mod2::{mod3, mod4};
9912
9913 fn fn_1(param1: bool, param2: &str) {
9914 «ˇlet var1 = "hello world";»
9915 }
9916 "#},
9917 cx,
9918 );
9919 });
9920}
9921
9922#[gpui::test]
9923async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9924 init_test(cx, |_| {});
9925
9926 let mut cx = EditorTestContext::new(cx).await;
9927
9928 let language = Arc::new(Language::new(
9929 LanguageConfig::default(),
9930 Some(tree_sitter_rust::LANGUAGE.into()),
9931 ));
9932
9933 cx.update_buffer(|buffer, cx| {
9934 buffer.set_language(Some(language), cx);
9935 });
9936
9937 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9938 cx.update_editor(|editor, window, cx| {
9939 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9940 });
9941
9942 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9943
9944 cx.set_state(indoc! { r#"fn a() {
9945 // what
9946 // a
9947 // ˇlong
9948 // method
9949 // I
9950 // sure
9951 // hope
9952 // it
9953 // works
9954 }"# });
9955
9956 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9957 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9958 cx.update(|_, cx| {
9959 multi_buffer.update(cx, |multi_buffer, cx| {
9960 multi_buffer.set_excerpts_for_path(
9961 PathKey::for_buffer(&buffer, cx),
9962 buffer,
9963 [Point::new(1, 0)..Point::new(1, 0)],
9964 3,
9965 cx,
9966 );
9967 });
9968 });
9969
9970 let editor2 = cx.new_window_entity(|window, cx| {
9971 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9972 });
9973
9974 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9975 cx.update_editor(|editor, window, cx| {
9976 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9977 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9978 })
9979 });
9980
9981 cx.assert_editor_state(indoc! { "
9982 fn a() {
9983 // what
9984 // a
9985 ˇ // long
9986 // method"});
9987
9988 cx.update_editor(|editor, window, cx| {
9989 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9990 });
9991
9992 // Although we could potentially make the action work when the syntax node
9993 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9994 // did. Maybe we could also expand the excerpt to contain the range?
9995 cx.assert_editor_state(indoc! { "
9996 fn a() {
9997 // what
9998 // a
9999 ˇ // long
10000 // method"});
10001}
10002
10003#[gpui::test]
10004async fn test_fold_function_bodies(cx: &mut TestAppContext) {
10005 init_test(cx, |_| {});
10006
10007 let base_text = r#"
10008 impl A {
10009 // this is an uncommitted comment
10010
10011 fn b() {
10012 c();
10013 }
10014
10015 // this is another uncommitted comment
10016
10017 fn d() {
10018 // e
10019 // f
10020 }
10021 }
10022
10023 fn g() {
10024 // h
10025 }
10026 "#
10027 .unindent();
10028
10029 let text = r#"
10030 ˇimpl A {
10031
10032 fn b() {
10033 c();
10034 }
10035
10036 fn d() {
10037 // e
10038 // f
10039 }
10040 }
10041
10042 fn g() {
10043 // h
10044 }
10045 "#
10046 .unindent();
10047
10048 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10049 cx.set_state(&text);
10050 cx.set_head_text(&base_text);
10051 cx.update_editor(|editor, window, cx| {
10052 editor.expand_all_diff_hunks(&Default::default(), window, cx);
10053 });
10054
10055 cx.assert_state_with_diff(
10056 "
10057 ˇimpl A {
10058 - // this is an uncommitted comment
10059
10060 fn b() {
10061 c();
10062 }
10063
10064 - // this is another uncommitted comment
10065 -
10066 fn d() {
10067 // e
10068 // f
10069 }
10070 }
10071
10072 fn g() {
10073 // h
10074 }
10075 "
10076 .unindent(),
10077 );
10078
10079 let expected_display_text = "
10080 impl A {
10081 // this is an uncommitted comment
10082
10083 fn b() {
10084 ⋯
10085 }
10086
10087 // this is another uncommitted comment
10088
10089 fn d() {
10090 ⋯
10091 }
10092 }
10093
10094 fn g() {
10095 ⋯
10096 }
10097 "
10098 .unindent();
10099
10100 cx.update_editor(|editor, window, cx| {
10101 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
10102 assert_eq!(editor.display_text(cx), expected_display_text);
10103 });
10104}
10105
10106#[gpui::test]
10107async fn test_autoindent(cx: &mut TestAppContext) {
10108 init_test(cx, |_| {});
10109
10110 let language = Arc::new(
10111 Language::new(
10112 LanguageConfig {
10113 brackets: BracketPairConfig {
10114 pairs: vec![
10115 BracketPair {
10116 start: "{".to_string(),
10117 end: "}".to_string(),
10118 close: false,
10119 surround: false,
10120 newline: true,
10121 },
10122 BracketPair {
10123 start: "(".to_string(),
10124 end: ")".to_string(),
10125 close: false,
10126 surround: false,
10127 newline: true,
10128 },
10129 ],
10130 ..Default::default()
10131 },
10132 ..Default::default()
10133 },
10134 Some(tree_sitter_rust::LANGUAGE.into()),
10135 )
10136 .with_indents_query(
10137 r#"
10138 (_ "(" ")" @end) @indent
10139 (_ "{" "}" @end) @indent
10140 "#,
10141 )
10142 .unwrap(),
10143 );
10144
10145 let text = "fn a() {}";
10146
10147 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10148 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10149 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10150 editor
10151 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10152 .await;
10153
10154 editor.update_in(cx, |editor, window, cx| {
10155 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10156 s.select_ranges([
10157 MultiBufferOffset(5)..MultiBufferOffset(5),
10158 MultiBufferOffset(8)..MultiBufferOffset(8),
10159 MultiBufferOffset(9)..MultiBufferOffset(9),
10160 ])
10161 });
10162 editor.newline(&Newline, window, cx);
10163 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
10164 assert_eq!(
10165 editor.selections.ranges(&editor.display_snapshot(cx)),
10166 &[
10167 Point::new(1, 4)..Point::new(1, 4),
10168 Point::new(3, 4)..Point::new(3, 4),
10169 Point::new(5, 0)..Point::new(5, 0)
10170 ]
10171 );
10172 });
10173}
10174
10175#[gpui::test]
10176async fn test_autoindent_disabled(cx: &mut TestAppContext) {
10177 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
10178
10179 let language = Arc::new(
10180 Language::new(
10181 LanguageConfig {
10182 brackets: BracketPairConfig {
10183 pairs: vec![
10184 BracketPair {
10185 start: "{".to_string(),
10186 end: "}".to_string(),
10187 close: false,
10188 surround: false,
10189 newline: true,
10190 },
10191 BracketPair {
10192 start: "(".to_string(),
10193 end: ")".to_string(),
10194 close: false,
10195 surround: false,
10196 newline: true,
10197 },
10198 ],
10199 ..Default::default()
10200 },
10201 ..Default::default()
10202 },
10203 Some(tree_sitter_rust::LANGUAGE.into()),
10204 )
10205 .with_indents_query(
10206 r#"
10207 (_ "(" ")" @end) @indent
10208 (_ "{" "}" @end) @indent
10209 "#,
10210 )
10211 .unwrap(),
10212 );
10213
10214 let text = "fn a() {}";
10215
10216 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10217 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10218 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10219 editor
10220 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10221 .await;
10222
10223 editor.update_in(cx, |editor, window, cx| {
10224 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10225 s.select_ranges([
10226 MultiBufferOffset(5)..MultiBufferOffset(5),
10227 MultiBufferOffset(8)..MultiBufferOffset(8),
10228 MultiBufferOffset(9)..MultiBufferOffset(9),
10229 ])
10230 });
10231 editor.newline(&Newline, window, cx);
10232 assert_eq!(
10233 editor.text(cx),
10234 indoc!(
10235 "
10236 fn a(
10237
10238 ) {
10239
10240 }
10241 "
10242 )
10243 );
10244 assert_eq!(
10245 editor.selections.ranges(&editor.display_snapshot(cx)),
10246 &[
10247 Point::new(1, 0)..Point::new(1, 0),
10248 Point::new(3, 0)..Point::new(3, 0),
10249 Point::new(5, 0)..Point::new(5, 0)
10250 ]
10251 );
10252 });
10253}
10254
10255#[gpui::test]
10256async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
10257 init_test(cx, |settings| {
10258 settings.defaults.auto_indent = Some(true);
10259 settings.languages.0.insert(
10260 "python".into(),
10261 LanguageSettingsContent {
10262 auto_indent: Some(false),
10263 ..Default::default()
10264 },
10265 );
10266 });
10267
10268 let mut cx = EditorTestContext::new(cx).await;
10269
10270 let injected_language = Arc::new(
10271 Language::new(
10272 LanguageConfig {
10273 brackets: BracketPairConfig {
10274 pairs: vec![
10275 BracketPair {
10276 start: "{".to_string(),
10277 end: "}".to_string(),
10278 close: false,
10279 surround: false,
10280 newline: true,
10281 },
10282 BracketPair {
10283 start: "(".to_string(),
10284 end: ")".to_string(),
10285 close: true,
10286 surround: false,
10287 newline: true,
10288 },
10289 ],
10290 ..Default::default()
10291 },
10292 name: "python".into(),
10293 ..Default::default()
10294 },
10295 Some(tree_sitter_python::LANGUAGE.into()),
10296 )
10297 .with_indents_query(
10298 r#"
10299 (_ "(" ")" @end) @indent
10300 (_ "{" "}" @end) @indent
10301 "#,
10302 )
10303 .unwrap(),
10304 );
10305
10306 let language = Arc::new(
10307 Language::new(
10308 LanguageConfig {
10309 brackets: BracketPairConfig {
10310 pairs: vec![
10311 BracketPair {
10312 start: "{".to_string(),
10313 end: "}".to_string(),
10314 close: false,
10315 surround: false,
10316 newline: true,
10317 },
10318 BracketPair {
10319 start: "(".to_string(),
10320 end: ")".to_string(),
10321 close: true,
10322 surround: false,
10323 newline: true,
10324 },
10325 ],
10326 ..Default::default()
10327 },
10328 name: LanguageName::new_static("rust"),
10329 ..Default::default()
10330 },
10331 Some(tree_sitter_rust::LANGUAGE.into()),
10332 )
10333 .with_indents_query(
10334 r#"
10335 (_ "(" ")" @end) @indent
10336 (_ "{" "}" @end) @indent
10337 "#,
10338 )
10339 .unwrap()
10340 .with_injection_query(
10341 r#"
10342 (macro_invocation
10343 macro: (identifier) @_macro_name
10344 (token_tree) @injection.content
10345 (#set! injection.language "python"))
10346 "#,
10347 )
10348 .unwrap(),
10349 );
10350
10351 cx.language_registry().add(injected_language);
10352 cx.language_registry().add(language.clone());
10353
10354 cx.update_buffer(|buffer, cx| {
10355 buffer.set_language(Some(language), cx);
10356 });
10357
10358 cx.set_state(r#"struct A {ˇ}"#);
10359
10360 cx.update_editor(|editor, window, cx| {
10361 editor.newline(&Default::default(), window, cx);
10362 });
10363
10364 cx.assert_editor_state(indoc!(
10365 "struct A {
10366 ˇ
10367 }"
10368 ));
10369
10370 cx.set_state(r#"select_biased!(ˇ)"#);
10371
10372 cx.update_editor(|editor, window, cx| {
10373 editor.newline(&Default::default(), window, cx);
10374 editor.handle_input("def ", window, cx);
10375 editor.handle_input("(", window, cx);
10376 editor.newline(&Default::default(), window, cx);
10377 editor.handle_input("a", window, cx);
10378 });
10379
10380 cx.assert_editor_state(indoc!(
10381 "select_biased!(
10382 def (
10383 aˇ
10384 )
10385 )"
10386 ));
10387}
10388
10389#[gpui::test]
10390async fn test_autoindent_selections(cx: &mut TestAppContext) {
10391 init_test(cx, |_| {});
10392
10393 {
10394 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10395 cx.set_state(indoc! {"
10396 impl A {
10397
10398 fn b() {}
10399
10400 «fn c() {
10401
10402 }ˇ»
10403 }
10404 "});
10405
10406 cx.update_editor(|editor, window, cx| {
10407 editor.autoindent(&Default::default(), window, cx);
10408 });
10409 cx.wait_for_autoindent_applied().await;
10410
10411 cx.assert_editor_state(indoc! {"
10412 impl A {
10413
10414 fn b() {}
10415
10416 «fn c() {
10417
10418 }ˇ»
10419 }
10420 "});
10421 }
10422
10423 {
10424 let mut cx = EditorTestContext::new_multibuffer(
10425 cx,
10426 [indoc! { "
10427 impl A {
10428 «
10429 // a
10430 fn b(){}
10431 »
10432 «
10433 }
10434 fn c(){}
10435 »
10436 "}],
10437 );
10438
10439 let buffer = cx.update_editor(|editor, _, cx| {
10440 let buffer = editor.buffer().update(cx, |buffer, _| {
10441 buffer.all_buffers().iter().next().unwrap().clone()
10442 });
10443 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10444 buffer
10445 });
10446
10447 cx.run_until_parked();
10448 cx.update_editor(|editor, window, cx| {
10449 editor.select_all(&Default::default(), window, cx);
10450 editor.autoindent(&Default::default(), window, cx)
10451 });
10452 cx.run_until_parked();
10453
10454 cx.update(|_, cx| {
10455 assert_eq!(
10456 buffer.read(cx).text(),
10457 indoc! { "
10458 impl A {
10459
10460 // a
10461 fn b(){}
10462
10463
10464 }
10465 fn c(){}
10466
10467 " }
10468 )
10469 });
10470 }
10471}
10472
10473#[gpui::test]
10474async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10475 init_test(cx, |_| {});
10476
10477 let mut cx = EditorTestContext::new(cx).await;
10478
10479 let language = Arc::new(Language::new(
10480 LanguageConfig {
10481 brackets: BracketPairConfig {
10482 pairs: vec![
10483 BracketPair {
10484 start: "{".to_string(),
10485 end: "}".to_string(),
10486 close: true,
10487 surround: true,
10488 newline: true,
10489 },
10490 BracketPair {
10491 start: "(".to_string(),
10492 end: ")".to_string(),
10493 close: true,
10494 surround: true,
10495 newline: true,
10496 },
10497 BracketPair {
10498 start: "/*".to_string(),
10499 end: " */".to_string(),
10500 close: true,
10501 surround: true,
10502 newline: true,
10503 },
10504 BracketPair {
10505 start: "[".to_string(),
10506 end: "]".to_string(),
10507 close: false,
10508 surround: false,
10509 newline: true,
10510 },
10511 BracketPair {
10512 start: "\"".to_string(),
10513 end: "\"".to_string(),
10514 close: true,
10515 surround: true,
10516 newline: false,
10517 },
10518 BracketPair {
10519 start: "<".to_string(),
10520 end: ">".to_string(),
10521 close: false,
10522 surround: true,
10523 newline: true,
10524 },
10525 ],
10526 ..Default::default()
10527 },
10528 autoclose_before: "})]".to_string(),
10529 ..Default::default()
10530 },
10531 Some(tree_sitter_rust::LANGUAGE.into()),
10532 ));
10533
10534 cx.language_registry().add(language.clone());
10535 cx.update_buffer(|buffer, cx| {
10536 buffer.set_language(Some(language), cx);
10537 });
10538
10539 cx.set_state(
10540 &r#"
10541 🏀ˇ
10542 εˇ
10543 ❤️ˇ
10544 "#
10545 .unindent(),
10546 );
10547
10548 // autoclose multiple nested brackets at multiple cursors
10549 cx.update_editor(|editor, window, cx| {
10550 editor.handle_input("{", window, cx);
10551 editor.handle_input("{", window, cx);
10552 editor.handle_input("{", window, cx);
10553 });
10554 cx.assert_editor_state(
10555 &"
10556 🏀{{{ˇ}}}
10557 ε{{{ˇ}}}
10558 ❤️{{{ˇ}}}
10559 "
10560 .unindent(),
10561 );
10562
10563 // insert a different closing bracket
10564 cx.update_editor(|editor, window, cx| {
10565 editor.handle_input(")", window, cx);
10566 });
10567 cx.assert_editor_state(
10568 &"
10569 🏀{{{)ˇ}}}
10570 ε{{{)ˇ}}}
10571 ❤️{{{)ˇ}}}
10572 "
10573 .unindent(),
10574 );
10575
10576 // skip over the auto-closed brackets when typing a closing bracket
10577 cx.update_editor(|editor, window, cx| {
10578 editor.move_right(&MoveRight, window, cx);
10579 editor.handle_input("}", window, cx);
10580 editor.handle_input("}", window, cx);
10581 editor.handle_input("}", window, cx);
10582 });
10583 cx.assert_editor_state(
10584 &"
10585 🏀{{{)}}}}ˇ
10586 ε{{{)}}}}ˇ
10587 ❤️{{{)}}}}ˇ
10588 "
10589 .unindent(),
10590 );
10591
10592 // autoclose multi-character pairs
10593 cx.set_state(
10594 &"
10595 ˇ
10596 ˇ
10597 "
10598 .unindent(),
10599 );
10600 cx.update_editor(|editor, window, cx| {
10601 editor.handle_input("/", window, cx);
10602 editor.handle_input("*", window, cx);
10603 });
10604 cx.assert_editor_state(
10605 &"
10606 /*ˇ */
10607 /*ˇ */
10608 "
10609 .unindent(),
10610 );
10611
10612 // one cursor autocloses a multi-character pair, one cursor
10613 // does not autoclose.
10614 cx.set_state(
10615 &"
10616 /ˇ
10617 ˇ
10618 "
10619 .unindent(),
10620 );
10621 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10622 cx.assert_editor_state(
10623 &"
10624 /*ˇ */
10625 *ˇ
10626 "
10627 .unindent(),
10628 );
10629
10630 // Don't autoclose if the next character isn't whitespace and isn't
10631 // listed in the language's "autoclose_before" section.
10632 cx.set_state("ˇa b");
10633 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10634 cx.assert_editor_state("{ˇa b");
10635
10636 // Don't autoclose if `close` is false for the bracket pair
10637 cx.set_state("ˇ");
10638 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10639 cx.assert_editor_state("[ˇ");
10640
10641 // Surround with brackets if text is selected
10642 cx.set_state("«aˇ» b");
10643 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10644 cx.assert_editor_state("{«aˇ»} b");
10645
10646 // Autoclose when not immediately after a word character
10647 cx.set_state("a ˇ");
10648 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10649 cx.assert_editor_state("a \"ˇ\"");
10650
10651 // Autoclose pair where the start and end characters are the same
10652 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10653 cx.assert_editor_state("a \"\"ˇ");
10654
10655 // Don't autoclose when immediately after a word character
10656 cx.set_state("aˇ");
10657 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10658 cx.assert_editor_state("a\"ˇ");
10659
10660 // Do autoclose when after a non-word character
10661 cx.set_state("{ˇ");
10662 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10663 cx.assert_editor_state("{\"ˇ\"");
10664
10665 // Non identical pairs autoclose regardless of preceding character
10666 cx.set_state("aˇ");
10667 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10668 cx.assert_editor_state("a{ˇ}");
10669
10670 // Don't autoclose pair if autoclose is disabled
10671 cx.set_state("ˇ");
10672 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10673 cx.assert_editor_state("<ˇ");
10674
10675 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10676 cx.set_state("«aˇ» b");
10677 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10678 cx.assert_editor_state("<«aˇ»> b");
10679}
10680
10681#[gpui::test]
10682async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10683 init_test(cx, |settings| {
10684 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10685 });
10686
10687 let mut cx = EditorTestContext::new(cx).await;
10688
10689 let language = Arc::new(Language::new(
10690 LanguageConfig {
10691 brackets: BracketPairConfig {
10692 pairs: vec![
10693 BracketPair {
10694 start: "{".to_string(),
10695 end: "}".to_string(),
10696 close: true,
10697 surround: true,
10698 newline: true,
10699 },
10700 BracketPair {
10701 start: "(".to_string(),
10702 end: ")".to_string(),
10703 close: true,
10704 surround: true,
10705 newline: true,
10706 },
10707 BracketPair {
10708 start: "[".to_string(),
10709 end: "]".to_string(),
10710 close: false,
10711 surround: false,
10712 newline: true,
10713 },
10714 ],
10715 ..Default::default()
10716 },
10717 autoclose_before: "})]".to_string(),
10718 ..Default::default()
10719 },
10720 Some(tree_sitter_rust::LANGUAGE.into()),
10721 ));
10722
10723 cx.language_registry().add(language.clone());
10724 cx.update_buffer(|buffer, cx| {
10725 buffer.set_language(Some(language), cx);
10726 });
10727
10728 cx.set_state(
10729 &"
10730 ˇ
10731 ˇ
10732 ˇ
10733 "
10734 .unindent(),
10735 );
10736
10737 // ensure only matching closing brackets are skipped over
10738 cx.update_editor(|editor, window, cx| {
10739 editor.handle_input("}", window, cx);
10740 editor.move_left(&MoveLeft, window, cx);
10741 editor.handle_input(")", window, cx);
10742 editor.move_left(&MoveLeft, window, cx);
10743 });
10744 cx.assert_editor_state(
10745 &"
10746 ˇ)}
10747 ˇ)}
10748 ˇ)}
10749 "
10750 .unindent(),
10751 );
10752
10753 // skip-over closing brackets at multiple cursors
10754 cx.update_editor(|editor, window, cx| {
10755 editor.handle_input(")", window, cx);
10756 editor.handle_input("}", window, cx);
10757 });
10758 cx.assert_editor_state(
10759 &"
10760 )}ˇ
10761 )}ˇ
10762 )}ˇ
10763 "
10764 .unindent(),
10765 );
10766
10767 // ignore non-close brackets
10768 cx.update_editor(|editor, window, cx| {
10769 editor.handle_input("]", window, cx);
10770 editor.move_left(&MoveLeft, window, cx);
10771 editor.handle_input("]", window, cx);
10772 });
10773 cx.assert_editor_state(
10774 &"
10775 )}]ˇ]
10776 )}]ˇ]
10777 )}]ˇ]
10778 "
10779 .unindent(),
10780 );
10781}
10782
10783#[gpui::test]
10784async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10785 init_test(cx, |_| {});
10786
10787 let mut cx = EditorTestContext::new(cx).await;
10788
10789 let html_language = Arc::new(
10790 Language::new(
10791 LanguageConfig {
10792 name: "HTML".into(),
10793 brackets: BracketPairConfig {
10794 pairs: vec![
10795 BracketPair {
10796 start: "<".into(),
10797 end: ">".into(),
10798 close: true,
10799 ..Default::default()
10800 },
10801 BracketPair {
10802 start: "{".into(),
10803 end: "}".into(),
10804 close: true,
10805 ..Default::default()
10806 },
10807 BracketPair {
10808 start: "(".into(),
10809 end: ")".into(),
10810 close: true,
10811 ..Default::default()
10812 },
10813 ],
10814 ..Default::default()
10815 },
10816 autoclose_before: "})]>".into(),
10817 ..Default::default()
10818 },
10819 Some(tree_sitter_html::LANGUAGE.into()),
10820 )
10821 .with_injection_query(
10822 r#"
10823 (script_element
10824 (raw_text) @injection.content
10825 (#set! injection.language "javascript"))
10826 "#,
10827 )
10828 .unwrap(),
10829 );
10830
10831 let javascript_language = Arc::new(Language::new(
10832 LanguageConfig {
10833 name: "JavaScript".into(),
10834 brackets: BracketPairConfig {
10835 pairs: vec![
10836 BracketPair {
10837 start: "/*".into(),
10838 end: " */".into(),
10839 close: true,
10840 ..Default::default()
10841 },
10842 BracketPair {
10843 start: "{".into(),
10844 end: "}".into(),
10845 close: true,
10846 ..Default::default()
10847 },
10848 BracketPair {
10849 start: "(".into(),
10850 end: ")".into(),
10851 close: true,
10852 ..Default::default()
10853 },
10854 ],
10855 ..Default::default()
10856 },
10857 autoclose_before: "})]>".into(),
10858 ..Default::default()
10859 },
10860 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10861 ));
10862
10863 cx.language_registry().add(html_language.clone());
10864 cx.language_registry().add(javascript_language);
10865 cx.executor().run_until_parked();
10866
10867 cx.update_buffer(|buffer, cx| {
10868 buffer.set_language(Some(html_language), cx);
10869 });
10870
10871 cx.set_state(
10872 &r#"
10873 <body>ˇ
10874 <script>
10875 var x = 1;ˇ
10876 </script>
10877 </body>ˇ
10878 "#
10879 .unindent(),
10880 );
10881
10882 // Precondition: different languages are active at different locations.
10883 cx.update_editor(|editor, window, cx| {
10884 let snapshot = editor.snapshot(window, cx);
10885 let cursors = editor
10886 .selections
10887 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
10888 let languages = cursors
10889 .iter()
10890 .map(|c| snapshot.language_at(c.start).unwrap().name())
10891 .collect::<Vec<_>>();
10892 assert_eq!(
10893 languages,
10894 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10895 );
10896 });
10897
10898 // Angle brackets autoclose in HTML, but not JavaScript.
10899 cx.update_editor(|editor, window, cx| {
10900 editor.handle_input("<", window, cx);
10901 editor.handle_input("a", window, cx);
10902 });
10903 cx.assert_editor_state(
10904 &r#"
10905 <body><aˇ>
10906 <script>
10907 var x = 1;<aˇ
10908 </script>
10909 </body><aˇ>
10910 "#
10911 .unindent(),
10912 );
10913
10914 // Curly braces and parens autoclose in both HTML and JavaScript.
10915 cx.update_editor(|editor, window, cx| {
10916 editor.handle_input(" b=", window, cx);
10917 editor.handle_input("{", window, cx);
10918 editor.handle_input("c", window, cx);
10919 editor.handle_input("(", window, cx);
10920 });
10921 cx.assert_editor_state(
10922 &r#"
10923 <body><a b={c(ˇ)}>
10924 <script>
10925 var x = 1;<a b={c(ˇ)}
10926 </script>
10927 </body><a b={c(ˇ)}>
10928 "#
10929 .unindent(),
10930 );
10931
10932 // Brackets that were already autoclosed are skipped.
10933 cx.update_editor(|editor, window, cx| {
10934 editor.handle_input(")", window, cx);
10935 editor.handle_input("d", window, cx);
10936 editor.handle_input("}", window, cx);
10937 });
10938 cx.assert_editor_state(
10939 &r#"
10940 <body><a b={c()d}ˇ>
10941 <script>
10942 var x = 1;<a b={c()d}ˇ
10943 </script>
10944 </body><a b={c()d}ˇ>
10945 "#
10946 .unindent(),
10947 );
10948 cx.update_editor(|editor, window, cx| {
10949 editor.handle_input(">", window, cx);
10950 });
10951 cx.assert_editor_state(
10952 &r#"
10953 <body><a b={c()d}>ˇ
10954 <script>
10955 var x = 1;<a b={c()d}>ˇ
10956 </script>
10957 </body><a b={c()d}>ˇ
10958 "#
10959 .unindent(),
10960 );
10961
10962 // Reset
10963 cx.set_state(
10964 &r#"
10965 <body>ˇ
10966 <script>
10967 var x = 1;ˇ
10968 </script>
10969 </body>ˇ
10970 "#
10971 .unindent(),
10972 );
10973
10974 cx.update_editor(|editor, window, cx| {
10975 editor.handle_input("<", window, cx);
10976 });
10977 cx.assert_editor_state(
10978 &r#"
10979 <body><ˇ>
10980 <script>
10981 var x = 1;<ˇ
10982 </script>
10983 </body><ˇ>
10984 "#
10985 .unindent(),
10986 );
10987
10988 // When backspacing, the closing angle brackets are removed.
10989 cx.update_editor(|editor, window, cx| {
10990 editor.backspace(&Backspace, window, cx);
10991 });
10992 cx.assert_editor_state(
10993 &r#"
10994 <body>ˇ
10995 <script>
10996 var x = 1;ˇ
10997 </script>
10998 </body>ˇ
10999 "#
11000 .unindent(),
11001 );
11002
11003 // Block comments autoclose in JavaScript, but not HTML.
11004 cx.update_editor(|editor, window, cx| {
11005 editor.handle_input("/", window, cx);
11006 editor.handle_input("*", window, cx);
11007 });
11008 cx.assert_editor_state(
11009 &r#"
11010 <body>/*ˇ
11011 <script>
11012 var x = 1;/*ˇ */
11013 </script>
11014 </body>/*ˇ
11015 "#
11016 .unindent(),
11017 );
11018}
11019
11020#[gpui::test]
11021async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
11022 init_test(cx, |_| {});
11023
11024 let mut cx = EditorTestContext::new(cx).await;
11025
11026 let rust_language = Arc::new(
11027 Language::new(
11028 LanguageConfig {
11029 name: "Rust".into(),
11030 brackets: serde_json::from_value(json!([
11031 { "start": "{", "end": "}", "close": true, "newline": true },
11032 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
11033 ]))
11034 .unwrap(),
11035 autoclose_before: "})]>".into(),
11036 ..Default::default()
11037 },
11038 Some(tree_sitter_rust::LANGUAGE.into()),
11039 )
11040 .with_override_query("(string_literal) @string")
11041 .unwrap(),
11042 );
11043
11044 cx.language_registry().add(rust_language.clone());
11045 cx.update_buffer(|buffer, cx| {
11046 buffer.set_language(Some(rust_language), cx);
11047 });
11048
11049 cx.set_state(
11050 &r#"
11051 let x = ˇ
11052 "#
11053 .unindent(),
11054 );
11055
11056 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
11057 cx.update_editor(|editor, window, cx| {
11058 editor.handle_input("\"", window, cx);
11059 });
11060 cx.assert_editor_state(
11061 &r#"
11062 let x = "ˇ"
11063 "#
11064 .unindent(),
11065 );
11066
11067 // Inserting another quotation mark. The cursor moves across the existing
11068 // automatically-inserted quotation mark.
11069 cx.update_editor(|editor, window, cx| {
11070 editor.handle_input("\"", window, cx);
11071 });
11072 cx.assert_editor_state(
11073 &r#"
11074 let x = ""ˇ
11075 "#
11076 .unindent(),
11077 );
11078
11079 // Reset
11080 cx.set_state(
11081 &r#"
11082 let x = ˇ
11083 "#
11084 .unindent(),
11085 );
11086
11087 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
11088 cx.update_editor(|editor, window, cx| {
11089 editor.handle_input("\"", window, cx);
11090 editor.handle_input(" ", window, cx);
11091 editor.move_left(&Default::default(), window, cx);
11092 editor.handle_input("\\", window, cx);
11093 editor.handle_input("\"", window, cx);
11094 });
11095 cx.assert_editor_state(
11096 &r#"
11097 let x = "\"ˇ "
11098 "#
11099 .unindent(),
11100 );
11101
11102 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
11103 // mark. Nothing is inserted.
11104 cx.update_editor(|editor, window, cx| {
11105 editor.move_right(&Default::default(), window, cx);
11106 editor.handle_input("\"", window, cx);
11107 });
11108 cx.assert_editor_state(
11109 &r#"
11110 let x = "\" "ˇ
11111 "#
11112 .unindent(),
11113 );
11114}
11115
11116#[gpui::test]
11117async fn test_autoclose_quotes_with_scope_awareness(cx: &mut TestAppContext) {
11118 init_test(cx, |_| {});
11119
11120 let mut cx = EditorTestContext::new(cx).await;
11121 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
11122
11123 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11124
11125 // Double quote inside single-quoted string
11126 cx.set_state(indoc! {r#"
11127 def main():
11128 items = ['"', ˇ]
11129 "#});
11130 cx.update_editor(|editor, window, cx| {
11131 editor.handle_input("\"", window, cx);
11132 });
11133 cx.assert_editor_state(indoc! {r#"
11134 def main():
11135 items = ['"', "ˇ"]
11136 "#});
11137
11138 // Two double quotes inside single-quoted string
11139 cx.set_state(indoc! {r#"
11140 def main():
11141 items = ['""', ˇ]
11142 "#});
11143 cx.update_editor(|editor, window, cx| {
11144 editor.handle_input("\"", window, cx);
11145 });
11146 cx.assert_editor_state(indoc! {r#"
11147 def main():
11148 items = ['""', "ˇ"]
11149 "#});
11150
11151 // Single quote inside double-quoted string
11152 cx.set_state(indoc! {r#"
11153 def main():
11154 items = ["'", ˇ]
11155 "#});
11156 cx.update_editor(|editor, window, cx| {
11157 editor.handle_input("'", window, cx);
11158 });
11159 cx.assert_editor_state(indoc! {r#"
11160 def main():
11161 items = ["'", 'ˇ']
11162 "#});
11163
11164 // Two single quotes inside double-quoted string
11165 cx.set_state(indoc! {r#"
11166 def main():
11167 items = ["''", ˇ]
11168 "#});
11169 cx.update_editor(|editor, window, cx| {
11170 editor.handle_input("'", window, cx);
11171 });
11172 cx.assert_editor_state(indoc! {r#"
11173 def main():
11174 items = ["''", 'ˇ']
11175 "#});
11176
11177 // Mixed quotes on same line
11178 cx.set_state(indoc! {r#"
11179 def main():
11180 items = ['"""', "'''''", ˇ]
11181 "#});
11182 cx.update_editor(|editor, window, cx| {
11183 editor.handle_input("\"", window, cx);
11184 });
11185 cx.assert_editor_state(indoc! {r#"
11186 def main():
11187 items = ['"""', "'''''", "ˇ"]
11188 "#});
11189 cx.update_editor(|editor, window, cx| {
11190 editor.move_right(&MoveRight, window, cx);
11191 });
11192 cx.update_editor(|editor, window, cx| {
11193 editor.handle_input(", ", window, cx);
11194 });
11195 cx.update_editor(|editor, window, cx| {
11196 editor.handle_input("'", window, cx);
11197 });
11198 cx.assert_editor_state(indoc! {r#"
11199 def main():
11200 items = ['"""', "'''''", "", 'ˇ']
11201 "#});
11202}
11203
11204#[gpui::test]
11205async fn test_autoclose_quotes_with_multibyte_characters(cx: &mut TestAppContext) {
11206 init_test(cx, |_| {});
11207
11208 let mut cx = EditorTestContext::new(cx).await;
11209 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
11210 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11211
11212 cx.set_state(indoc! {r#"
11213 def main():
11214 items = ["🎉", ˇ]
11215 "#});
11216 cx.update_editor(|editor, window, cx| {
11217 editor.handle_input("\"", window, cx);
11218 });
11219 cx.assert_editor_state(indoc! {r#"
11220 def main():
11221 items = ["🎉", "ˇ"]
11222 "#});
11223}
11224
11225#[gpui::test]
11226async fn test_surround_with_pair(cx: &mut TestAppContext) {
11227 init_test(cx, |_| {});
11228
11229 let language = Arc::new(Language::new(
11230 LanguageConfig {
11231 brackets: BracketPairConfig {
11232 pairs: vec![
11233 BracketPair {
11234 start: "{".to_string(),
11235 end: "}".to_string(),
11236 close: true,
11237 surround: true,
11238 newline: true,
11239 },
11240 BracketPair {
11241 start: "/* ".to_string(),
11242 end: "*/".to_string(),
11243 close: true,
11244 surround: true,
11245 ..Default::default()
11246 },
11247 ],
11248 ..Default::default()
11249 },
11250 ..Default::default()
11251 },
11252 Some(tree_sitter_rust::LANGUAGE.into()),
11253 ));
11254
11255 let text = r#"
11256 a
11257 b
11258 c
11259 "#
11260 .unindent();
11261
11262 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11263 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11264 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11265 editor
11266 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11267 .await;
11268
11269 editor.update_in(cx, |editor, window, cx| {
11270 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11271 s.select_display_ranges([
11272 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11273 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11274 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
11275 ])
11276 });
11277
11278 editor.handle_input("{", window, cx);
11279 editor.handle_input("{", window, cx);
11280 editor.handle_input("{", window, cx);
11281 assert_eq!(
11282 editor.text(cx),
11283 "
11284 {{{a}}}
11285 {{{b}}}
11286 {{{c}}}
11287 "
11288 .unindent()
11289 );
11290 assert_eq!(
11291 display_ranges(editor, cx),
11292 [
11293 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
11294 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
11295 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
11296 ]
11297 );
11298
11299 editor.undo(&Undo, window, cx);
11300 editor.undo(&Undo, window, cx);
11301 editor.undo(&Undo, window, cx);
11302 assert_eq!(
11303 editor.text(cx),
11304 "
11305 a
11306 b
11307 c
11308 "
11309 .unindent()
11310 );
11311 assert_eq!(
11312 display_ranges(editor, cx),
11313 [
11314 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11315 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11316 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11317 ]
11318 );
11319
11320 // Ensure inserting the first character of a multi-byte bracket pair
11321 // doesn't surround the selections with the bracket.
11322 editor.handle_input("/", window, cx);
11323 assert_eq!(
11324 editor.text(cx),
11325 "
11326 /
11327 /
11328 /
11329 "
11330 .unindent()
11331 );
11332 assert_eq!(
11333 display_ranges(editor, cx),
11334 [
11335 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11336 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11337 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11338 ]
11339 );
11340
11341 editor.undo(&Undo, window, cx);
11342 assert_eq!(
11343 editor.text(cx),
11344 "
11345 a
11346 b
11347 c
11348 "
11349 .unindent()
11350 );
11351 assert_eq!(
11352 display_ranges(editor, cx),
11353 [
11354 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11355 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11356 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11357 ]
11358 );
11359
11360 // Ensure inserting the last character of a multi-byte bracket pair
11361 // doesn't surround the selections with the bracket.
11362 editor.handle_input("*", window, cx);
11363 assert_eq!(
11364 editor.text(cx),
11365 "
11366 *
11367 *
11368 *
11369 "
11370 .unindent()
11371 );
11372 assert_eq!(
11373 display_ranges(editor, cx),
11374 [
11375 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11376 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11377 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11378 ]
11379 );
11380 });
11381}
11382
11383#[gpui::test]
11384async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
11385 init_test(cx, |_| {});
11386
11387 let language = Arc::new(Language::new(
11388 LanguageConfig {
11389 brackets: BracketPairConfig {
11390 pairs: vec![BracketPair {
11391 start: "{".to_string(),
11392 end: "}".to_string(),
11393 close: true,
11394 surround: true,
11395 newline: true,
11396 }],
11397 ..Default::default()
11398 },
11399 autoclose_before: "}".to_string(),
11400 ..Default::default()
11401 },
11402 Some(tree_sitter_rust::LANGUAGE.into()),
11403 ));
11404
11405 let text = r#"
11406 a
11407 b
11408 c
11409 "#
11410 .unindent();
11411
11412 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11413 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11414 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11415 editor
11416 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11417 .await;
11418
11419 editor.update_in(cx, |editor, window, cx| {
11420 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11421 s.select_ranges([
11422 Point::new(0, 1)..Point::new(0, 1),
11423 Point::new(1, 1)..Point::new(1, 1),
11424 Point::new(2, 1)..Point::new(2, 1),
11425 ])
11426 });
11427
11428 editor.handle_input("{", window, cx);
11429 editor.handle_input("{", window, cx);
11430 editor.handle_input("_", window, cx);
11431 assert_eq!(
11432 editor.text(cx),
11433 "
11434 a{{_}}
11435 b{{_}}
11436 c{{_}}
11437 "
11438 .unindent()
11439 );
11440 assert_eq!(
11441 editor
11442 .selections
11443 .ranges::<Point>(&editor.display_snapshot(cx)),
11444 [
11445 Point::new(0, 4)..Point::new(0, 4),
11446 Point::new(1, 4)..Point::new(1, 4),
11447 Point::new(2, 4)..Point::new(2, 4)
11448 ]
11449 );
11450
11451 editor.backspace(&Default::default(), window, cx);
11452 editor.backspace(&Default::default(), window, cx);
11453 assert_eq!(
11454 editor.text(cx),
11455 "
11456 a{}
11457 b{}
11458 c{}
11459 "
11460 .unindent()
11461 );
11462 assert_eq!(
11463 editor
11464 .selections
11465 .ranges::<Point>(&editor.display_snapshot(cx)),
11466 [
11467 Point::new(0, 2)..Point::new(0, 2),
11468 Point::new(1, 2)..Point::new(1, 2),
11469 Point::new(2, 2)..Point::new(2, 2)
11470 ]
11471 );
11472
11473 editor.delete_to_previous_word_start(&Default::default(), window, cx);
11474 assert_eq!(
11475 editor.text(cx),
11476 "
11477 a
11478 b
11479 c
11480 "
11481 .unindent()
11482 );
11483 assert_eq!(
11484 editor
11485 .selections
11486 .ranges::<Point>(&editor.display_snapshot(cx)),
11487 [
11488 Point::new(0, 1)..Point::new(0, 1),
11489 Point::new(1, 1)..Point::new(1, 1),
11490 Point::new(2, 1)..Point::new(2, 1)
11491 ]
11492 );
11493 });
11494}
11495
11496#[gpui::test]
11497async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
11498 init_test(cx, |settings| {
11499 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11500 });
11501
11502 let mut cx = EditorTestContext::new(cx).await;
11503
11504 let language = Arc::new(Language::new(
11505 LanguageConfig {
11506 brackets: BracketPairConfig {
11507 pairs: vec![
11508 BracketPair {
11509 start: "{".to_string(),
11510 end: "}".to_string(),
11511 close: true,
11512 surround: true,
11513 newline: true,
11514 },
11515 BracketPair {
11516 start: "(".to_string(),
11517 end: ")".to_string(),
11518 close: true,
11519 surround: true,
11520 newline: true,
11521 },
11522 BracketPair {
11523 start: "[".to_string(),
11524 end: "]".to_string(),
11525 close: false,
11526 surround: true,
11527 newline: true,
11528 },
11529 ],
11530 ..Default::default()
11531 },
11532 autoclose_before: "})]".to_string(),
11533 ..Default::default()
11534 },
11535 Some(tree_sitter_rust::LANGUAGE.into()),
11536 ));
11537
11538 cx.language_registry().add(language.clone());
11539 cx.update_buffer(|buffer, cx| {
11540 buffer.set_language(Some(language), cx);
11541 });
11542
11543 cx.set_state(
11544 &"
11545 {(ˇ)}
11546 [[ˇ]]
11547 {(ˇ)}
11548 "
11549 .unindent(),
11550 );
11551
11552 cx.update_editor(|editor, window, cx| {
11553 editor.backspace(&Default::default(), window, cx);
11554 editor.backspace(&Default::default(), window, cx);
11555 });
11556
11557 cx.assert_editor_state(
11558 &"
11559 ˇ
11560 ˇ]]
11561 ˇ
11562 "
11563 .unindent(),
11564 );
11565
11566 cx.update_editor(|editor, window, cx| {
11567 editor.handle_input("{", window, cx);
11568 editor.handle_input("{", window, cx);
11569 editor.move_right(&MoveRight, window, cx);
11570 editor.move_right(&MoveRight, window, cx);
11571 editor.move_left(&MoveLeft, window, cx);
11572 editor.move_left(&MoveLeft, window, cx);
11573 editor.backspace(&Default::default(), window, cx);
11574 });
11575
11576 cx.assert_editor_state(
11577 &"
11578 {ˇ}
11579 {ˇ}]]
11580 {ˇ}
11581 "
11582 .unindent(),
11583 );
11584
11585 cx.update_editor(|editor, window, cx| {
11586 editor.backspace(&Default::default(), window, cx);
11587 });
11588
11589 cx.assert_editor_state(
11590 &"
11591 ˇ
11592 ˇ]]
11593 ˇ
11594 "
11595 .unindent(),
11596 );
11597}
11598
11599#[gpui::test]
11600async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11601 init_test(cx, |_| {});
11602
11603 let language = Arc::new(Language::new(
11604 LanguageConfig::default(),
11605 Some(tree_sitter_rust::LANGUAGE.into()),
11606 ));
11607
11608 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11609 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11610 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11611 editor
11612 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11613 .await;
11614
11615 editor.update_in(cx, |editor, window, cx| {
11616 editor.set_auto_replace_emoji_shortcode(true);
11617
11618 editor.handle_input("Hello ", window, cx);
11619 editor.handle_input(":wave", window, cx);
11620 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11621
11622 editor.handle_input(":", window, cx);
11623 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11624
11625 editor.handle_input(" :smile", window, cx);
11626 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11627
11628 editor.handle_input(":", window, cx);
11629 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11630
11631 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11632 editor.handle_input(":wave", window, cx);
11633 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11634
11635 editor.handle_input(":", window, cx);
11636 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11637
11638 editor.handle_input(":1", window, cx);
11639 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11640
11641 editor.handle_input(":", window, cx);
11642 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11643
11644 // Ensure shortcode does not get replaced when it is part of a word
11645 editor.handle_input(" Test:wave", window, cx);
11646 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11647
11648 editor.handle_input(":", window, cx);
11649 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11650
11651 editor.set_auto_replace_emoji_shortcode(false);
11652
11653 // Ensure shortcode does not get replaced when auto replace is off
11654 editor.handle_input(" :wave", window, cx);
11655 assert_eq!(
11656 editor.text(cx),
11657 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11658 );
11659
11660 editor.handle_input(":", window, cx);
11661 assert_eq!(
11662 editor.text(cx),
11663 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11664 );
11665 });
11666}
11667
11668#[gpui::test]
11669async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11670 init_test(cx, |_| {});
11671
11672 let (text, insertion_ranges) = marked_text_ranges(
11673 indoc! {"
11674 ˇ
11675 "},
11676 false,
11677 );
11678
11679 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11680 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11681
11682 _ = editor.update_in(cx, |editor, window, cx| {
11683 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11684
11685 editor
11686 .insert_snippet(
11687 &insertion_ranges
11688 .iter()
11689 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11690 .collect::<Vec<_>>(),
11691 snippet,
11692 window,
11693 cx,
11694 )
11695 .unwrap();
11696
11697 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11698 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11699 assert_eq!(editor.text(cx), expected_text);
11700 assert_eq!(
11701 editor.selections.ranges(&editor.display_snapshot(cx)),
11702 selection_ranges
11703 .iter()
11704 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11705 .collect::<Vec<_>>()
11706 );
11707 }
11708
11709 assert(
11710 editor,
11711 cx,
11712 indoc! {"
11713 type «» =•
11714 "},
11715 );
11716
11717 assert!(editor.context_menu_visible(), "There should be a matches");
11718 });
11719}
11720
11721#[gpui::test]
11722async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11723 init_test(cx, |_| {});
11724
11725 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11726 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11727 assert_eq!(editor.text(cx), expected_text);
11728 assert_eq!(
11729 editor.selections.ranges(&editor.display_snapshot(cx)),
11730 selection_ranges
11731 .iter()
11732 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11733 .collect::<Vec<_>>()
11734 );
11735 }
11736
11737 let (text, insertion_ranges) = marked_text_ranges(
11738 indoc! {"
11739 ˇ
11740 "},
11741 false,
11742 );
11743
11744 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11745 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11746
11747 _ = editor.update_in(cx, |editor, window, cx| {
11748 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11749
11750 editor
11751 .insert_snippet(
11752 &insertion_ranges
11753 .iter()
11754 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11755 .collect::<Vec<_>>(),
11756 snippet,
11757 window,
11758 cx,
11759 )
11760 .unwrap();
11761
11762 assert_state(
11763 editor,
11764 cx,
11765 indoc! {"
11766 type «» = ;•
11767 "},
11768 );
11769
11770 assert!(
11771 editor.context_menu_visible(),
11772 "Context menu should be visible for placeholder choices"
11773 );
11774
11775 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11776
11777 assert_state(
11778 editor,
11779 cx,
11780 indoc! {"
11781 type = «»;•
11782 "},
11783 );
11784
11785 assert!(
11786 !editor.context_menu_visible(),
11787 "Context menu should be hidden after moving to next tabstop"
11788 );
11789
11790 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11791
11792 assert_state(
11793 editor,
11794 cx,
11795 indoc! {"
11796 type = ; ˇ
11797 "},
11798 );
11799
11800 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11801
11802 assert_state(
11803 editor,
11804 cx,
11805 indoc! {"
11806 type = ; ˇ
11807 "},
11808 );
11809 });
11810
11811 _ = editor.update_in(cx, |editor, window, cx| {
11812 editor.select_all(&SelectAll, window, cx);
11813 editor.backspace(&Backspace, window, cx);
11814
11815 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11816 let insertion_ranges = editor
11817 .selections
11818 .all(&editor.display_snapshot(cx))
11819 .iter()
11820 .map(|s| s.range())
11821 .collect::<Vec<_>>();
11822
11823 editor
11824 .insert_snippet(&insertion_ranges, snippet, window, cx)
11825 .unwrap();
11826
11827 assert_state(editor, cx, "fn «» = value;•");
11828
11829 assert!(
11830 editor.context_menu_visible(),
11831 "Context menu should be visible for placeholder choices"
11832 );
11833
11834 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11835
11836 assert_state(editor, cx, "fn = «valueˇ»;•");
11837
11838 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11839
11840 assert_state(editor, cx, "fn «» = value;•");
11841
11842 assert!(
11843 editor.context_menu_visible(),
11844 "Context menu should be visible again after returning to first tabstop"
11845 );
11846
11847 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11848
11849 assert_state(editor, cx, "fn «» = value;•");
11850 });
11851}
11852
11853#[gpui::test]
11854async fn test_snippets(cx: &mut TestAppContext) {
11855 init_test(cx, |_| {});
11856
11857 let mut cx = EditorTestContext::new(cx).await;
11858
11859 cx.set_state(indoc! {"
11860 a.ˇ b
11861 a.ˇ b
11862 a.ˇ b
11863 "});
11864
11865 cx.update_editor(|editor, window, cx| {
11866 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11867 let insertion_ranges = editor
11868 .selections
11869 .all(&editor.display_snapshot(cx))
11870 .iter()
11871 .map(|s| s.range())
11872 .collect::<Vec<_>>();
11873 editor
11874 .insert_snippet(&insertion_ranges, snippet, window, cx)
11875 .unwrap();
11876 });
11877
11878 cx.assert_editor_state(indoc! {"
11879 a.f(«oneˇ», two, «threeˇ») b
11880 a.f(«oneˇ», two, «threeˇ») b
11881 a.f(«oneˇ», two, «threeˇ») b
11882 "});
11883
11884 // Can't move earlier than the first tab stop
11885 cx.update_editor(|editor, window, cx| {
11886 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11887 });
11888 cx.assert_editor_state(indoc! {"
11889 a.f(«oneˇ», two, «threeˇ») b
11890 a.f(«oneˇ», two, «threeˇ») b
11891 a.f(«oneˇ», two, «threeˇ») b
11892 "});
11893
11894 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11895 cx.assert_editor_state(indoc! {"
11896 a.f(one, «twoˇ», three) b
11897 a.f(one, «twoˇ», three) b
11898 a.f(one, «twoˇ», three) b
11899 "});
11900
11901 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11902 cx.assert_editor_state(indoc! {"
11903 a.f(«oneˇ», two, «threeˇ») b
11904 a.f(«oneˇ», two, «threeˇ») b
11905 a.f(«oneˇ», two, «threeˇ») b
11906 "});
11907
11908 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11909 cx.assert_editor_state(indoc! {"
11910 a.f(one, «twoˇ», three) b
11911 a.f(one, «twoˇ», three) b
11912 a.f(one, «twoˇ», three) b
11913 "});
11914 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11915 cx.assert_editor_state(indoc! {"
11916 a.f(one, two, three)ˇ b
11917 a.f(one, two, three)ˇ b
11918 a.f(one, two, three)ˇ b
11919 "});
11920
11921 // As soon as the last tab stop is reached, snippet state is gone
11922 cx.update_editor(|editor, window, cx| {
11923 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11924 });
11925 cx.assert_editor_state(indoc! {"
11926 a.f(one, two, three)ˇ b
11927 a.f(one, two, three)ˇ b
11928 a.f(one, two, three)ˇ b
11929 "});
11930}
11931
11932#[gpui::test]
11933async fn test_snippet_indentation(cx: &mut TestAppContext) {
11934 init_test(cx, |_| {});
11935
11936 let mut cx = EditorTestContext::new(cx).await;
11937
11938 cx.update_editor(|editor, window, cx| {
11939 let snippet = Snippet::parse(indoc! {"
11940 /*
11941 * Multiline comment with leading indentation
11942 *
11943 * $1
11944 */
11945 $0"})
11946 .unwrap();
11947 let insertion_ranges = editor
11948 .selections
11949 .all(&editor.display_snapshot(cx))
11950 .iter()
11951 .map(|s| s.range())
11952 .collect::<Vec<_>>();
11953 editor
11954 .insert_snippet(&insertion_ranges, snippet, window, cx)
11955 .unwrap();
11956 });
11957
11958 cx.assert_editor_state(indoc! {"
11959 /*
11960 * Multiline comment with leading indentation
11961 *
11962 * ˇ
11963 */
11964 "});
11965
11966 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11967 cx.assert_editor_state(indoc! {"
11968 /*
11969 * Multiline comment with leading indentation
11970 *
11971 *•
11972 */
11973 ˇ"});
11974}
11975
11976#[gpui::test]
11977async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11978 init_test(cx, |_| {});
11979
11980 let mut cx = EditorTestContext::new(cx).await;
11981 cx.update_editor(|editor, _, cx| {
11982 editor.project().unwrap().update(cx, |project, cx| {
11983 project.snippets().update(cx, |snippets, _cx| {
11984 let snippet = project::snippet_provider::Snippet {
11985 prefix: vec!["multi word".to_string()],
11986 body: "this is many words".to_string(),
11987 description: Some("description".to_string()),
11988 name: "multi-word snippet test".to_string(),
11989 };
11990 snippets.add_snippet_for_test(
11991 None,
11992 PathBuf::from("test_snippets.json"),
11993 vec![Arc::new(snippet)],
11994 );
11995 });
11996 })
11997 });
11998
11999 for (input_to_simulate, should_match_snippet) in [
12000 ("m", true),
12001 ("m ", true),
12002 ("m w", true),
12003 ("aa m w", true),
12004 ("aa m g", false),
12005 ] {
12006 cx.set_state("ˇ");
12007 cx.simulate_input(input_to_simulate); // fails correctly
12008
12009 cx.update_editor(|editor, _, _| {
12010 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
12011 else {
12012 assert!(!should_match_snippet); // no completions! don't even show the menu
12013 return;
12014 };
12015 assert!(context_menu.visible());
12016 let completions = context_menu.completions.borrow();
12017
12018 assert_eq!(!completions.is_empty(), should_match_snippet);
12019 });
12020 }
12021}
12022
12023#[gpui::test]
12024async fn test_document_format_during_save(cx: &mut TestAppContext) {
12025 init_test(cx, |_| {});
12026
12027 let fs = FakeFs::new(cx.executor());
12028 fs.insert_file(path!("/file.rs"), Default::default()).await;
12029
12030 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
12031
12032 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12033 language_registry.add(rust_lang());
12034 let mut fake_servers = language_registry.register_fake_lsp(
12035 "Rust",
12036 FakeLspAdapter {
12037 capabilities: lsp::ServerCapabilities {
12038 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12039 ..Default::default()
12040 },
12041 ..Default::default()
12042 },
12043 );
12044
12045 let buffer = project
12046 .update(cx, |project, cx| {
12047 project.open_local_buffer(path!("/file.rs"), cx)
12048 })
12049 .await
12050 .unwrap();
12051
12052 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12053 let (editor, cx) = cx.add_window_view(|window, cx| {
12054 build_editor_with_project(project.clone(), buffer, window, cx)
12055 });
12056 editor.update_in(cx, |editor, window, cx| {
12057 editor.set_text("one\ntwo\nthree\n", window, cx)
12058 });
12059 assert!(cx.read(|cx| editor.is_dirty(cx)));
12060
12061 let fake_server = fake_servers.next().await.unwrap();
12062
12063 {
12064 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12065 move |params, _| async move {
12066 assert_eq!(
12067 params.text_document.uri,
12068 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12069 );
12070 assert_eq!(params.options.tab_size, 4);
12071 Ok(Some(vec![lsp::TextEdit::new(
12072 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12073 ", ".to_string(),
12074 )]))
12075 },
12076 );
12077 let save = editor
12078 .update_in(cx, |editor, window, cx| {
12079 editor.save(
12080 SaveOptions {
12081 format: true,
12082 autosave: false,
12083 },
12084 project.clone(),
12085 window,
12086 cx,
12087 )
12088 })
12089 .unwrap();
12090 save.await;
12091
12092 assert_eq!(
12093 editor.update(cx, |editor, cx| editor.text(cx)),
12094 "one, two\nthree\n"
12095 );
12096 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12097 }
12098
12099 {
12100 editor.update_in(cx, |editor, window, cx| {
12101 editor.set_text("one\ntwo\nthree\n", window, cx)
12102 });
12103 assert!(cx.read(|cx| editor.is_dirty(cx)));
12104
12105 // Ensure we can still save even if formatting hangs.
12106 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12107 move |params, _| async move {
12108 assert_eq!(
12109 params.text_document.uri,
12110 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12111 );
12112 futures::future::pending::<()>().await;
12113 unreachable!()
12114 },
12115 );
12116 let save = editor
12117 .update_in(cx, |editor, window, cx| {
12118 editor.save(
12119 SaveOptions {
12120 format: true,
12121 autosave: false,
12122 },
12123 project.clone(),
12124 window,
12125 cx,
12126 )
12127 })
12128 .unwrap();
12129 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12130 save.await;
12131 assert_eq!(
12132 editor.update(cx, |editor, cx| editor.text(cx)),
12133 "one\ntwo\nthree\n"
12134 );
12135 }
12136
12137 // Set rust language override and assert overridden tabsize is sent to language server
12138 update_test_language_settings(cx, |settings| {
12139 settings.languages.0.insert(
12140 "Rust".into(),
12141 LanguageSettingsContent {
12142 tab_size: NonZeroU32::new(8),
12143 ..Default::default()
12144 },
12145 );
12146 });
12147
12148 {
12149 editor.update_in(cx, |editor, window, cx| {
12150 editor.set_text("somehting_new\n", window, cx)
12151 });
12152 assert!(cx.read(|cx| editor.is_dirty(cx)));
12153 let _formatting_request_signal = fake_server
12154 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12155 assert_eq!(
12156 params.text_document.uri,
12157 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12158 );
12159 assert_eq!(params.options.tab_size, 8);
12160 Ok(Some(vec![]))
12161 });
12162 let save = editor
12163 .update_in(cx, |editor, window, cx| {
12164 editor.save(
12165 SaveOptions {
12166 format: true,
12167 autosave: false,
12168 },
12169 project.clone(),
12170 window,
12171 cx,
12172 )
12173 })
12174 .unwrap();
12175 save.await;
12176 }
12177}
12178
12179#[gpui::test]
12180async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
12181 init_test(cx, |settings| {
12182 settings.defaults.ensure_final_newline_on_save = Some(false);
12183 });
12184
12185 let fs = FakeFs::new(cx.executor());
12186 fs.insert_file(path!("/file.txt"), "foo".into()).await;
12187
12188 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
12189
12190 let buffer = project
12191 .update(cx, |project, cx| {
12192 project.open_local_buffer(path!("/file.txt"), cx)
12193 })
12194 .await
12195 .unwrap();
12196
12197 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12198 let (editor, cx) = cx.add_window_view(|window, cx| {
12199 build_editor_with_project(project.clone(), buffer, window, cx)
12200 });
12201 editor.update_in(cx, |editor, window, cx| {
12202 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
12203 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
12204 });
12205 });
12206 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12207
12208 editor.update_in(cx, |editor, window, cx| {
12209 editor.handle_input("\n", window, cx)
12210 });
12211 cx.run_until_parked();
12212 save(&editor, &project, cx).await;
12213 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
12214
12215 editor.update_in(cx, |editor, window, cx| {
12216 editor.undo(&Default::default(), window, cx);
12217 });
12218 save(&editor, &project, cx).await;
12219 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
12220
12221 editor.update_in(cx, |editor, window, cx| {
12222 editor.redo(&Default::default(), window, cx);
12223 });
12224 cx.run_until_parked();
12225 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
12226
12227 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
12228 let save = editor
12229 .update_in(cx, |editor, window, cx| {
12230 editor.save(
12231 SaveOptions {
12232 format: true,
12233 autosave: false,
12234 },
12235 project.clone(),
12236 window,
12237 cx,
12238 )
12239 })
12240 .unwrap();
12241 save.await;
12242 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12243 }
12244}
12245
12246#[gpui::test]
12247async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
12248 init_test(cx, |_| {});
12249
12250 let cols = 4;
12251 let rows = 10;
12252 let sample_text_1 = sample_text(rows, cols, 'a');
12253 assert_eq!(
12254 sample_text_1,
12255 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12256 );
12257 let sample_text_2 = sample_text(rows, cols, 'l');
12258 assert_eq!(
12259 sample_text_2,
12260 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12261 );
12262 let sample_text_3 = sample_text(rows, cols, 'v');
12263 assert_eq!(
12264 sample_text_3,
12265 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12266 );
12267
12268 let fs = FakeFs::new(cx.executor());
12269 fs.insert_tree(
12270 path!("/a"),
12271 json!({
12272 "main.rs": sample_text_1,
12273 "other.rs": sample_text_2,
12274 "lib.rs": sample_text_3,
12275 }),
12276 )
12277 .await;
12278
12279 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12280 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12281 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12282
12283 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12284 language_registry.add(rust_lang());
12285 let mut fake_servers = language_registry.register_fake_lsp(
12286 "Rust",
12287 FakeLspAdapter {
12288 capabilities: lsp::ServerCapabilities {
12289 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12290 ..Default::default()
12291 },
12292 ..Default::default()
12293 },
12294 );
12295
12296 let worktree = project.update(cx, |project, cx| {
12297 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
12298 assert_eq!(worktrees.len(), 1);
12299 worktrees.pop().unwrap()
12300 });
12301 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12302
12303 let buffer_1 = project
12304 .update(cx, |project, cx| {
12305 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
12306 })
12307 .await
12308 .unwrap();
12309 let buffer_2 = project
12310 .update(cx, |project, cx| {
12311 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
12312 })
12313 .await
12314 .unwrap();
12315 let buffer_3 = project
12316 .update(cx, |project, cx| {
12317 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
12318 })
12319 .await
12320 .unwrap();
12321
12322 let multi_buffer = cx.new(|cx| {
12323 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12324 multi_buffer.push_excerpts(
12325 buffer_1.clone(),
12326 [
12327 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12328 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12329 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12330 ],
12331 cx,
12332 );
12333 multi_buffer.push_excerpts(
12334 buffer_2.clone(),
12335 [
12336 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12337 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12338 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12339 ],
12340 cx,
12341 );
12342 multi_buffer.push_excerpts(
12343 buffer_3.clone(),
12344 [
12345 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12346 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12347 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12348 ],
12349 cx,
12350 );
12351 multi_buffer
12352 });
12353 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12354 Editor::new(
12355 EditorMode::full(),
12356 multi_buffer,
12357 Some(project.clone()),
12358 window,
12359 cx,
12360 )
12361 });
12362
12363 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12364 editor.change_selections(
12365 SelectionEffects::scroll(Autoscroll::Next),
12366 window,
12367 cx,
12368 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
12369 );
12370 editor.insert("|one|two|three|", window, cx);
12371 });
12372 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12373 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12374 editor.change_selections(
12375 SelectionEffects::scroll(Autoscroll::Next),
12376 window,
12377 cx,
12378 |s| s.select_ranges(Some(MultiBufferOffset(60)..MultiBufferOffset(70))),
12379 );
12380 editor.insert("|four|five|six|", window, cx);
12381 });
12382 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12383
12384 // First two buffers should be edited, but not the third one.
12385 assert_eq!(
12386 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12387 "a|one|two|three|aa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\nllll\nmmmm\nnnnn|four|five|six|\nr\n\nuuuu\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
12388 );
12389 buffer_1.update(cx, |buffer, _| {
12390 assert!(buffer.is_dirty());
12391 assert_eq!(
12392 buffer.text(),
12393 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
12394 )
12395 });
12396 buffer_2.update(cx, |buffer, _| {
12397 assert!(buffer.is_dirty());
12398 assert_eq!(
12399 buffer.text(),
12400 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
12401 )
12402 });
12403 buffer_3.update(cx, |buffer, _| {
12404 assert!(!buffer.is_dirty());
12405 assert_eq!(buffer.text(), sample_text_3,)
12406 });
12407 cx.executor().run_until_parked();
12408
12409 let save = multi_buffer_editor
12410 .update_in(cx, |editor, window, cx| {
12411 editor.save(
12412 SaveOptions {
12413 format: true,
12414 autosave: false,
12415 },
12416 project.clone(),
12417 window,
12418 cx,
12419 )
12420 })
12421 .unwrap();
12422
12423 let fake_server = fake_servers.next().await.unwrap();
12424 fake_server
12425 .server
12426 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
12427 Ok(Some(vec![lsp::TextEdit::new(
12428 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12429 format!("[{} formatted]", params.text_document.uri),
12430 )]))
12431 })
12432 .detach();
12433 save.await;
12434
12435 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
12436 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
12437 assert_eq!(
12438 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12439 uri!(
12440 "a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}"
12441 ),
12442 );
12443 buffer_1.update(cx, |buffer, _| {
12444 assert!(!buffer.is_dirty());
12445 assert_eq!(
12446 buffer.text(),
12447 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
12448 )
12449 });
12450 buffer_2.update(cx, |buffer, _| {
12451 assert!(!buffer.is_dirty());
12452 assert_eq!(
12453 buffer.text(),
12454 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
12455 )
12456 });
12457 buffer_3.update(cx, |buffer, _| {
12458 assert!(!buffer.is_dirty());
12459 assert_eq!(buffer.text(), sample_text_3,)
12460 });
12461}
12462
12463#[gpui::test]
12464async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
12465 init_test(cx, |_| {});
12466
12467 let fs = FakeFs::new(cx.executor());
12468 fs.insert_tree(
12469 path!("/dir"),
12470 json!({
12471 "file1.rs": "fn main() { println!(\"hello\"); }",
12472 "file2.rs": "fn test() { println!(\"test\"); }",
12473 "file3.rs": "fn other() { println!(\"other\"); }\n",
12474 }),
12475 )
12476 .await;
12477
12478 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
12479 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12480 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12481
12482 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12483 language_registry.add(rust_lang());
12484
12485 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
12486 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12487
12488 // Open three buffers
12489 let buffer_1 = project
12490 .update(cx, |project, cx| {
12491 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
12492 })
12493 .await
12494 .unwrap();
12495 let buffer_2 = project
12496 .update(cx, |project, cx| {
12497 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
12498 })
12499 .await
12500 .unwrap();
12501 let buffer_3 = project
12502 .update(cx, |project, cx| {
12503 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
12504 })
12505 .await
12506 .unwrap();
12507
12508 // Create a multi-buffer with all three buffers
12509 let multi_buffer = cx.new(|cx| {
12510 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12511 multi_buffer.push_excerpts(
12512 buffer_1.clone(),
12513 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12514 cx,
12515 );
12516 multi_buffer.push_excerpts(
12517 buffer_2.clone(),
12518 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12519 cx,
12520 );
12521 multi_buffer.push_excerpts(
12522 buffer_3.clone(),
12523 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12524 cx,
12525 );
12526 multi_buffer
12527 });
12528
12529 let editor = cx.new_window_entity(|window, cx| {
12530 Editor::new(
12531 EditorMode::full(),
12532 multi_buffer,
12533 Some(project.clone()),
12534 window,
12535 cx,
12536 )
12537 });
12538
12539 // Edit only the first buffer
12540 editor.update_in(cx, |editor, window, cx| {
12541 editor.change_selections(
12542 SelectionEffects::scroll(Autoscroll::Next),
12543 window,
12544 cx,
12545 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
12546 );
12547 editor.insert("// edited", window, cx);
12548 });
12549
12550 // Verify that only buffer 1 is dirty
12551 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
12552 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12553 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12554
12555 // Get write counts after file creation (files were created with initial content)
12556 // We expect each file to have been written once during creation
12557 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12558 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12559 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12560
12561 // Perform autosave
12562 let save_task = editor.update_in(cx, |editor, window, cx| {
12563 editor.save(
12564 SaveOptions {
12565 format: true,
12566 autosave: true,
12567 },
12568 project.clone(),
12569 window,
12570 cx,
12571 )
12572 });
12573 save_task.await.unwrap();
12574
12575 // Only the dirty buffer should have been saved
12576 assert_eq!(
12577 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12578 1,
12579 "Buffer 1 was dirty, so it should have been written once during autosave"
12580 );
12581 assert_eq!(
12582 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12583 0,
12584 "Buffer 2 was clean, so it should not have been written during autosave"
12585 );
12586 assert_eq!(
12587 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12588 0,
12589 "Buffer 3 was clean, so it should not have been written during autosave"
12590 );
12591
12592 // Verify buffer states after autosave
12593 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12594 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12595 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12596
12597 // Now perform a manual save (format = true)
12598 let save_task = editor.update_in(cx, |editor, window, cx| {
12599 editor.save(
12600 SaveOptions {
12601 format: true,
12602 autosave: false,
12603 },
12604 project.clone(),
12605 window,
12606 cx,
12607 )
12608 });
12609 save_task.await.unwrap();
12610
12611 // During manual save, clean buffers don't get written to disk
12612 // They just get did_save called for language server notifications
12613 assert_eq!(
12614 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12615 1,
12616 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12617 );
12618 assert_eq!(
12619 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12620 0,
12621 "Buffer 2 should not have been written at all"
12622 );
12623 assert_eq!(
12624 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12625 0,
12626 "Buffer 3 should not have been written at all"
12627 );
12628}
12629
12630async fn setup_range_format_test(
12631 cx: &mut TestAppContext,
12632) -> (
12633 Entity<Project>,
12634 Entity<Editor>,
12635 &mut gpui::VisualTestContext,
12636 lsp::FakeLanguageServer,
12637) {
12638 init_test(cx, |_| {});
12639
12640 let fs = FakeFs::new(cx.executor());
12641 fs.insert_file(path!("/file.rs"), Default::default()).await;
12642
12643 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12644
12645 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12646 language_registry.add(rust_lang());
12647 let mut fake_servers = language_registry.register_fake_lsp(
12648 "Rust",
12649 FakeLspAdapter {
12650 capabilities: lsp::ServerCapabilities {
12651 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12652 ..lsp::ServerCapabilities::default()
12653 },
12654 ..FakeLspAdapter::default()
12655 },
12656 );
12657
12658 let buffer = project
12659 .update(cx, |project, cx| {
12660 project.open_local_buffer(path!("/file.rs"), cx)
12661 })
12662 .await
12663 .unwrap();
12664
12665 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12666 let (editor, cx) = cx.add_window_view(|window, cx| {
12667 build_editor_with_project(project.clone(), buffer, window, cx)
12668 });
12669
12670 let fake_server = fake_servers.next().await.unwrap();
12671
12672 (project, editor, cx, fake_server)
12673}
12674
12675#[gpui::test]
12676async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12677 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12678
12679 editor.update_in(cx, |editor, window, cx| {
12680 editor.set_text("one\ntwo\nthree\n", window, cx)
12681 });
12682 assert!(cx.read(|cx| editor.is_dirty(cx)));
12683
12684 let save = editor
12685 .update_in(cx, |editor, window, cx| {
12686 editor.save(
12687 SaveOptions {
12688 format: true,
12689 autosave: false,
12690 },
12691 project.clone(),
12692 window,
12693 cx,
12694 )
12695 })
12696 .unwrap();
12697 fake_server
12698 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12699 assert_eq!(
12700 params.text_document.uri,
12701 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12702 );
12703 assert_eq!(params.options.tab_size, 4);
12704 Ok(Some(vec![lsp::TextEdit::new(
12705 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12706 ", ".to_string(),
12707 )]))
12708 })
12709 .next()
12710 .await;
12711 save.await;
12712 assert_eq!(
12713 editor.update(cx, |editor, cx| editor.text(cx)),
12714 "one, two\nthree\n"
12715 );
12716 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12717}
12718
12719#[gpui::test]
12720async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12721 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12722
12723 editor.update_in(cx, |editor, window, cx| {
12724 editor.set_text("one\ntwo\nthree\n", window, cx)
12725 });
12726 assert!(cx.read(|cx| editor.is_dirty(cx)));
12727
12728 // Test that save still works when formatting hangs
12729 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12730 move |params, _| async move {
12731 assert_eq!(
12732 params.text_document.uri,
12733 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12734 );
12735 futures::future::pending::<()>().await;
12736 unreachable!()
12737 },
12738 );
12739 let save = editor
12740 .update_in(cx, |editor, window, cx| {
12741 editor.save(
12742 SaveOptions {
12743 format: true,
12744 autosave: false,
12745 },
12746 project.clone(),
12747 window,
12748 cx,
12749 )
12750 })
12751 .unwrap();
12752 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12753 save.await;
12754 assert_eq!(
12755 editor.update(cx, |editor, cx| editor.text(cx)),
12756 "one\ntwo\nthree\n"
12757 );
12758 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12759}
12760
12761#[gpui::test]
12762async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12763 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12764
12765 // Buffer starts clean, no formatting should be requested
12766 let save = editor
12767 .update_in(cx, |editor, window, cx| {
12768 editor.save(
12769 SaveOptions {
12770 format: false,
12771 autosave: false,
12772 },
12773 project.clone(),
12774 window,
12775 cx,
12776 )
12777 })
12778 .unwrap();
12779 let _pending_format_request = fake_server
12780 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12781 panic!("Should not be invoked");
12782 })
12783 .next();
12784 save.await;
12785 cx.run_until_parked();
12786}
12787
12788#[gpui::test]
12789async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12790 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12791
12792 // Set Rust language override and assert overridden tabsize is sent to language server
12793 update_test_language_settings(cx, |settings| {
12794 settings.languages.0.insert(
12795 "Rust".into(),
12796 LanguageSettingsContent {
12797 tab_size: NonZeroU32::new(8),
12798 ..Default::default()
12799 },
12800 );
12801 });
12802
12803 editor.update_in(cx, |editor, window, cx| {
12804 editor.set_text("something_new\n", window, cx)
12805 });
12806 assert!(cx.read(|cx| editor.is_dirty(cx)));
12807 let save = editor
12808 .update_in(cx, |editor, window, cx| {
12809 editor.save(
12810 SaveOptions {
12811 format: true,
12812 autosave: false,
12813 },
12814 project.clone(),
12815 window,
12816 cx,
12817 )
12818 })
12819 .unwrap();
12820 fake_server
12821 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12822 assert_eq!(
12823 params.text_document.uri,
12824 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12825 );
12826 assert_eq!(params.options.tab_size, 8);
12827 Ok(Some(Vec::new()))
12828 })
12829 .next()
12830 .await;
12831 save.await;
12832}
12833
12834#[gpui::test]
12835async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12836 init_test(cx, |settings| {
12837 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12838 settings::LanguageServerFormatterSpecifier::Current,
12839 )))
12840 });
12841
12842 let fs = FakeFs::new(cx.executor());
12843 fs.insert_file(path!("/file.rs"), Default::default()).await;
12844
12845 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12846
12847 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12848 language_registry.add(Arc::new(Language::new(
12849 LanguageConfig {
12850 name: "Rust".into(),
12851 matcher: LanguageMatcher {
12852 path_suffixes: vec!["rs".to_string()],
12853 ..Default::default()
12854 },
12855 ..LanguageConfig::default()
12856 },
12857 Some(tree_sitter_rust::LANGUAGE.into()),
12858 )));
12859 update_test_language_settings(cx, |settings| {
12860 // Enable Prettier formatting for the same buffer, and ensure
12861 // LSP is called instead of Prettier.
12862 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12863 });
12864 let mut fake_servers = language_registry.register_fake_lsp(
12865 "Rust",
12866 FakeLspAdapter {
12867 capabilities: lsp::ServerCapabilities {
12868 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12869 ..Default::default()
12870 },
12871 ..Default::default()
12872 },
12873 );
12874
12875 let buffer = project
12876 .update(cx, |project, cx| {
12877 project.open_local_buffer(path!("/file.rs"), cx)
12878 })
12879 .await
12880 .unwrap();
12881
12882 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12883 let (editor, cx) = cx.add_window_view(|window, cx| {
12884 build_editor_with_project(project.clone(), buffer, window, cx)
12885 });
12886 editor.update_in(cx, |editor, window, cx| {
12887 editor.set_text("one\ntwo\nthree\n", window, cx)
12888 });
12889
12890 let fake_server = fake_servers.next().await.unwrap();
12891
12892 let format = editor
12893 .update_in(cx, |editor, window, cx| {
12894 editor.perform_format(
12895 project.clone(),
12896 FormatTrigger::Manual,
12897 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12898 window,
12899 cx,
12900 )
12901 })
12902 .unwrap();
12903 fake_server
12904 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12905 assert_eq!(
12906 params.text_document.uri,
12907 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12908 );
12909 assert_eq!(params.options.tab_size, 4);
12910 Ok(Some(vec![lsp::TextEdit::new(
12911 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12912 ", ".to_string(),
12913 )]))
12914 })
12915 .next()
12916 .await;
12917 format.await;
12918 assert_eq!(
12919 editor.update(cx, |editor, cx| editor.text(cx)),
12920 "one, two\nthree\n"
12921 );
12922
12923 editor.update_in(cx, |editor, window, cx| {
12924 editor.set_text("one\ntwo\nthree\n", window, cx)
12925 });
12926 // Ensure we don't lock if formatting hangs.
12927 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12928 move |params, _| async move {
12929 assert_eq!(
12930 params.text_document.uri,
12931 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12932 );
12933 futures::future::pending::<()>().await;
12934 unreachable!()
12935 },
12936 );
12937 let format = editor
12938 .update_in(cx, |editor, window, cx| {
12939 editor.perform_format(
12940 project,
12941 FormatTrigger::Manual,
12942 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12943 window,
12944 cx,
12945 )
12946 })
12947 .unwrap();
12948 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12949 format.await;
12950 assert_eq!(
12951 editor.update(cx, |editor, cx| editor.text(cx)),
12952 "one\ntwo\nthree\n"
12953 );
12954}
12955
12956#[gpui::test]
12957async fn test_multiple_formatters(cx: &mut TestAppContext) {
12958 init_test(cx, |settings| {
12959 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12960 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12961 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12962 Formatter::CodeAction("code-action-1".into()),
12963 Formatter::CodeAction("code-action-2".into()),
12964 ]))
12965 });
12966
12967 let fs = FakeFs::new(cx.executor());
12968 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12969 .await;
12970
12971 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12972 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12973 language_registry.add(rust_lang());
12974
12975 let mut fake_servers = language_registry.register_fake_lsp(
12976 "Rust",
12977 FakeLspAdapter {
12978 capabilities: lsp::ServerCapabilities {
12979 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12980 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12981 commands: vec!["the-command-for-code-action-1".into()],
12982 ..Default::default()
12983 }),
12984 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12985 ..Default::default()
12986 },
12987 ..Default::default()
12988 },
12989 );
12990
12991 let buffer = project
12992 .update(cx, |project, cx| {
12993 project.open_local_buffer(path!("/file.rs"), cx)
12994 })
12995 .await
12996 .unwrap();
12997
12998 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12999 let (editor, cx) = cx.add_window_view(|window, cx| {
13000 build_editor_with_project(project.clone(), buffer, window, cx)
13001 });
13002
13003 let fake_server = fake_servers.next().await.unwrap();
13004 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
13005 move |_params, _| async move {
13006 Ok(Some(vec![lsp::TextEdit::new(
13007 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
13008 "applied-formatting\n".to_string(),
13009 )]))
13010 },
13011 );
13012 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
13013 move |params, _| async move {
13014 let requested_code_actions = params.context.only.expect("Expected code action request");
13015 assert_eq!(requested_code_actions.len(), 1);
13016
13017 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
13018 let code_action = match requested_code_actions[0].as_str() {
13019 "code-action-1" => lsp::CodeAction {
13020 kind: Some("code-action-1".into()),
13021 edit: Some(lsp::WorkspaceEdit::new(
13022 [(
13023 uri,
13024 vec![lsp::TextEdit::new(
13025 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
13026 "applied-code-action-1-edit\n".to_string(),
13027 )],
13028 )]
13029 .into_iter()
13030 .collect(),
13031 )),
13032 command: Some(lsp::Command {
13033 command: "the-command-for-code-action-1".into(),
13034 ..Default::default()
13035 }),
13036 ..Default::default()
13037 },
13038 "code-action-2" => lsp::CodeAction {
13039 kind: Some("code-action-2".into()),
13040 edit: Some(lsp::WorkspaceEdit::new(
13041 [(
13042 uri,
13043 vec![lsp::TextEdit::new(
13044 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
13045 "applied-code-action-2-edit\n".to_string(),
13046 )],
13047 )]
13048 .into_iter()
13049 .collect(),
13050 )),
13051 ..Default::default()
13052 },
13053 req => panic!("Unexpected code action request: {:?}", req),
13054 };
13055 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
13056 code_action,
13057 )]))
13058 },
13059 );
13060
13061 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
13062 move |params, _| async move { Ok(params) }
13063 });
13064
13065 let command_lock = Arc::new(futures::lock::Mutex::new(()));
13066 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
13067 let fake = fake_server.clone();
13068 let lock = command_lock.clone();
13069 move |params, _| {
13070 assert_eq!(params.command, "the-command-for-code-action-1");
13071 let fake = fake.clone();
13072 let lock = lock.clone();
13073 async move {
13074 lock.lock().await;
13075 fake.server
13076 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
13077 label: None,
13078 edit: lsp::WorkspaceEdit {
13079 changes: Some(
13080 [(
13081 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
13082 vec![lsp::TextEdit {
13083 range: lsp::Range::new(
13084 lsp::Position::new(0, 0),
13085 lsp::Position::new(0, 0),
13086 ),
13087 new_text: "applied-code-action-1-command\n".into(),
13088 }],
13089 )]
13090 .into_iter()
13091 .collect(),
13092 ),
13093 ..Default::default()
13094 },
13095 })
13096 .await
13097 .into_response()
13098 .unwrap();
13099 Ok(Some(json!(null)))
13100 }
13101 }
13102 });
13103
13104 editor
13105 .update_in(cx, |editor, window, cx| {
13106 editor.perform_format(
13107 project.clone(),
13108 FormatTrigger::Manual,
13109 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
13110 window,
13111 cx,
13112 )
13113 })
13114 .unwrap()
13115 .await;
13116 editor.update(cx, |editor, cx| {
13117 assert_eq!(
13118 editor.text(cx),
13119 r#"
13120 applied-code-action-2-edit
13121 applied-code-action-1-command
13122 applied-code-action-1-edit
13123 applied-formatting
13124 one
13125 two
13126 three
13127 "#
13128 .unindent()
13129 );
13130 });
13131
13132 editor.update_in(cx, |editor, window, cx| {
13133 editor.undo(&Default::default(), window, cx);
13134 assert_eq!(editor.text(cx), "one \ntwo \nthree");
13135 });
13136
13137 // Perform a manual edit while waiting for an LSP command
13138 // that's being run as part of a formatting code action.
13139 let lock_guard = command_lock.lock().await;
13140 let format = editor
13141 .update_in(cx, |editor, window, cx| {
13142 editor.perform_format(
13143 project.clone(),
13144 FormatTrigger::Manual,
13145 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
13146 window,
13147 cx,
13148 )
13149 })
13150 .unwrap();
13151 cx.run_until_parked();
13152 editor.update(cx, |editor, cx| {
13153 assert_eq!(
13154 editor.text(cx),
13155 r#"
13156 applied-code-action-1-edit
13157 applied-formatting
13158 one
13159 two
13160 three
13161 "#
13162 .unindent()
13163 );
13164
13165 editor.buffer.update(cx, |buffer, cx| {
13166 let ix = buffer.len(cx);
13167 buffer.edit([(ix..ix, "edited\n")], None, cx);
13168 });
13169 });
13170
13171 // Allow the LSP command to proceed. Because the buffer was edited,
13172 // the second code action will not be run.
13173 drop(lock_guard);
13174 format.await;
13175 editor.update_in(cx, |editor, window, cx| {
13176 assert_eq!(
13177 editor.text(cx),
13178 r#"
13179 applied-code-action-1-command
13180 applied-code-action-1-edit
13181 applied-formatting
13182 one
13183 two
13184 three
13185 edited
13186 "#
13187 .unindent()
13188 );
13189
13190 // The manual edit is undone first, because it is the last thing the user did
13191 // (even though the command completed afterwards).
13192 editor.undo(&Default::default(), window, cx);
13193 assert_eq!(
13194 editor.text(cx),
13195 r#"
13196 applied-code-action-1-command
13197 applied-code-action-1-edit
13198 applied-formatting
13199 one
13200 two
13201 three
13202 "#
13203 .unindent()
13204 );
13205
13206 // All the formatting (including the command, which completed after the manual edit)
13207 // is undone together.
13208 editor.undo(&Default::default(), window, cx);
13209 assert_eq!(editor.text(cx), "one \ntwo \nthree");
13210 });
13211}
13212
13213#[gpui::test]
13214async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
13215 init_test(cx, |settings| {
13216 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
13217 settings::LanguageServerFormatterSpecifier::Current,
13218 )]))
13219 });
13220
13221 let fs = FakeFs::new(cx.executor());
13222 fs.insert_file(path!("/file.ts"), Default::default()).await;
13223
13224 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13225
13226 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13227 language_registry.add(Arc::new(Language::new(
13228 LanguageConfig {
13229 name: "TypeScript".into(),
13230 matcher: LanguageMatcher {
13231 path_suffixes: vec!["ts".to_string()],
13232 ..Default::default()
13233 },
13234 ..LanguageConfig::default()
13235 },
13236 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13237 )));
13238 update_test_language_settings(cx, |settings| {
13239 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
13240 });
13241 let mut fake_servers = language_registry.register_fake_lsp(
13242 "TypeScript",
13243 FakeLspAdapter {
13244 capabilities: lsp::ServerCapabilities {
13245 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
13246 ..Default::default()
13247 },
13248 ..Default::default()
13249 },
13250 );
13251
13252 let buffer = project
13253 .update(cx, |project, cx| {
13254 project.open_local_buffer(path!("/file.ts"), cx)
13255 })
13256 .await
13257 .unwrap();
13258
13259 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13260 let (editor, cx) = cx.add_window_view(|window, cx| {
13261 build_editor_with_project(project.clone(), buffer, window, cx)
13262 });
13263 editor.update_in(cx, |editor, window, cx| {
13264 editor.set_text(
13265 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13266 window,
13267 cx,
13268 )
13269 });
13270
13271 let fake_server = fake_servers.next().await.unwrap();
13272
13273 let format = editor
13274 .update_in(cx, |editor, window, cx| {
13275 editor.perform_code_action_kind(
13276 project.clone(),
13277 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13278 window,
13279 cx,
13280 )
13281 })
13282 .unwrap();
13283 fake_server
13284 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
13285 assert_eq!(
13286 params.text_document.uri,
13287 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13288 );
13289 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
13290 lsp::CodeAction {
13291 title: "Organize Imports".to_string(),
13292 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
13293 edit: Some(lsp::WorkspaceEdit {
13294 changes: Some(
13295 [(
13296 params.text_document.uri.clone(),
13297 vec![lsp::TextEdit::new(
13298 lsp::Range::new(
13299 lsp::Position::new(1, 0),
13300 lsp::Position::new(2, 0),
13301 ),
13302 "".to_string(),
13303 )],
13304 )]
13305 .into_iter()
13306 .collect(),
13307 ),
13308 ..Default::default()
13309 }),
13310 ..Default::default()
13311 },
13312 )]))
13313 })
13314 .next()
13315 .await;
13316 format.await;
13317 assert_eq!(
13318 editor.update(cx, |editor, cx| editor.text(cx)),
13319 "import { a } from 'module';\n\nconst x = a;\n"
13320 );
13321
13322 editor.update_in(cx, |editor, window, cx| {
13323 editor.set_text(
13324 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13325 window,
13326 cx,
13327 )
13328 });
13329 // Ensure we don't lock if code action hangs.
13330 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
13331 move |params, _| async move {
13332 assert_eq!(
13333 params.text_document.uri,
13334 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13335 );
13336 futures::future::pending::<()>().await;
13337 unreachable!()
13338 },
13339 );
13340 let format = editor
13341 .update_in(cx, |editor, window, cx| {
13342 editor.perform_code_action_kind(
13343 project,
13344 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13345 window,
13346 cx,
13347 )
13348 })
13349 .unwrap();
13350 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
13351 format.await;
13352 assert_eq!(
13353 editor.update(cx, |editor, cx| editor.text(cx)),
13354 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
13355 );
13356}
13357
13358#[gpui::test]
13359async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
13360 init_test(cx, |_| {});
13361
13362 let mut cx = EditorLspTestContext::new_rust(
13363 lsp::ServerCapabilities {
13364 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13365 ..Default::default()
13366 },
13367 cx,
13368 )
13369 .await;
13370
13371 cx.set_state(indoc! {"
13372 one.twoˇ
13373 "});
13374
13375 // The format request takes a long time. When it completes, it inserts
13376 // a newline and an indent before the `.`
13377 cx.lsp
13378 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
13379 let executor = cx.background_executor().clone();
13380 async move {
13381 executor.timer(Duration::from_millis(100)).await;
13382 Ok(Some(vec![lsp::TextEdit {
13383 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
13384 new_text: "\n ".into(),
13385 }]))
13386 }
13387 });
13388
13389 // Submit a format request.
13390 let format_1 = cx
13391 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13392 .unwrap();
13393 cx.executor().run_until_parked();
13394
13395 // Submit a second format request.
13396 let format_2 = cx
13397 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13398 .unwrap();
13399 cx.executor().run_until_parked();
13400
13401 // Wait for both format requests to complete
13402 cx.executor().advance_clock(Duration::from_millis(200));
13403 format_1.await.unwrap();
13404 format_2.await.unwrap();
13405
13406 // The formatting edits only happens once.
13407 cx.assert_editor_state(indoc! {"
13408 one
13409 .twoˇ
13410 "});
13411}
13412
13413#[gpui::test]
13414async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
13415 init_test(cx, |settings| {
13416 settings.defaults.formatter = Some(FormatterList::default())
13417 });
13418
13419 let mut cx = EditorLspTestContext::new_rust(
13420 lsp::ServerCapabilities {
13421 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13422 ..Default::default()
13423 },
13424 cx,
13425 )
13426 .await;
13427
13428 // Record which buffer changes have been sent to the language server
13429 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
13430 cx.lsp
13431 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
13432 let buffer_changes = buffer_changes.clone();
13433 move |params, _| {
13434 buffer_changes.lock().extend(
13435 params
13436 .content_changes
13437 .into_iter()
13438 .map(|e| (e.range.unwrap(), e.text)),
13439 );
13440 }
13441 });
13442 // Handle formatting requests to the language server.
13443 cx.lsp
13444 .set_request_handler::<lsp::request::Formatting, _, _>({
13445 move |_, _| {
13446 // Insert blank lines between each line of the buffer.
13447 async move {
13448 // TODO: this assertion is not reliably true. Currently nothing guarantees that we deliver
13449 // DidChangedTextDocument to the LSP before sending the formatting request.
13450 // assert_eq!(
13451 // &buffer_changes.lock()[1..],
13452 // &[
13453 // (
13454 // lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13455 // "".into()
13456 // ),
13457 // (
13458 // lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13459 // "".into()
13460 // ),
13461 // (
13462 // lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13463 // "\n".into()
13464 // ),
13465 // ]
13466 // );
13467
13468 Ok(Some(vec![
13469 lsp::TextEdit {
13470 range: lsp::Range::new(
13471 lsp::Position::new(1, 0),
13472 lsp::Position::new(1, 0),
13473 ),
13474 new_text: "\n".into(),
13475 },
13476 lsp::TextEdit {
13477 range: lsp::Range::new(
13478 lsp::Position::new(2, 0),
13479 lsp::Position::new(2, 0),
13480 ),
13481 new_text: "\n".into(),
13482 },
13483 ]))
13484 }
13485 }
13486 });
13487
13488 // Set up a buffer white some trailing whitespace and no trailing newline.
13489 cx.set_state(
13490 &[
13491 "one ", //
13492 "twoˇ", //
13493 "three ", //
13494 "four", //
13495 ]
13496 .join("\n"),
13497 );
13498
13499 // Submit a format request.
13500 let format = cx
13501 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13502 .unwrap();
13503
13504 cx.run_until_parked();
13505 // After formatting the buffer, the trailing whitespace is stripped,
13506 // a newline is appended, and the edits provided by the language server
13507 // have been applied.
13508 format.await.unwrap();
13509
13510 cx.assert_editor_state(
13511 &[
13512 "one", //
13513 "", //
13514 "twoˇ", //
13515 "", //
13516 "three", //
13517 "four", //
13518 "", //
13519 ]
13520 .join("\n"),
13521 );
13522
13523 // Undoing the formatting undoes the trailing whitespace removal, the
13524 // trailing newline, and the LSP edits.
13525 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13526 cx.assert_editor_state(
13527 &[
13528 "one ", //
13529 "twoˇ", //
13530 "three ", //
13531 "four", //
13532 ]
13533 .join("\n"),
13534 );
13535}
13536
13537#[gpui::test]
13538async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13539 cx: &mut TestAppContext,
13540) {
13541 init_test(cx, |_| {});
13542
13543 cx.update(|cx| {
13544 cx.update_global::<SettingsStore, _>(|settings, cx| {
13545 settings.update_user_settings(cx, |settings| {
13546 settings.editor.auto_signature_help = Some(true);
13547 settings.editor.hover_popover_delay = Some(DelayMs(300));
13548 });
13549 });
13550 });
13551
13552 let mut cx = EditorLspTestContext::new_rust(
13553 lsp::ServerCapabilities {
13554 signature_help_provider: Some(lsp::SignatureHelpOptions {
13555 ..Default::default()
13556 }),
13557 ..Default::default()
13558 },
13559 cx,
13560 )
13561 .await;
13562
13563 let language = Language::new(
13564 LanguageConfig {
13565 name: "Rust".into(),
13566 brackets: BracketPairConfig {
13567 pairs: vec![
13568 BracketPair {
13569 start: "{".to_string(),
13570 end: "}".to_string(),
13571 close: true,
13572 surround: true,
13573 newline: true,
13574 },
13575 BracketPair {
13576 start: "(".to_string(),
13577 end: ")".to_string(),
13578 close: true,
13579 surround: true,
13580 newline: true,
13581 },
13582 BracketPair {
13583 start: "/*".to_string(),
13584 end: " */".to_string(),
13585 close: true,
13586 surround: true,
13587 newline: true,
13588 },
13589 BracketPair {
13590 start: "[".to_string(),
13591 end: "]".to_string(),
13592 close: false,
13593 surround: false,
13594 newline: true,
13595 },
13596 BracketPair {
13597 start: "\"".to_string(),
13598 end: "\"".to_string(),
13599 close: true,
13600 surround: true,
13601 newline: false,
13602 },
13603 BracketPair {
13604 start: "<".to_string(),
13605 end: ">".to_string(),
13606 close: false,
13607 surround: true,
13608 newline: true,
13609 },
13610 ],
13611 ..Default::default()
13612 },
13613 autoclose_before: "})]".to_string(),
13614 ..Default::default()
13615 },
13616 Some(tree_sitter_rust::LANGUAGE.into()),
13617 );
13618 let language = Arc::new(language);
13619
13620 cx.language_registry().add(language.clone());
13621 cx.update_buffer(|buffer, cx| {
13622 buffer.set_language(Some(language), cx);
13623 });
13624
13625 cx.set_state(
13626 &r#"
13627 fn main() {
13628 sampleˇ
13629 }
13630 "#
13631 .unindent(),
13632 );
13633
13634 cx.update_editor(|editor, window, cx| {
13635 editor.handle_input("(", window, cx);
13636 });
13637 cx.assert_editor_state(
13638 &"
13639 fn main() {
13640 sample(ˇ)
13641 }
13642 "
13643 .unindent(),
13644 );
13645
13646 let mocked_response = lsp::SignatureHelp {
13647 signatures: vec![lsp::SignatureInformation {
13648 label: "fn sample(param1: u8, param2: u8)".to_string(),
13649 documentation: None,
13650 parameters: Some(vec![
13651 lsp::ParameterInformation {
13652 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13653 documentation: None,
13654 },
13655 lsp::ParameterInformation {
13656 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13657 documentation: None,
13658 },
13659 ]),
13660 active_parameter: None,
13661 }],
13662 active_signature: Some(0),
13663 active_parameter: Some(0),
13664 };
13665 handle_signature_help_request(&mut cx, mocked_response).await;
13666
13667 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13668 .await;
13669
13670 cx.editor(|editor, _, _| {
13671 let signature_help_state = editor.signature_help_state.popover().cloned();
13672 let signature = signature_help_state.unwrap();
13673 assert_eq!(
13674 signature.signatures[signature.current_signature].label,
13675 "fn sample(param1: u8, param2: u8)"
13676 );
13677 });
13678}
13679
13680#[gpui::test]
13681async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13682 init_test(cx, |_| {});
13683
13684 cx.update(|cx| {
13685 cx.update_global::<SettingsStore, _>(|settings, cx| {
13686 settings.update_user_settings(cx, |settings| {
13687 settings.editor.auto_signature_help = Some(false);
13688 settings.editor.show_signature_help_after_edits = Some(false);
13689 });
13690 });
13691 });
13692
13693 let mut cx = EditorLspTestContext::new_rust(
13694 lsp::ServerCapabilities {
13695 signature_help_provider: Some(lsp::SignatureHelpOptions {
13696 ..Default::default()
13697 }),
13698 ..Default::default()
13699 },
13700 cx,
13701 )
13702 .await;
13703
13704 let language = Language::new(
13705 LanguageConfig {
13706 name: "Rust".into(),
13707 brackets: BracketPairConfig {
13708 pairs: vec![
13709 BracketPair {
13710 start: "{".to_string(),
13711 end: "}".to_string(),
13712 close: true,
13713 surround: true,
13714 newline: true,
13715 },
13716 BracketPair {
13717 start: "(".to_string(),
13718 end: ")".to_string(),
13719 close: true,
13720 surround: true,
13721 newline: true,
13722 },
13723 BracketPair {
13724 start: "/*".to_string(),
13725 end: " */".to_string(),
13726 close: true,
13727 surround: true,
13728 newline: true,
13729 },
13730 BracketPair {
13731 start: "[".to_string(),
13732 end: "]".to_string(),
13733 close: false,
13734 surround: false,
13735 newline: true,
13736 },
13737 BracketPair {
13738 start: "\"".to_string(),
13739 end: "\"".to_string(),
13740 close: true,
13741 surround: true,
13742 newline: false,
13743 },
13744 BracketPair {
13745 start: "<".to_string(),
13746 end: ">".to_string(),
13747 close: false,
13748 surround: true,
13749 newline: true,
13750 },
13751 ],
13752 ..Default::default()
13753 },
13754 autoclose_before: "})]".to_string(),
13755 ..Default::default()
13756 },
13757 Some(tree_sitter_rust::LANGUAGE.into()),
13758 );
13759 let language = Arc::new(language);
13760
13761 cx.language_registry().add(language.clone());
13762 cx.update_buffer(|buffer, cx| {
13763 buffer.set_language(Some(language), cx);
13764 });
13765
13766 // Ensure that signature_help is not called when no signature help is enabled.
13767 cx.set_state(
13768 &r#"
13769 fn main() {
13770 sampleˇ
13771 }
13772 "#
13773 .unindent(),
13774 );
13775 cx.update_editor(|editor, window, cx| {
13776 editor.handle_input("(", window, cx);
13777 });
13778 cx.assert_editor_state(
13779 &"
13780 fn main() {
13781 sample(ˇ)
13782 }
13783 "
13784 .unindent(),
13785 );
13786 cx.editor(|editor, _, _| {
13787 assert!(editor.signature_help_state.task().is_none());
13788 });
13789
13790 let mocked_response = lsp::SignatureHelp {
13791 signatures: vec![lsp::SignatureInformation {
13792 label: "fn sample(param1: u8, param2: u8)".to_string(),
13793 documentation: None,
13794 parameters: Some(vec![
13795 lsp::ParameterInformation {
13796 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13797 documentation: None,
13798 },
13799 lsp::ParameterInformation {
13800 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13801 documentation: None,
13802 },
13803 ]),
13804 active_parameter: None,
13805 }],
13806 active_signature: Some(0),
13807 active_parameter: Some(0),
13808 };
13809
13810 // Ensure that signature_help is called when enabled afte edits
13811 cx.update(|_, cx| {
13812 cx.update_global::<SettingsStore, _>(|settings, cx| {
13813 settings.update_user_settings(cx, |settings| {
13814 settings.editor.auto_signature_help = Some(false);
13815 settings.editor.show_signature_help_after_edits = Some(true);
13816 });
13817 });
13818 });
13819 cx.set_state(
13820 &r#"
13821 fn main() {
13822 sampleˇ
13823 }
13824 "#
13825 .unindent(),
13826 );
13827 cx.update_editor(|editor, window, cx| {
13828 editor.handle_input("(", window, cx);
13829 });
13830 cx.assert_editor_state(
13831 &"
13832 fn main() {
13833 sample(ˇ)
13834 }
13835 "
13836 .unindent(),
13837 );
13838 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13839 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13840 .await;
13841 cx.update_editor(|editor, _, _| {
13842 let signature_help_state = editor.signature_help_state.popover().cloned();
13843 assert!(signature_help_state.is_some());
13844 let signature = signature_help_state.unwrap();
13845 assert_eq!(
13846 signature.signatures[signature.current_signature].label,
13847 "fn sample(param1: u8, param2: u8)"
13848 );
13849 editor.signature_help_state = SignatureHelpState::default();
13850 });
13851
13852 // Ensure that signature_help is called when auto signature help override is enabled
13853 cx.update(|_, cx| {
13854 cx.update_global::<SettingsStore, _>(|settings, cx| {
13855 settings.update_user_settings(cx, |settings| {
13856 settings.editor.auto_signature_help = Some(true);
13857 settings.editor.show_signature_help_after_edits = Some(false);
13858 });
13859 });
13860 });
13861 cx.set_state(
13862 &r#"
13863 fn main() {
13864 sampleˇ
13865 }
13866 "#
13867 .unindent(),
13868 );
13869 cx.update_editor(|editor, window, cx| {
13870 editor.handle_input("(", window, cx);
13871 });
13872 cx.assert_editor_state(
13873 &"
13874 fn main() {
13875 sample(ˇ)
13876 }
13877 "
13878 .unindent(),
13879 );
13880 handle_signature_help_request(&mut cx, mocked_response).await;
13881 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13882 .await;
13883 cx.editor(|editor, _, _| {
13884 let signature_help_state = editor.signature_help_state.popover().cloned();
13885 assert!(signature_help_state.is_some());
13886 let signature = signature_help_state.unwrap();
13887 assert_eq!(
13888 signature.signatures[signature.current_signature].label,
13889 "fn sample(param1: u8, param2: u8)"
13890 );
13891 });
13892}
13893
13894#[gpui::test]
13895async fn test_signature_help(cx: &mut TestAppContext) {
13896 init_test(cx, |_| {});
13897 cx.update(|cx| {
13898 cx.update_global::<SettingsStore, _>(|settings, cx| {
13899 settings.update_user_settings(cx, |settings| {
13900 settings.editor.auto_signature_help = Some(true);
13901 });
13902 });
13903 });
13904
13905 let mut cx = EditorLspTestContext::new_rust(
13906 lsp::ServerCapabilities {
13907 signature_help_provider: Some(lsp::SignatureHelpOptions {
13908 ..Default::default()
13909 }),
13910 ..Default::default()
13911 },
13912 cx,
13913 )
13914 .await;
13915
13916 // A test that directly calls `show_signature_help`
13917 cx.update_editor(|editor, window, cx| {
13918 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13919 });
13920
13921 let mocked_response = lsp::SignatureHelp {
13922 signatures: vec![lsp::SignatureInformation {
13923 label: "fn sample(param1: u8, param2: u8)".to_string(),
13924 documentation: None,
13925 parameters: Some(vec![
13926 lsp::ParameterInformation {
13927 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13928 documentation: None,
13929 },
13930 lsp::ParameterInformation {
13931 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13932 documentation: None,
13933 },
13934 ]),
13935 active_parameter: None,
13936 }],
13937 active_signature: Some(0),
13938 active_parameter: Some(0),
13939 };
13940 handle_signature_help_request(&mut cx, mocked_response).await;
13941
13942 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13943 .await;
13944
13945 cx.editor(|editor, _, _| {
13946 let signature_help_state = editor.signature_help_state.popover().cloned();
13947 assert!(signature_help_state.is_some());
13948 let signature = signature_help_state.unwrap();
13949 assert_eq!(
13950 signature.signatures[signature.current_signature].label,
13951 "fn sample(param1: u8, param2: u8)"
13952 );
13953 });
13954
13955 // When exiting outside from inside the brackets, `signature_help` is closed.
13956 cx.set_state(indoc! {"
13957 fn main() {
13958 sample(ˇ);
13959 }
13960
13961 fn sample(param1: u8, param2: u8) {}
13962 "});
13963
13964 cx.update_editor(|editor, window, cx| {
13965 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13966 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13967 });
13968 });
13969
13970 let mocked_response = lsp::SignatureHelp {
13971 signatures: Vec::new(),
13972 active_signature: None,
13973 active_parameter: None,
13974 };
13975 handle_signature_help_request(&mut cx, mocked_response).await;
13976
13977 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13978 .await;
13979
13980 cx.editor(|editor, _, _| {
13981 assert!(!editor.signature_help_state.is_shown());
13982 });
13983
13984 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13985 cx.set_state(indoc! {"
13986 fn main() {
13987 sample(ˇ);
13988 }
13989
13990 fn sample(param1: u8, param2: u8) {}
13991 "});
13992
13993 let mocked_response = lsp::SignatureHelp {
13994 signatures: vec![lsp::SignatureInformation {
13995 label: "fn sample(param1: u8, param2: u8)".to_string(),
13996 documentation: None,
13997 parameters: Some(vec![
13998 lsp::ParameterInformation {
13999 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14000 documentation: None,
14001 },
14002 lsp::ParameterInformation {
14003 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14004 documentation: None,
14005 },
14006 ]),
14007 active_parameter: None,
14008 }],
14009 active_signature: Some(0),
14010 active_parameter: Some(0),
14011 };
14012 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14013 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14014 .await;
14015 cx.editor(|editor, _, _| {
14016 assert!(editor.signature_help_state.is_shown());
14017 });
14018
14019 // Restore the popover with more parameter input
14020 cx.set_state(indoc! {"
14021 fn main() {
14022 sample(param1, param2ˇ);
14023 }
14024
14025 fn sample(param1: u8, param2: u8) {}
14026 "});
14027
14028 let mocked_response = lsp::SignatureHelp {
14029 signatures: vec![lsp::SignatureInformation {
14030 label: "fn sample(param1: u8, param2: u8)".to_string(),
14031 documentation: None,
14032 parameters: Some(vec![
14033 lsp::ParameterInformation {
14034 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14035 documentation: None,
14036 },
14037 lsp::ParameterInformation {
14038 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14039 documentation: None,
14040 },
14041 ]),
14042 active_parameter: None,
14043 }],
14044 active_signature: Some(0),
14045 active_parameter: Some(1),
14046 };
14047 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14048 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14049 .await;
14050
14051 // When selecting a range, the popover is gone.
14052 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
14053 cx.update_editor(|editor, window, cx| {
14054 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14055 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
14056 })
14057 });
14058 cx.assert_editor_state(indoc! {"
14059 fn main() {
14060 sample(param1, «ˇparam2»);
14061 }
14062
14063 fn sample(param1: u8, param2: u8) {}
14064 "});
14065 cx.editor(|editor, _, _| {
14066 assert!(!editor.signature_help_state.is_shown());
14067 });
14068
14069 // When unselecting again, the popover is back if within the brackets.
14070 cx.update_editor(|editor, window, cx| {
14071 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14072 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14073 })
14074 });
14075 cx.assert_editor_state(indoc! {"
14076 fn main() {
14077 sample(param1, ˇparam2);
14078 }
14079
14080 fn sample(param1: u8, param2: u8) {}
14081 "});
14082 handle_signature_help_request(&mut cx, mocked_response).await;
14083 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14084 .await;
14085 cx.editor(|editor, _, _| {
14086 assert!(editor.signature_help_state.is_shown());
14087 });
14088
14089 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
14090 cx.update_editor(|editor, window, cx| {
14091 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14092 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
14093 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14094 })
14095 });
14096 cx.assert_editor_state(indoc! {"
14097 fn main() {
14098 sample(param1, ˇparam2);
14099 }
14100
14101 fn sample(param1: u8, param2: u8) {}
14102 "});
14103
14104 let mocked_response = lsp::SignatureHelp {
14105 signatures: vec![lsp::SignatureInformation {
14106 label: "fn sample(param1: u8, param2: u8)".to_string(),
14107 documentation: None,
14108 parameters: Some(vec![
14109 lsp::ParameterInformation {
14110 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14111 documentation: None,
14112 },
14113 lsp::ParameterInformation {
14114 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14115 documentation: None,
14116 },
14117 ]),
14118 active_parameter: None,
14119 }],
14120 active_signature: Some(0),
14121 active_parameter: Some(1),
14122 };
14123 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14124 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14125 .await;
14126 cx.update_editor(|editor, _, cx| {
14127 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
14128 });
14129 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
14130 .await;
14131 cx.update_editor(|editor, window, cx| {
14132 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14133 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
14134 })
14135 });
14136 cx.assert_editor_state(indoc! {"
14137 fn main() {
14138 sample(param1, «ˇparam2»);
14139 }
14140
14141 fn sample(param1: u8, param2: u8) {}
14142 "});
14143 cx.update_editor(|editor, window, cx| {
14144 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14145 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14146 })
14147 });
14148 cx.assert_editor_state(indoc! {"
14149 fn main() {
14150 sample(param1, ˇparam2);
14151 }
14152
14153 fn sample(param1: u8, param2: u8) {}
14154 "});
14155 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
14156 .await;
14157}
14158
14159#[gpui::test]
14160async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
14161 init_test(cx, |_| {});
14162
14163 let mut cx = EditorLspTestContext::new_rust(
14164 lsp::ServerCapabilities {
14165 signature_help_provider: Some(lsp::SignatureHelpOptions {
14166 ..Default::default()
14167 }),
14168 ..Default::default()
14169 },
14170 cx,
14171 )
14172 .await;
14173
14174 cx.set_state(indoc! {"
14175 fn main() {
14176 overloadedˇ
14177 }
14178 "});
14179
14180 cx.update_editor(|editor, window, cx| {
14181 editor.handle_input("(", window, cx);
14182 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14183 });
14184
14185 // Mock response with 3 signatures
14186 let mocked_response = lsp::SignatureHelp {
14187 signatures: vec![
14188 lsp::SignatureInformation {
14189 label: "fn overloaded(x: i32)".to_string(),
14190 documentation: None,
14191 parameters: Some(vec![lsp::ParameterInformation {
14192 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14193 documentation: None,
14194 }]),
14195 active_parameter: None,
14196 },
14197 lsp::SignatureInformation {
14198 label: "fn overloaded(x: i32, y: i32)".to_string(),
14199 documentation: None,
14200 parameters: Some(vec![
14201 lsp::ParameterInformation {
14202 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14203 documentation: None,
14204 },
14205 lsp::ParameterInformation {
14206 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14207 documentation: None,
14208 },
14209 ]),
14210 active_parameter: None,
14211 },
14212 lsp::SignatureInformation {
14213 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
14214 documentation: None,
14215 parameters: Some(vec![
14216 lsp::ParameterInformation {
14217 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14218 documentation: None,
14219 },
14220 lsp::ParameterInformation {
14221 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14222 documentation: None,
14223 },
14224 lsp::ParameterInformation {
14225 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
14226 documentation: None,
14227 },
14228 ]),
14229 active_parameter: None,
14230 },
14231 ],
14232 active_signature: Some(1),
14233 active_parameter: Some(0),
14234 };
14235 handle_signature_help_request(&mut cx, mocked_response).await;
14236
14237 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14238 .await;
14239
14240 // Verify we have multiple signatures and the right one is selected
14241 cx.editor(|editor, _, _| {
14242 let popover = editor.signature_help_state.popover().cloned().unwrap();
14243 assert_eq!(popover.signatures.len(), 3);
14244 // active_signature was 1, so that should be the current
14245 assert_eq!(popover.current_signature, 1);
14246 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
14247 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
14248 assert_eq!(
14249 popover.signatures[2].label,
14250 "fn overloaded(x: i32, y: i32, z: i32)"
14251 );
14252 });
14253
14254 // Test navigation functionality
14255 cx.update_editor(|editor, window, cx| {
14256 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14257 });
14258
14259 cx.editor(|editor, _, _| {
14260 let popover = editor.signature_help_state.popover().cloned().unwrap();
14261 assert_eq!(popover.current_signature, 2);
14262 });
14263
14264 // Test wrap around
14265 cx.update_editor(|editor, window, cx| {
14266 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14267 });
14268
14269 cx.editor(|editor, _, _| {
14270 let popover = editor.signature_help_state.popover().cloned().unwrap();
14271 assert_eq!(popover.current_signature, 0);
14272 });
14273
14274 // Test previous navigation
14275 cx.update_editor(|editor, window, cx| {
14276 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
14277 });
14278
14279 cx.editor(|editor, _, _| {
14280 let popover = editor.signature_help_state.popover().cloned().unwrap();
14281 assert_eq!(popover.current_signature, 2);
14282 });
14283}
14284
14285#[gpui::test]
14286async fn test_completion_mode(cx: &mut TestAppContext) {
14287 init_test(cx, |_| {});
14288 let mut cx = EditorLspTestContext::new_rust(
14289 lsp::ServerCapabilities {
14290 completion_provider: Some(lsp::CompletionOptions {
14291 resolve_provider: Some(true),
14292 ..Default::default()
14293 }),
14294 ..Default::default()
14295 },
14296 cx,
14297 )
14298 .await;
14299
14300 struct Run {
14301 run_description: &'static str,
14302 initial_state: String,
14303 buffer_marked_text: String,
14304 completion_label: &'static str,
14305 completion_text: &'static str,
14306 expected_with_insert_mode: String,
14307 expected_with_replace_mode: String,
14308 expected_with_replace_subsequence_mode: String,
14309 expected_with_replace_suffix_mode: String,
14310 }
14311
14312 let runs = [
14313 Run {
14314 run_description: "Start of word matches completion text",
14315 initial_state: "before ediˇ after".into(),
14316 buffer_marked_text: "before <edi|> after".into(),
14317 completion_label: "editor",
14318 completion_text: "editor",
14319 expected_with_insert_mode: "before editorˇ after".into(),
14320 expected_with_replace_mode: "before editorˇ after".into(),
14321 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14322 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14323 },
14324 Run {
14325 run_description: "Accept same text at the middle of the word",
14326 initial_state: "before ediˇtor after".into(),
14327 buffer_marked_text: "before <edi|tor> after".into(),
14328 completion_label: "editor",
14329 completion_text: "editor",
14330 expected_with_insert_mode: "before editorˇtor after".into(),
14331 expected_with_replace_mode: "before editorˇ after".into(),
14332 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14333 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14334 },
14335 Run {
14336 run_description: "End of word matches completion text -- cursor at end",
14337 initial_state: "before torˇ after".into(),
14338 buffer_marked_text: "before <tor|> after".into(),
14339 completion_label: "editor",
14340 completion_text: "editor",
14341 expected_with_insert_mode: "before editorˇ after".into(),
14342 expected_with_replace_mode: "before editorˇ after".into(),
14343 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14344 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14345 },
14346 Run {
14347 run_description: "End of word matches completion text -- cursor at start",
14348 initial_state: "before ˇtor after".into(),
14349 buffer_marked_text: "before <|tor> after".into(),
14350 completion_label: "editor",
14351 completion_text: "editor",
14352 expected_with_insert_mode: "before editorˇtor after".into(),
14353 expected_with_replace_mode: "before editorˇ after".into(),
14354 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14355 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14356 },
14357 Run {
14358 run_description: "Prepend text containing whitespace",
14359 initial_state: "pˇfield: bool".into(),
14360 buffer_marked_text: "<p|field>: bool".into(),
14361 completion_label: "pub ",
14362 completion_text: "pub ",
14363 expected_with_insert_mode: "pub ˇfield: bool".into(),
14364 expected_with_replace_mode: "pub ˇ: bool".into(),
14365 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
14366 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
14367 },
14368 Run {
14369 run_description: "Add element to start of list",
14370 initial_state: "[element_ˇelement_2]".into(),
14371 buffer_marked_text: "[<element_|element_2>]".into(),
14372 completion_label: "element_1",
14373 completion_text: "element_1",
14374 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
14375 expected_with_replace_mode: "[element_1ˇ]".into(),
14376 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
14377 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
14378 },
14379 Run {
14380 run_description: "Add element to start of list -- first and second elements are equal",
14381 initial_state: "[elˇelement]".into(),
14382 buffer_marked_text: "[<el|element>]".into(),
14383 completion_label: "element",
14384 completion_text: "element",
14385 expected_with_insert_mode: "[elementˇelement]".into(),
14386 expected_with_replace_mode: "[elementˇ]".into(),
14387 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
14388 expected_with_replace_suffix_mode: "[elementˇ]".into(),
14389 },
14390 Run {
14391 run_description: "Ends with matching suffix",
14392 initial_state: "SubˇError".into(),
14393 buffer_marked_text: "<Sub|Error>".into(),
14394 completion_label: "SubscriptionError",
14395 completion_text: "SubscriptionError",
14396 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
14397 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14398 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14399 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
14400 },
14401 Run {
14402 run_description: "Suffix is a subsequence -- contiguous",
14403 initial_state: "SubˇErr".into(),
14404 buffer_marked_text: "<Sub|Err>".into(),
14405 completion_label: "SubscriptionError",
14406 completion_text: "SubscriptionError",
14407 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
14408 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14409 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14410 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
14411 },
14412 Run {
14413 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
14414 initial_state: "Suˇscrirr".into(),
14415 buffer_marked_text: "<Su|scrirr>".into(),
14416 completion_label: "SubscriptionError",
14417 completion_text: "SubscriptionError",
14418 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
14419 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14420 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14421 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
14422 },
14423 Run {
14424 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
14425 initial_state: "foo(indˇix)".into(),
14426 buffer_marked_text: "foo(<ind|ix>)".into(),
14427 completion_label: "node_index",
14428 completion_text: "node_index",
14429 expected_with_insert_mode: "foo(node_indexˇix)".into(),
14430 expected_with_replace_mode: "foo(node_indexˇ)".into(),
14431 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
14432 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
14433 },
14434 Run {
14435 run_description: "Replace range ends before cursor - should extend to cursor",
14436 initial_state: "before editˇo after".into(),
14437 buffer_marked_text: "before <{ed}>it|o after".into(),
14438 completion_label: "editor",
14439 completion_text: "editor",
14440 expected_with_insert_mode: "before editorˇo after".into(),
14441 expected_with_replace_mode: "before editorˇo after".into(),
14442 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
14443 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14444 },
14445 Run {
14446 run_description: "Uses label for suffix matching",
14447 initial_state: "before ediˇtor after".into(),
14448 buffer_marked_text: "before <edi|tor> after".into(),
14449 completion_label: "editor",
14450 completion_text: "editor()",
14451 expected_with_insert_mode: "before editor()ˇtor after".into(),
14452 expected_with_replace_mode: "before editor()ˇ after".into(),
14453 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14454 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14455 },
14456 Run {
14457 run_description: "Case insensitive subsequence and suffix matching",
14458 initial_state: "before EDiˇtoR after".into(),
14459 buffer_marked_text: "before <EDi|toR> after".into(),
14460 completion_label: "editor",
14461 completion_text: "editor",
14462 expected_with_insert_mode: "before editorˇtoR after".into(),
14463 expected_with_replace_mode: "before editorˇ after".into(),
14464 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14465 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14466 },
14467 ];
14468
14469 for run in runs {
14470 let run_variations = [
14471 (LspInsertMode::Insert, run.expected_with_insert_mode),
14472 (LspInsertMode::Replace, run.expected_with_replace_mode),
14473 (
14474 LspInsertMode::ReplaceSubsequence,
14475 run.expected_with_replace_subsequence_mode,
14476 ),
14477 (
14478 LspInsertMode::ReplaceSuffix,
14479 run.expected_with_replace_suffix_mode,
14480 ),
14481 ];
14482
14483 for (lsp_insert_mode, expected_text) in run_variations {
14484 eprintln!(
14485 "run = {:?}, mode = {lsp_insert_mode:.?}",
14486 run.run_description,
14487 );
14488
14489 update_test_language_settings(&mut cx, |settings| {
14490 settings.defaults.completions = Some(CompletionSettingsContent {
14491 lsp_insert_mode: Some(lsp_insert_mode),
14492 words: Some(WordsCompletionMode::Disabled),
14493 words_min_length: Some(0),
14494 ..Default::default()
14495 });
14496 });
14497
14498 cx.set_state(&run.initial_state);
14499
14500 // Set up resolve handler before showing completions, since resolve may be
14501 // triggered when menu becomes visible (for documentation), not just on confirm.
14502 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(
14503 move |_, _, _| async move {
14504 Ok(lsp::CompletionItem {
14505 additional_text_edits: None,
14506 ..Default::default()
14507 })
14508 },
14509 );
14510
14511 cx.update_editor(|editor, window, cx| {
14512 editor.show_completions(&ShowCompletions, window, cx);
14513 });
14514
14515 let counter = Arc::new(AtomicUsize::new(0));
14516 handle_completion_request_with_insert_and_replace(
14517 &mut cx,
14518 &run.buffer_marked_text,
14519 vec![(run.completion_label, run.completion_text)],
14520 counter.clone(),
14521 )
14522 .await;
14523 cx.condition(|editor, _| editor.context_menu_visible())
14524 .await;
14525 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14526
14527 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14528 editor
14529 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14530 .unwrap()
14531 });
14532 cx.assert_editor_state(&expected_text);
14533 apply_additional_edits.await.unwrap();
14534 }
14535 }
14536}
14537
14538#[gpui::test]
14539async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14540 init_test(cx, |_| {});
14541 let mut cx = EditorLspTestContext::new_rust(
14542 lsp::ServerCapabilities {
14543 completion_provider: Some(lsp::CompletionOptions {
14544 resolve_provider: Some(true),
14545 ..Default::default()
14546 }),
14547 ..Default::default()
14548 },
14549 cx,
14550 )
14551 .await;
14552
14553 let initial_state = "SubˇError";
14554 let buffer_marked_text = "<Sub|Error>";
14555 let completion_text = "SubscriptionError";
14556 let expected_with_insert_mode = "SubscriptionErrorˇError";
14557 let expected_with_replace_mode = "SubscriptionErrorˇ";
14558
14559 update_test_language_settings(&mut cx, |settings| {
14560 settings.defaults.completions = Some(CompletionSettingsContent {
14561 words: Some(WordsCompletionMode::Disabled),
14562 words_min_length: Some(0),
14563 // set the opposite here to ensure that the action is overriding the default behavior
14564 lsp_insert_mode: Some(LspInsertMode::Insert),
14565 ..Default::default()
14566 });
14567 });
14568
14569 cx.set_state(initial_state);
14570 cx.update_editor(|editor, window, cx| {
14571 editor.show_completions(&ShowCompletions, window, cx);
14572 });
14573
14574 let counter = Arc::new(AtomicUsize::new(0));
14575 handle_completion_request_with_insert_and_replace(
14576 &mut cx,
14577 buffer_marked_text,
14578 vec![(completion_text, completion_text)],
14579 counter.clone(),
14580 )
14581 .await;
14582 cx.condition(|editor, _| editor.context_menu_visible())
14583 .await;
14584 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14585
14586 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14587 editor
14588 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14589 .unwrap()
14590 });
14591 cx.assert_editor_state(expected_with_replace_mode);
14592 handle_resolve_completion_request(&mut cx, None).await;
14593 apply_additional_edits.await.unwrap();
14594
14595 update_test_language_settings(&mut cx, |settings| {
14596 settings.defaults.completions = Some(CompletionSettingsContent {
14597 words: Some(WordsCompletionMode::Disabled),
14598 words_min_length: Some(0),
14599 // set the opposite here to ensure that the action is overriding the default behavior
14600 lsp_insert_mode: Some(LspInsertMode::Replace),
14601 ..Default::default()
14602 });
14603 });
14604
14605 cx.set_state(initial_state);
14606 cx.update_editor(|editor, window, cx| {
14607 editor.show_completions(&ShowCompletions, window, cx);
14608 });
14609 handle_completion_request_with_insert_and_replace(
14610 &mut cx,
14611 buffer_marked_text,
14612 vec![(completion_text, completion_text)],
14613 counter.clone(),
14614 )
14615 .await;
14616 cx.condition(|editor, _| editor.context_menu_visible())
14617 .await;
14618 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14619
14620 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14621 editor
14622 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14623 .unwrap()
14624 });
14625 cx.assert_editor_state(expected_with_insert_mode);
14626 handle_resolve_completion_request(&mut cx, None).await;
14627 apply_additional_edits.await.unwrap();
14628}
14629
14630#[gpui::test]
14631async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14632 init_test(cx, |_| {});
14633 let mut cx = EditorLspTestContext::new_rust(
14634 lsp::ServerCapabilities {
14635 completion_provider: Some(lsp::CompletionOptions {
14636 resolve_provider: Some(true),
14637 ..Default::default()
14638 }),
14639 ..Default::default()
14640 },
14641 cx,
14642 )
14643 .await;
14644
14645 // scenario: surrounding text matches completion text
14646 let completion_text = "to_offset";
14647 let initial_state = indoc! {"
14648 1. buf.to_offˇsuffix
14649 2. buf.to_offˇsuf
14650 3. buf.to_offˇfix
14651 4. buf.to_offˇ
14652 5. into_offˇensive
14653 6. ˇsuffix
14654 7. let ˇ //
14655 8. aaˇzz
14656 9. buf.to_off«zzzzzˇ»suffix
14657 10. buf.«ˇzzzzz»suffix
14658 11. to_off«ˇzzzzz»
14659
14660 buf.to_offˇsuffix // newest cursor
14661 "};
14662 let completion_marked_buffer = indoc! {"
14663 1. buf.to_offsuffix
14664 2. buf.to_offsuf
14665 3. buf.to_offfix
14666 4. buf.to_off
14667 5. into_offensive
14668 6. suffix
14669 7. let //
14670 8. aazz
14671 9. buf.to_offzzzzzsuffix
14672 10. buf.zzzzzsuffix
14673 11. to_offzzzzz
14674
14675 buf.<to_off|suffix> // newest cursor
14676 "};
14677 let expected = indoc! {"
14678 1. buf.to_offsetˇ
14679 2. buf.to_offsetˇsuf
14680 3. buf.to_offsetˇfix
14681 4. buf.to_offsetˇ
14682 5. into_offsetˇensive
14683 6. to_offsetˇsuffix
14684 7. let to_offsetˇ //
14685 8. aato_offsetˇzz
14686 9. buf.to_offsetˇ
14687 10. buf.to_offsetˇsuffix
14688 11. to_offsetˇ
14689
14690 buf.to_offsetˇ // newest cursor
14691 "};
14692 cx.set_state(initial_state);
14693 cx.update_editor(|editor, window, cx| {
14694 editor.show_completions(&ShowCompletions, window, cx);
14695 });
14696 handle_completion_request_with_insert_and_replace(
14697 &mut cx,
14698 completion_marked_buffer,
14699 vec![(completion_text, completion_text)],
14700 Arc::new(AtomicUsize::new(0)),
14701 )
14702 .await;
14703 cx.condition(|editor, _| editor.context_menu_visible())
14704 .await;
14705 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14706 editor
14707 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14708 .unwrap()
14709 });
14710 cx.assert_editor_state(expected);
14711 handle_resolve_completion_request(&mut cx, None).await;
14712 apply_additional_edits.await.unwrap();
14713
14714 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14715 let completion_text = "foo_and_bar";
14716 let initial_state = indoc! {"
14717 1. ooanbˇ
14718 2. zooanbˇ
14719 3. ooanbˇz
14720 4. zooanbˇz
14721 5. ooanˇ
14722 6. oanbˇ
14723
14724 ooanbˇ
14725 "};
14726 let completion_marked_buffer = indoc! {"
14727 1. ooanb
14728 2. zooanb
14729 3. ooanbz
14730 4. zooanbz
14731 5. ooan
14732 6. oanb
14733
14734 <ooanb|>
14735 "};
14736 let expected = indoc! {"
14737 1. foo_and_barˇ
14738 2. zfoo_and_barˇ
14739 3. foo_and_barˇz
14740 4. zfoo_and_barˇz
14741 5. ooanfoo_and_barˇ
14742 6. oanbfoo_and_barˇ
14743
14744 foo_and_barˇ
14745 "};
14746 cx.set_state(initial_state);
14747 cx.update_editor(|editor, window, cx| {
14748 editor.show_completions(&ShowCompletions, window, cx);
14749 });
14750 handle_completion_request_with_insert_and_replace(
14751 &mut cx,
14752 completion_marked_buffer,
14753 vec![(completion_text, completion_text)],
14754 Arc::new(AtomicUsize::new(0)),
14755 )
14756 .await;
14757 cx.condition(|editor, _| editor.context_menu_visible())
14758 .await;
14759 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14760 editor
14761 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14762 .unwrap()
14763 });
14764 cx.assert_editor_state(expected);
14765 handle_resolve_completion_request(&mut cx, None).await;
14766 apply_additional_edits.await.unwrap();
14767
14768 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14769 // (expects the same as if it was inserted at the end)
14770 let completion_text = "foo_and_bar";
14771 let initial_state = indoc! {"
14772 1. ooˇanb
14773 2. zooˇanb
14774 3. ooˇanbz
14775 4. zooˇanbz
14776
14777 ooˇanb
14778 "};
14779 let completion_marked_buffer = indoc! {"
14780 1. ooanb
14781 2. zooanb
14782 3. ooanbz
14783 4. zooanbz
14784
14785 <oo|anb>
14786 "};
14787 let expected = indoc! {"
14788 1. foo_and_barˇ
14789 2. zfoo_and_barˇ
14790 3. foo_and_barˇz
14791 4. zfoo_and_barˇz
14792
14793 foo_and_barˇ
14794 "};
14795 cx.set_state(initial_state);
14796 cx.update_editor(|editor, window, cx| {
14797 editor.show_completions(&ShowCompletions, window, cx);
14798 });
14799 handle_completion_request_with_insert_and_replace(
14800 &mut cx,
14801 completion_marked_buffer,
14802 vec![(completion_text, completion_text)],
14803 Arc::new(AtomicUsize::new(0)),
14804 )
14805 .await;
14806 cx.condition(|editor, _| editor.context_menu_visible())
14807 .await;
14808 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14809 editor
14810 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14811 .unwrap()
14812 });
14813 cx.assert_editor_state(expected);
14814 handle_resolve_completion_request(&mut cx, None).await;
14815 apply_additional_edits.await.unwrap();
14816}
14817
14818// This used to crash
14819#[gpui::test]
14820async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14821 init_test(cx, |_| {});
14822
14823 let buffer_text = indoc! {"
14824 fn main() {
14825 10.satu;
14826
14827 //
14828 // separate cursors so they open in different excerpts (manually reproducible)
14829 //
14830
14831 10.satu20;
14832 }
14833 "};
14834 let multibuffer_text_with_selections = indoc! {"
14835 fn main() {
14836 10.satuˇ;
14837
14838 //
14839
14840 //
14841
14842 10.satuˇ20;
14843 }
14844 "};
14845 let expected_multibuffer = indoc! {"
14846 fn main() {
14847 10.saturating_sub()ˇ;
14848
14849 //
14850
14851 //
14852
14853 10.saturating_sub()ˇ;
14854 }
14855 "};
14856
14857 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14858 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14859
14860 let fs = FakeFs::new(cx.executor());
14861 fs.insert_tree(
14862 path!("/a"),
14863 json!({
14864 "main.rs": buffer_text,
14865 }),
14866 )
14867 .await;
14868
14869 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14870 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14871 language_registry.add(rust_lang());
14872 let mut fake_servers = language_registry.register_fake_lsp(
14873 "Rust",
14874 FakeLspAdapter {
14875 capabilities: lsp::ServerCapabilities {
14876 completion_provider: Some(lsp::CompletionOptions {
14877 resolve_provider: None,
14878 ..lsp::CompletionOptions::default()
14879 }),
14880 ..lsp::ServerCapabilities::default()
14881 },
14882 ..FakeLspAdapter::default()
14883 },
14884 );
14885 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14886 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14887 let buffer = project
14888 .update(cx, |project, cx| {
14889 project.open_local_buffer(path!("/a/main.rs"), cx)
14890 })
14891 .await
14892 .unwrap();
14893
14894 let multi_buffer = cx.new(|cx| {
14895 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14896 multi_buffer.push_excerpts(
14897 buffer.clone(),
14898 [ExcerptRange::new(0..first_excerpt_end)],
14899 cx,
14900 );
14901 multi_buffer.push_excerpts(
14902 buffer.clone(),
14903 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14904 cx,
14905 );
14906 multi_buffer
14907 });
14908
14909 let editor = workspace
14910 .update(cx, |_, window, cx| {
14911 cx.new(|cx| {
14912 Editor::new(
14913 EditorMode::Full {
14914 scale_ui_elements_with_buffer_font_size: false,
14915 show_active_line_background: false,
14916 sizing_behavior: SizingBehavior::Default,
14917 },
14918 multi_buffer.clone(),
14919 Some(project.clone()),
14920 window,
14921 cx,
14922 )
14923 })
14924 })
14925 .unwrap();
14926
14927 let pane = workspace
14928 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14929 .unwrap();
14930 pane.update_in(cx, |pane, window, cx| {
14931 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14932 });
14933
14934 let fake_server = fake_servers.next().await.unwrap();
14935 cx.run_until_parked();
14936
14937 editor.update_in(cx, |editor, window, cx| {
14938 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14939 s.select_ranges([
14940 Point::new(1, 11)..Point::new(1, 11),
14941 Point::new(7, 11)..Point::new(7, 11),
14942 ])
14943 });
14944
14945 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14946 });
14947
14948 editor.update_in(cx, |editor, window, cx| {
14949 editor.show_completions(&ShowCompletions, window, cx);
14950 });
14951
14952 fake_server
14953 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14954 let completion_item = lsp::CompletionItem {
14955 label: "saturating_sub()".into(),
14956 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14957 lsp::InsertReplaceEdit {
14958 new_text: "saturating_sub()".to_owned(),
14959 insert: lsp::Range::new(
14960 lsp::Position::new(7, 7),
14961 lsp::Position::new(7, 11),
14962 ),
14963 replace: lsp::Range::new(
14964 lsp::Position::new(7, 7),
14965 lsp::Position::new(7, 13),
14966 ),
14967 },
14968 )),
14969 ..lsp::CompletionItem::default()
14970 };
14971
14972 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14973 })
14974 .next()
14975 .await
14976 .unwrap();
14977
14978 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14979 .await;
14980
14981 editor
14982 .update_in(cx, |editor, window, cx| {
14983 editor
14984 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14985 .unwrap()
14986 })
14987 .await
14988 .unwrap();
14989
14990 editor.update(cx, |editor, cx| {
14991 assert_text_with_selections(editor, expected_multibuffer, cx);
14992 })
14993}
14994
14995#[gpui::test]
14996async fn test_completion(cx: &mut TestAppContext) {
14997 init_test(cx, |_| {});
14998
14999 let mut cx = EditorLspTestContext::new_rust(
15000 lsp::ServerCapabilities {
15001 completion_provider: Some(lsp::CompletionOptions {
15002 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15003 resolve_provider: Some(true),
15004 ..Default::default()
15005 }),
15006 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15007 ..Default::default()
15008 },
15009 cx,
15010 )
15011 .await;
15012 let counter = Arc::new(AtomicUsize::new(0));
15013
15014 cx.set_state(indoc! {"
15015 oneˇ
15016 two
15017 three
15018 "});
15019 cx.simulate_keystroke(".");
15020 handle_completion_request(
15021 indoc! {"
15022 one.|<>
15023 two
15024 three
15025 "},
15026 vec!["first_completion", "second_completion"],
15027 true,
15028 counter.clone(),
15029 &mut cx,
15030 )
15031 .await;
15032 cx.condition(|editor, _| editor.context_menu_visible())
15033 .await;
15034 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15035
15036 let _handler = handle_signature_help_request(
15037 &mut cx,
15038 lsp::SignatureHelp {
15039 signatures: vec![lsp::SignatureInformation {
15040 label: "test signature".to_string(),
15041 documentation: None,
15042 parameters: Some(vec![lsp::ParameterInformation {
15043 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
15044 documentation: None,
15045 }]),
15046 active_parameter: None,
15047 }],
15048 active_signature: None,
15049 active_parameter: None,
15050 },
15051 );
15052 cx.update_editor(|editor, window, cx| {
15053 assert!(
15054 !editor.signature_help_state.is_shown(),
15055 "No signature help was called for"
15056 );
15057 editor.show_signature_help(&ShowSignatureHelp, window, cx);
15058 });
15059 cx.run_until_parked();
15060 cx.update_editor(|editor, _, _| {
15061 assert!(
15062 !editor.signature_help_state.is_shown(),
15063 "No signature help should be shown when completions menu is open"
15064 );
15065 });
15066
15067 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15068 editor.context_menu_next(&Default::default(), window, cx);
15069 editor
15070 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15071 .unwrap()
15072 });
15073 cx.assert_editor_state(indoc! {"
15074 one.second_completionˇ
15075 two
15076 three
15077 "});
15078
15079 handle_resolve_completion_request(
15080 &mut cx,
15081 Some(vec![
15082 (
15083 //This overlaps with the primary completion edit which is
15084 //misbehavior from the LSP spec, test that we filter it out
15085 indoc! {"
15086 one.second_ˇcompletion
15087 two
15088 threeˇ
15089 "},
15090 "overlapping additional edit",
15091 ),
15092 (
15093 indoc! {"
15094 one.second_completion
15095 two
15096 threeˇ
15097 "},
15098 "\nadditional edit",
15099 ),
15100 ]),
15101 )
15102 .await;
15103 apply_additional_edits.await.unwrap();
15104 cx.assert_editor_state(indoc! {"
15105 one.second_completionˇ
15106 two
15107 three
15108 additional edit
15109 "});
15110
15111 cx.set_state(indoc! {"
15112 one.second_completion
15113 twoˇ
15114 threeˇ
15115 additional edit
15116 "});
15117 cx.simulate_keystroke(" ");
15118 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15119 cx.simulate_keystroke("s");
15120 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15121
15122 cx.assert_editor_state(indoc! {"
15123 one.second_completion
15124 two sˇ
15125 three sˇ
15126 additional edit
15127 "});
15128 handle_completion_request(
15129 indoc! {"
15130 one.second_completion
15131 two s
15132 three <s|>
15133 additional edit
15134 "},
15135 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
15136 true,
15137 counter.clone(),
15138 &mut cx,
15139 )
15140 .await;
15141 cx.condition(|editor, _| editor.context_menu_visible())
15142 .await;
15143 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15144
15145 cx.simulate_keystroke("i");
15146
15147 handle_completion_request(
15148 indoc! {"
15149 one.second_completion
15150 two si
15151 three <si|>
15152 additional edit
15153 "},
15154 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
15155 true,
15156 counter.clone(),
15157 &mut cx,
15158 )
15159 .await;
15160 cx.condition(|editor, _| editor.context_menu_visible())
15161 .await;
15162 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15163
15164 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15165 editor
15166 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15167 .unwrap()
15168 });
15169 cx.assert_editor_state(indoc! {"
15170 one.second_completion
15171 two sixth_completionˇ
15172 three sixth_completionˇ
15173 additional edit
15174 "});
15175
15176 apply_additional_edits.await.unwrap();
15177
15178 update_test_language_settings(&mut cx, |settings| {
15179 settings.defaults.show_completions_on_input = Some(false);
15180 });
15181 cx.set_state("editorˇ");
15182 cx.simulate_keystroke(".");
15183 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15184 cx.simulate_keystrokes("c l o");
15185 cx.assert_editor_state("editor.cloˇ");
15186 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15187 cx.update_editor(|editor, window, cx| {
15188 editor.show_completions(&ShowCompletions, window, cx);
15189 });
15190 handle_completion_request(
15191 "editor.<clo|>",
15192 vec!["close", "clobber"],
15193 true,
15194 counter.clone(),
15195 &mut cx,
15196 )
15197 .await;
15198 cx.condition(|editor, _| editor.context_menu_visible())
15199 .await;
15200 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
15201
15202 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15203 editor
15204 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15205 .unwrap()
15206 });
15207 cx.assert_editor_state("editor.clobberˇ");
15208 handle_resolve_completion_request(&mut cx, None).await;
15209 apply_additional_edits.await.unwrap();
15210}
15211
15212#[gpui::test]
15213async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
15214 init_test(cx, |_| {});
15215
15216 let fs = FakeFs::new(cx.executor());
15217 fs.insert_tree(
15218 path!("/a"),
15219 json!({
15220 "main.rs": "",
15221 }),
15222 )
15223 .await;
15224
15225 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15226 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15227 language_registry.add(rust_lang());
15228 let command_calls = Arc::new(AtomicUsize::new(0));
15229 let registered_command = "_the/command";
15230
15231 let closure_command_calls = command_calls.clone();
15232 let mut fake_servers = language_registry.register_fake_lsp(
15233 "Rust",
15234 FakeLspAdapter {
15235 capabilities: lsp::ServerCapabilities {
15236 completion_provider: Some(lsp::CompletionOptions {
15237 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15238 ..lsp::CompletionOptions::default()
15239 }),
15240 execute_command_provider: Some(lsp::ExecuteCommandOptions {
15241 commands: vec![registered_command.to_owned()],
15242 ..lsp::ExecuteCommandOptions::default()
15243 }),
15244 ..lsp::ServerCapabilities::default()
15245 },
15246 initializer: Some(Box::new(move |fake_server| {
15247 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15248 move |params, _| async move {
15249 Ok(Some(lsp::CompletionResponse::Array(vec![
15250 lsp::CompletionItem {
15251 label: "registered_command".to_owned(),
15252 text_edit: gen_text_edit(¶ms, ""),
15253 command: Some(lsp::Command {
15254 title: registered_command.to_owned(),
15255 command: "_the/command".to_owned(),
15256 arguments: Some(vec![serde_json::Value::Bool(true)]),
15257 }),
15258 ..lsp::CompletionItem::default()
15259 },
15260 lsp::CompletionItem {
15261 label: "unregistered_command".to_owned(),
15262 text_edit: gen_text_edit(¶ms, ""),
15263 command: Some(lsp::Command {
15264 title: "????????????".to_owned(),
15265 command: "????????????".to_owned(),
15266 arguments: Some(vec![serde_json::Value::Null]),
15267 }),
15268 ..lsp::CompletionItem::default()
15269 },
15270 ])))
15271 },
15272 );
15273 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
15274 let command_calls = closure_command_calls.clone();
15275 move |params, _| {
15276 assert_eq!(params.command, registered_command);
15277 let command_calls = command_calls.clone();
15278 async move {
15279 command_calls.fetch_add(1, atomic::Ordering::Release);
15280 Ok(Some(json!(null)))
15281 }
15282 }
15283 });
15284 })),
15285 ..FakeLspAdapter::default()
15286 },
15287 );
15288 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15289 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15290 let editor = workspace
15291 .update(cx, |workspace, window, cx| {
15292 workspace.open_abs_path(
15293 PathBuf::from(path!("/a/main.rs")),
15294 OpenOptions::default(),
15295 window,
15296 cx,
15297 )
15298 })
15299 .unwrap()
15300 .await
15301 .unwrap()
15302 .downcast::<Editor>()
15303 .unwrap();
15304 let _fake_server = fake_servers.next().await.unwrap();
15305 cx.run_until_parked();
15306
15307 editor.update_in(cx, |editor, window, cx| {
15308 cx.focus_self(window);
15309 editor.move_to_end(&MoveToEnd, window, cx);
15310 editor.handle_input(".", window, cx);
15311 });
15312 cx.run_until_parked();
15313 editor.update(cx, |editor, _| {
15314 assert!(editor.context_menu_visible());
15315 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15316 {
15317 let completion_labels = menu
15318 .completions
15319 .borrow()
15320 .iter()
15321 .map(|c| c.label.text.clone())
15322 .collect::<Vec<_>>();
15323 assert_eq!(
15324 completion_labels,
15325 &["registered_command", "unregistered_command",],
15326 );
15327 } else {
15328 panic!("expected completion menu to be open");
15329 }
15330 });
15331
15332 editor
15333 .update_in(cx, |editor, window, cx| {
15334 editor
15335 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15336 .unwrap()
15337 })
15338 .await
15339 .unwrap();
15340 cx.run_until_parked();
15341 assert_eq!(
15342 command_calls.load(atomic::Ordering::Acquire),
15343 1,
15344 "For completion with a registered command, Zed should send a command execution request",
15345 );
15346
15347 editor.update_in(cx, |editor, window, cx| {
15348 cx.focus_self(window);
15349 editor.handle_input(".", window, cx);
15350 });
15351 cx.run_until_parked();
15352 editor.update(cx, |editor, _| {
15353 assert!(editor.context_menu_visible());
15354 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15355 {
15356 let completion_labels = menu
15357 .completions
15358 .borrow()
15359 .iter()
15360 .map(|c| c.label.text.clone())
15361 .collect::<Vec<_>>();
15362 assert_eq!(
15363 completion_labels,
15364 &["registered_command", "unregistered_command",],
15365 );
15366 } else {
15367 panic!("expected completion menu to be open");
15368 }
15369 });
15370 editor
15371 .update_in(cx, |editor, window, cx| {
15372 editor.context_menu_next(&Default::default(), window, cx);
15373 editor
15374 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15375 .unwrap()
15376 })
15377 .await
15378 .unwrap();
15379 cx.run_until_parked();
15380 assert_eq!(
15381 command_calls.load(atomic::Ordering::Acquire),
15382 1,
15383 "For completion with an unregistered command, Zed should not send a command execution request",
15384 );
15385}
15386
15387#[gpui::test]
15388async fn test_completion_reuse(cx: &mut TestAppContext) {
15389 init_test(cx, |_| {});
15390
15391 let mut cx = EditorLspTestContext::new_rust(
15392 lsp::ServerCapabilities {
15393 completion_provider: Some(lsp::CompletionOptions {
15394 trigger_characters: Some(vec![".".to_string()]),
15395 ..Default::default()
15396 }),
15397 ..Default::default()
15398 },
15399 cx,
15400 )
15401 .await;
15402
15403 let counter = Arc::new(AtomicUsize::new(0));
15404 cx.set_state("objˇ");
15405 cx.simulate_keystroke(".");
15406
15407 // Initial completion request returns complete results
15408 let is_incomplete = false;
15409 handle_completion_request(
15410 "obj.|<>",
15411 vec!["a", "ab", "abc"],
15412 is_incomplete,
15413 counter.clone(),
15414 &mut cx,
15415 )
15416 .await;
15417 cx.run_until_parked();
15418 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15419 cx.assert_editor_state("obj.ˇ");
15420 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15421
15422 // Type "a" - filters existing completions
15423 cx.simulate_keystroke("a");
15424 cx.run_until_parked();
15425 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15426 cx.assert_editor_state("obj.aˇ");
15427 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15428
15429 // Type "b" - filters existing completions
15430 cx.simulate_keystroke("b");
15431 cx.run_until_parked();
15432 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15433 cx.assert_editor_state("obj.abˇ");
15434 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15435
15436 // Type "c" - filters existing completions
15437 cx.simulate_keystroke("c");
15438 cx.run_until_parked();
15439 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15440 cx.assert_editor_state("obj.abcˇ");
15441 check_displayed_completions(vec!["abc"], &mut cx);
15442
15443 // Backspace to delete "c" - filters existing completions
15444 cx.update_editor(|editor, window, cx| {
15445 editor.backspace(&Backspace, window, cx);
15446 });
15447 cx.run_until_parked();
15448 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15449 cx.assert_editor_state("obj.abˇ");
15450 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15451
15452 // Moving cursor to the left dismisses menu.
15453 cx.update_editor(|editor, window, cx| {
15454 editor.move_left(&MoveLeft, window, cx);
15455 });
15456 cx.run_until_parked();
15457 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15458 cx.assert_editor_state("obj.aˇb");
15459 cx.update_editor(|editor, _, _| {
15460 assert_eq!(editor.context_menu_visible(), false);
15461 });
15462
15463 // Type "b" - new request
15464 cx.simulate_keystroke("b");
15465 let is_incomplete = false;
15466 handle_completion_request(
15467 "obj.<ab|>a",
15468 vec!["ab", "abc"],
15469 is_incomplete,
15470 counter.clone(),
15471 &mut cx,
15472 )
15473 .await;
15474 cx.run_until_parked();
15475 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15476 cx.assert_editor_state("obj.abˇb");
15477 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15478
15479 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
15480 cx.update_editor(|editor, window, cx| {
15481 editor.backspace(&Backspace, window, cx);
15482 });
15483 let is_incomplete = false;
15484 handle_completion_request(
15485 "obj.<a|>b",
15486 vec!["a", "ab", "abc"],
15487 is_incomplete,
15488 counter.clone(),
15489 &mut cx,
15490 )
15491 .await;
15492 cx.run_until_parked();
15493 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15494 cx.assert_editor_state("obj.aˇb");
15495 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15496
15497 // Backspace to delete "a" - dismisses menu.
15498 cx.update_editor(|editor, window, cx| {
15499 editor.backspace(&Backspace, window, cx);
15500 });
15501 cx.run_until_parked();
15502 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15503 cx.assert_editor_state("obj.ˇb");
15504 cx.update_editor(|editor, _, _| {
15505 assert_eq!(editor.context_menu_visible(), false);
15506 });
15507}
15508
15509#[gpui::test]
15510async fn test_word_completion(cx: &mut TestAppContext) {
15511 let lsp_fetch_timeout_ms = 10;
15512 init_test(cx, |language_settings| {
15513 language_settings.defaults.completions = Some(CompletionSettingsContent {
15514 words_min_length: Some(0),
15515 lsp_fetch_timeout_ms: Some(10),
15516 lsp_insert_mode: Some(LspInsertMode::Insert),
15517 ..Default::default()
15518 });
15519 });
15520
15521 let mut cx = EditorLspTestContext::new_rust(
15522 lsp::ServerCapabilities {
15523 completion_provider: Some(lsp::CompletionOptions {
15524 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15525 ..lsp::CompletionOptions::default()
15526 }),
15527 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15528 ..lsp::ServerCapabilities::default()
15529 },
15530 cx,
15531 )
15532 .await;
15533
15534 let throttle_completions = Arc::new(AtomicBool::new(false));
15535
15536 let lsp_throttle_completions = throttle_completions.clone();
15537 let _completion_requests_handler =
15538 cx.lsp
15539 .server
15540 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
15541 let lsp_throttle_completions = lsp_throttle_completions.clone();
15542 let cx = cx.clone();
15543 async move {
15544 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
15545 cx.background_executor()
15546 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
15547 .await;
15548 }
15549 Ok(Some(lsp::CompletionResponse::Array(vec![
15550 lsp::CompletionItem {
15551 label: "first".into(),
15552 ..lsp::CompletionItem::default()
15553 },
15554 lsp::CompletionItem {
15555 label: "last".into(),
15556 ..lsp::CompletionItem::default()
15557 },
15558 ])))
15559 }
15560 });
15561
15562 cx.set_state(indoc! {"
15563 oneˇ
15564 two
15565 three
15566 "});
15567 cx.simulate_keystroke(".");
15568 cx.executor().run_until_parked();
15569 cx.condition(|editor, _| editor.context_menu_visible())
15570 .await;
15571 cx.update_editor(|editor, window, cx| {
15572 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15573 {
15574 assert_eq!(
15575 completion_menu_entries(menu),
15576 &["first", "last"],
15577 "When LSP server is fast to reply, no fallback word completions are used"
15578 );
15579 } else {
15580 panic!("expected completion menu to be open");
15581 }
15582 editor.cancel(&Cancel, window, cx);
15583 });
15584 cx.executor().run_until_parked();
15585 cx.condition(|editor, _| !editor.context_menu_visible())
15586 .await;
15587
15588 throttle_completions.store(true, atomic::Ordering::Release);
15589 cx.simulate_keystroke(".");
15590 cx.executor()
15591 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
15592 cx.executor().run_until_parked();
15593 cx.condition(|editor, _| editor.context_menu_visible())
15594 .await;
15595 cx.update_editor(|editor, _, _| {
15596 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15597 {
15598 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
15599 "When LSP server is slow, document words can be shown instead, if configured accordingly");
15600 } else {
15601 panic!("expected completion menu to be open");
15602 }
15603 });
15604}
15605
15606#[gpui::test]
15607async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
15608 init_test(cx, |language_settings| {
15609 language_settings.defaults.completions = Some(CompletionSettingsContent {
15610 words: Some(WordsCompletionMode::Enabled),
15611 words_min_length: Some(0),
15612 lsp_insert_mode: Some(LspInsertMode::Insert),
15613 ..Default::default()
15614 });
15615 });
15616
15617 let mut cx = EditorLspTestContext::new_rust(
15618 lsp::ServerCapabilities {
15619 completion_provider: Some(lsp::CompletionOptions {
15620 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15621 ..lsp::CompletionOptions::default()
15622 }),
15623 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15624 ..lsp::ServerCapabilities::default()
15625 },
15626 cx,
15627 )
15628 .await;
15629
15630 let _completion_requests_handler =
15631 cx.lsp
15632 .server
15633 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15634 Ok(Some(lsp::CompletionResponse::Array(vec![
15635 lsp::CompletionItem {
15636 label: "first".into(),
15637 ..lsp::CompletionItem::default()
15638 },
15639 lsp::CompletionItem {
15640 label: "last".into(),
15641 ..lsp::CompletionItem::default()
15642 },
15643 ])))
15644 });
15645
15646 cx.set_state(indoc! {"ˇ
15647 first
15648 last
15649 second
15650 "});
15651 cx.simulate_keystroke(".");
15652 cx.executor().run_until_parked();
15653 cx.condition(|editor, _| editor.context_menu_visible())
15654 .await;
15655 cx.update_editor(|editor, _, _| {
15656 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15657 {
15658 assert_eq!(
15659 completion_menu_entries(menu),
15660 &["first", "last", "second"],
15661 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15662 );
15663 } else {
15664 panic!("expected completion menu to be open");
15665 }
15666 });
15667}
15668
15669#[gpui::test]
15670async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15671 init_test(cx, |language_settings| {
15672 language_settings.defaults.completions = Some(CompletionSettingsContent {
15673 words: Some(WordsCompletionMode::Disabled),
15674 words_min_length: Some(0),
15675 lsp_insert_mode: Some(LspInsertMode::Insert),
15676 ..Default::default()
15677 });
15678 });
15679
15680 let mut cx = EditorLspTestContext::new_rust(
15681 lsp::ServerCapabilities {
15682 completion_provider: Some(lsp::CompletionOptions {
15683 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15684 ..lsp::CompletionOptions::default()
15685 }),
15686 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15687 ..lsp::ServerCapabilities::default()
15688 },
15689 cx,
15690 )
15691 .await;
15692
15693 let _completion_requests_handler =
15694 cx.lsp
15695 .server
15696 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15697 panic!("LSP completions should not be queried when dealing with word completions")
15698 });
15699
15700 cx.set_state(indoc! {"ˇ
15701 first
15702 last
15703 second
15704 "});
15705 cx.update_editor(|editor, window, cx| {
15706 editor.show_word_completions(&ShowWordCompletions, window, cx);
15707 });
15708 cx.executor().run_until_parked();
15709 cx.condition(|editor, _| editor.context_menu_visible())
15710 .await;
15711 cx.update_editor(|editor, _, _| {
15712 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15713 {
15714 assert_eq!(
15715 completion_menu_entries(menu),
15716 &["first", "last", "second"],
15717 "`ShowWordCompletions` action should show word completions"
15718 );
15719 } else {
15720 panic!("expected completion menu to be open");
15721 }
15722 });
15723
15724 cx.simulate_keystroke("l");
15725 cx.executor().run_until_parked();
15726 cx.condition(|editor, _| editor.context_menu_visible())
15727 .await;
15728 cx.update_editor(|editor, _, _| {
15729 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15730 {
15731 assert_eq!(
15732 completion_menu_entries(menu),
15733 &["last"],
15734 "After showing word completions, further editing should filter them and not query the LSP"
15735 );
15736 } else {
15737 panic!("expected completion menu to be open");
15738 }
15739 });
15740}
15741
15742#[gpui::test]
15743async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15744 init_test(cx, |language_settings| {
15745 language_settings.defaults.completions = Some(CompletionSettingsContent {
15746 words_min_length: Some(0),
15747 lsp: Some(false),
15748 lsp_insert_mode: Some(LspInsertMode::Insert),
15749 ..Default::default()
15750 });
15751 });
15752
15753 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15754
15755 cx.set_state(indoc! {"ˇ
15756 0_usize
15757 let
15758 33
15759 4.5f32
15760 "});
15761 cx.update_editor(|editor, window, cx| {
15762 editor.show_completions(&ShowCompletions, window, cx);
15763 });
15764 cx.executor().run_until_parked();
15765 cx.condition(|editor, _| editor.context_menu_visible())
15766 .await;
15767 cx.update_editor(|editor, window, cx| {
15768 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15769 {
15770 assert_eq!(
15771 completion_menu_entries(menu),
15772 &["let"],
15773 "With no digits in the completion query, no digits should be in the word completions"
15774 );
15775 } else {
15776 panic!("expected completion menu to be open");
15777 }
15778 editor.cancel(&Cancel, window, cx);
15779 });
15780
15781 cx.set_state(indoc! {"3ˇ
15782 0_usize
15783 let
15784 3
15785 33.35f32
15786 "});
15787 cx.update_editor(|editor, window, cx| {
15788 editor.show_completions(&ShowCompletions, window, cx);
15789 });
15790 cx.executor().run_until_parked();
15791 cx.condition(|editor, _| editor.context_menu_visible())
15792 .await;
15793 cx.update_editor(|editor, _, _| {
15794 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15795 {
15796 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15797 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15798 } else {
15799 panic!("expected completion menu to be open");
15800 }
15801 });
15802}
15803
15804#[gpui::test]
15805async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15806 init_test(cx, |language_settings| {
15807 language_settings.defaults.completions = Some(CompletionSettingsContent {
15808 words: Some(WordsCompletionMode::Enabled),
15809 words_min_length: Some(3),
15810 lsp_insert_mode: Some(LspInsertMode::Insert),
15811 ..Default::default()
15812 });
15813 });
15814
15815 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15816 cx.set_state(indoc! {"ˇ
15817 wow
15818 wowen
15819 wowser
15820 "});
15821 cx.simulate_keystroke("w");
15822 cx.executor().run_until_parked();
15823 cx.update_editor(|editor, _, _| {
15824 if editor.context_menu.borrow_mut().is_some() {
15825 panic!(
15826 "expected completion menu to be hidden, as words completion threshold is not met"
15827 );
15828 }
15829 });
15830
15831 cx.update_editor(|editor, window, cx| {
15832 editor.show_word_completions(&ShowWordCompletions, window, cx);
15833 });
15834 cx.executor().run_until_parked();
15835 cx.update_editor(|editor, window, cx| {
15836 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15837 {
15838 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");
15839 } else {
15840 panic!("expected completion menu to be open after the word completions are called with an action");
15841 }
15842
15843 editor.cancel(&Cancel, window, cx);
15844 });
15845 cx.update_editor(|editor, _, _| {
15846 if editor.context_menu.borrow_mut().is_some() {
15847 panic!("expected completion menu to be hidden after canceling");
15848 }
15849 });
15850
15851 cx.simulate_keystroke("o");
15852 cx.executor().run_until_parked();
15853 cx.update_editor(|editor, _, _| {
15854 if editor.context_menu.borrow_mut().is_some() {
15855 panic!(
15856 "expected completion menu to be hidden, as words completion threshold is not met still"
15857 );
15858 }
15859 });
15860
15861 cx.simulate_keystroke("w");
15862 cx.executor().run_until_parked();
15863 cx.update_editor(|editor, _, _| {
15864 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15865 {
15866 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15867 } else {
15868 panic!("expected completion menu to be open after the word completions threshold is met");
15869 }
15870 });
15871}
15872
15873#[gpui::test]
15874async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15875 init_test(cx, |language_settings| {
15876 language_settings.defaults.completions = Some(CompletionSettingsContent {
15877 words: Some(WordsCompletionMode::Enabled),
15878 words_min_length: Some(0),
15879 lsp_insert_mode: Some(LspInsertMode::Insert),
15880 ..Default::default()
15881 });
15882 });
15883
15884 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15885 cx.update_editor(|editor, _, _| {
15886 editor.disable_word_completions();
15887 });
15888 cx.set_state(indoc! {"ˇ
15889 wow
15890 wowen
15891 wowser
15892 "});
15893 cx.simulate_keystroke("w");
15894 cx.executor().run_until_parked();
15895 cx.update_editor(|editor, _, _| {
15896 if editor.context_menu.borrow_mut().is_some() {
15897 panic!(
15898 "expected completion menu to be hidden, as words completion are disabled for this editor"
15899 );
15900 }
15901 });
15902
15903 cx.update_editor(|editor, window, cx| {
15904 editor.show_word_completions(&ShowWordCompletions, window, cx);
15905 });
15906 cx.executor().run_until_parked();
15907 cx.update_editor(|editor, _, _| {
15908 if editor.context_menu.borrow_mut().is_some() {
15909 panic!(
15910 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15911 );
15912 }
15913 });
15914}
15915
15916#[gpui::test]
15917async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15918 init_test(cx, |language_settings| {
15919 language_settings.defaults.completions = Some(CompletionSettingsContent {
15920 words: Some(WordsCompletionMode::Disabled),
15921 words_min_length: Some(0),
15922 lsp_insert_mode: Some(LspInsertMode::Insert),
15923 ..Default::default()
15924 });
15925 });
15926
15927 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15928 cx.update_editor(|editor, _, _| {
15929 editor.set_completion_provider(None);
15930 });
15931 cx.set_state(indoc! {"ˇ
15932 wow
15933 wowen
15934 wowser
15935 "});
15936 cx.simulate_keystroke("w");
15937 cx.executor().run_until_parked();
15938 cx.update_editor(|editor, _, _| {
15939 if editor.context_menu.borrow_mut().is_some() {
15940 panic!("expected completion menu to be hidden, as disabled in settings");
15941 }
15942 });
15943}
15944
15945fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15946 let position = || lsp::Position {
15947 line: params.text_document_position.position.line,
15948 character: params.text_document_position.position.character,
15949 };
15950 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15951 range: lsp::Range {
15952 start: position(),
15953 end: position(),
15954 },
15955 new_text: text.to_string(),
15956 }))
15957}
15958
15959#[gpui::test]
15960async fn test_multiline_completion(cx: &mut TestAppContext) {
15961 init_test(cx, |_| {});
15962
15963 let fs = FakeFs::new(cx.executor());
15964 fs.insert_tree(
15965 path!("/a"),
15966 json!({
15967 "main.ts": "a",
15968 }),
15969 )
15970 .await;
15971
15972 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15973 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15974 let typescript_language = Arc::new(Language::new(
15975 LanguageConfig {
15976 name: "TypeScript".into(),
15977 matcher: LanguageMatcher {
15978 path_suffixes: vec!["ts".to_string()],
15979 ..LanguageMatcher::default()
15980 },
15981 line_comments: vec!["// ".into()],
15982 ..LanguageConfig::default()
15983 },
15984 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15985 ));
15986 language_registry.add(typescript_language.clone());
15987 let mut fake_servers = language_registry.register_fake_lsp(
15988 "TypeScript",
15989 FakeLspAdapter {
15990 capabilities: lsp::ServerCapabilities {
15991 completion_provider: Some(lsp::CompletionOptions {
15992 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15993 ..lsp::CompletionOptions::default()
15994 }),
15995 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15996 ..lsp::ServerCapabilities::default()
15997 },
15998 // Emulate vtsls label generation
15999 label_for_completion: Some(Box::new(|item, _| {
16000 let text = if let Some(description) = item
16001 .label_details
16002 .as_ref()
16003 .and_then(|label_details| label_details.description.as_ref())
16004 {
16005 format!("{} {}", item.label, description)
16006 } else if let Some(detail) = &item.detail {
16007 format!("{} {}", item.label, detail)
16008 } else {
16009 item.label.clone()
16010 };
16011 Some(language::CodeLabel::plain(text, None))
16012 })),
16013 ..FakeLspAdapter::default()
16014 },
16015 );
16016 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16017 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16018 let worktree_id = workspace
16019 .update(cx, |workspace, _window, cx| {
16020 workspace.project().update(cx, |project, cx| {
16021 project.worktrees(cx).next().unwrap().read(cx).id()
16022 })
16023 })
16024 .unwrap();
16025 let _buffer = project
16026 .update(cx, |project, cx| {
16027 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
16028 })
16029 .await
16030 .unwrap();
16031 let editor = workspace
16032 .update(cx, |workspace, window, cx| {
16033 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
16034 })
16035 .unwrap()
16036 .await
16037 .unwrap()
16038 .downcast::<Editor>()
16039 .unwrap();
16040 let fake_server = fake_servers.next().await.unwrap();
16041 cx.run_until_parked();
16042
16043 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
16044 let multiline_label_2 = "a\nb\nc\n";
16045 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
16046 let multiline_description = "d\ne\nf\n";
16047 let multiline_detail_2 = "g\nh\ni\n";
16048
16049 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
16050 move |params, _| async move {
16051 Ok(Some(lsp::CompletionResponse::Array(vec![
16052 lsp::CompletionItem {
16053 label: multiline_label.to_string(),
16054 text_edit: gen_text_edit(¶ms, "new_text_1"),
16055 ..lsp::CompletionItem::default()
16056 },
16057 lsp::CompletionItem {
16058 label: "single line label 1".to_string(),
16059 detail: Some(multiline_detail.to_string()),
16060 text_edit: gen_text_edit(¶ms, "new_text_2"),
16061 ..lsp::CompletionItem::default()
16062 },
16063 lsp::CompletionItem {
16064 label: "single line label 2".to_string(),
16065 label_details: Some(lsp::CompletionItemLabelDetails {
16066 description: Some(multiline_description.to_string()),
16067 detail: None,
16068 }),
16069 text_edit: gen_text_edit(¶ms, "new_text_2"),
16070 ..lsp::CompletionItem::default()
16071 },
16072 lsp::CompletionItem {
16073 label: multiline_label_2.to_string(),
16074 detail: Some(multiline_detail_2.to_string()),
16075 text_edit: gen_text_edit(¶ms, "new_text_3"),
16076 ..lsp::CompletionItem::default()
16077 },
16078 lsp::CompletionItem {
16079 label: "Label with many spaces and \t but without newlines".to_string(),
16080 detail: Some(
16081 "Details with many spaces and \t but without newlines".to_string(),
16082 ),
16083 text_edit: gen_text_edit(¶ms, "new_text_4"),
16084 ..lsp::CompletionItem::default()
16085 },
16086 ])))
16087 },
16088 );
16089
16090 editor.update_in(cx, |editor, window, cx| {
16091 cx.focus_self(window);
16092 editor.move_to_end(&MoveToEnd, window, cx);
16093 editor.handle_input(".", window, cx);
16094 });
16095 cx.run_until_parked();
16096 completion_handle.next().await.unwrap();
16097
16098 editor.update(cx, |editor, _| {
16099 assert!(editor.context_menu_visible());
16100 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16101 {
16102 let completion_labels = menu
16103 .completions
16104 .borrow()
16105 .iter()
16106 .map(|c| c.label.text.clone())
16107 .collect::<Vec<_>>();
16108 assert_eq!(
16109 completion_labels,
16110 &[
16111 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
16112 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
16113 "single line label 2 d e f ",
16114 "a b c g h i ",
16115 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
16116 ],
16117 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
16118 );
16119
16120 for completion in menu
16121 .completions
16122 .borrow()
16123 .iter() {
16124 assert_eq!(
16125 completion.label.filter_range,
16126 0..completion.label.text.len(),
16127 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
16128 );
16129 }
16130 } else {
16131 panic!("expected completion menu to be open");
16132 }
16133 });
16134}
16135
16136#[gpui::test]
16137async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
16138 init_test(cx, |_| {});
16139 let mut cx = EditorLspTestContext::new_rust(
16140 lsp::ServerCapabilities {
16141 completion_provider: Some(lsp::CompletionOptions {
16142 trigger_characters: Some(vec![".".to_string()]),
16143 ..Default::default()
16144 }),
16145 ..Default::default()
16146 },
16147 cx,
16148 )
16149 .await;
16150 cx.lsp
16151 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16152 Ok(Some(lsp::CompletionResponse::Array(vec![
16153 lsp::CompletionItem {
16154 label: "first".into(),
16155 ..Default::default()
16156 },
16157 lsp::CompletionItem {
16158 label: "last".into(),
16159 ..Default::default()
16160 },
16161 ])))
16162 });
16163 cx.set_state("variableˇ");
16164 cx.simulate_keystroke(".");
16165 cx.executor().run_until_parked();
16166
16167 cx.update_editor(|editor, _, _| {
16168 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16169 {
16170 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
16171 } else {
16172 panic!("expected completion menu to be open");
16173 }
16174 });
16175
16176 cx.update_editor(|editor, window, cx| {
16177 editor.move_page_down(&MovePageDown::default(), window, cx);
16178 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16179 {
16180 assert!(
16181 menu.selected_item == 1,
16182 "expected PageDown to select the last item from the context menu"
16183 );
16184 } else {
16185 panic!("expected completion menu to stay open after PageDown");
16186 }
16187 });
16188
16189 cx.update_editor(|editor, window, cx| {
16190 editor.move_page_up(&MovePageUp::default(), window, cx);
16191 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16192 {
16193 assert!(
16194 menu.selected_item == 0,
16195 "expected PageUp to select the first item from the context menu"
16196 );
16197 } else {
16198 panic!("expected completion menu to stay open after PageUp");
16199 }
16200 });
16201}
16202
16203#[gpui::test]
16204async fn test_as_is_completions(cx: &mut TestAppContext) {
16205 init_test(cx, |_| {});
16206 let mut cx = EditorLspTestContext::new_rust(
16207 lsp::ServerCapabilities {
16208 completion_provider: Some(lsp::CompletionOptions {
16209 ..Default::default()
16210 }),
16211 ..Default::default()
16212 },
16213 cx,
16214 )
16215 .await;
16216 cx.lsp
16217 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16218 Ok(Some(lsp::CompletionResponse::Array(vec![
16219 lsp::CompletionItem {
16220 label: "unsafe".into(),
16221 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16222 range: lsp::Range {
16223 start: lsp::Position {
16224 line: 1,
16225 character: 2,
16226 },
16227 end: lsp::Position {
16228 line: 1,
16229 character: 3,
16230 },
16231 },
16232 new_text: "unsafe".to_string(),
16233 })),
16234 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
16235 ..Default::default()
16236 },
16237 ])))
16238 });
16239 cx.set_state("fn a() {}\n nˇ");
16240 cx.executor().run_until_parked();
16241 cx.update_editor(|editor, window, cx| {
16242 editor.trigger_completion_on_input("n", true, window, cx)
16243 });
16244 cx.executor().run_until_parked();
16245
16246 cx.update_editor(|editor, window, cx| {
16247 editor.confirm_completion(&Default::default(), window, cx)
16248 });
16249 cx.executor().run_until_parked();
16250 cx.assert_editor_state("fn a() {}\n unsafeˇ");
16251}
16252
16253#[gpui::test]
16254async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
16255 init_test(cx, |_| {});
16256 let language =
16257 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
16258 let mut cx = EditorLspTestContext::new(
16259 language,
16260 lsp::ServerCapabilities {
16261 completion_provider: Some(lsp::CompletionOptions {
16262 ..lsp::CompletionOptions::default()
16263 }),
16264 ..lsp::ServerCapabilities::default()
16265 },
16266 cx,
16267 )
16268 .await;
16269
16270 cx.set_state(
16271 "#ifndef BAR_H
16272#define BAR_H
16273
16274#include <stdbool.h>
16275
16276int fn_branch(bool do_branch1, bool do_branch2);
16277
16278#endif // BAR_H
16279ˇ",
16280 );
16281 cx.executor().run_until_parked();
16282 cx.update_editor(|editor, window, cx| {
16283 editor.handle_input("#", window, cx);
16284 });
16285 cx.executor().run_until_parked();
16286 cx.update_editor(|editor, window, cx| {
16287 editor.handle_input("i", window, cx);
16288 });
16289 cx.executor().run_until_parked();
16290 cx.update_editor(|editor, window, cx| {
16291 editor.handle_input("n", window, cx);
16292 });
16293 cx.executor().run_until_parked();
16294 cx.assert_editor_state(
16295 "#ifndef BAR_H
16296#define BAR_H
16297
16298#include <stdbool.h>
16299
16300int fn_branch(bool do_branch1, bool do_branch2);
16301
16302#endif // BAR_H
16303#inˇ",
16304 );
16305
16306 cx.lsp
16307 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16308 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16309 is_incomplete: false,
16310 item_defaults: None,
16311 items: vec![lsp::CompletionItem {
16312 kind: Some(lsp::CompletionItemKind::SNIPPET),
16313 label_details: Some(lsp::CompletionItemLabelDetails {
16314 detail: Some("header".to_string()),
16315 description: None,
16316 }),
16317 label: " include".to_string(),
16318 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16319 range: lsp::Range {
16320 start: lsp::Position {
16321 line: 8,
16322 character: 1,
16323 },
16324 end: lsp::Position {
16325 line: 8,
16326 character: 1,
16327 },
16328 },
16329 new_text: "include \"$0\"".to_string(),
16330 })),
16331 sort_text: Some("40b67681include".to_string()),
16332 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16333 filter_text: Some("include".to_string()),
16334 insert_text: Some("include \"$0\"".to_string()),
16335 ..lsp::CompletionItem::default()
16336 }],
16337 })))
16338 });
16339 cx.update_editor(|editor, window, cx| {
16340 editor.show_completions(&ShowCompletions, window, cx);
16341 });
16342 cx.executor().run_until_parked();
16343 cx.update_editor(|editor, window, cx| {
16344 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16345 });
16346 cx.executor().run_until_parked();
16347 cx.assert_editor_state(
16348 "#ifndef BAR_H
16349#define BAR_H
16350
16351#include <stdbool.h>
16352
16353int fn_branch(bool do_branch1, bool do_branch2);
16354
16355#endif // BAR_H
16356#include \"ˇ\"",
16357 );
16358
16359 cx.lsp
16360 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16361 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16362 is_incomplete: true,
16363 item_defaults: None,
16364 items: vec![lsp::CompletionItem {
16365 kind: Some(lsp::CompletionItemKind::FILE),
16366 label: "AGL/".to_string(),
16367 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16368 range: lsp::Range {
16369 start: lsp::Position {
16370 line: 8,
16371 character: 10,
16372 },
16373 end: lsp::Position {
16374 line: 8,
16375 character: 11,
16376 },
16377 },
16378 new_text: "AGL/".to_string(),
16379 })),
16380 sort_text: Some("40b67681AGL/".to_string()),
16381 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16382 filter_text: Some("AGL/".to_string()),
16383 insert_text: Some("AGL/".to_string()),
16384 ..lsp::CompletionItem::default()
16385 }],
16386 })))
16387 });
16388 cx.update_editor(|editor, window, cx| {
16389 editor.show_completions(&ShowCompletions, window, cx);
16390 });
16391 cx.executor().run_until_parked();
16392 cx.update_editor(|editor, window, cx| {
16393 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16394 });
16395 cx.executor().run_until_parked();
16396 cx.assert_editor_state(
16397 r##"#ifndef BAR_H
16398#define BAR_H
16399
16400#include <stdbool.h>
16401
16402int fn_branch(bool do_branch1, bool do_branch2);
16403
16404#endif // BAR_H
16405#include "AGL/ˇ"##,
16406 );
16407
16408 cx.update_editor(|editor, window, cx| {
16409 editor.handle_input("\"", window, cx);
16410 });
16411 cx.executor().run_until_parked();
16412 cx.assert_editor_state(
16413 r##"#ifndef BAR_H
16414#define BAR_H
16415
16416#include <stdbool.h>
16417
16418int fn_branch(bool do_branch1, bool do_branch2);
16419
16420#endif // BAR_H
16421#include "AGL/"ˇ"##,
16422 );
16423}
16424
16425#[gpui::test]
16426async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
16427 init_test(cx, |_| {});
16428
16429 let mut cx = EditorLspTestContext::new_rust(
16430 lsp::ServerCapabilities {
16431 completion_provider: Some(lsp::CompletionOptions {
16432 trigger_characters: Some(vec![".".to_string()]),
16433 resolve_provider: Some(true),
16434 ..Default::default()
16435 }),
16436 ..Default::default()
16437 },
16438 cx,
16439 )
16440 .await;
16441
16442 cx.set_state("fn main() { let a = 2ˇ; }");
16443 cx.simulate_keystroke(".");
16444 let completion_item = lsp::CompletionItem {
16445 label: "Some".into(),
16446 kind: Some(lsp::CompletionItemKind::SNIPPET),
16447 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16448 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16449 kind: lsp::MarkupKind::Markdown,
16450 value: "```rust\nSome(2)\n```".to_string(),
16451 })),
16452 deprecated: Some(false),
16453 sort_text: Some("Some".to_string()),
16454 filter_text: Some("Some".to_string()),
16455 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16456 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16457 range: lsp::Range {
16458 start: lsp::Position {
16459 line: 0,
16460 character: 22,
16461 },
16462 end: lsp::Position {
16463 line: 0,
16464 character: 22,
16465 },
16466 },
16467 new_text: "Some(2)".to_string(),
16468 })),
16469 additional_text_edits: Some(vec![lsp::TextEdit {
16470 range: lsp::Range {
16471 start: lsp::Position {
16472 line: 0,
16473 character: 20,
16474 },
16475 end: lsp::Position {
16476 line: 0,
16477 character: 22,
16478 },
16479 },
16480 new_text: "".to_string(),
16481 }]),
16482 ..Default::default()
16483 };
16484
16485 let closure_completion_item = completion_item.clone();
16486 let counter = Arc::new(AtomicUsize::new(0));
16487 let counter_clone = counter.clone();
16488 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16489 let task_completion_item = closure_completion_item.clone();
16490 counter_clone.fetch_add(1, atomic::Ordering::Release);
16491 async move {
16492 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16493 is_incomplete: true,
16494 item_defaults: None,
16495 items: vec![task_completion_item],
16496 })))
16497 }
16498 });
16499
16500 cx.condition(|editor, _| editor.context_menu_visible())
16501 .await;
16502 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
16503 assert!(request.next().await.is_some());
16504 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16505
16506 cx.simulate_keystrokes("S o m");
16507 cx.condition(|editor, _| editor.context_menu_visible())
16508 .await;
16509 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
16510 assert!(request.next().await.is_some());
16511 assert!(request.next().await.is_some());
16512 assert!(request.next().await.is_some());
16513 request.close();
16514 assert!(request.next().await.is_none());
16515 assert_eq!(
16516 counter.load(atomic::Ordering::Acquire),
16517 4,
16518 "With the completions menu open, only one LSP request should happen per input"
16519 );
16520}
16521
16522#[gpui::test]
16523async fn test_toggle_comment(cx: &mut TestAppContext) {
16524 init_test(cx, |_| {});
16525 let mut cx = EditorTestContext::new(cx).await;
16526 let language = Arc::new(Language::new(
16527 LanguageConfig {
16528 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16529 ..Default::default()
16530 },
16531 Some(tree_sitter_rust::LANGUAGE.into()),
16532 ));
16533 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16534
16535 // If multiple selections intersect a line, the line is only toggled once.
16536 cx.set_state(indoc! {"
16537 fn a() {
16538 «//b();
16539 ˇ»// «c();
16540 //ˇ» d();
16541 }
16542 "});
16543
16544 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16545
16546 cx.assert_editor_state(indoc! {"
16547 fn a() {
16548 «b();
16549 ˇ»«c();
16550 ˇ» d();
16551 }
16552 "});
16553
16554 // The comment prefix is inserted at the same column for every line in a
16555 // selection.
16556 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16557
16558 cx.assert_editor_state(indoc! {"
16559 fn a() {
16560 // «b();
16561 ˇ»// «c();
16562 ˇ» // d();
16563 }
16564 "});
16565
16566 // If a selection ends at the beginning of a line, that line is not toggled.
16567 cx.set_selections_state(indoc! {"
16568 fn a() {
16569 // b();
16570 «// c();
16571 ˇ» // d();
16572 }
16573 "});
16574
16575 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16576
16577 cx.assert_editor_state(indoc! {"
16578 fn a() {
16579 // b();
16580 «c();
16581 ˇ» // d();
16582 }
16583 "});
16584
16585 // If a selection span a single line and is empty, the line is toggled.
16586 cx.set_state(indoc! {"
16587 fn a() {
16588 a();
16589 b();
16590 ˇ
16591 }
16592 "});
16593
16594 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16595
16596 cx.assert_editor_state(indoc! {"
16597 fn a() {
16598 a();
16599 b();
16600 //•ˇ
16601 }
16602 "});
16603
16604 // If a selection span multiple lines, empty lines are not toggled.
16605 cx.set_state(indoc! {"
16606 fn a() {
16607 «a();
16608
16609 c();ˇ»
16610 }
16611 "});
16612
16613 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16614
16615 cx.assert_editor_state(indoc! {"
16616 fn a() {
16617 // «a();
16618
16619 // c();ˇ»
16620 }
16621 "});
16622
16623 // If a selection includes multiple comment prefixes, all lines are uncommented.
16624 cx.set_state(indoc! {"
16625 fn a() {
16626 «// a();
16627 /// b();
16628 //! c();ˇ»
16629 }
16630 "});
16631
16632 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16633
16634 cx.assert_editor_state(indoc! {"
16635 fn a() {
16636 «a();
16637 b();
16638 c();ˇ»
16639 }
16640 "});
16641}
16642
16643#[gpui::test]
16644async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16645 init_test(cx, |_| {});
16646 let mut cx = EditorTestContext::new(cx).await;
16647 let language = Arc::new(Language::new(
16648 LanguageConfig {
16649 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16650 ..Default::default()
16651 },
16652 Some(tree_sitter_rust::LANGUAGE.into()),
16653 ));
16654 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16655
16656 let toggle_comments = &ToggleComments {
16657 advance_downwards: false,
16658 ignore_indent: true,
16659 };
16660
16661 // If multiple selections intersect a line, the line is only toggled once.
16662 cx.set_state(indoc! {"
16663 fn a() {
16664 // «b();
16665 // c();
16666 // ˇ» d();
16667 }
16668 "});
16669
16670 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16671
16672 cx.assert_editor_state(indoc! {"
16673 fn a() {
16674 «b();
16675 c();
16676 ˇ» d();
16677 }
16678 "});
16679
16680 // The comment prefix is inserted at the beginning of each line
16681 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16682
16683 cx.assert_editor_state(indoc! {"
16684 fn a() {
16685 // «b();
16686 // c();
16687 // ˇ» d();
16688 }
16689 "});
16690
16691 // If a selection ends at the beginning of a line, that line is not toggled.
16692 cx.set_selections_state(indoc! {"
16693 fn a() {
16694 // b();
16695 // «c();
16696 ˇ»// d();
16697 }
16698 "});
16699
16700 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16701
16702 cx.assert_editor_state(indoc! {"
16703 fn a() {
16704 // b();
16705 «c();
16706 ˇ»// d();
16707 }
16708 "});
16709
16710 // If a selection span a single line and is empty, the line is toggled.
16711 cx.set_state(indoc! {"
16712 fn a() {
16713 a();
16714 b();
16715 ˇ
16716 }
16717 "});
16718
16719 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16720
16721 cx.assert_editor_state(indoc! {"
16722 fn a() {
16723 a();
16724 b();
16725 //ˇ
16726 }
16727 "});
16728
16729 // If a selection span multiple lines, empty lines are not toggled.
16730 cx.set_state(indoc! {"
16731 fn a() {
16732 «a();
16733
16734 c();ˇ»
16735 }
16736 "});
16737
16738 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16739
16740 cx.assert_editor_state(indoc! {"
16741 fn a() {
16742 // «a();
16743
16744 // c();ˇ»
16745 }
16746 "});
16747
16748 // If a selection includes multiple comment prefixes, all lines are uncommented.
16749 cx.set_state(indoc! {"
16750 fn a() {
16751 // «a();
16752 /// b();
16753 //! c();ˇ»
16754 }
16755 "});
16756
16757 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16758
16759 cx.assert_editor_state(indoc! {"
16760 fn a() {
16761 «a();
16762 b();
16763 c();ˇ»
16764 }
16765 "});
16766}
16767
16768#[gpui::test]
16769async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16770 init_test(cx, |_| {});
16771
16772 let language = Arc::new(Language::new(
16773 LanguageConfig {
16774 line_comments: vec!["// ".into()],
16775 ..Default::default()
16776 },
16777 Some(tree_sitter_rust::LANGUAGE.into()),
16778 ));
16779
16780 let mut cx = EditorTestContext::new(cx).await;
16781
16782 cx.language_registry().add(language.clone());
16783 cx.update_buffer(|buffer, cx| {
16784 buffer.set_language(Some(language), cx);
16785 });
16786
16787 let toggle_comments = &ToggleComments {
16788 advance_downwards: true,
16789 ignore_indent: false,
16790 };
16791
16792 // Single cursor on one line -> advance
16793 // Cursor moves horizontally 3 characters as well on non-blank line
16794 cx.set_state(indoc!(
16795 "fn a() {
16796 ˇdog();
16797 cat();
16798 }"
16799 ));
16800 cx.update_editor(|editor, window, cx| {
16801 editor.toggle_comments(toggle_comments, window, cx);
16802 });
16803 cx.assert_editor_state(indoc!(
16804 "fn a() {
16805 // dog();
16806 catˇ();
16807 }"
16808 ));
16809
16810 // Single selection on one line -> don't advance
16811 cx.set_state(indoc!(
16812 "fn a() {
16813 «dog()ˇ»;
16814 cat();
16815 }"
16816 ));
16817 cx.update_editor(|editor, window, cx| {
16818 editor.toggle_comments(toggle_comments, window, cx);
16819 });
16820 cx.assert_editor_state(indoc!(
16821 "fn a() {
16822 // «dog()ˇ»;
16823 cat();
16824 }"
16825 ));
16826
16827 // Multiple cursors on one line -> advance
16828 cx.set_state(indoc!(
16829 "fn a() {
16830 ˇdˇog();
16831 cat();
16832 }"
16833 ));
16834 cx.update_editor(|editor, window, cx| {
16835 editor.toggle_comments(toggle_comments, window, cx);
16836 });
16837 cx.assert_editor_state(indoc!(
16838 "fn a() {
16839 // dog();
16840 catˇ(ˇ);
16841 }"
16842 ));
16843
16844 // Multiple cursors on one line, with selection -> don't advance
16845 cx.set_state(indoc!(
16846 "fn a() {
16847 ˇdˇog«()ˇ»;
16848 cat();
16849 }"
16850 ));
16851 cx.update_editor(|editor, window, cx| {
16852 editor.toggle_comments(toggle_comments, window, cx);
16853 });
16854 cx.assert_editor_state(indoc!(
16855 "fn a() {
16856 // ˇdˇog«()ˇ»;
16857 cat();
16858 }"
16859 ));
16860
16861 // Single cursor on one line -> advance
16862 // Cursor moves to column 0 on blank line
16863 cx.set_state(indoc!(
16864 "fn a() {
16865 ˇdog();
16866
16867 cat();
16868 }"
16869 ));
16870 cx.update_editor(|editor, window, cx| {
16871 editor.toggle_comments(toggle_comments, window, cx);
16872 });
16873 cx.assert_editor_state(indoc!(
16874 "fn a() {
16875 // dog();
16876 ˇ
16877 cat();
16878 }"
16879 ));
16880
16881 // Single cursor on one line -> advance
16882 // Cursor starts and ends at column 0
16883 cx.set_state(indoc!(
16884 "fn a() {
16885 ˇ dog();
16886 cat();
16887 }"
16888 ));
16889 cx.update_editor(|editor, window, cx| {
16890 editor.toggle_comments(toggle_comments, window, cx);
16891 });
16892 cx.assert_editor_state(indoc!(
16893 "fn a() {
16894 // dog();
16895 ˇ cat();
16896 }"
16897 ));
16898}
16899
16900#[gpui::test]
16901async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16902 init_test(cx, |_| {});
16903
16904 let mut cx = EditorTestContext::new(cx).await;
16905
16906 let html_language = Arc::new(
16907 Language::new(
16908 LanguageConfig {
16909 name: "HTML".into(),
16910 block_comment: Some(BlockCommentConfig {
16911 start: "<!-- ".into(),
16912 prefix: "".into(),
16913 end: " -->".into(),
16914 tab_size: 0,
16915 }),
16916 ..Default::default()
16917 },
16918 Some(tree_sitter_html::LANGUAGE.into()),
16919 )
16920 .with_injection_query(
16921 r#"
16922 (script_element
16923 (raw_text) @injection.content
16924 (#set! injection.language "javascript"))
16925 "#,
16926 )
16927 .unwrap(),
16928 );
16929
16930 let javascript_language = Arc::new(Language::new(
16931 LanguageConfig {
16932 name: "JavaScript".into(),
16933 line_comments: vec!["// ".into()],
16934 ..Default::default()
16935 },
16936 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16937 ));
16938
16939 cx.language_registry().add(html_language.clone());
16940 cx.language_registry().add(javascript_language);
16941 cx.update_buffer(|buffer, cx| {
16942 buffer.set_language(Some(html_language), cx);
16943 });
16944
16945 // Toggle comments for empty selections
16946 cx.set_state(
16947 &r#"
16948 <p>A</p>ˇ
16949 <p>B</p>ˇ
16950 <p>C</p>ˇ
16951 "#
16952 .unindent(),
16953 );
16954 cx.update_editor(|editor, window, cx| {
16955 editor.toggle_comments(&ToggleComments::default(), window, cx)
16956 });
16957 cx.assert_editor_state(
16958 &r#"
16959 <!-- <p>A</p>ˇ -->
16960 <!-- <p>B</p>ˇ -->
16961 <!-- <p>C</p>ˇ -->
16962 "#
16963 .unindent(),
16964 );
16965 cx.update_editor(|editor, window, cx| {
16966 editor.toggle_comments(&ToggleComments::default(), window, cx)
16967 });
16968 cx.assert_editor_state(
16969 &r#"
16970 <p>A</p>ˇ
16971 <p>B</p>ˇ
16972 <p>C</p>ˇ
16973 "#
16974 .unindent(),
16975 );
16976
16977 // Toggle comments for mixture of empty and non-empty selections, where
16978 // multiple selections occupy a given line.
16979 cx.set_state(
16980 &r#"
16981 <p>A«</p>
16982 <p>ˇ»B</p>ˇ
16983 <p>C«</p>
16984 <p>ˇ»D</p>ˇ
16985 "#
16986 .unindent(),
16987 );
16988
16989 cx.update_editor(|editor, window, cx| {
16990 editor.toggle_comments(&ToggleComments::default(), window, cx)
16991 });
16992 cx.assert_editor_state(
16993 &r#"
16994 <!-- <p>A«</p>
16995 <p>ˇ»B</p>ˇ -->
16996 <!-- <p>C«</p>
16997 <p>ˇ»D</p>ˇ -->
16998 "#
16999 .unindent(),
17000 );
17001 cx.update_editor(|editor, window, cx| {
17002 editor.toggle_comments(&ToggleComments::default(), window, cx)
17003 });
17004 cx.assert_editor_state(
17005 &r#"
17006 <p>A«</p>
17007 <p>ˇ»B</p>ˇ
17008 <p>C«</p>
17009 <p>ˇ»D</p>ˇ
17010 "#
17011 .unindent(),
17012 );
17013
17014 // Toggle comments when different languages are active for different
17015 // selections.
17016 cx.set_state(
17017 &r#"
17018 ˇ<script>
17019 ˇvar x = new Y();
17020 ˇ</script>
17021 "#
17022 .unindent(),
17023 );
17024 cx.executor().run_until_parked();
17025 cx.update_editor(|editor, window, cx| {
17026 editor.toggle_comments(&ToggleComments::default(), window, cx)
17027 });
17028 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
17029 // Uncommenting and commenting from this position brings in even more wrong artifacts.
17030 cx.assert_editor_state(
17031 &r#"
17032 <!-- ˇ<script> -->
17033 // ˇvar x = new Y();
17034 <!-- ˇ</script> -->
17035 "#
17036 .unindent(),
17037 );
17038}
17039
17040#[gpui::test]
17041fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
17042 init_test(cx, |_| {});
17043
17044 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17045 let multibuffer = cx.new(|cx| {
17046 let mut multibuffer = MultiBuffer::new(ReadWrite);
17047 multibuffer.push_excerpts(
17048 buffer.clone(),
17049 [
17050 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
17051 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
17052 ],
17053 cx,
17054 );
17055 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
17056 multibuffer
17057 });
17058
17059 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
17060 editor.update_in(cx, |editor, window, cx| {
17061 assert_eq!(editor.text(cx), "aaaa\nbbbb");
17062 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17063 s.select_ranges([
17064 Point::new(0, 0)..Point::new(0, 0),
17065 Point::new(1, 0)..Point::new(1, 0),
17066 ])
17067 });
17068
17069 editor.handle_input("X", window, cx);
17070 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
17071 assert_eq!(
17072 editor.selections.ranges(&editor.display_snapshot(cx)),
17073 [
17074 Point::new(0, 1)..Point::new(0, 1),
17075 Point::new(1, 1)..Point::new(1, 1),
17076 ]
17077 );
17078
17079 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
17080 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17081 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
17082 });
17083 editor.backspace(&Default::default(), window, cx);
17084 assert_eq!(editor.text(cx), "Xa\nbbb");
17085 assert_eq!(
17086 editor.selections.ranges(&editor.display_snapshot(cx)),
17087 [Point::new(1, 0)..Point::new(1, 0)]
17088 );
17089
17090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17091 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
17092 });
17093 editor.backspace(&Default::default(), window, cx);
17094 assert_eq!(editor.text(cx), "X\nbb");
17095 assert_eq!(
17096 editor.selections.ranges(&editor.display_snapshot(cx)),
17097 [Point::new(0, 1)..Point::new(0, 1)]
17098 );
17099 });
17100}
17101
17102#[gpui::test]
17103fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
17104 init_test(cx, |_| {});
17105
17106 let markers = vec![('[', ']').into(), ('(', ')').into()];
17107 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
17108 indoc! {"
17109 [aaaa
17110 (bbbb]
17111 cccc)",
17112 },
17113 markers.clone(),
17114 );
17115 let excerpt_ranges = markers.into_iter().map(|marker| {
17116 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
17117 ExcerptRange::new(context)
17118 });
17119 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
17120 let multibuffer = cx.new(|cx| {
17121 let mut multibuffer = MultiBuffer::new(ReadWrite);
17122 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
17123 multibuffer
17124 });
17125
17126 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
17127 editor.update_in(cx, |editor, window, cx| {
17128 let (expected_text, selection_ranges) = marked_text_ranges(
17129 indoc! {"
17130 aaaa
17131 bˇbbb
17132 bˇbbˇb
17133 cccc"
17134 },
17135 true,
17136 );
17137 assert_eq!(editor.text(cx), expected_text);
17138 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17139 s.select_ranges(
17140 selection_ranges
17141 .iter()
17142 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
17143 )
17144 });
17145
17146 editor.handle_input("X", window, cx);
17147
17148 let (expected_text, expected_selections) = marked_text_ranges(
17149 indoc! {"
17150 aaaa
17151 bXˇbbXb
17152 bXˇbbXˇb
17153 cccc"
17154 },
17155 false,
17156 );
17157 assert_eq!(editor.text(cx), expected_text);
17158 assert_eq!(
17159 editor.selections.ranges(&editor.display_snapshot(cx)),
17160 expected_selections
17161 .iter()
17162 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
17163 .collect::<Vec<_>>()
17164 );
17165
17166 editor.newline(&Newline, window, cx);
17167 let (expected_text, expected_selections) = marked_text_ranges(
17168 indoc! {"
17169 aaaa
17170 bX
17171 ˇbbX
17172 b
17173 bX
17174 ˇbbX
17175 ˇb
17176 cccc"
17177 },
17178 false,
17179 );
17180 assert_eq!(editor.text(cx), expected_text);
17181 assert_eq!(
17182 editor.selections.ranges(&editor.display_snapshot(cx)),
17183 expected_selections
17184 .iter()
17185 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
17186 .collect::<Vec<_>>()
17187 );
17188 });
17189}
17190
17191#[gpui::test]
17192fn test_refresh_selections(cx: &mut TestAppContext) {
17193 init_test(cx, |_| {});
17194
17195 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17196 let mut excerpt1_id = None;
17197 let multibuffer = cx.new(|cx| {
17198 let mut multibuffer = MultiBuffer::new(ReadWrite);
17199 excerpt1_id = multibuffer
17200 .push_excerpts(
17201 buffer.clone(),
17202 [
17203 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17204 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17205 ],
17206 cx,
17207 )
17208 .into_iter()
17209 .next();
17210 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17211 multibuffer
17212 });
17213
17214 let editor = cx.add_window(|window, cx| {
17215 let mut editor = build_editor(multibuffer.clone(), window, cx);
17216 let snapshot = editor.snapshot(window, cx);
17217 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17218 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
17219 });
17220 editor.begin_selection(
17221 Point::new(2, 1).to_display_point(&snapshot),
17222 true,
17223 1,
17224 window,
17225 cx,
17226 );
17227 assert_eq!(
17228 editor.selections.ranges(&editor.display_snapshot(cx)),
17229 [
17230 Point::new(1, 3)..Point::new(1, 3),
17231 Point::new(2, 1)..Point::new(2, 1),
17232 ]
17233 );
17234 editor
17235 });
17236
17237 // Refreshing selections is a no-op when excerpts haven't changed.
17238 _ = editor.update(cx, |editor, window, cx| {
17239 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17240 assert_eq!(
17241 editor.selections.ranges(&editor.display_snapshot(cx)),
17242 [
17243 Point::new(1, 3)..Point::new(1, 3),
17244 Point::new(2, 1)..Point::new(2, 1),
17245 ]
17246 );
17247 });
17248
17249 multibuffer.update(cx, |multibuffer, cx| {
17250 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17251 });
17252 _ = editor.update(cx, |editor, window, cx| {
17253 // Removing an excerpt causes the first selection to become degenerate.
17254 assert_eq!(
17255 editor.selections.ranges(&editor.display_snapshot(cx)),
17256 [
17257 Point::new(0, 0)..Point::new(0, 0),
17258 Point::new(0, 1)..Point::new(0, 1)
17259 ]
17260 );
17261
17262 // Refreshing selections will relocate the first selection to the original buffer
17263 // location.
17264 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17265 assert_eq!(
17266 editor.selections.ranges(&editor.display_snapshot(cx)),
17267 [
17268 Point::new(0, 1)..Point::new(0, 1),
17269 Point::new(0, 3)..Point::new(0, 3)
17270 ]
17271 );
17272 assert!(editor.selections.pending_anchor().is_some());
17273 });
17274}
17275
17276#[gpui::test]
17277fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
17278 init_test(cx, |_| {});
17279
17280 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17281 let mut excerpt1_id = None;
17282 let multibuffer = cx.new(|cx| {
17283 let mut multibuffer = MultiBuffer::new(ReadWrite);
17284 excerpt1_id = multibuffer
17285 .push_excerpts(
17286 buffer.clone(),
17287 [
17288 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17289 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17290 ],
17291 cx,
17292 )
17293 .into_iter()
17294 .next();
17295 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17296 multibuffer
17297 });
17298
17299 let editor = cx.add_window(|window, cx| {
17300 let mut editor = build_editor(multibuffer.clone(), window, cx);
17301 let snapshot = editor.snapshot(window, cx);
17302 editor.begin_selection(
17303 Point::new(1, 3).to_display_point(&snapshot),
17304 false,
17305 1,
17306 window,
17307 cx,
17308 );
17309 assert_eq!(
17310 editor.selections.ranges(&editor.display_snapshot(cx)),
17311 [Point::new(1, 3)..Point::new(1, 3)]
17312 );
17313 editor
17314 });
17315
17316 multibuffer.update(cx, |multibuffer, cx| {
17317 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17318 });
17319 _ = editor.update(cx, |editor, window, cx| {
17320 assert_eq!(
17321 editor.selections.ranges(&editor.display_snapshot(cx)),
17322 [Point::new(0, 0)..Point::new(0, 0)]
17323 );
17324
17325 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
17326 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17327 assert_eq!(
17328 editor.selections.ranges(&editor.display_snapshot(cx)),
17329 [Point::new(0, 3)..Point::new(0, 3)]
17330 );
17331 assert!(editor.selections.pending_anchor().is_some());
17332 });
17333}
17334
17335#[gpui::test]
17336async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
17337 init_test(cx, |_| {});
17338
17339 let language = Arc::new(
17340 Language::new(
17341 LanguageConfig {
17342 brackets: BracketPairConfig {
17343 pairs: vec![
17344 BracketPair {
17345 start: "{".to_string(),
17346 end: "}".to_string(),
17347 close: true,
17348 surround: true,
17349 newline: true,
17350 },
17351 BracketPair {
17352 start: "/* ".to_string(),
17353 end: " */".to_string(),
17354 close: true,
17355 surround: true,
17356 newline: true,
17357 },
17358 ],
17359 ..Default::default()
17360 },
17361 ..Default::default()
17362 },
17363 Some(tree_sitter_rust::LANGUAGE.into()),
17364 )
17365 .with_indents_query("")
17366 .unwrap(),
17367 );
17368
17369 let text = concat!(
17370 "{ }\n", //
17371 " x\n", //
17372 " /* */\n", //
17373 "x\n", //
17374 "{{} }\n", //
17375 );
17376
17377 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17378 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17379 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17380 editor
17381 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
17382 .await;
17383
17384 editor.update_in(cx, |editor, window, cx| {
17385 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17386 s.select_display_ranges([
17387 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
17388 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
17389 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
17390 ])
17391 });
17392 editor.newline(&Newline, window, cx);
17393
17394 assert_eq!(
17395 editor.buffer().read(cx).read(cx).text(),
17396 concat!(
17397 "{ \n", // Suppress rustfmt
17398 "\n", //
17399 "}\n", //
17400 " x\n", //
17401 " /* \n", //
17402 " \n", //
17403 " */\n", //
17404 "x\n", //
17405 "{{} \n", //
17406 "}\n", //
17407 )
17408 );
17409 });
17410}
17411
17412#[gpui::test]
17413fn test_highlighted_ranges(cx: &mut TestAppContext) {
17414 init_test(cx, |_| {});
17415
17416 let editor = cx.add_window(|window, cx| {
17417 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
17418 build_editor(buffer, window, cx)
17419 });
17420
17421 _ = editor.update(cx, |editor, window, cx| {
17422 struct Type1;
17423 struct Type2;
17424
17425 let buffer = editor.buffer.read(cx).snapshot(cx);
17426
17427 let anchor_range =
17428 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
17429
17430 editor.highlight_background::<Type1>(
17431 &[
17432 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
17433 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
17434 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
17435 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
17436 ],
17437 |_, _| Hsla::red(),
17438 cx,
17439 );
17440 editor.highlight_background::<Type2>(
17441 &[
17442 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
17443 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
17444 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
17445 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
17446 ],
17447 |_, _| Hsla::green(),
17448 cx,
17449 );
17450
17451 let snapshot = editor.snapshot(window, cx);
17452 let highlighted_ranges = editor.sorted_background_highlights_in_range(
17453 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
17454 &snapshot,
17455 cx.theme(),
17456 );
17457 assert_eq!(
17458 highlighted_ranges,
17459 &[
17460 (
17461 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
17462 Hsla::green(),
17463 ),
17464 (
17465 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
17466 Hsla::red(),
17467 ),
17468 (
17469 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
17470 Hsla::green(),
17471 ),
17472 (
17473 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17474 Hsla::red(),
17475 ),
17476 ]
17477 );
17478 assert_eq!(
17479 editor.sorted_background_highlights_in_range(
17480 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
17481 &snapshot,
17482 cx.theme(),
17483 ),
17484 &[(
17485 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17486 Hsla::red(),
17487 )]
17488 );
17489 });
17490}
17491
17492#[gpui::test]
17493async fn test_following(cx: &mut TestAppContext) {
17494 init_test(cx, |_| {});
17495
17496 let fs = FakeFs::new(cx.executor());
17497 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17498
17499 let buffer = project.update(cx, |project, cx| {
17500 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
17501 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
17502 });
17503 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17504 let follower = cx.update(|cx| {
17505 cx.open_window(
17506 WindowOptions {
17507 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
17508 gpui::Point::new(px(0.), px(0.)),
17509 gpui::Point::new(px(10.), px(80.)),
17510 ))),
17511 ..Default::default()
17512 },
17513 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
17514 )
17515 .unwrap()
17516 });
17517
17518 let is_still_following = Rc::new(RefCell::new(true));
17519 let follower_edit_event_count = Rc::new(RefCell::new(0));
17520 let pending_update = Rc::new(RefCell::new(None));
17521 let leader_entity = leader.root(cx).unwrap();
17522 let follower_entity = follower.root(cx).unwrap();
17523 _ = follower.update(cx, {
17524 let update = pending_update.clone();
17525 let is_still_following = is_still_following.clone();
17526 let follower_edit_event_count = follower_edit_event_count.clone();
17527 |_, window, cx| {
17528 cx.subscribe_in(
17529 &leader_entity,
17530 window,
17531 move |_, leader, event, window, cx| {
17532 leader.read(cx).add_event_to_update_proto(
17533 event,
17534 &mut update.borrow_mut(),
17535 window,
17536 cx,
17537 );
17538 },
17539 )
17540 .detach();
17541
17542 cx.subscribe_in(
17543 &follower_entity,
17544 window,
17545 move |_, _, event: &EditorEvent, _window, _cx| {
17546 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
17547 *is_still_following.borrow_mut() = false;
17548 }
17549
17550 if let EditorEvent::BufferEdited = event {
17551 *follower_edit_event_count.borrow_mut() += 1;
17552 }
17553 },
17554 )
17555 .detach();
17556 }
17557 });
17558
17559 // Update the selections only
17560 _ = leader.update(cx, |leader, window, cx| {
17561 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17562 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17563 });
17564 });
17565 follower
17566 .update(cx, |follower, window, cx| {
17567 follower.apply_update_proto(
17568 &project,
17569 pending_update.borrow_mut().take().unwrap(),
17570 window,
17571 cx,
17572 )
17573 })
17574 .unwrap()
17575 .await
17576 .unwrap();
17577 _ = follower.update(cx, |follower, _, cx| {
17578 assert_eq!(
17579 follower.selections.ranges(&follower.display_snapshot(cx)),
17580 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
17581 );
17582 });
17583 assert!(*is_still_following.borrow());
17584 assert_eq!(*follower_edit_event_count.borrow(), 0);
17585
17586 // Update the scroll position only
17587 _ = leader.update(cx, |leader, window, cx| {
17588 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17589 });
17590 follower
17591 .update(cx, |follower, window, cx| {
17592 follower.apply_update_proto(
17593 &project,
17594 pending_update.borrow_mut().take().unwrap(),
17595 window,
17596 cx,
17597 )
17598 })
17599 .unwrap()
17600 .await
17601 .unwrap();
17602 assert_eq!(
17603 follower
17604 .update(cx, |follower, _, cx| follower.scroll_position(cx))
17605 .unwrap(),
17606 gpui::Point::new(1.5, 3.5)
17607 );
17608 assert!(*is_still_following.borrow());
17609 assert_eq!(*follower_edit_event_count.borrow(), 0);
17610
17611 // Update the selections and scroll position. The follower's scroll position is updated
17612 // via autoscroll, not via the leader's exact scroll position.
17613 _ = leader.update(cx, |leader, window, cx| {
17614 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17615 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
17616 });
17617 leader.request_autoscroll(Autoscroll::newest(), cx);
17618 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17619 });
17620 follower
17621 .update(cx, |follower, window, cx| {
17622 follower.apply_update_proto(
17623 &project,
17624 pending_update.borrow_mut().take().unwrap(),
17625 window,
17626 cx,
17627 )
17628 })
17629 .unwrap()
17630 .await
17631 .unwrap();
17632 _ = follower.update(cx, |follower, _, cx| {
17633 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17634 assert_eq!(
17635 follower.selections.ranges(&follower.display_snapshot(cx)),
17636 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17637 );
17638 });
17639 assert!(*is_still_following.borrow());
17640
17641 // Creating a pending selection that precedes another selection
17642 _ = leader.update(cx, |leader, window, cx| {
17643 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17644 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17645 });
17646 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17647 });
17648 follower
17649 .update(cx, |follower, window, cx| {
17650 follower.apply_update_proto(
17651 &project,
17652 pending_update.borrow_mut().take().unwrap(),
17653 window,
17654 cx,
17655 )
17656 })
17657 .unwrap()
17658 .await
17659 .unwrap();
17660 _ = follower.update(cx, |follower, _, cx| {
17661 assert_eq!(
17662 follower.selections.ranges(&follower.display_snapshot(cx)),
17663 vec![
17664 MultiBufferOffset(0)..MultiBufferOffset(0),
17665 MultiBufferOffset(1)..MultiBufferOffset(1)
17666 ]
17667 );
17668 });
17669 assert!(*is_still_following.borrow());
17670
17671 // Extend the pending selection so that it surrounds another selection
17672 _ = leader.update(cx, |leader, window, cx| {
17673 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17674 });
17675 follower
17676 .update(cx, |follower, window, cx| {
17677 follower.apply_update_proto(
17678 &project,
17679 pending_update.borrow_mut().take().unwrap(),
17680 window,
17681 cx,
17682 )
17683 })
17684 .unwrap()
17685 .await
17686 .unwrap();
17687 _ = follower.update(cx, |follower, _, cx| {
17688 assert_eq!(
17689 follower.selections.ranges(&follower.display_snapshot(cx)),
17690 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17691 );
17692 });
17693
17694 // Scrolling locally breaks the follow
17695 _ = follower.update(cx, |follower, window, cx| {
17696 let top_anchor = follower
17697 .buffer()
17698 .read(cx)
17699 .read(cx)
17700 .anchor_after(MultiBufferOffset(0));
17701 follower.set_scroll_anchor(
17702 ScrollAnchor {
17703 anchor: top_anchor,
17704 offset: gpui::Point::new(0.0, 0.5),
17705 },
17706 window,
17707 cx,
17708 );
17709 });
17710 assert!(!(*is_still_following.borrow()));
17711}
17712
17713#[gpui::test]
17714async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17715 init_test(cx, |_| {});
17716
17717 let fs = FakeFs::new(cx.executor());
17718 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17719 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17720 let pane = workspace
17721 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17722 .unwrap();
17723
17724 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17725
17726 let leader = pane.update_in(cx, |_, window, cx| {
17727 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17728 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17729 });
17730
17731 // Start following the editor when it has no excerpts.
17732 let mut state_message =
17733 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17734 let workspace_entity = workspace.root(cx).unwrap();
17735 let follower_1 = cx
17736 .update_window(*workspace.deref(), |_, window, cx| {
17737 Editor::from_state_proto(
17738 workspace_entity,
17739 ViewId {
17740 creator: CollaboratorId::PeerId(PeerId::default()),
17741 id: 0,
17742 },
17743 &mut state_message,
17744 window,
17745 cx,
17746 )
17747 })
17748 .unwrap()
17749 .unwrap()
17750 .await
17751 .unwrap();
17752
17753 let update_message = Rc::new(RefCell::new(None));
17754 follower_1.update_in(cx, {
17755 let update = update_message.clone();
17756 |_, window, cx| {
17757 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17758 leader.read(cx).add_event_to_update_proto(
17759 event,
17760 &mut update.borrow_mut(),
17761 window,
17762 cx,
17763 );
17764 })
17765 .detach();
17766 }
17767 });
17768
17769 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17770 (
17771 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17772 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17773 )
17774 });
17775
17776 // Insert some excerpts.
17777 leader.update(cx, |leader, cx| {
17778 leader.buffer.update(cx, |multibuffer, cx| {
17779 multibuffer.set_excerpts_for_path(
17780 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17781 buffer_1.clone(),
17782 vec![
17783 Point::row_range(0..3),
17784 Point::row_range(1..6),
17785 Point::row_range(12..15),
17786 ],
17787 0,
17788 cx,
17789 );
17790 multibuffer.set_excerpts_for_path(
17791 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17792 buffer_2.clone(),
17793 vec![Point::row_range(0..6), Point::row_range(8..12)],
17794 0,
17795 cx,
17796 );
17797 });
17798 });
17799
17800 // Apply the update of adding the excerpts.
17801 follower_1
17802 .update_in(cx, |follower, window, cx| {
17803 follower.apply_update_proto(
17804 &project,
17805 update_message.borrow().clone().unwrap(),
17806 window,
17807 cx,
17808 )
17809 })
17810 .await
17811 .unwrap();
17812 assert_eq!(
17813 follower_1.update(cx, |editor, cx| editor.text(cx)),
17814 leader.update(cx, |editor, cx| editor.text(cx))
17815 );
17816 update_message.borrow_mut().take();
17817
17818 // Start following separately after it already has excerpts.
17819 let mut state_message =
17820 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17821 let workspace_entity = workspace.root(cx).unwrap();
17822 let follower_2 = cx
17823 .update_window(*workspace.deref(), |_, window, cx| {
17824 Editor::from_state_proto(
17825 workspace_entity,
17826 ViewId {
17827 creator: CollaboratorId::PeerId(PeerId::default()),
17828 id: 0,
17829 },
17830 &mut state_message,
17831 window,
17832 cx,
17833 )
17834 })
17835 .unwrap()
17836 .unwrap()
17837 .await
17838 .unwrap();
17839 assert_eq!(
17840 follower_2.update(cx, |editor, cx| editor.text(cx)),
17841 leader.update(cx, |editor, cx| editor.text(cx))
17842 );
17843
17844 // Remove some excerpts.
17845 leader.update(cx, |leader, cx| {
17846 leader.buffer.update(cx, |multibuffer, cx| {
17847 let excerpt_ids = multibuffer.excerpt_ids();
17848 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17849 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17850 });
17851 });
17852
17853 // Apply the update of removing the excerpts.
17854 follower_1
17855 .update_in(cx, |follower, window, cx| {
17856 follower.apply_update_proto(
17857 &project,
17858 update_message.borrow().clone().unwrap(),
17859 window,
17860 cx,
17861 )
17862 })
17863 .await
17864 .unwrap();
17865 follower_2
17866 .update_in(cx, |follower, window, cx| {
17867 follower.apply_update_proto(
17868 &project,
17869 update_message.borrow().clone().unwrap(),
17870 window,
17871 cx,
17872 )
17873 })
17874 .await
17875 .unwrap();
17876 update_message.borrow_mut().take();
17877 assert_eq!(
17878 follower_1.update(cx, |editor, cx| editor.text(cx)),
17879 leader.update(cx, |editor, cx| editor.text(cx))
17880 );
17881}
17882
17883#[gpui::test]
17884async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17885 init_test(cx, |_| {});
17886
17887 let mut cx = EditorTestContext::new(cx).await;
17888 let lsp_store =
17889 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17890
17891 cx.set_state(indoc! {"
17892 ˇfn func(abc def: i32) -> u32 {
17893 }
17894 "});
17895
17896 cx.update(|_, cx| {
17897 lsp_store.update(cx, |lsp_store, cx| {
17898 lsp_store
17899 .update_diagnostics(
17900 LanguageServerId(0),
17901 lsp::PublishDiagnosticsParams {
17902 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17903 version: None,
17904 diagnostics: vec![
17905 lsp::Diagnostic {
17906 range: lsp::Range::new(
17907 lsp::Position::new(0, 11),
17908 lsp::Position::new(0, 12),
17909 ),
17910 severity: Some(lsp::DiagnosticSeverity::ERROR),
17911 ..Default::default()
17912 },
17913 lsp::Diagnostic {
17914 range: lsp::Range::new(
17915 lsp::Position::new(0, 12),
17916 lsp::Position::new(0, 15),
17917 ),
17918 severity: Some(lsp::DiagnosticSeverity::ERROR),
17919 ..Default::default()
17920 },
17921 lsp::Diagnostic {
17922 range: lsp::Range::new(
17923 lsp::Position::new(0, 25),
17924 lsp::Position::new(0, 28),
17925 ),
17926 severity: Some(lsp::DiagnosticSeverity::ERROR),
17927 ..Default::default()
17928 },
17929 ],
17930 },
17931 None,
17932 DiagnosticSourceKind::Pushed,
17933 &[],
17934 cx,
17935 )
17936 .unwrap()
17937 });
17938 });
17939
17940 executor.run_until_parked();
17941
17942 cx.update_editor(|editor, window, cx| {
17943 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17944 });
17945
17946 cx.assert_editor_state(indoc! {"
17947 fn func(abc def: i32) -> ˇu32 {
17948 }
17949 "});
17950
17951 cx.update_editor(|editor, window, cx| {
17952 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17953 });
17954
17955 cx.assert_editor_state(indoc! {"
17956 fn func(abc ˇdef: i32) -> u32 {
17957 }
17958 "});
17959
17960 cx.update_editor(|editor, window, cx| {
17961 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17962 });
17963
17964 cx.assert_editor_state(indoc! {"
17965 fn func(abcˇ def: i32) -> u32 {
17966 }
17967 "});
17968
17969 cx.update_editor(|editor, window, cx| {
17970 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17971 });
17972
17973 cx.assert_editor_state(indoc! {"
17974 fn func(abc def: i32) -> ˇu32 {
17975 }
17976 "});
17977}
17978
17979#[gpui::test]
17980async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17981 init_test(cx, |_| {});
17982
17983 let mut cx = EditorTestContext::new(cx).await;
17984
17985 let diff_base = r#"
17986 use some::mod;
17987
17988 const A: u32 = 42;
17989
17990 fn main() {
17991 println!("hello");
17992
17993 println!("world");
17994 }
17995 "#
17996 .unindent();
17997
17998 // Edits are modified, removed, modified, added
17999 cx.set_state(
18000 &r#"
18001 use some::modified;
18002
18003 ˇ
18004 fn main() {
18005 println!("hello there");
18006
18007 println!("around the");
18008 println!("world");
18009 }
18010 "#
18011 .unindent(),
18012 );
18013
18014 cx.set_head_text(&diff_base);
18015 executor.run_until_parked();
18016
18017 cx.update_editor(|editor, window, cx| {
18018 //Wrap around the bottom of the buffer
18019 for _ in 0..3 {
18020 editor.go_to_next_hunk(&GoToHunk, window, cx);
18021 }
18022 });
18023
18024 cx.assert_editor_state(
18025 &r#"
18026 ˇuse some::modified;
18027
18028
18029 fn main() {
18030 println!("hello there");
18031
18032 println!("around the");
18033 println!("world");
18034 }
18035 "#
18036 .unindent(),
18037 );
18038
18039 cx.update_editor(|editor, window, cx| {
18040 //Wrap around the top of the buffer
18041 for _ in 0..2 {
18042 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18043 }
18044 });
18045
18046 cx.assert_editor_state(
18047 &r#"
18048 use some::modified;
18049
18050
18051 fn main() {
18052 ˇ println!("hello there");
18053
18054 println!("around the");
18055 println!("world");
18056 }
18057 "#
18058 .unindent(),
18059 );
18060
18061 cx.update_editor(|editor, window, cx| {
18062 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18063 });
18064
18065 cx.assert_editor_state(
18066 &r#"
18067 use some::modified;
18068
18069 ˇ
18070 fn main() {
18071 println!("hello there");
18072
18073 println!("around the");
18074 println!("world");
18075 }
18076 "#
18077 .unindent(),
18078 );
18079
18080 cx.update_editor(|editor, window, cx| {
18081 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18082 });
18083
18084 cx.assert_editor_state(
18085 &r#"
18086 ˇuse some::modified;
18087
18088
18089 fn main() {
18090 println!("hello there");
18091
18092 println!("around the");
18093 println!("world");
18094 }
18095 "#
18096 .unindent(),
18097 );
18098
18099 cx.update_editor(|editor, window, cx| {
18100 for _ in 0..2 {
18101 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18102 }
18103 });
18104
18105 cx.assert_editor_state(
18106 &r#"
18107 use some::modified;
18108
18109
18110 fn main() {
18111 ˇ println!("hello there");
18112
18113 println!("around the");
18114 println!("world");
18115 }
18116 "#
18117 .unindent(),
18118 );
18119
18120 cx.update_editor(|editor, window, cx| {
18121 editor.fold(&Fold, window, cx);
18122 });
18123
18124 cx.update_editor(|editor, window, cx| {
18125 editor.go_to_next_hunk(&GoToHunk, window, cx);
18126 });
18127
18128 cx.assert_editor_state(
18129 &r#"
18130 ˇuse some::modified;
18131
18132
18133 fn main() {
18134 println!("hello there");
18135
18136 println!("around the");
18137 println!("world");
18138 }
18139 "#
18140 .unindent(),
18141 );
18142}
18143
18144#[test]
18145fn test_split_words() {
18146 fn split(text: &str) -> Vec<&str> {
18147 split_words(text).collect()
18148 }
18149
18150 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
18151 assert_eq!(split("hello_world"), &["hello_", "world"]);
18152 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
18153 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
18154 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
18155 assert_eq!(split("helloworld"), &["helloworld"]);
18156
18157 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
18158}
18159
18160#[test]
18161fn test_split_words_for_snippet_prefix() {
18162 fn split(text: &str) -> Vec<&str> {
18163 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
18164 }
18165
18166 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
18167 assert_eq!(split("hello_world"), &["hello_world"]);
18168 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
18169 assert_eq!(split("Hello_World"), &["Hello_World"]);
18170 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
18171 assert_eq!(split("helloworld"), &["helloworld"]);
18172 assert_eq!(
18173 split("this@is!@#$^many . symbols"),
18174 &[
18175 "symbols",
18176 " symbols",
18177 ". symbols",
18178 " . symbols",
18179 " . symbols",
18180 " . symbols",
18181 "many . symbols",
18182 "^many . symbols",
18183 "$^many . symbols",
18184 "#$^many . symbols",
18185 "@#$^many . symbols",
18186 "!@#$^many . symbols",
18187 "is!@#$^many . symbols",
18188 "@is!@#$^many . symbols",
18189 "this@is!@#$^many . symbols",
18190 ],
18191 );
18192 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
18193}
18194
18195#[gpui::test]
18196async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
18197 init_test(cx, |_| {});
18198
18199 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
18200
18201 #[track_caller]
18202 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
18203 let _state_context = cx.set_state(before);
18204 cx.run_until_parked();
18205 cx.update_editor(|editor, window, cx| {
18206 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
18207 });
18208 cx.run_until_parked();
18209 cx.assert_editor_state(after);
18210 }
18211
18212 // Outside bracket jumps to outside of matching bracket
18213 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
18214 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
18215
18216 // Inside bracket jumps to inside of matching bracket
18217 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
18218 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
18219
18220 // When outside a bracket and inside, favor jumping to the inside bracket
18221 assert(
18222 "console.log('foo', [1, 2, 3]ˇ);",
18223 "console.log('foo', ˇ[1, 2, 3]);",
18224 &mut cx,
18225 );
18226 assert(
18227 "console.log(ˇ'foo', [1, 2, 3]);",
18228 "console.log('foo'ˇ, [1, 2, 3]);",
18229 &mut cx,
18230 );
18231
18232 // Bias forward if two options are equally likely
18233 assert(
18234 "let result = curried_fun()ˇ();",
18235 "let result = curried_fun()()ˇ;",
18236 &mut cx,
18237 );
18238
18239 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
18240 assert(
18241 indoc! {"
18242 function test() {
18243 console.log('test')ˇ
18244 }"},
18245 indoc! {"
18246 function test() {
18247 console.logˇ('test')
18248 }"},
18249 &mut cx,
18250 );
18251}
18252
18253#[gpui::test]
18254async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
18255 init_test(cx, |_| {});
18256 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
18257 language_registry.add(markdown_lang());
18258 language_registry.add(rust_lang());
18259 let buffer = cx.new(|cx| {
18260 let mut buffer = language::Buffer::local(
18261 indoc! {"
18262 ```rs
18263 impl Worktree {
18264 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18265 }
18266 }
18267 ```
18268 "},
18269 cx,
18270 );
18271 buffer.set_language_registry(language_registry.clone());
18272 buffer.set_language(Some(markdown_lang()), cx);
18273 buffer
18274 });
18275 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18276 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
18277 cx.executor().run_until_parked();
18278 _ = editor.update(cx, |editor, window, cx| {
18279 // Case 1: Test outer enclosing brackets
18280 select_ranges(
18281 editor,
18282 &indoc! {"
18283 ```rs
18284 impl Worktree {
18285 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18286 }
18287 }ˇ
18288 ```
18289 "},
18290 window,
18291 cx,
18292 );
18293 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18294 assert_text_with_selections(
18295 editor,
18296 &indoc! {"
18297 ```rs
18298 impl Worktree ˇ{
18299 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18300 }
18301 }
18302 ```
18303 "},
18304 cx,
18305 );
18306 // Case 2: Test inner enclosing brackets
18307 select_ranges(
18308 editor,
18309 &indoc! {"
18310 ```rs
18311 impl Worktree {
18312 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18313 }ˇ
18314 }
18315 ```
18316 "},
18317 window,
18318 cx,
18319 );
18320 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18321 assert_text_with_selections(
18322 editor,
18323 &indoc! {"
18324 ```rs
18325 impl Worktree {
18326 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
18327 }
18328 }
18329 ```
18330 "},
18331 cx,
18332 );
18333 });
18334}
18335
18336#[gpui::test]
18337async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
18338 init_test(cx, |_| {});
18339
18340 let fs = FakeFs::new(cx.executor());
18341 fs.insert_tree(
18342 path!("/a"),
18343 json!({
18344 "main.rs": "fn main() { let a = 5; }",
18345 "other.rs": "// Test file",
18346 }),
18347 )
18348 .await;
18349 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18350
18351 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18352 language_registry.add(Arc::new(Language::new(
18353 LanguageConfig {
18354 name: "Rust".into(),
18355 matcher: LanguageMatcher {
18356 path_suffixes: vec!["rs".to_string()],
18357 ..Default::default()
18358 },
18359 brackets: BracketPairConfig {
18360 pairs: vec![BracketPair {
18361 start: "{".to_string(),
18362 end: "}".to_string(),
18363 close: true,
18364 surround: true,
18365 newline: true,
18366 }],
18367 disabled_scopes_by_bracket_ix: Vec::new(),
18368 },
18369 ..Default::default()
18370 },
18371 Some(tree_sitter_rust::LANGUAGE.into()),
18372 )));
18373 let mut fake_servers = language_registry.register_fake_lsp(
18374 "Rust",
18375 FakeLspAdapter {
18376 capabilities: lsp::ServerCapabilities {
18377 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18378 first_trigger_character: "{".to_string(),
18379 more_trigger_character: None,
18380 }),
18381 ..Default::default()
18382 },
18383 ..Default::default()
18384 },
18385 );
18386
18387 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18388
18389 let cx = &mut VisualTestContext::from_window(*workspace, cx);
18390
18391 let worktree_id = workspace
18392 .update(cx, |workspace, _, cx| {
18393 workspace.project().update(cx, |project, cx| {
18394 project.worktrees(cx).next().unwrap().read(cx).id()
18395 })
18396 })
18397 .unwrap();
18398
18399 let buffer = project
18400 .update(cx, |project, cx| {
18401 project.open_local_buffer(path!("/a/main.rs"), cx)
18402 })
18403 .await
18404 .unwrap();
18405 let editor_handle = workspace
18406 .update(cx, |workspace, window, cx| {
18407 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
18408 })
18409 .unwrap()
18410 .await
18411 .unwrap()
18412 .downcast::<Editor>()
18413 .unwrap();
18414
18415 let fake_server = fake_servers.next().await.unwrap();
18416
18417 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
18418 |params, _| async move {
18419 assert_eq!(
18420 params.text_document_position.text_document.uri,
18421 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
18422 );
18423 assert_eq!(
18424 params.text_document_position.position,
18425 lsp::Position::new(0, 21),
18426 );
18427
18428 Ok(Some(vec![lsp::TextEdit {
18429 new_text: "]".to_string(),
18430 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18431 }]))
18432 },
18433 );
18434
18435 editor_handle.update_in(cx, |editor, window, cx| {
18436 window.focus(&editor.focus_handle(cx), cx);
18437 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18438 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
18439 });
18440 editor.handle_input("{", window, cx);
18441 });
18442
18443 cx.executor().run_until_parked();
18444
18445 buffer.update(cx, |buffer, _| {
18446 assert_eq!(
18447 buffer.text(),
18448 "fn main() { let a = {5}; }",
18449 "No extra braces from on type formatting should appear in the buffer"
18450 )
18451 });
18452}
18453
18454#[gpui::test(iterations = 20, seeds(31))]
18455async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
18456 init_test(cx, |_| {});
18457
18458 let mut cx = EditorLspTestContext::new_rust(
18459 lsp::ServerCapabilities {
18460 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18461 first_trigger_character: ".".to_string(),
18462 more_trigger_character: None,
18463 }),
18464 ..Default::default()
18465 },
18466 cx,
18467 )
18468 .await;
18469
18470 cx.update_buffer(|buffer, _| {
18471 // This causes autoindent to be async.
18472 buffer.set_sync_parse_timeout(None)
18473 });
18474
18475 cx.set_state("fn c() {\n d()ˇ\n}\n");
18476 cx.simulate_keystroke("\n");
18477 cx.run_until_parked();
18478
18479 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
18480 let mut request =
18481 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
18482 let buffer_cloned = buffer_cloned.clone();
18483 async move {
18484 buffer_cloned.update(&mut cx, |buffer, _| {
18485 assert_eq!(
18486 buffer.text(),
18487 "fn c() {\n d()\n .\n}\n",
18488 "OnTypeFormatting should triggered after autoindent applied"
18489 )
18490 });
18491
18492 Ok(Some(vec![]))
18493 }
18494 });
18495
18496 cx.simulate_keystroke(".");
18497 cx.run_until_parked();
18498
18499 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
18500 assert!(request.next().await.is_some());
18501 request.close();
18502 assert!(request.next().await.is_none());
18503}
18504
18505#[gpui::test]
18506async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
18507 init_test(cx, |_| {});
18508
18509 let fs = FakeFs::new(cx.executor());
18510 fs.insert_tree(
18511 path!("/a"),
18512 json!({
18513 "main.rs": "fn main() { let a = 5; }",
18514 "other.rs": "// Test file",
18515 }),
18516 )
18517 .await;
18518
18519 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18520
18521 let server_restarts = Arc::new(AtomicUsize::new(0));
18522 let closure_restarts = Arc::clone(&server_restarts);
18523 let language_server_name = "test language server";
18524 let language_name: LanguageName = "Rust".into();
18525
18526 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18527 language_registry.add(Arc::new(Language::new(
18528 LanguageConfig {
18529 name: language_name.clone(),
18530 matcher: LanguageMatcher {
18531 path_suffixes: vec!["rs".to_string()],
18532 ..Default::default()
18533 },
18534 ..Default::default()
18535 },
18536 Some(tree_sitter_rust::LANGUAGE.into()),
18537 )));
18538 let mut fake_servers = language_registry.register_fake_lsp(
18539 "Rust",
18540 FakeLspAdapter {
18541 name: language_server_name,
18542 initialization_options: Some(json!({
18543 "testOptionValue": true
18544 })),
18545 initializer: Some(Box::new(move |fake_server| {
18546 let task_restarts = Arc::clone(&closure_restarts);
18547 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
18548 task_restarts.fetch_add(1, atomic::Ordering::Release);
18549 futures::future::ready(Ok(()))
18550 });
18551 })),
18552 ..Default::default()
18553 },
18554 );
18555
18556 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18557 let _buffer = project
18558 .update(cx, |project, cx| {
18559 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
18560 })
18561 .await
18562 .unwrap();
18563 let _fake_server = fake_servers.next().await.unwrap();
18564 update_test_language_settings(cx, |language_settings| {
18565 language_settings.languages.0.insert(
18566 language_name.clone().0.to_string(),
18567 LanguageSettingsContent {
18568 tab_size: NonZeroU32::new(8),
18569 ..Default::default()
18570 },
18571 );
18572 });
18573 cx.executor().run_until_parked();
18574 assert_eq!(
18575 server_restarts.load(atomic::Ordering::Acquire),
18576 0,
18577 "Should not restart LSP server on an unrelated change"
18578 );
18579
18580 update_test_project_settings(cx, |project_settings| {
18581 project_settings.lsp.0.insert(
18582 "Some other server name".into(),
18583 LspSettings {
18584 binary: None,
18585 settings: None,
18586 initialization_options: Some(json!({
18587 "some other init value": false
18588 })),
18589 enable_lsp_tasks: false,
18590 fetch: None,
18591 },
18592 );
18593 });
18594 cx.executor().run_until_parked();
18595 assert_eq!(
18596 server_restarts.load(atomic::Ordering::Acquire),
18597 0,
18598 "Should not restart LSP server on an unrelated LSP settings change"
18599 );
18600
18601 update_test_project_settings(cx, |project_settings| {
18602 project_settings.lsp.0.insert(
18603 language_server_name.into(),
18604 LspSettings {
18605 binary: None,
18606 settings: None,
18607 initialization_options: Some(json!({
18608 "anotherInitValue": false
18609 })),
18610 enable_lsp_tasks: false,
18611 fetch: None,
18612 },
18613 );
18614 });
18615 cx.executor().run_until_parked();
18616 assert_eq!(
18617 server_restarts.load(atomic::Ordering::Acquire),
18618 1,
18619 "Should restart LSP server on a related LSP settings change"
18620 );
18621
18622 update_test_project_settings(cx, |project_settings| {
18623 project_settings.lsp.0.insert(
18624 language_server_name.into(),
18625 LspSettings {
18626 binary: None,
18627 settings: None,
18628 initialization_options: Some(json!({
18629 "anotherInitValue": false
18630 })),
18631 enable_lsp_tasks: false,
18632 fetch: None,
18633 },
18634 );
18635 });
18636 cx.executor().run_until_parked();
18637 assert_eq!(
18638 server_restarts.load(atomic::Ordering::Acquire),
18639 1,
18640 "Should not restart LSP server on a related LSP settings change that is the same"
18641 );
18642
18643 update_test_project_settings(cx, |project_settings| {
18644 project_settings.lsp.0.insert(
18645 language_server_name.into(),
18646 LspSettings {
18647 binary: None,
18648 settings: None,
18649 initialization_options: None,
18650 enable_lsp_tasks: false,
18651 fetch: None,
18652 },
18653 );
18654 });
18655 cx.executor().run_until_parked();
18656 assert_eq!(
18657 server_restarts.load(atomic::Ordering::Acquire),
18658 2,
18659 "Should restart LSP server on another related LSP settings change"
18660 );
18661}
18662
18663#[gpui::test]
18664async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18665 init_test(cx, |_| {});
18666
18667 let mut cx = EditorLspTestContext::new_rust(
18668 lsp::ServerCapabilities {
18669 completion_provider: Some(lsp::CompletionOptions {
18670 trigger_characters: Some(vec![".".to_string()]),
18671 resolve_provider: Some(true),
18672 ..Default::default()
18673 }),
18674 ..Default::default()
18675 },
18676 cx,
18677 )
18678 .await;
18679
18680 cx.set_state("fn main() { let a = 2ˇ; }");
18681 cx.simulate_keystroke(".");
18682 let completion_item = lsp::CompletionItem {
18683 label: "some".into(),
18684 kind: Some(lsp::CompletionItemKind::SNIPPET),
18685 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18686 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18687 kind: lsp::MarkupKind::Markdown,
18688 value: "```rust\nSome(2)\n```".to_string(),
18689 })),
18690 deprecated: Some(false),
18691 sort_text: Some("fffffff2".to_string()),
18692 filter_text: Some("some".to_string()),
18693 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18694 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18695 range: lsp::Range {
18696 start: lsp::Position {
18697 line: 0,
18698 character: 22,
18699 },
18700 end: lsp::Position {
18701 line: 0,
18702 character: 22,
18703 },
18704 },
18705 new_text: "Some(2)".to_string(),
18706 })),
18707 additional_text_edits: Some(vec![lsp::TextEdit {
18708 range: lsp::Range {
18709 start: lsp::Position {
18710 line: 0,
18711 character: 20,
18712 },
18713 end: lsp::Position {
18714 line: 0,
18715 character: 22,
18716 },
18717 },
18718 new_text: "".to_string(),
18719 }]),
18720 ..Default::default()
18721 };
18722
18723 let closure_completion_item = completion_item.clone();
18724 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18725 let task_completion_item = closure_completion_item.clone();
18726 async move {
18727 Ok(Some(lsp::CompletionResponse::Array(vec![
18728 task_completion_item,
18729 ])))
18730 }
18731 });
18732
18733 request.next().await;
18734
18735 cx.condition(|editor, _| editor.context_menu_visible())
18736 .await;
18737 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18738 editor
18739 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18740 .unwrap()
18741 });
18742 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18743
18744 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18745 let task_completion_item = completion_item.clone();
18746 async move { Ok(task_completion_item) }
18747 })
18748 .next()
18749 .await
18750 .unwrap();
18751 apply_additional_edits.await.unwrap();
18752 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18753}
18754
18755#[gpui::test]
18756async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18757 init_test(cx, |_| {});
18758
18759 let mut cx = EditorLspTestContext::new_rust(
18760 lsp::ServerCapabilities {
18761 completion_provider: Some(lsp::CompletionOptions {
18762 trigger_characters: Some(vec![".".to_string()]),
18763 resolve_provider: Some(true),
18764 ..Default::default()
18765 }),
18766 ..Default::default()
18767 },
18768 cx,
18769 )
18770 .await;
18771
18772 cx.set_state("fn main() { let a = 2ˇ; }");
18773 cx.simulate_keystroke(".");
18774
18775 let item1 = lsp::CompletionItem {
18776 label: "method id()".to_string(),
18777 filter_text: Some("id".to_string()),
18778 detail: None,
18779 documentation: None,
18780 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18781 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18782 new_text: ".id".to_string(),
18783 })),
18784 ..lsp::CompletionItem::default()
18785 };
18786
18787 let item2 = lsp::CompletionItem {
18788 label: "other".to_string(),
18789 filter_text: Some("other".to_string()),
18790 detail: None,
18791 documentation: None,
18792 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18793 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18794 new_text: ".other".to_string(),
18795 })),
18796 ..lsp::CompletionItem::default()
18797 };
18798
18799 let item1 = item1.clone();
18800 cx.set_request_handler::<lsp::request::Completion, _, _>({
18801 let item1 = item1.clone();
18802 move |_, _, _| {
18803 let item1 = item1.clone();
18804 let item2 = item2.clone();
18805 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
18806 }
18807 })
18808 .next()
18809 .await;
18810
18811 cx.condition(|editor, _| editor.context_menu_visible())
18812 .await;
18813 cx.update_editor(|editor, _, _| {
18814 let context_menu = editor.context_menu.borrow_mut();
18815 let context_menu = context_menu
18816 .as_ref()
18817 .expect("Should have the context menu deployed");
18818 match context_menu {
18819 CodeContextMenu::Completions(completions_menu) => {
18820 let completions = completions_menu.completions.borrow_mut();
18821 assert_eq!(
18822 completions
18823 .iter()
18824 .map(|completion| &completion.label.text)
18825 .collect::<Vec<_>>(),
18826 vec!["method id()", "other"]
18827 )
18828 }
18829 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18830 }
18831 });
18832
18833 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
18834 let item1 = item1.clone();
18835 move |_, item_to_resolve, _| {
18836 let item1 = item1.clone();
18837 async move {
18838 if item1 == item_to_resolve {
18839 Ok(lsp::CompletionItem {
18840 label: "method id()".to_string(),
18841 filter_text: Some("id".to_string()),
18842 detail: Some("Now resolved!".to_string()),
18843 documentation: Some(lsp::Documentation::String("Docs".to_string())),
18844 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18845 range: lsp::Range::new(
18846 lsp::Position::new(0, 22),
18847 lsp::Position::new(0, 22),
18848 ),
18849 new_text: ".id".to_string(),
18850 })),
18851 ..lsp::CompletionItem::default()
18852 })
18853 } else {
18854 Ok(item_to_resolve)
18855 }
18856 }
18857 }
18858 })
18859 .next()
18860 .await
18861 .unwrap();
18862 cx.run_until_parked();
18863
18864 cx.update_editor(|editor, window, cx| {
18865 editor.context_menu_next(&Default::default(), window, cx);
18866 });
18867 cx.run_until_parked();
18868
18869 cx.update_editor(|editor, _, _| {
18870 let context_menu = editor.context_menu.borrow_mut();
18871 let context_menu = context_menu
18872 .as_ref()
18873 .expect("Should have the context menu deployed");
18874 match context_menu {
18875 CodeContextMenu::Completions(completions_menu) => {
18876 let completions = completions_menu.completions.borrow_mut();
18877 assert_eq!(
18878 completions
18879 .iter()
18880 .map(|completion| &completion.label.text)
18881 .collect::<Vec<_>>(),
18882 vec!["method id() Now resolved!", "other"],
18883 "Should update first completion label, but not second as the filter text did not match."
18884 );
18885 }
18886 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18887 }
18888 });
18889}
18890
18891#[gpui::test]
18892async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
18893 init_test(cx, |_| {});
18894 let mut cx = EditorLspTestContext::new_rust(
18895 lsp::ServerCapabilities {
18896 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
18897 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
18898 completion_provider: Some(lsp::CompletionOptions {
18899 resolve_provider: Some(true),
18900 ..Default::default()
18901 }),
18902 ..Default::default()
18903 },
18904 cx,
18905 )
18906 .await;
18907 cx.set_state(indoc! {"
18908 struct TestStruct {
18909 field: i32
18910 }
18911
18912 fn mainˇ() {
18913 let unused_var = 42;
18914 let test_struct = TestStruct { field: 42 };
18915 }
18916 "});
18917 let symbol_range = cx.lsp_range(indoc! {"
18918 struct TestStruct {
18919 field: i32
18920 }
18921
18922 «fn main»() {
18923 let unused_var = 42;
18924 let test_struct = TestStruct { field: 42 };
18925 }
18926 "});
18927 let mut hover_requests =
18928 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18929 Ok(Some(lsp::Hover {
18930 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18931 kind: lsp::MarkupKind::Markdown,
18932 value: "Function documentation".to_string(),
18933 }),
18934 range: Some(symbol_range),
18935 }))
18936 });
18937
18938 // Case 1: Test that code action menu hide hover popover
18939 cx.dispatch_action(Hover);
18940 hover_requests.next().await;
18941 cx.condition(|editor, _| editor.hover_state.visible()).await;
18942 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18943 move |_, _, _| async move {
18944 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18945 lsp::CodeAction {
18946 title: "Remove unused variable".to_string(),
18947 kind: Some(CodeActionKind::QUICKFIX),
18948 edit: Some(lsp::WorkspaceEdit {
18949 changes: Some(
18950 [(
18951 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18952 vec![lsp::TextEdit {
18953 range: lsp::Range::new(
18954 lsp::Position::new(5, 4),
18955 lsp::Position::new(5, 27),
18956 ),
18957 new_text: "".to_string(),
18958 }],
18959 )]
18960 .into_iter()
18961 .collect(),
18962 ),
18963 ..Default::default()
18964 }),
18965 ..Default::default()
18966 },
18967 )]))
18968 },
18969 );
18970 cx.update_editor(|editor, window, cx| {
18971 editor.toggle_code_actions(
18972 &ToggleCodeActions {
18973 deployed_from: None,
18974 quick_launch: false,
18975 },
18976 window,
18977 cx,
18978 );
18979 });
18980 code_action_requests.next().await;
18981 cx.run_until_parked();
18982 cx.condition(|editor, _| editor.context_menu_visible())
18983 .await;
18984 cx.update_editor(|editor, _, _| {
18985 assert!(
18986 !editor.hover_state.visible(),
18987 "Hover popover should be hidden when code action menu is shown"
18988 );
18989 // Hide code actions
18990 editor.context_menu.take();
18991 });
18992
18993 // Case 2: Test that code completions hide hover popover
18994 cx.dispatch_action(Hover);
18995 hover_requests.next().await;
18996 cx.condition(|editor, _| editor.hover_state.visible()).await;
18997 let counter = Arc::new(AtomicUsize::new(0));
18998 let mut completion_requests =
18999 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19000 let counter = counter.clone();
19001 async move {
19002 counter.fetch_add(1, atomic::Ordering::Release);
19003 Ok(Some(lsp::CompletionResponse::Array(vec![
19004 lsp::CompletionItem {
19005 label: "main".into(),
19006 kind: Some(lsp::CompletionItemKind::FUNCTION),
19007 detail: Some("() -> ()".to_string()),
19008 ..Default::default()
19009 },
19010 lsp::CompletionItem {
19011 label: "TestStruct".into(),
19012 kind: Some(lsp::CompletionItemKind::STRUCT),
19013 detail: Some("struct TestStruct".to_string()),
19014 ..Default::default()
19015 },
19016 ])))
19017 }
19018 });
19019 cx.update_editor(|editor, window, cx| {
19020 editor.show_completions(&ShowCompletions, window, cx);
19021 });
19022 completion_requests.next().await;
19023 cx.condition(|editor, _| editor.context_menu_visible())
19024 .await;
19025 cx.update_editor(|editor, _, _| {
19026 assert!(
19027 !editor.hover_state.visible(),
19028 "Hover popover should be hidden when completion menu is shown"
19029 );
19030 });
19031}
19032
19033#[gpui::test]
19034async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
19035 init_test(cx, |_| {});
19036
19037 let mut cx = EditorLspTestContext::new_rust(
19038 lsp::ServerCapabilities {
19039 completion_provider: Some(lsp::CompletionOptions {
19040 trigger_characters: Some(vec![".".to_string()]),
19041 resolve_provider: Some(true),
19042 ..Default::default()
19043 }),
19044 ..Default::default()
19045 },
19046 cx,
19047 )
19048 .await;
19049
19050 cx.set_state("fn main() { let a = 2ˇ; }");
19051 cx.simulate_keystroke(".");
19052
19053 let unresolved_item_1 = lsp::CompletionItem {
19054 label: "id".to_string(),
19055 filter_text: Some("id".to_string()),
19056 detail: None,
19057 documentation: None,
19058 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19059 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19060 new_text: ".id".to_string(),
19061 })),
19062 ..lsp::CompletionItem::default()
19063 };
19064 let resolved_item_1 = lsp::CompletionItem {
19065 additional_text_edits: Some(vec![lsp::TextEdit {
19066 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
19067 new_text: "!!".to_string(),
19068 }]),
19069 ..unresolved_item_1.clone()
19070 };
19071 let unresolved_item_2 = lsp::CompletionItem {
19072 label: "other".to_string(),
19073 filter_text: Some("other".to_string()),
19074 detail: None,
19075 documentation: None,
19076 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19077 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19078 new_text: ".other".to_string(),
19079 })),
19080 ..lsp::CompletionItem::default()
19081 };
19082 let resolved_item_2 = lsp::CompletionItem {
19083 additional_text_edits: Some(vec![lsp::TextEdit {
19084 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
19085 new_text: "??".to_string(),
19086 }]),
19087 ..unresolved_item_2.clone()
19088 };
19089
19090 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
19091 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
19092 cx.lsp
19093 .server
19094 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19095 let unresolved_item_1 = unresolved_item_1.clone();
19096 let resolved_item_1 = resolved_item_1.clone();
19097 let unresolved_item_2 = unresolved_item_2.clone();
19098 let resolved_item_2 = resolved_item_2.clone();
19099 let resolve_requests_1 = resolve_requests_1.clone();
19100 let resolve_requests_2 = resolve_requests_2.clone();
19101 move |unresolved_request, _| {
19102 let unresolved_item_1 = unresolved_item_1.clone();
19103 let resolved_item_1 = resolved_item_1.clone();
19104 let unresolved_item_2 = unresolved_item_2.clone();
19105 let resolved_item_2 = resolved_item_2.clone();
19106 let resolve_requests_1 = resolve_requests_1.clone();
19107 let resolve_requests_2 = resolve_requests_2.clone();
19108 async move {
19109 if unresolved_request == unresolved_item_1 {
19110 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
19111 Ok(resolved_item_1.clone())
19112 } else if unresolved_request == unresolved_item_2 {
19113 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
19114 Ok(resolved_item_2.clone())
19115 } else {
19116 panic!("Unexpected completion item {unresolved_request:?}")
19117 }
19118 }
19119 }
19120 })
19121 .detach();
19122
19123 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19124 let unresolved_item_1 = unresolved_item_1.clone();
19125 let unresolved_item_2 = unresolved_item_2.clone();
19126 async move {
19127 Ok(Some(lsp::CompletionResponse::Array(vec![
19128 unresolved_item_1,
19129 unresolved_item_2,
19130 ])))
19131 }
19132 })
19133 .next()
19134 .await;
19135
19136 cx.condition(|editor, _| editor.context_menu_visible())
19137 .await;
19138 cx.update_editor(|editor, _, _| {
19139 let context_menu = editor.context_menu.borrow_mut();
19140 let context_menu = context_menu
19141 .as_ref()
19142 .expect("Should have the context menu deployed");
19143 match context_menu {
19144 CodeContextMenu::Completions(completions_menu) => {
19145 let completions = completions_menu.completions.borrow_mut();
19146 assert_eq!(
19147 completions
19148 .iter()
19149 .map(|completion| &completion.label.text)
19150 .collect::<Vec<_>>(),
19151 vec!["id", "other"]
19152 )
19153 }
19154 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
19155 }
19156 });
19157 cx.run_until_parked();
19158
19159 cx.update_editor(|editor, window, cx| {
19160 editor.context_menu_next(&ContextMenuNext, window, cx);
19161 });
19162 cx.run_until_parked();
19163 cx.update_editor(|editor, window, cx| {
19164 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19165 });
19166 cx.run_until_parked();
19167 cx.update_editor(|editor, window, cx| {
19168 editor.context_menu_next(&ContextMenuNext, window, cx);
19169 });
19170 cx.run_until_parked();
19171 cx.update_editor(|editor, window, cx| {
19172 editor
19173 .compose_completion(&ComposeCompletion::default(), window, cx)
19174 .expect("No task returned")
19175 })
19176 .await
19177 .expect("Completion failed");
19178 cx.run_until_parked();
19179
19180 cx.update_editor(|editor, _, cx| {
19181 assert_eq!(
19182 resolve_requests_1.load(atomic::Ordering::Acquire),
19183 1,
19184 "Should always resolve once despite multiple selections"
19185 );
19186 assert_eq!(
19187 resolve_requests_2.load(atomic::Ordering::Acquire),
19188 1,
19189 "Should always resolve once after multiple selections and applying the completion"
19190 );
19191 assert_eq!(
19192 editor.text(cx),
19193 "fn main() { let a = ??.other; }",
19194 "Should use resolved data when applying the completion"
19195 );
19196 });
19197}
19198
19199#[gpui::test]
19200async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
19201 init_test(cx, |_| {});
19202
19203 let item_0 = lsp::CompletionItem {
19204 label: "abs".into(),
19205 insert_text: Some("abs".into()),
19206 data: Some(json!({ "very": "special"})),
19207 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
19208 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19209 lsp::InsertReplaceEdit {
19210 new_text: "abs".to_string(),
19211 insert: lsp::Range::default(),
19212 replace: lsp::Range::default(),
19213 },
19214 )),
19215 ..lsp::CompletionItem::default()
19216 };
19217 let items = iter::once(item_0.clone())
19218 .chain((11..51).map(|i| lsp::CompletionItem {
19219 label: format!("item_{}", i),
19220 insert_text: Some(format!("item_{}", i)),
19221 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
19222 ..lsp::CompletionItem::default()
19223 }))
19224 .collect::<Vec<_>>();
19225
19226 let default_commit_characters = vec!["?".to_string()];
19227 let default_data = json!({ "default": "data"});
19228 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
19229 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
19230 let default_edit_range = lsp::Range {
19231 start: lsp::Position {
19232 line: 0,
19233 character: 5,
19234 },
19235 end: lsp::Position {
19236 line: 0,
19237 character: 5,
19238 },
19239 };
19240
19241 let mut cx = EditorLspTestContext::new_rust(
19242 lsp::ServerCapabilities {
19243 completion_provider: Some(lsp::CompletionOptions {
19244 trigger_characters: Some(vec![".".to_string()]),
19245 resolve_provider: Some(true),
19246 ..Default::default()
19247 }),
19248 ..Default::default()
19249 },
19250 cx,
19251 )
19252 .await;
19253
19254 cx.set_state("fn main() { let a = 2ˇ; }");
19255 cx.simulate_keystroke(".");
19256
19257 let completion_data = default_data.clone();
19258 let completion_characters = default_commit_characters.clone();
19259 let completion_items = items.clone();
19260 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19261 let default_data = completion_data.clone();
19262 let default_commit_characters = completion_characters.clone();
19263 let items = completion_items.clone();
19264 async move {
19265 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
19266 items,
19267 item_defaults: Some(lsp::CompletionListItemDefaults {
19268 data: Some(default_data.clone()),
19269 commit_characters: Some(default_commit_characters.clone()),
19270 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
19271 default_edit_range,
19272 )),
19273 insert_text_format: Some(default_insert_text_format),
19274 insert_text_mode: Some(default_insert_text_mode),
19275 }),
19276 ..lsp::CompletionList::default()
19277 })))
19278 }
19279 })
19280 .next()
19281 .await;
19282
19283 let resolved_items = Arc::new(Mutex::new(Vec::new()));
19284 cx.lsp
19285 .server
19286 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19287 let closure_resolved_items = resolved_items.clone();
19288 move |item_to_resolve, _| {
19289 let closure_resolved_items = closure_resolved_items.clone();
19290 async move {
19291 closure_resolved_items.lock().push(item_to_resolve.clone());
19292 Ok(item_to_resolve)
19293 }
19294 }
19295 })
19296 .detach();
19297
19298 cx.condition(|editor, _| editor.context_menu_visible())
19299 .await;
19300 cx.run_until_parked();
19301 cx.update_editor(|editor, _, _| {
19302 let menu = editor.context_menu.borrow_mut();
19303 match menu.as_ref().expect("should have the completions menu") {
19304 CodeContextMenu::Completions(completions_menu) => {
19305 assert_eq!(
19306 completions_menu
19307 .entries
19308 .borrow()
19309 .iter()
19310 .map(|mat| mat.string.clone())
19311 .collect::<Vec<String>>(),
19312 items
19313 .iter()
19314 .map(|completion| completion.label.clone())
19315 .collect::<Vec<String>>()
19316 );
19317 }
19318 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
19319 }
19320 });
19321 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
19322 // with 4 from the end.
19323 assert_eq!(
19324 *resolved_items.lock(),
19325 [&items[0..16], &items[items.len() - 4..items.len()]]
19326 .concat()
19327 .iter()
19328 .cloned()
19329 .map(|mut item| {
19330 if item.data.is_none() {
19331 item.data = Some(default_data.clone());
19332 }
19333 item
19334 })
19335 .collect::<Vec<lsp::CompletionItem>>(),
19336 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
19337 );
19338 resolved_items.lock().clear();
19339
19340 cx.update_editor(|editor, window, cx| {
19341 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19342 });
19343 cx.run_until_parked();
19344 // Completions that have already been resolved are skipped.
19345 assert_eq!(
19346 *resolved_items.lock(),
19347 items[items.len() - 17..items.len() - 4]
19348 .iter()
19349 .cloned()
19350 .map(|mut item| {
19351 if item.data.is_none() {
19352 item.data = Some(default_data.clone());
19353 }
19354 item
19355 })
19356 .collect::<Vec<lsp::CompletionItem>>()
19357 );
19358 resolved_items.lock().clear();
19359}
19360
19361#[gpui::test]
19362async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
19363 init_test(cx, |_| {});
19364
19365 let mut cx = EditorLspTestContext::new(
19366 Language::new(
19367 LanguageConfig {
19368 matcher: LanguageMatcher {
19369 path_suffixes: vec!["jsx".into()],
19370 ..Default::default()
19371 },
19372 overrides: [(
19373 "element".into(),
19374 LanguageConfigOverride {
19375 completion_query_characters: Override::Set(['-'].into_iter().collect()),
19376 ..Default::default()
19377 },
19378 )]
19379 .into_iter()
19380 .collect(),
19381 ..Default::default()
19382 },
19383 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
19384 )
19385 .with_override_query("(jsx_self_closing_element) @element")
19386 .unwrap(),
19387 lsp::ServerCapabilities {
19388 completion_provider: Some(lsp::CompletionOptions {
19389 trigger_characters: Some(vec![":".to_string()]),
19390 ..Default::default()
19391 }),
19392 ..Default::default()
19393 },
19394 cx,
19395 )
19396 .await;
19397
19398 cx.lsp
19399 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19400 Ok(Some(lsp::CompletionResponse::Array(vec![
19401 lsp::CompletionItem {
19402 label: "bg-blue".into(),
19403 ..Default::default()
19404 },
19405 lsp::CompletionItem {
19406 label: "bg-red".into(),
19407 ..Default::default()
19408 },
19409 lsp::CompletionItem {
19410 label: "bg-yellow".into(),
19411 ..Default::default()
19412 },
19413 ])))
19414 });
19415
19416 cx.set_state(r#"<p class="bgˇ" />"#);
19417
19418 // Trigger completion when typing a dash, because the dash is an extra
19419 // word character in the 'element' scope, which contains the cursor.
19420 cx.simulate_keystroke("-");
19421 cx.executor().run_until_parked();
19422 cx.update_editor(|editor, _, _| {
19423 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19424 {
19425 assert_eq!(
19426 completion_menu_entries(menu),
19427 &["bg-blue", "bg-red", "bg-yellow"]
19428 );
19429 } else {
19430 panic!("expected completion menu to be open");
19431 }
19432 });
19433
19434 cx.simulate_keystroke("l");
19435 cx.executor().run_until_parked();
19436 cx.update_editor(|editor, _, _| {
19437 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19438 {
19439 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
19440 } else {
19441 panic!("expected completion menu to be open");
19442 }
19443 });
19444
19445 // When filtering completions, consider the character after the '-' to
19446 // be the start of a subword.
19447 cx.set_state(r#"<p class="yelˇ" />"#);
19448 cx.simulate_keystroke("l");
19449 cx.executor().run_until_parked();
19450 cx.update_editor(|editor, _, _| {
19451 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19452 {
19453 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
19454 } else {
19455 panic!("expected completion menu to be open");
19456 }
19457 });
19458}
19459
19460fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
19461 let entries = menu.entries.borrow();
19462 entries.iter().map(|mat| mat.string.clone()).collect()
19463}
19464
19465#[gpui::test]
19466async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
19467 init_test(cx, |settings| {
19468 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19469 });
19470
19471 let fs = FakeFs::new(cx.executor());
19472 fs.insert_file(path!("/file.ts"), Default::default()).await;
19473
19474 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
19475 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19476
19477 language_registry.add(Arc::new(Language::new(
19478 LanguageConfig {
19479 name: "TypeScript".into(),
19480 matcher: LanguageMatcher {
19481 path_suffixes: vec!["ts".to_string()],
19482 ..Default::default()
19483 },
19484 ..Default::default()
19485 },
19486 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19487 )));
19488 update_test_language_settings(cx, |settings| {
19489 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19490 });
19491
19492 let test_plugin = "test_plugin";
19493 let _ = language_registry.register_fake_lsp(
19494 "TypeScript",
19495 FakeLspAdapter {
19496 prettier_plugins: vec![test_plugin],
19497 ..Default::default()
19498 },
19499 );
19500
19501 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19502 let buffer = project
19503 .update(cx, |project, cx| {
19504 project.open_local_buffer(path!("/file.ts"), cx)
19505 })
19506 .await
19507 .unwrap();
19508
19509 let buffer_text = "one\ntwo\nthree\n";
19510 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19511 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19512 editor.update_in(cx, |editor, window, cx| {
19513 editor.set_text(buffer_text, window, cx)
19514 });
19515
19516 editor
19517 .update_in(cx, |editor, window, cx| {
19518 editor.perform_format(
19519 project.clone(),
19520 FormatTrigger::Manual,
19521 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19522 window,
19523 cx,
19524 )
19525 })
19526 .unwrap()
19527 .await;
19528 assert_eq!(
19529 editor.update(cx, |editor, cx| editor.text(cx)),
19530 buffer_text.to_string() + prettier_format_suffix,
19531 "Test prettier formatting was not applied to the original buffer text",
19532 );
19533
19534 update_test_language_settings(cx, |settings| {
19535 settings.defaults.formatter = Some(FormatterList::default())
19536 });
19537 let format = editor.update_in(cx, |editor, window, cx| {
19538 editor.perform_format(
19539 project.clone(),
19540 FormatTrigger::Manual,
19541 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19542 window,
19543 cx,
19544 )
19545 });
19546 format.await.unwrap();
19547 assert_eq!(
19548 editor.update(cx, |editor, cx| editor.text(cx)),
19549 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
19550 "Autoformatting (via test prettier) was not applied to the original buffer text",
19551 );
19552}
19553
19554#[gpui::test]
19555async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
19556 init_test(cx, |settings| {
19557 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19558 });
19559
19560 let fs = FakeFs::new(cx.executor());
19561 fs.insert_file(path!("/file.settings"), Default::default())
19562 .await;
19563
19564 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
19565 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19566
19567 let ts_lang = Arc::new(Language::new(
19568 LanguageConfig {
19569 name: "TypeScript".into(),
19570 matcher: LanguageMatcher {
19571 path_suffixes: vec!["ts".to_string()],
19572 ..LanguageMatcher::default()
19573 },
19574 prettier_parser_name: Some("typescript".to_string()),
19575 ..LanguageConfig::default()
19576 },
19577 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19578 ));
19579
19580 language_registry.add(ts_lang.clone());
19581
19582 update_test_language_settings(cx, |settings| {
19583 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19584 });
19585
19586 let test_plugin = "test_plugin";
19587 let _ = language_registry.register_fake_lsp(
19588 "TypeScript",
19589 FakeLspAdapter {
19590 prettier_plugins: vec![test_plugin],
19591 ..Default::default()
19592 },
19593 );
19594
19595 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19596 let buffer = project
19597 .update(cx, |project, cx| {
19598 project.open_local_buffer(path!("/file.settings"), cx)
19599 })
19600 .await
19601 .unwrap();
19602
19603 project.update(cx, |project, cx| {
19604 project.set_language_for_buffer(&buffer, ts_lang, cx)
19605 });
19606
19607 let buffer_text = "one\ntwo\nthree\n";
19608 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19609 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19610 editor.update_in(cx, |editor, window, cx| {
19611 editor.set_text(buffer_text, window, cx)
19612 });
19613
19614 editor
19615 .update_in(cx, |editor, window, cx| {
19616 editor.perform_format(
19617 project.clone(),
19618 FormatTrigger::Manual,
19619 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19620 window,
19621 cx,
19622 )
19623 })
19624 .unwrap()
19625 .await;
19626 assert_eq!(
19627 editor.update(cx, |editor, cx| editor.text(cx)),
19628 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
19629 "Test prettier formatting was not applied to the original buffer text",
19630 );
19631
19632 update_test_language_settings(cx, |settings| {
19633 settings.defaults.formatter = Some(FormatterList::default())
19634 });
19635 let format = editor.update_in(cx, |editor, window, cx| {
19636 editor.perform_format(
19637 project.clone(),
19638 FormatTrigger::Manual,
19639 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19640 window,
19641 cx,
19642 )
19643 });
19644 format.await.unwrap();
19645
19646 assert_eq!(
19647 editor.update(cx, |editor, cx| editor.text(cx)),
19648 buffer_text.to_string()
19649 + prettier_format_suffix
19650 + "\ntypescript\n"
19651 + prettier_format_suffix
19652 + "\ntypescript",
19653 "Autoformatting (via test prettier) was not applied to the original buffer text",
19654 );
19655}
19656
19657#[gpui::test]
19658async fn test_addition_reverts(cx: &mut TestAppContext) {
19659 init_test(cx, |_| {});
19660 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19661 let base_text = indoc! {r#"
19662 struct Row;
19663 struct Row1;
19664 struct Row2;
19665
19666 struct Row4;
19667 struct Row5;
19668 struct Row6;
19669
19670 struct Row8;
19671 struct Row9;
19672 struct Row10;"#};
19673
19674 // When addition hunks are not adjacent to carets, no hunk revert is performed
19675 assert_hunk_revert(
19676 indoc! {r#"struct Row;
19677 struct Row1;
19678 struct Row1.1;
19679 struct Row1.2;
19680 struct Row2;ˇ
19681
19682 struct Row4;
19683 struct Row5;
19684 struct Row6;
19685
19686 struct Row8;
19687 ˇstruct Row9;
19688 struct Row9.1;
19689 struct Row9.2;
19690 struct Row9.3;
19691 struct Row10;"#},
19692 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19693 indoc! {r#"struct Row;
19694 struct Row1;
19695 struct Row1.1;
19696 struct Row1.2;
19697 struct Row2;ˇ
19698
19699 struct Row4;
19700 struct Row5;
19701 struct Row6;
19702
19703 struct Row8;
19704 ˇstruct Row9;
19705 struct Row9.1;
19706 struct Row9.2;
19707 struct Row9.3;
19708 struct Row10;"#},
19709 base_text,
19710 &mut cx,
19711 );
19712 // Same for selections
19713 assert_hunk_revert(
19714 indoc! {r#"struct Row;
19715 struct Row1;
19716 struct Row2;
19717 struct Row2.1;
19718 struct Row2.2;
19719 «ˇ
19720 struct Row4;
19721 struct» Row5;
19722 «struct Row6;
19723 ˇ»
19724 struct Row9.1;
19725 struct Row9.2;
19726 struct Row9.3;
19727 struct Row8;
19728 struct Row9;
19729 struct Row10;"#},
19730 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19731 indoc! {r#"struct Row;
19732 struct Row1;
19733 struct Row2;
19734 struct Row2.1;
19735 struct Row2.2;
19736 «ˇ
19737 struct Row4;
19738 struct» Row5;
19739 «struct Row6;
19740 ˇ»
19741 struct Row9.1;
19742 struct Row9.2;
19743 struct Row9.3;
19744 struct Row8;
19745 struct Row9;
19746 struct Row10;"#},
19747 base_text,
19748 &mut cx,
19749 );
19750
19751 // When carets and selections intersect the addition hunks, those are reverted.
19752 // Adjacent carets got merged.
19753 assert_hunk_revert(
19754 indoc! {r#"struct Row;
19755 ˇ// something on the top
19756 struct Row1;
19757 struct Row2;
19758 struct Roˇw3.1;
19759 struct Row2.2;
19760 struct Row2.3;ˇ
19761
19762 struct Row4;
19763 struct ˇRow5.1;
19764 struct Row5.2;
19765 struct «Rowˇ»5.3;
19766 struct Row5;
19767 struct Row6;
19768 ˇ
19769 struct Row9.1;
19770 struct «Rowˇ»9.2;
19771 struct «ˇRow»9.3;
19772 struct Row8;
19773 struct Row9;
19774 «ˇ// something on bottom»
19775 struct Row10;"#},
19776 vec![
19777 DiffHunkStatusKind::Added,
19778 DiffHunkStatusKind::Added,
19779 DiffHunkStatusKind::Added,
19780 DiffHunkStatusKind::Added,
19781 DiffHunkStatusKind::Added,
19782 ],
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}
19798
19799#[gpui::test]
19800async fn test_modification_reverts(cx: &mut TestAppContext) {
19801 init_test(cx, |_| {});
19802 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19803 let base_text = indoc! {r#"
19804 struct Row;
19805 struct Row1;
19806 struct Row2;
19807
19808 struct Row4;
19809 struct Row5;
19810 struct Row6;
19811
19812 struct Row8;
19813 struct Row9;
19814 struct Row10;"#};
19815
19816 // Modification hunks behave the same as the addition ones.
19817 assert_hunk_revert(
19818 indoc! {r#"struct Row;
19819 struct Row1;
19820 struct Row33;
19821 ˇ
19822 struct Row4;
19823 struct Row5;
19824 struct Row6;
19825 ˇ
19826 struct Row99;
19827 struct Row9;
19828 struct Row10;"#},
19829 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19830 indoc! {r#"struct Row;
19831 struct Row1;
19832 struct Row33;
19833 ˇ
19834 struct Row4;
19835 struct Row5;
19836 struct Row6;
19837 ˇ
19838 struct Row99;
19839 struct Row9;
19840 struct Row10;"#},
19841 base_text,
19842 &mut cx,
19843 );
19844 assert_hunk_revert(
19845 indoc! {r#"struct Row;
19846 struct Row1;
19847 struct Row33;
19848 «ˇ
19849 struct Row4;
19850 struct» Row5;
19851 «struct Row6;
19852 ˇ»
19853 struct Row99;
19854 struct Row9;
19855 struct Row10;"#},
19856 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19857 indoc! {r#"struct Row;
19858 struct Row1;
19859 struct Row33;
19860 «ˇ
19861 struct Row4;
19862 struct» Row5;
19863 «struct Row6;
19864 ˇ»
19865 struct Row99;
19866 struct Row9;
19867 struct Row10;"#},
19868 base_text,
19869 &mut cx,
19870 );
19871
19872 assert_hunk_revert(
19873 indoc! {r#"ˇstruct Row1.1;
19874 struct Row1;
19875 «ˇstr»uct Row22;
19876
19877 struct ˇRow44;
19878 struct Row5;
19879 struct «Rˇ»ow66;ˇ
19880
19881 «struˇ»ct Row88;
19882 struct Row9;
19883 struct Row1011;ˇ"#},
19884 vec![
19885 DiffHunkStatusKind::Modified,
19886 DiffHunkStatusKind::Modified,
19887 DiffHunkStatusKind::Modified,
19888 DiffHunkStatusKind::Modified,
19889 DiffHunkStatusKind::Modified,
19890 DiffHunkStatusKind::Modified,
19891 ],
19892 indoc! {r#"struct Row;
19893 ˇstruct Row1;
19894 struct Row2;
19895 ˇ
19896 struct Row4;
19897 ˇstruct Row5;
19898 struct Row6;
19899 ˇ
19900 struct Row8;
19901 ˇstruct Row9;
19902 struct Row10;ˇ"#},
19903 base_text,
19904 &mut cx,
19905 );
19906}
19907
19908#[gpui::test]
19909async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
19910 init_test(cx, |_| {});
19911 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19912 let base_text = indoc! {r#"
19913 one
19914
19915 two
19916 three
19917 "#};
19918
19919 cx.set_head_text(base_text);
19920 cx.set_state("\nˇ\n");
19921 cx.executor().run_until_parked();
19922 cx.update_editor(|editor, _window, cx| {
19923 editor.expand_selected_diff_hunks(cx);
19924 });
19925 cx.executor().run_until_parked();
19926 cx.update_editor(|editor, window, cx| {
19927 editor.backspace(&Default::default(), window, cx);
19928 });
19929 cx.run_until_parked();
19930 cx.assert_state_with_diff(
19931 indoc! {r#"
19932
19933 - two
19934 - threeˇ
19935 +
19936 "#}
19937 .to_string(),
19938 );
19939}
19940
19941#[gpui::test]
19942async fn test_deletion_reverts(cx: &mut TestAppContext) {
19943 init_test(cx, |_| {});
19944 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19945 let base_text = indoc! {r#"struct Row;
19946struct Row1;
19947struct Row2;
19948
19949struct Row4;
19950struct Row5;
19951struct Row6;
19952
19953struct Row8;
19954struct Row9;
19955struct Row10;"#};
19956
19957 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
19958 assert_hunk_revert(
19959 indoc! {r#"struct Row;
19960 struct Row2;
19961
19962 ˇstruct Row4;
19963 struct Row5;
19964 struct Row6;
19965 ˇ
19966 struct Row8;
19967 struct Row10;"#},
19968 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19969 indoc! {r#"struct Row;
19970 struct Row2;
19971
19972 ˇstruct Row4;
19973 struct Row5;
19974 struct Row6;
19975 ˇ
19976 struct Row8;
19977 struct Row10;"#},
19978 base_text,
19979 &mut cx,
19980 );
19981 assert_hunk_revert(
19982 indoc! {r#"struct Row;
19983 struct Row2;
19984
19985 «ˇstruct Row4;
19986 struct» Row5;
19987 «struct Row6;
19988 ˇ»
19989 struct Row8;
19990 struct Row10;"#},
19991 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19992 indoc! {r#"struct Row;
19993 struct Row2;
19994
19995 «ˇstruct Row4;
19996 struct» Row5;
19997 «struct Row6;
19998 ˇ»
19999 struct Row8;
20000 struct Row10;"#},
20001 base_text,
20002 &mut cx,
20003 );
20004
20005 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
20006 assert_hunk_revert(
20007 indoc! {r#"struct Row;
20008 ˇstruct Row2;
20009
20010 struct Row4;
20011 struct Row5;
20012 struct Row6;
20013
20014 struct Row8;ˇ
20015 struct Row10;"#},
20016 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
20017 indoc! {r#"struct Row;
20018 struct Row1;
20019 ˇstruct Row2;
20020
20021 struct Row4;
20022 struct Row5;
20023 struct Row6;
20024
20025 struct Row8;ˇ
20026 struct Row9;
20027 struct Row10;"#},
20028 base_text,
20029 &mut cx,
20030 );
20031 assert_hunk_revert(
20032 indoc! {r#"struct Row;
20033 struct Row2«ˇ;
20034 struct Row4;
20035 struct» Row5;
20036 «struct Row6;
20037
20038 struct Row8;ˇ»
20039 struct Row10;"#},
20040 vec![
20041 DiffHunkStatusKind::Deleted,
20042 DiffHunkStatusKind::Deleted,
20043 DiffHunkStatusKind::Deleted,
20044 ],
20045 indoc! {r#"struct Row;
20046 struct Row1;
20047 struct Row2«ˇ;
20048
20049 struct Row4;
20050 struct» Row5;
20051 «struct Row6;
20052
20053 struct Row8;ˇ»
20054 struct Row9;
20055 struct Row10;"#},
20056 base_text,
20057 &mut cx,
20058 );
20059}
20060
20061#[gpui::test]
20062async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
20063 init_test(cx, |_| {});
20064
20065 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
20066 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
20067 let base_text_3 =
20068 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
20069
20070 let text_1 = edit_first_char_of_every_line(base_text_1);
20071 let text_2 = edit_first_char_of_every_line(base_text_2);
20072 let text_3 = edit_first_char_of_every_line(base_text_3);
20073
20074 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
20075 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
20076 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
20077
20078 let multibuffer = cx.new(|cx| {
20079 let mut multibuffer = MultiBuffer::new(ReadWrite);
20080 multibuffer.push_excerpts(
20081 buffer_1.clone(),
20082 [
20083 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20084 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20085 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20086 ],
20087 cx,
20088 );
20089 multibuffer.push_excerpts(
20090 buffer_2.clone(),
20091 [
20092 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20093 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20094 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20095 ],
20096 cx,
20097 );
20098 multibuffer.push_excerpts(
20099 buffer_3.clone(),
20100 [
20101 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20102 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20103 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20104 ],
20105 cx,
20106 );
20107 multibuffer
20108 });
20109
20110 let fs = FakeFs::new(cx.executor());
20111 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20112 let (editor, cx) = cx
20113 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
20114 editor.update_in(cx, |editor, _window, cx| {
20115 for (buffer, diff_base) in [
20116 (buffer_1.clone(), base_text_1),
20117 (buffer_2.clone(), base_text_2),
20118 (buffer_3.clone(), base_text_3),
20119 ] {
20120 let diff = cx.new(|cx| {
20121 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
20122 });
20123 editor
20124 .buffer
20125 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20126 }
20127 });
20128 cx.executor().run_until_parked();
20129
20130 editor.update_in(cx, |editor, window, cx| {
20131 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}");
20132 editor.select_all(&SelectAll, window, cx);
20133 editor.git_restore(&Default::default(), window, cx);
20134 });
20135 cx.executor().run_until_parked();
20136
20137 // When all ranges are selected, all buffer hunks are reverted.
20138 editor.update(cx, |editor, cx| {
20139 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");
20140 });
20141 buffer_1.update(cx, |buffer, _| {
20142 assert_eq!(buffer.text(), base_text_1);
20143 });
20144 buffer_2.update(cx, |buffer, _| {
20145 assert_eq!(buffer.text(), base_text_2);
20146 });
20147 buffer_3.update(cx, |buffer, _| {
20148 assert_eq!(buffer.text(), base_text_3);
20149 });
20150
20151 editor.update_in(cx, |editor, window, cx| {
20152 editor.undo(&Default::default(), window, cx);
20153 });
20154
20155 editor.update_in(cx, |editor, window, cx| {
20156 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20157 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
20158 });
20159 editor.git_restore(&Default::default(), window, cx);
20160 });
20161
20162 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
20163 // but not affect buffer_2 and its related excerpts.
20164 editor.update(cx, |editor, cx| {
20165 assert_eq!(
20166 editor.text(cx),
20167 "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}"
20168 );
20169 });
20170 buffer_1.update(cx, |buffer, _| {
20171 assert_eq!(buffer.text(), base_text_1);
20172 });
20173 buffer_2.update(cx, |buffer, _| {
20174 assert_eq!(
20175 buffer.text(),
20176 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
20177 );
20178 });
20179 buffer_3.update(cx, |buffer, _| {
20180 assert_eq!(
20181 buffer.text(),
20182 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
20183 );
20184 });
20185
20186 fn edit_first_char_of_every_line(text: &str) -> String {
20187 text.split('\n')
20188 .map(|line| format!("X{}", &line[1..]))
20189 .collect::<Vec<_>>()
20190 .join("\n")
20191 }
20192}
20193
20194#[gpui::test]
20195async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
20196 init_test(cx, |_| {});
20197
20198 let cols = 4;
20199 let rows = 10;
20200 let sample_text_1 = sample_text(rows, cols, 'a');
20201 assert_eq!(
20202 sample_text_1,
20203 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
20204 );
20205 let sample_text_2 = sample_text(rows, cols, 'l');
20206 assert_eq!(
20207 sample_text_2,
20208 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
20209 );
20210 let sample_text_3 = sample_text(rows, cols, 'v');
20211 assert_eq!(
20212 sample_text_3,
20213 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
20214 );
20215
20216 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
20217 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
20218 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
20219
20220 let multi_buffer = cx.new(|cx| {
20221 let mut multibuffer = MultiBuffer::new(ReadWrite);
20222 multibuffer.push_excerpts(
20223 buffer_1.clone(),
20224 [
20225 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20226 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20227 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20228 ],
20229 cx,
20230 );
20231 multibuffer.push_excerpts(
20232 buffer_2.clone(),
20233 [
20234 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20235 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20236 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20237 ],
20238 cx,
20239 );
20240 multibuffer.push_excerpts(
20241 buffer_3.clone(),
20242 [
20243 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20244 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20245 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20246 ],
20247 cx,
20248 );
20249 multibuffer
20250 });
20251
20252 let fs = FakeFs::new(cx.executor());
20253 fs.insert_tree(
20254 "/a",
20255 json!({
20256 "main.rs": sample_text_1,
20257 "other.rs": sample_text_2,
20258 "lib.rs": sample_text_3,
20259 }),
20260 )
20261 .await;
20262 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20263 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20264 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20265 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20266 Editor::new(
20267 EditorMode::full(),
20268 multi_buffer,
20269 Some(project.clone()),
20270 window,
20271 cx,
20272 )
20273 });
20274 let multibuffer_item_id = workspace
20275 .update(cx, |workspace, window, cx| {
20276 assert!(
20277 workspace.active_item(cx).is_none(),
20278 "active item should be None before the first item is added"
20279 );
20280 workspace.add_item_to_active_pane(
20281 Box::new(multi_buffer_editor.clone()),
20282 None,
20283 true,
20284 window,
20285 cx,
20286 );
20287 let active_item = workspace
20288 .active_item(cx)
20289 .expect("should have an active item after adding the multi buffer");
20290 assert_eq!(
20291 active_item.buffer_kind(cx),
20292 ItemBufferKind::Multibuffer,
20293 "A multi buffer was expected to active after adding"
20294 );
20295 active_item.item_id()
20296 })
20297 .unwrap();
20298 cx.executor().run_until_parked();
20299
20300 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20301 editor.change_selections(
20302 SelectionEffects::scroll(Autoscroll::Next),
20303 window,
20304 cx,
20305 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
20306 );
20307 editor.open_excerpts(&OpenExcerpts, window, cx);
20308 });
20309 cx.executor().run_until_parked();
20310 let first_item_id = workspace
20311 .update(cx, |workspace, window, cx| {
20312 let active_item = workspace
20313 .active_item(cx)
20314 .expect("should have an active item after navigating into the 1st buffer");
20315 let first_item_id = active_item.item_id();
20316 assert_ne!(
20317 first_item_id, multibuffer_item_id,
20318 "Should navigate into the 1st buffer and activate it"
20319 );
20320 assert_eq!(
20321 active_item.buffer_kind(cx),
20322 ItemBufferKind::Singleton,
20323 "New active item should be a singleton buffer"
20324 );
20325 assert_eq!(
20326 active_item
20327 .act_as::<Editor>(cx)
20328 .expect("should have navigated into an editor for the 1st buffer")
20329 .read(cx)
20330 .text(cx),
20331 sample_text_1
20332 );
20333
20334 workspace
20335 .go_back(workspace.active_pane().downgrade(), window, cx)
20336 .detach_and_log_err(cx);
20337
20338 first_item_id
20339 })
20340 .unwrap();
20341 cx.executor().run_until_parked();
20342 workspace
20343 .update(cx, |workspace, _, cx| {
20344 let active_item = workspace
20345 .active_item(cx)
20346 .expect("should have an active item after navigating back");
20347 assert_eq!(
20348 active_item.item_id(),
20349 multibuffer_item_id,
20350 "Should navigate back to the multi buffer"
20351 );
20352 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20353 })
20354 .unwrap();
20355
20356 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20357 editor.change_selections(
20358 SelectionEffects::scroll(Autoscroll::Next),
20359 window,
20360 cx,
20361 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
20362 );
20363 editor.open_excerpts(&OpenExcerpts, window, cx);
20364 });
20365 cx.executor().run_until_parked();
20366 let second_item_id = workspace
20367 .update(cx, |workspace, window, cx| {
20368 let active_item = workspace
20369 .active_item(cx)
20370 .expect("should have an active item after navigating into the 2nd buffer");
20371 let second_item_id = active_item.item_id();
20372 assert_ne!(
20373 second_item_id, multibuffer_item_id,
20374 "Should navigate away from the multibuffer"
20375 );
20376 assert_ne!(
20377 second_item_id, first_item_id,
20378 "Should navigate into the 2nd buffer and activate it"
20379 );
20380 assert_eq!(
20381 active_item.buffer_kind(cx),
20382 ItemBufferKind::Singleton,
20383 "New active item should be a singleton buffer"
20384 );
20385 assert_eq!(
20386 active_item
20387 .act_as::<Editor>(cx)
20388 .expect("should have navigated into an editor")
20389 .read(cx)
20390 .text(cx),
20391 sample_text_2
20392 );
20393
20394 workspace
20395 .go_back(workspace.active_pane().downgrade(), window, cx)
20396 .detach_and_log_err(cx);
20397
20398 second_item_id
20399 })
20400 .unwrap();
20401 cx.executor().run_until_parked();
20402 workspace
20403 .update(cx, |workspace, _, cx| {
20404 let active_item = workspace
20405 .active_item(cx)
20406 .expect("should have an active item after navigating back from the 2nd buffer");
20407 assert_eq!(
20408 active_item.item_id(),
20409 multibuffer_item_id,
20410 "Should navigate back from the 2nd buffer to the multi buffer"
20411 );
20412 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20413 })
20414 .unwrap();
20415
20416 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20417 editor.change_selections(
20418 SelectionEffects::scroll(Autoscroll::Next),
20419 window,
20420 cx,
20421 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
20422 );
20423 editor.open_excerpts(&OpenExcerpts, window, cx);
20424 });
20425 cx.executor().run_until_parked();
20426 workspace
20427 .update(cx, |workspace, window, cx| {
20428 let active_item = workspace
20429 .active_item(cx)
20430 .expect("should have an active item after navigating into the 3rd buffer");
20431 let third_item_id = active_item.item_id();
20432 assert_ne!(
20433 third_item_id, multibuffer_item_id,
20434 "Should navigate into the 3rd buffer and activate it"
20435 );
20436 assert_ne!(third_item_id, first_item_id);
20437 assert_ne!(third_item_id, second_item_id);
20438 assert_eq!(
20439 active_item.buffer_kind(cx),
20440 ItemBufferKind::Singleton,
20441 "New active item should be a singleton buffer"
20442 );
20443 assert_eq!(
20444 active_item
20445 .act_as::<Editor>(cx)
20446 .expect("should have navigated into an editor")
20447 .read(cx)
20448 .text(cx),
20449 sample_text_3
20450 );
20451
20452 workspace
20453 .go_back(workspace.active_pane().downgrade(), window, cx)
20454 .detach_and_log_err(cx);
20455 })
20456 .unwrap();
20457 cx.executor().run_until_parked();
20458 workspace
20459 .update(cx, |workspace, _, cx| {
20460 let active_item = workspace
20461 .active_item(cx)
20462 .expect("should have an active item after navigating back from the 3rd buffer");
20463 assert_eq!(
20464 active_item.item_id(),
20465 multibuffer_item_id,
20466 "Should navigate back from the 3rd buffer to the multi buffer"
20467 );
20468 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20469 })
20470 .unwrap();
20471}
20472
20473#[gpui::test]
20474async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20475 init_test(cx, |_| {});
20476
20477 let mut cx = EditorTestContext::new(cx).await;
20478
20479 let diff_base = r#"
20480 use some::mod;
20481
20482 const A: u32 = 42;
20483
20484 fn main() {
20485 println!("hello");
20486
20487 println!("world");
20488 }
20489 "#
20490 .unindent();
20491
20492 cx.set_state(
20493 &r#"
20494 use some::modified;
20495
20496 ˇ
20497 fn main() {
20498 println!("hello there");
20499
20500 println!("around the");
20501 println!("world");
20502 }
20503 "#
20504 .unindent(),
20505 );
20506
20507 cx.set_head_text(&diff_base);
20508 executor.run_until_parked();
20509
20510 cx.update_editor(|editor, window, cx| {
20511 editor.go_to_next_hunk(&GoToHunk, window, cx);
20512 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20513 });
20514 executor.run_until_parked();
20515 cx.assert_state_with_diff(
20516 r#"
20517 use some::modified;
20518
20519
20520 fn main() {
20521 - println!("hello");
20522 + ˇ println!("hello there");
20523
20524 println!("around the");
20525 println!("world");
20526 }
20527 "#
20528 .unindent(),
20529 );
20530
20531 cx.update_editor(|editor, window, cx| {
20532 for _ in 0..2 {
20533 editor.go_to_next_hunk(&GoToHunk, window, cx);
20534 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20535 }
20536 });
20537 executor.run_until_parked();
20538 cx.assert_state_with_diff(
20539 r#"
20540 - use some::mod;
20541 + ˇuse some::modified;
20542
20543
20544 fn main() {
20545 - println!("hello");
20546 + println!("hello there");
20547
20548 + println!("around the");
20549 println!("world");
20550 }
20551 "#
20552 .unindent(),
20553 );
20554
20555 cx.update_editor(|editor, window, cx| {
20556 editor.go_to_next_hunk(&GoToHunk, window, cx);
20557 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20558 });
20559 executor.run_until_parked();
20560 cx.assert_state_with_diff(
20561 r#"
20562 - use some::mod;
20563 + use some::modified;
20564
20565 - const A: u32 = 42;
20566 ˇ
20567 fn main() {
20568 - println!("hello");
20569 + println!("hello there");
20570
20571 + println!("around the");
20572 println!("world");
20573 }
20574 "#
20575 .unindent(),
20576 );
20577
20578 cx.update_editor(|editor, window, cx| {
20579 editor.cancel(&Cancel, window, cx);
20580 });
20581
20582 cx.assert_state_with_diff(
20583 r#"
20584 use some::modified;
20585
20586 ˇ
20587 fn main() {
20588 println!("hello there");
20589
20590 println!("around the");
20591 println!("world");
20592 }
20593 "#
20594 .unindent(),
20595 );
20596}
20597
20598#[gpui::test]
20599async fn test_diff_base_change_with_expanded_diff_hunks(
20600 executor: BackgroundExecutor,
20601 cx: &mut TestAppContext,
20602) {
20603 init_test(cx, |_| {});
20604
20605 let mut cx = EditorTestContext::new(cx).await;
20606
20607 let diff_base = r#"
20608 use some::mod1;
20609 use some::mod2;
20610
20611 const A: u32 = 42;
20612 const B: u32 = 42;
20613 const C: u32 = 42;
20614
20615 fn main() {
20616 println!("hello");
20617
20618 println!("world");
20619 }
20620 "#
20621 .unindent();
20622
20623 cx.set_state(
20624 &r#"
20625 use some::mod2;
20626
20627 const A: u32 = 42;
20628 const C: u32 = 42;
20629
20630 fn main(ˇ) {
20631 //println!("hello");
20632
20633 println!("world");
20634 //
20635 //
20636 }
20637 "#
20638 .unindent(),
20639 );
20640
20641 cx.set_head_text(&diff_base);
20642 executor.run_until_parked();
20643
20644 cx.update_editor(|editor, window, cx| {
20645 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20646 });
20647 executor.run_until_parked();
20648 cx.assert_state_with_diff(
20649 r#"
20650 - use some::mod1;
20651 use some::mod2;
20652
20653 const A: u32 = 42;
20654 - const B: u32 = 42;
20655 const C: u32 = 42;
20656
20657 fn main(ˇ) {
20658 - println!("hello");
20659 + //println!("hello");
20660
20661 println!("world");
20662 + //
20663 + //
20664 }
20665 "#
20666 .unindent(),
20667 );
20668
20669 cx.set_head_text("new diff base!");
20670 executor.run_until_parked();
20671 cx.assert_state_with_diff(
20672 r#"
20673 - new diff base!
20674 + use some::mod2;
20675 +
20676 + const A: u32 = 42;
20677 + const C: u32 = 42;
20678 +
20679 + fn main(ˇ) {
20680 + //println!("hello");
20681 +
20682 + println!("world");
20683 + //
20684 + //
20685 + }
20686 "#
20687 .unindent(),
20688 );
20689}
20690
20691#[gpui::test]
20692async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
20693 init_test(cx, |_| {});
20694
20695 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20696 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20697 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20698 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20699 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
20700 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
20701
20702 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
20703 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
20704 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
20705
20706 let multi_buffer = cx.new(|cx| {
20707 let mut multibuffer = MultiBuffer::new(ReadWrite);
20708 multibuffer.push_excerpts(
20709 buffer_1.clone(),
20710 [
20711 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20712 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20713 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20714 ],
20715 cx,
20716 );
20717 multibuffer.push_excerpts(
20718 buffer_2.clone(),
20719 [
20720 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20721 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20722 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20723 ],
20724 cx,
20725 );
20726 multibuffer.push_excerpts(
20727 buffer_3.clone(),
20728 [
20729 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20730 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20731 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20732 ],
20733 cx,
20734 );
20735 multibuffer
20736 });
20737
20738 let editor =
20739 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20740 editor
20741 .update(cx, |editor, _window, cx| {
20742 for (buffer, diff_base) in [
20743 (buffer_1.clone(), file_1_old),
20744 (buffer_2.clone(), file_2_old),
20745 (buffer_3.clone(), file_3_old),
20746 ] {
20747 let diff = cx.new(|cx| {
20748 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
20749 });
20750 editor
20751 .buffer
20752 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20753 }
20754 })
20755 .unwrap();
20756
20757 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20758 cx.run_until_parked();
20759
20760 cx.assert_editor_state(
20761 &"
20762 ˇaaa
20763 ccc
20764 ddd
20765
20766 ggg
20767 hhh
20768
20769
20770 lll
20771 mmm
20772 NNN
20773
20774 qqq
20775 rrr
20776
20777 uuu
20778 111
20779 222
20780 333
20781
20782 666
20783 777
20784
20785 000
20786 !!!"
20787 .unindent(),
20788 );
20789
20790 cx.update_editor(|editor, window, cx| {
20791 editor.select_all(&SelectAll, window, cx);
20792 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20793 });
20794 cx.executor().run_until_parked();
20795
20796 cx.assert_state_with_diff(
20797 "
20798 «aaa
20799 - bbb
20800 ccc
20801 ddd
20802
20803 ggg
20804 hhh
20805
20806
20807 lll
20808 mmm
20809 - nnn
20810 + NNN
20811
20812 qqq
20813 rrr
20814
20815 uuu
20816 111
20817 222
20818 333
20819
20820 + 666
20821 777
20822
20823 000
20824 !!!ˇ»"
20825 .unindent(),
20826 );
20827}
20828
20829#[gpui::test]
20830async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
20831 init_test(cx, |_| {});
20832
20833 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
20834 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
20835
20836 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
20837 let multi_buffer = cx.new(|cx| {
20838 let mut multibuffer = MultiBuffer::new(ReadWrite);
20839 multibuffer.push_excerpts(
20840 buffer.clone(),
20841 [
20842 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
20843 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
20844 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
20845 ],
20846 cx,
20847 );
20848 multibuffer
20849 });
20850
20851 let editor =
20852 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20853 editor
20854 .update(cx, |editor, _window, cx| {
20855 let diff = cx.new(|cx| {
20856 BufferDiff::new_with_base_text(base, &buffer.read(cx).text_snapshot(), cx)
20857 });
20858 editor
20859 .buffer
20860 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
20861 })
20862 .unwrap();
20863
20864 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20865 cx.run_until_parked();
20866
20867 cx.update_editor(|editor, window, cx| {
20868 editor.expand_all_diff_hunks(&Default::default(), window, cx)
20869 });
20870 cx.executor().run_until_parked();
20871
20872 // When the start of a hunk coincides with the start of its excerpt,
20873 // the hunk is expanded. When the start of a hunk is earlier than
20874 // the start of its excerpt, the hunk is not expanded.
20875 cx.assert_state_with_diff(
20876 "
20877 ˇaaa
20878 - bbb
20879 + BBB
20880
20881 - ddd
20882 - eee
20883 + DDD
20884 + EEE
20885 fff
20886
20887 iii
20888 "
20889 .unindent(),
20890 );
20891}
20892
20893#[gpui::test]
20894async fn test_edits_around_expanded_insertion_hunks(
20895 executor: BackgroundExecutor,
20896 cx: &mut TestAppContext,
20897) {
20898 init_test(cx, |_| {});
20899
20900 let mut cx = EditorTestContext::new(cx).await;
20901
20902 let diff_base = r#"
20903 use some::mod1;
20904 use some::mod2;
20905
20906 const A: u32 = 42;
20907
20908 fn main() {
20909 println!("hello");
20910
20911 println!("world");
20912 }
20913 "#
20914 .unindent();
20915 executor.run_until_parked();
20916 cx.set_state(
20917 &r#"
20918 use some::mod1;
20919 use some::mod2;
20920
20921 const A: u32 = 42;
20922 const B: u32 = 42;
20923 const C: u32 = 42;
20924 ˇ
20925
20926 fn main() {
20927 println!("hello");
20928
20929 println!("world");
20930 }
20931 "#
20932 .unindent(),
20933 );
20934
20935 cx.set_head_text(&diff_base);
20936 executor.run_until_parked();
20937
20938 cx.update_editor(|editor, window, cx| {
20939 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20940 });
20941 executor.run_until_parked();
20942
20943 cx.assert_state_with_diff(
20944 r#"
20945 use some::mod1;
20946 use some::mod2;
20947
20948 const A: u32 = 42;
20949 + const B: u32 = 42;
20950 + const C: u32 = 42;
20951 + ˇ
20952
20953 fn main() {
20954 println!("hello");
20955
20956 println!("world");
20957 }
20958 "#
20959 .unindent(),
20960 );
20961
20962 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
20963 executor.run_until_parked();
20964
20965 cx.assert_state_with_diff(
20966 r#"
20967 use some::mod1;
20968 use some::mod2;
20969
20970 const A: u32 = 42;
20971 + const B: u32 = 42;
20972 + const C: u32 = 42;
20973 + const D: u32 = 42;
20974 + ˇ
20975
20976 fn main() {
20977 println!("hello");
20978
20979 println!("world");
20980 }
20981 "#
20982 .unindent(),
20983 );
20984
20985 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
20986 executor.run_until_parked();
20987
20988 cx.assert_state_with_diff(
20989 r#"
20990 use some::mod1;
20991 use some::mod2;
20992
20993 const A: u32 = 42;
20994 + const B: u32 = 42;
20995 + const C: u32 = 42;
20996 + const D: u32 = 42;
20997 + const E: u32 = 42;
20998 + ˇ
20999
21000 fn main() {
21001 println!("hello");
21002
21003 println!("world");
21004 }
21005 "#
21006 .unindent(),
21007 );
21008
21009 cx.update_editor(|editor, window, cx| {
21010 editor.delete_line(&DeleteLine, window, cx);
21011 });
21012 executor.run_until_parked();
21013
21014 cx.assert_state_with_diff(
21015 r#"
21016 use some::mod1;
21017 use some::mod2;
21018
21019 const A: u32 = 42;
21020 + const B: u32 = 42;
21021 + const C: u32 = 42;
21022 + const D: u32 = 42;
21023 + const E: u32 = 42;
21024 ˇ
21025 fn main() {
21026 println!("hello");
21027
21028 println!("world");
21029 }
21030 "#
21031 .unindent(),
21032 );
21033
21034 cx.update_editor(|editor, window, cx| {
21035 editor.move_up(&MoveUp, window, cx);
21036 editor.delete_line(&DeleteLine, window, cx);
21037 editor.move_up(&MoveUp, window, cx);
21038 editor.delete_line(&DeleteLine, window, cx);
21039 editor.move_up(&MoveUp, window, cx);
21040 editor.delete_line(&DeleteLine, 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 ˇ
21051 fn main() {
21052 println!("hello");
21053
21054 println!("world");
21055 }
21056 "#
21057 .unindent(),
21058 );
21059
21060 cx.update_editor(|editor, window, cx| {
21061 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
21062 editor.delete_line(&DeleteLine, window, cx);
21063 });
21064 executor.run_until_parked();
21065 cx.assert_state_with_diff(
21066 r#"
21067 ˇ
21068 fn main() {
21069 println!("hello");
21070
21071 println!("world");
21072 }
21073 "#
21074 .unindent(),
21075 );
21076}
21077
21078#[gpui::test]
21079async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
21080 init_test(cx, |_| {});
21081
21082 let mut cx = EditorTestContext::new(cx).await;
21083 cx.set_head_text(indoc! { "
21084 one
21085 two
21086 three
21087 four
21088 five
21089 "
21090 });
21091 cx.set_state(indoc! { "
21092 one
21093 ˇthree
21094 five
21095 "});
21096 cx.run_until_parked();
21097 cx.update_editor(|editor, window, cx| {
21098 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21099 });
21100 cx.assert_state_with_diff(
21101 indoc! { "
21102 one
21103 - two
21104 ˇthree
21105 - four
21106 five
21107 "}
21108 .to_string(),
21109 );
21110 cx.update_editor(|editor, window, cx| {
21111 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21112 });
21113
21114 cx.assert_state_with_diff(
21115 indoc! { "
21116 one
21117 ˇthree
21118 five
21119 "}
21120 .to_string(),
21121 );
21122
21123 cx.update_editor(|editor, window, cx| {
21124 editor.move_up(&MoveUp, window, cx);
21125 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21126 });
21127 cx.assert_state_with_diff(
21128 indoc! { "
21129 ˇone
21130 - two
21131 three
21132 five
21133 "}
21134 .to_string(),
21135 );
21136
21137 cx.update_editor(|editor, window, cx| {
21138 editor.move_down(&MoveDown, window, cx);
21139 editor.move_down(&MoveDown, window, cx);
21140 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21141 });
21142 cx.assert_state_with_diff(
21143 indoc! { "
21144 one
21145 - two
21146 ˇthree
21147 - four
21148 five
21149 "}
21150 .to_string(),
21151 );
21152
21153 cx.set_state(indoc! { "
21154 one
21155 ˇTWO
21156 three
21157 four
21158 five
21159 "});
21160 cx.run_until_parked();
21161 cx.update_editor(|editor, window, cx| {
21162 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21163 });
21164
21165 cx.assert_state_with_diff(
21166 indoc! { "
21167 one
21168 - two
21169 + ˇTWO
21170 three
21171 four
21172 five
21173 "}
21174 .to_string(),
21175 );
21176 cx.update_editor(|editor, window, cx| {
21177 editor.move_up(&Default::default(), window, cx);
21178 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21179 });
21180 cx.assert_state_with_diff(
21181 indoc! { "
21182 one
21183 ˇTWO
21184 three
21185 four
21186 five
21187 "}
21188 .to_string(),
21189 );
21190}
21191
21192#[gpui::test]
21193async fn test_toggling_adjacent_diff_hunks_2(
21194 executor: BackgroundExecutor,
21195 cx: &mut TestAppContext,
21196) {
21197 init_test(cx, |_| {});
21198
21199 let mut cx = EditorTestContext::new(cx).await;
21200
21201 let diff_base = r#"
21202 lineA
21203 lineB
21204 lineC
21205 lineD
21206 "#
21207 .unindent();
21208
21209 cx.set_state(
21210 &r#"
21211 ˇlineA1
21212 lineB
21213 lineD
21214 "#
21215 .unindent(),
21216 );
21217 cx.set_head_text(&diff_base);
21218 executor.run_until_parked();
21219
21220 cx.update_editor(|editor, window, cx| {
21221 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21222 });
21223 executor.run_until_parked();
21224 cx.assert_state_with_diff(
21225 r#"
21226 - lineA
21227 + ˇlineA1
21228 lineB
21229 lineD
21230 "#
21231 .unindent(),
21232 );
21233
21234 cx.update_editor(|editor, window, cx| {
21235 editor.move_down(&MoveDown, window, cx);
21236 editor.move_right(&MoveRight, window, cx);
21237 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21238 });
21239 executor.run_until_parked();
21240 cx.assert_state_with_diff(
21241 r#"
21242 - lineA
21243 + lineA1
21244 lˇineB
21245 - lineC
21246 lineD
21247 "#
21248 .unindent(),
21249 );
21250}
21251
21252#[gpui::test]
21253async fn test_edits_around_expanded_deletion_hunks(
21254 executor: BackgroundExecutor,
21255 cx: &mut TestAppContext,
21256) {
21257 init_test(cx, |_| {});
21258
21259 let mut cx = EditorTestContext::new(cx).await;
21260
21261 let diff_base = r#"
21262 use some::mod1;
21263 use some::mod2;
21264
21265 const A: u32 = 42;
21266 const B: u32 = 42;
21267 const C: u32 = 42;
21268
21269
21270 fn main() {
21271 println!("hello");
21272
21273 println!("world");
21274 }
21275 "#
21276 .unindent();
21277 executor.run_until_parked();
21278 cx.set_state(
21279 &r#"
21280 use some::mod1;
21281 use some::mod2;
21282
21283 ˇconst B: u32 = 42;
21284 const C: u32 = 42;
21285
21286
21287 fn main() {
21288 println!("hello");
21289
21290 println!("world");
21291 }
21292 "#
21293 .unindent(),
21294 );
21295
21296 cx.set_head_text(&diff_base);
21297 executor.run_until_parked();
21298
21299 cx.update_editor(|editor, window, cx| {
21300 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21301 });
21302 executor.run_until_parked();
21303
21304 cx.assert_state_with_diff(
21305 r#"
21306 use some::mod1;
21307 use some::mod2;
21308
21309 - const A: u32 = 42;
21310 ˇconst B: u32 = 42;
21311 const C: u32 = 42;
21312
21313
21314 fn main() {
21315 println!("hello");
21316
21317 println!("world");
21318 }
21319 "#
21320 .unindent(),
21321 );
21322
21323 cx.update_editor(|editor, window, cx| {
21324 editor.delete_line(&DeleteLine, window, cx);
21325 });
21326 executor.run_until_parked();
21327 cx.assert_state_with_diff(
21328 r#"
21329 use some::mod1;
21330 use some::mod2;
21331
21332 - const A: u32 = 42;
21333 - const B: u32 = 42;
21334 ˇconst C: u32 = 42;
21335
21336
21337 fn main() {
21338 println!("hello");
21339
21340 println!("world");
21341 }
21342 "#
21343 .unindent(),
21344 );
21345
21346 cx.update_editor(|editor, window, cx| {
21347 editor.delete_line(&DeleteLine, window, cx);
21348 });
21349 executor.run_until_parked();
21350 cx.assert_state_with_diff(
21351 r#"
21352 use some::mod1;
21353 use some::mod2;
21354
21355 - const A: u32 = 42;
21356 - const B: u32 = 42;
21357 - const C: u32 = 42;
21358 ˇ
21359
21360 fn main() {
21361 println!("hello");
21362
21363 println!("world");
21364 }
21365 "#
21366 .unindent(),
21367 );
21368
21369 cx.update_editor(|editor, window, cx| {
21370 editor.handle_input("replacement", window, cx);
21371 });
21372 executor.run_until_parked();
21373 cx.assert_state_with_diff(
21374 r#"
21375 use some::mod1;
21376 use some::mod2;
21377
21378 - const A: u32 = 42;
21379 - const B: u32 = 42;
21380 - const C: u32 = 42;
21381 -
21382 + replacementˇ
21383
21384 fn main() {
21385 println!("hello");
21386
21387 println!("world");
21388 }
21389 "#
21390 .unindent(),
21391 );
21392}
21393
21394#[gpui::test]
21395async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21396 init_test(cx, |_| {});
21397
21398 let mut cx = EditorTestContext::new(cx).await;
21399
21400 let base_text = r#"
21401 one
21402 two
21403 three
21404 four
21405 five
21406 "#
21407 .unindent();
21408 executor.run_until_parked();
21409 cx.set_state(
21410 &r#"
21411 one
21412 two
21413 fˇour
21414 five
21415 "#
21416 .unindent(),
21417 );
21418
21419 cx.set_head_text(&base_text);
21420 executor.run_until_parked();
21421
21422 cx.update_editor(|editor, window, cx| {
21423 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21424 });
21425 executor.run_until_parked();
21426
21427 cx.assert_state_with_diff(
21428 r#"
21429 one
21430 two
21431 - three
21432 fˇour
21433 five
21434 "#
21435 .unindent(),
21436 );
21437
21438 cx.update_editor(|editor, window, cx| {
21439 editor.backspace(&Backspace, window, cx);
21440 editor.backspace(&Backspace, window, cx);
21441 });
21442 executor.run_until_parked();
21443 cx.assert_state_with_diff(
21444 r#"
21445 one
21446 two
21447 - threeˇ
21448 - four
21449 + our
21450 five
21451 "#
21452 .unindent(),
21453 );
21454}
21455
21456#[gpui::test]
21457async fn test_edit_after_expanded_modification_hunk(
21458 executor: BackgroundExecutor,
21459 cx: &mut TestAppContext,
21460) {
21461 init_test(cx, |_| {});
21462
21463 let mut cx = EditorTestContext::new(cx).await;
21464
21465 let diff_base = r#"
21466 use some::mod1;
21467 use some::mod2;
21468
21469 const A: u32 = 42;
21470 const B: u32 = 42;
21471 const C: u32 = 42;
21472 const D: u32 = 42;
21473
21474
21475 fn main() {
21476 println!("hello");
21477
21478 println!("world");
21479 }"#
21480 .unindent();
21481
21482 cx.set_state(
21483 &r#"
21484 use some::mod1;
21485 use some::mod2;
21486
21487 const A: u32 = 42;
21488 const B: u32 = 42;
21489 const C: u32 = 43ˇ
21490 const D: u32 = 42;
21491
21492
21493 fn main() {
21494 println!("hello");
21495
21496 println!("world");
21497 }"#
21498 .unindent(),
21499 );
21500
21501 cx.set_head_text(&diff_base);
21502 executor.run_until_parked();
21503 cx.update_editor(|editor, window, cx| {
21504 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21505 });
21506 executor.run_until_parked();
21507
21508 cx.assert_state_with_diff(
21509 r#"
21510 use some::mod1;
21511 use some::mod2;
21512
21513 const A: u32 = 42;
21514 const B: u32 = 42;
21515 - const C: u32 = 42;
21516 + const C: u32 = 43ˇ
21517 const D: u32 = 42;
21518
21519
21520 fn main() {
21521 println!("hello");
21522
21523 println!("world");
21524 }"#
21525 .unindent(),
21526 );
21527
21528 cx.update_editor(|editor, window, cx| {
21529 editor.handle_input("\nnew_line\n", window, cx);
21530 });
21531 executor.run_until_parked();
21532
21533 cx.assert_state_with_diff(
21534 r#"
21535 use some::mod1;
21536 use some::mod2;
21537
21538 const A: u32 = 42;
21539 const B: u32 = 42;
21540 - const C: u32 = 42;
21541 + const C: u32 = 43
21542 + new_line
21543 + ˇ
21544 const D: u32 = 42;
21545
21546
21547 fn main() {
21548 println!("hello");
21549
21550 println!("world");
21551 }"#
21552 .unindent(),
21553 );
21554}
21555
21556#[gpui::test]
21557async fn test_stage_and_unstage_added_file_hunk(
21558 executor: BackgroundExecutor,
21559 cx: &mut TestAppContext,
21560) {
21561 init_test(cx, |_| {});
21562
21563 let mut cx = EditorTestContext::new(cx).await;
21564 cx.update_editor(|editor, _, cx| {
21565 editor.set_expand_all_diff_hunks(cx);
21566 });
21567
21568 let working_copy = r#"
21569 ˇfn main() {
21570 println!("hello, world!");
21571 }
21572 "#
21573 .unindent();
21574
21575 cx.set_state(&working_copy);
21576 executor.run_until_parked();
21577
21578 cx.assert_state_with_diff(
21579 r#"
21580 + ˇfn main() {
21581 + println!("hello, world!");
21582 + }
21583 "#
21584 .unindent(),
21585 );
21586 cx.assert_index_text(None);
21587
21588 cx.update_editor(|editor, window, cx| {
21589 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21590 });
21591 executor.run_until_parked();
21592 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
21593 cx.assert_state_with_diff(
21594 r#"
21595 + ˇfn main() {
21596 + println!("hello, world!");
21597 + }
21598 "#
21599 .unindent(),
21600 );
21601
21602 cx.update_editor(|editor, window, cx| {
21603 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21604 });
21605 executor.run_until_parked();
21606 cx.assert_index_text(None);
21607}
21608
21609async fn setup_indent_guides_editor(
21610 text: &str,
21611 cx: &mut TestAppContext,
21612) -> (BufferId, EditorTestContext) {
21613 init_test(cx, |_| {});
21614
21615 let mut cx = EditorTestContext::new(cx).await;
21616
21617 let buffer_id = cx.update_editor(|editor, window, cx| {
21618 editor.set_text(text, window, cx);
21619 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
21620
21621 buffer_ids[0]
21622 });
21623
21624 (buffer_id, cx)
21625}
21626
21627fn assert_indent_guides(
21628 range: Range<u32>,
21629 expected: Vec<IndentGuide>,
21630 active_indices: Option<Vec<usize>>,
21631 cx: &mut EditorTestContext,
21632) {
21633 let indent_guides = cx.update_editor(|editor, window, cx| {
21634 let snapshot = editor.snapshot(window, cx).display_snapshot;
21635 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
21636 editor,
21637 MultiBufferRow(range.start)..MultiBufferRow(range.end),
21638 true,
21639 &snapshot,
21640 cx,
21641 );
21642
21643 indent_guides.sort_by(|a, b| {
21644 a.depth.cmp(&b.depth).then(
21645 a.start_row
21646 .cmp(&b.start_row)
21647 .then(a.end_row.cmp(&b.end_row)),
21648 )
21649 });
21650 indent_guides
21651 });
21652
21653 if let Some(expected) = active_indices {
21654 let active_indices = cx.update_editor(|editor, window, cx| {
21655 let snapshot = editor.snapshot(window, cx).display_snapshot;
21656 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
21657 });
21658
21659 assert_eq!(
21660 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
21661 expected,
21662 "Active indent guide indices do not match"
21663 );
21664 }
21665
21666 assert_eq!(indent_guides, expected, "Indent guides do not match");
21667}
21668
21669fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
21670 IndentGuide {
21671 buffer_id,
21672 start_row: MultiBufferRow(start_row),
21673 end_row: MultiBufferRow(end_row),
21674 depth,
21675 tab_size: 4,
21676 settings: IndentGuideSettings {
21677 enabled: true,
21678 line_width: 1,
21679 active_line_width: 1,
21680 coloring: IndentGuideColoring::default(),
21681 background_coloring: IndentGuideBackgroundColoring::default(),
21682 },
21683 }
21684}
21685
21686#[gpui::test]
21687async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
21688 let (buffer_id, mut cx) = setup_indent_guides_editor(
21689 &"
21690 fn main() {
21691 let a = 1;
21692 }"
21693 .unindent(),
21694 cx,
21695 )
21696 .await;
21697
21698 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21699}
21700
21701#[gpui::test]
21702async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
21703 let (buffer_id, mut cx) = setup_indent_guides_editor(
21704 &"
21705 fn main() {
21706 let a = 1;
21707 let b = 2;
21708 }"
21709 .unindent(),
21710 cx,
21711 )
21712 .await;
21713
21714 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
21715}
21716
21717#[gpui::test]
21718async fn test_indent_guide_nested(cx: &mut TestAppContext) {
21719 let (buffer_id, mut cx) = setup_indent_guides_editor(
21720 &"
21721 fn main() {
21722 let a = 1;
21723 if a == 3 {
21724 let b = 2;
21725 } else {
21726 let c = 3;
21727 }
21728 }"
21729 .unindent(),
21730 cx,
21731 )
21732 .await;
21733
21734 assert_indent_guides(
21735 0..8,
21736 vec![
21737 indent_guide(buffer_id, 1, 6, 0),
21738 indent_guide(buffer_id, 3, 3, 1),
21739 indent_guide(buffer_id, 5, 5, 1),
21740 ],
21741 None,
21742 &mut cx,
21743 );
21744}
21745
21746#[gpui::test]
21747async fn test_indent_guide_tab(cx: &mut TestAppContext) {
21748 let (buffer_id, mut cx) = setup_indent_guides_editor(
21749 &"
21750 fn main() {
21751 let a = 1;
21752 let b = 2;
21753 let c = 3;
21754 }"
21755 .unindent(),
21756 cx,
21757 )
21758 .await;
21759
21760 assert_indent_guides(
21761 0..5,
21762 vec![
21763 indent_guide(buffer_id, 1, 3, 0),
21764 indent_guide(buffer_id, 2, 2, 1),
21765 ],
21766 None,
21767 &mut cx,
21768 );
21769}
21770
21771#[gpui::test]
21772async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
21773 let (buffer_id, mut cx) = setup_indent_guides_editor(
21774 &"
21775 fn main() {
21776 let a = 1;
21777
21778 let c = 3;
21779 }"
21780 .unindent(),
21781 cx,
21782 )
21783 .await;
21784
21785 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
21786}
21787
21788#[gpui::test]
21789async fn test_indent_guide_complex(cx: &mut TestAppContext) {
21790 let (buffer_id, mut cx) = setup_indent_guides_editor(
21791 &"
21792 fn main() {
21793 let a = 1;
21794
21795 let c = 3;
21796
21797 if a == 3 {
21798 let b = 2;
21799 } else {
21800 let c = 3;
21801 }
21802 }"
21803 .unindent(),
21804 cx,
21805 )
21806 .await;
21807
21808 assert_indent_guides(
21809 0..11,
21810 vec![
21811 indent_guide(buffer_id, 1, 9, 0),
21812 indent_guide(buffer_id, 6, 6, 1),
21813 indent_guide(buffer_id, 8, 8, 1),
21814 ],
21815 None,
21816 &mut cx,
21817 );
21818}
21819
21820#[gpui::test]
21821async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
21822 let (buffer_id, mut cx) = setup_indent_guides_editor(
21823 &"
21824 fn main() {
21825 let a = 1;
21826
21827 let c = 3;
21828
21829 if a == 3 {
21830 let b = 2;
21831 } else {
21832 let c = 3;
21833 }
21834 }"
21835 .unindent(),
21836 cx,
21837 )
21838 .await;
21839
21840 assert_indent_guides(
21841 1..11,
21842 vec![
21843 indent_guide(buffer_id, 1, 9, 0),
21844 indent_guide(buffer_id, 6, 6, 1),
21845 indent_guide(buffer_id, 8, 8, 1),
21846 ],
21847 None,
21848 &mut cx,
21849 );
21850}
21851
21852#[gpui::test]
21853async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
21854 let (buffer_id, mut cx) = setup_indent_guides_editor(
21855 &"
21856 fn main() {
21857 let a = 1;
21858
21859 let c = 3;
21860
21861 if a == 3 {
21862 let b = 2;
21863 } else {
21864 let c = 3;
21865 }
21866 }"
21867 .unindent(),
21868 cx,
21869 )
21870 .await;
21871
21872 assert_indent_guides(
21873 1..10,
21874 vec![
21875 indent_guide(buffer_id, 1, 9, 0),
21876 indent_guide(buffer_id, 6, 6, 1),
21877 indent_guide(buffer_id, 8, 8, 1),
21878 ],
21879 None,
21880 &mut cx,
21881 );
21882}
21883
21884#[gpui::test]
21885async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
21886 let (buffer_id, mut cx) = setup_indent_guides_editor(
21887 &"
21888 fn main() {
21889 if a {
21890 b(
21891 c,
21892 d,
21893 )
21894 } else {
21895 e(
21896 f
21897 )
21898 }
21899 }"
21900 .unindent(),
21901 cx,
21902 )
21903 .await;
21904
21905 assert_indent_guides(
21906 0..11,
21907 vec![
21908 indent_guide(buffer_id, 1, 10, 0),
21909 indent_guide(buffer_id, 2, 5, 1),
21910 indent_guide(buffer_id, 7, 9, 1),
21911 indent_guide(buffer_id, 3, 4, 2),
21912 indent_guide(buffer_id, 8, 8, 2),
21913 ],
21914 None,
21915 &mut cx,
21916 );
21917
21918 cx.update_editor(|editor, window, cx| {
21919 editor.fold_at(MultiBufferRow(2), window, cx);
21920 assert_eq!(
21921 editor.display_text(cx),
21922 "
21923 fn main() {
21924 if a {
21925 b(⋯
21926 )
21927 } else {
21928 e(
21929 f
21930 )
21931 }
21932 }"
21933 .unindent()
21934 );
21935 });
21936
21937 assert_indent_guides(
21938 0..11,
21939 vec![
21940 indent_guide(buffer_id, 1, 10, 0),
21941 indent_guide(buffer_id, 2, 5, 1),
21942 indent_guide(buffer_id, 7, 9, 1),
21943 indent_guide(buffer_id, 8, 8, 2),
21944 ],
21945 None,
21946 &mut cx,
21947 );
21948}
21949
21950#[gpui::test]
21951async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
21952 let (buffer_id, mut cx) = setup_indent_guides_editor(
21953 &"
21954 block1
21955 block2
21956 block3
21957 block4
21958 block2
21959 block1
21960 block1"
21961 .unindent(),
21962 cx,
21963 )
21964 .await;
21965
21966 assert_indent_guides(
21967 1..10,
21968 vec![
21969 indent_guide(buffer_id, 1, 4, 0),
21970 indent_guide(buffer_id, 2, 3, 1),
21971 indent_guide(buffer_id, 3, 3, 2),
21972 ],
21973 None,
21974 &mut cx,
21975 );
21976}
21977
21978#[gpui::test]
21979async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
21980 let (buffer_id, mut cx) = setup_indent_guides_editor(
21981 &"
21982 block1
21983 block2
21984 block3
21985
21986 block1
21987 block1"
21988 .unindent(),
21989 cx,
21990 )
21991 .await;
21992
21993 assert_indent_guides(
21994 0..6,
21995 vec![
21996 indent_guide(buffer_id, 1, 2, 0),
21997 indent_guide(buffer_id, 2, 2, 1),
21998 ],
21999 None,
22000 &mut cx,
22001 );
22002}
22003
22004#[gpui::test]
22005async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
22006 let (buffer_id, mut cx) = setup_indent_guides_editor(
22007 &"
22008 function component() {
22009 \treturn (
22010 \t\t\t
22011 \t\t<div>
22012 \t\t\t<abc></abc>
22013 \t\t</div>
22014 \t)
22015 }"
22016 .unindent(),
22017 cx,
22018 )
22019 .await;
22020
22021 assert_indent_guides(
22022 0..8,
22023 vec![
22024 indent_guide(buffer_id, 1, 6, 0),
22025 indent_guide(buffer_id, 2, 5, 1),
22026 indent_guide(buffer_id, 4, 4, 2),
22027 ],
22028 None,
22029 &mut cx,
22030 );
22031}
22032
22033#[gpui::test]
22034async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
22035 let (buffer_id, mut cx) = setup_indent_guides_editor(
22036 &"
22037 function component() {
22038 \treturn (
22039 \t
22040 \t\t<div>
22041 \t\t\t<abc></abc>
22042 \t\t</div>
22043 \t)
22044 }"
22045 .unindent(),
22046 cx,
22047 )
22048 .await;
22049
22050 assert_indent_guides(
22051 0..8,
22052 vec![
22053 indent_guide(buffer_id, 1, 6, 0),
22054 indent_guide(buffer_id, 2, 5, 1),
22055 indent_guide(buffer_id, 4, 4, 2),
22056 ],
22057 None,
22058 &mut cx,
22059 );
22060}
22061
22062#[gpui::test]
22063async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
22064 let (buffer_id, mut cx) = setup_indent_guides_editor(
22065 &"
22066 block1
22067
22068
22069
22070 block2
22071 "
22072 .unindent(),
22073 cx,
22074 )
22075 .await;
22076
22077 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
22078}
22079
22080#[gpui::test]
22081async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
22082 let (buffer_id, mut cx) = setup_indent_guides_editor(
22083 &"
22084 def a:
22085 \tb = 3
22086 \tif True:
22087 \t\tc = 4
22088 \t\td = 5
22089 \tprint(b)
22090 "
22091 .unindent(),
22092 cx,
22093 )
22094 .await;
22095
22096 assert_indent_guides(
22097 0..6,
22098 vec![
22099 indent_guide(buffer_id, 1, 5, 0),
22100 indent_guide(buffer_id, 3, 4, 1),
22101 ],
22102 None,
22103 &mut cx,
22104 );
22105}
22106
22107#[gpui::test]
22108async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
22109 let (buffer_id, mut cx) = setup_indent_guides_editor(
22110 &"
22111 fn main() {
22112 let a = 1;
22113 }"
22114 .unindent(),
22115 cx,
22116 )
22117 .await;
22118
22119 cx.update_editor(|editor, window, cx| {
22120 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22121 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22122 });
22123 });
22124
22125 assert_indent_guides(
22126 0..3,
22127 vec![indent_guide(buffer_id, 1, 1, 0)],
22128 Some(vec![0]),
22129 &mut cx,
22130 );
22131}
22132
22133#[gpui::test]
22134async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
22135 let (buffer_id, mut cx) = setup_indent_guides_editor(
22136 &"
22137 fn main() {
22138 if 1 == 2 {
22139 let a = 1;
22140 }
22141 }"
22142 .unindent(),
22143 cx,
22144 )
22145 .await;
22146
22147 cx.update_editor(|editor, window, cx| {
22148 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22149 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22150 });
22151 });
22152
22153 assert_indent_guides(
22154 0..4,
22155 vec![
22156 indent_guide(buffer_id, 1, 3, 0),
22157 indent_guide(buffer_id, 2, 2, 1),
22158 ],
22159 Some(vec![1]),
22160 &mut cx,
22161 );
22162
22163 cx.update_editor(|editor, window, cx| {
22164 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22165 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
22166 });
22167 });
22168
22169 assert_indent_guides(
22170 0..4,
22171 vec![
22172 indent_guide(buffer_id, 1, 3, 0),
22173 indent_guide(buffer_id, 2, 2, 1),
22174 ],
22175 Some(vec![1]),
22176 &mut cx,
22177 );
22178
22179 cx.update_editor(|editor, window, cx| {
22180 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22181 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
22182 });
22183 });
22184
22185 assert_indent_guides(
22186 0..4,
22187 vec![
22188 indent_guide(buffer_id, 1, 3, 0),
22189 indent_guide(buffer_id, 2, 2, 1),
22190 ],
22191 Some(vec![0]),
22192 &mut cx,
22193 );
22194}
22195
22196#[gpui::test]
22197async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
22198 let (buffer_id, mut cx) = setup_indent_guides_editor(
22199 &"
22200 fn main() {
22201 let a = 1;
22202
22203 let b = 2;
22204 }"
22205 .unindent(),
22206 cx,
22207 )
22208 .await;
22209
22210 cx.update_editor(|editor, window, cx| {
22211 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22212 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
22213 });
22214 });
22215
22216 assert_indent_guides(
22217 0..5,
22218 vec![indent_guide(buffer_id, 1, 3, 0)],
22219 Some(vec![0]),
22220 &mut cx,
22221 );
22222}
22223
22224#[gpui::test]
22225async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
22226 let (buffer_id, mut cx) = setup_indent_guides_editor(
22227 &"
22228 def m:
22229 a = 1
22230 pass"
22231 .unindent(),
22232 cx,
22233 )
22234 .await;
22235
22236 cx.update_editor(|editor, window, cx| {
22237 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22238 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22239 });
22240 });
22241
22242 assert_indent_guides(
22243 0..3,
22244 vec![indent_guide(buffer_id, 1, 2, 0)],
22245 Some(vec![0]),
22246 &mut cx,
22247 );
22248}
22249
22250#[gpui::test]
22251async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
22252 init_test(cx, |_| {});
22253 let mut cx = EditorTestContext::new(cx).await;
22254 let text = indoc! {
22255 "
22256 impl A {
22257 fn b() {
22258 0;
22259 3;
22260 5;
22261 6;
22262 7;
22263 }
22264 }
22265 "
22266 };
22267 let base_text = indoc! {
22268 "
22269 impl A {
22270 fn b() {
22271 0;
22272 1;
22273 2;
22274 3;
22275 4;
22276 }
22277 fn c() {
22278 5;
22279 6;
22280 7;
22281 }
22282 }
22283 "
22284 };
22285
22286 cx.update_editor(|editor, window, cx| {
22287 editor.set_text(text, window, cx);
22288
22289 editor.buffer().update(cx, |multibuffer, cx| {
22290 let buffer = multibuffer.as_singleton().unwrap();
22291 let diff = cx.new(|cx| {
22292 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
22293 });
22294
22295 multibuffer.set_all_diff_hunks_expanded(cx);
22296 multibuffer.add_diff(diff, cx);
22297
22298 buffer.read(cx).remote_id()
22299 })
22300 });
22301 cx.run_until_parked();
22302
22303 cx.assert_state_with_diff(
22304 indoc! { "
22305 impl A {
22306 fn b() {
22307 0;
22308 - 1;
22309 - 2;
22310 3;
22311 - 4;
22312 - }
22313 - fn c() {
22314 5;
22315 6;
22316 7;
22317 }
22318 }
22319 ˇ"
22320 }
22321 .to_string(),
22322 );
22323
22324 let mut actual_guides = cx.update_editor(|editor, window, cx| {
22325 editor
22326 .snapshot(window, cx)
22327 .buffer_snapshot()
22328 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
22329 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
22330 .collect::<Vec<_>>()
22331 });
22332 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
22333 assert_eq!(
22334 actual_guides,
22335 vec![
22336 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
22337 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
22338 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
22339 ]
22340 );
22341}
22342
22343#[gpui::test]
22344async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22345 init_test(cx, |_| {});
22346 let mut cx = EditorTestContext::new(cx).await;
22347
22348 let diff_base = r#"
22349 a
22350 b
22351 c
22352 "#
22353 .unindent();
22354
22355 cx.set_state(
22356 &r#"
22357 ˇA
22358 b
22359 C
22360 "#
22361 .unindent(),
22362 );
22363 cx.set_head_text(&diff_base);
22364 cx.update_editor(|editor, window, cx| {
22365 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22366 });
22367 executor.run_until_parked();
22368
22369 let both_hunks_expanded = r#"
22370 - a
22371 + ˇA
22372 b
22373 - c
22374 + C
22375 "#
22376 .unindent();
22377
22378 cx.assert_state_with_diff(both_hunks_expanded.clone());
22379
22380 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22381 let snapshot = editor.snapshot(window, cx);
22382 let hunks = editor
22383 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22384 .collect::<Vec<_>>();
22385 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22386 hunks
22387 .into_iter()
22388 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22389 .collect::<Vec<_>>()
22390 });
22391 assert_eq!(hunk_ranges.len(), 2);
22392
22393 cx.update_editor(|editor, _, cx| {
22394 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22395 });
22396 executor.run_until_parked();
22397
22398 let second_hunk_expanded = r#"
22399 ˇA
22400 b
22401 - c
22402 + C
22403 "#
22404 .unindent();
22405
22406 cx.assert_state_with_diff(second_hunk_expanded);
22407
22408 cx.update_editor(|editor, _, cx| {
22409 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22410 });
22411 executor.run_until_parked();
22412
22413 cx.assert_state_with_diff(both_hunks_expanded.clone());
22414
22415 cx.update_editor(|editor, _, cx| {
22416 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22417 });
22418 executor.run_until_parked();
22419
22420 let first_hunk_expanded = r#"
22421 - a
22422 + ˇA
22423 b
22424 C
22425 "#
22426 .unindent();
22427
22428 cx.assert_state_with_diff(first_hunk_expanded);
22429
22430 cx.update_editor(|editor, _, cx| {
22431 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22432 });
22433 executor.run_until_parked();
22434
22435 cx.assert_state_with_diff(both_hunks_expanded);
22436
22437 cx.set_state(
22438 &r#"
22439 ˇA
22440 b
22441 "#
22442 .unindent(),
22443 );
22444 cx.run_until_parked();
22445
22446 // TODO this cursor position seems bad
22447 cx.assert_state_with_diff(
22448 r#"
22449 - ˇa
22450 + A
22451 b
22452 "#
22453 .unindent(),
22454 );
22455
22456 cx.update_editor(|editor, window, cx| {
22457 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22458 });
22459
22460 cx.assert_state_with_diff(
22461 r#"
22462 - ˇa
22463 + A
22464 b
22465 - c
22466 "#
22467 .unindent(),
22468 );
22469
22470 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22471 let snapshot = editor.snapshot(window, cx);
22472 let hunks = editor
22473 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22474 .collect::<Vec<_>>();
22475 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22476 hunks
22477 .into_iter()
22478 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22479 .collect::<Vec<_>>()
22480 });
22481 assert_eq!(hunk_ranges.len(), 2);
22482
22483 cx.update_editor(|editor, _, cx| {
22484 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22485 });
22486 executor.run_until_parked();
22487
22488 cx.assert_state_with_diff(
22489 r#"
22490 - ˇa
22491 + A
22492 b
22493 "#
22494 .unindent(),
22495 );
22496}
22497
22498#[gpui::test]
22499async fn test_toggle_deletion_hunk_at_start_of_file(
22500 executor: BackgroundExecutor,
22501 cx: &mut TestAppContext,
22502) {
22503 init_test(cx, |_| {});
22504 let mut cx = EditorTestContext::new(cx).await;
22505
22506 let diff_base = r#"
22507 a
22508 b
22509 c
22510 "#
22511 .unindent();
22512
22513 cx.set_state(
22514 &r#"
22515 ˇb
22516 c
22517 "#
22518 .unindent(),
22519 );
22520 cx.set_head_text(&diff_base);
22521 cx.update_editor(|editor, window, cx| {
22522 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22523 });
22524 executor.run_until_parked();
22525
22526 let hunk_expanded = r#"
22527 - a
22528 ˇb
22529 c
22530 "#
22531 .unindent();
22532
22533 cx.assert_state_with_diff(hunk_expanded.clone());
22534
22535 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22536 let snapshot = editor.snapshot(window, cx);
22537 let hunks = editor
22538 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22539 .collect::<Vec<_>>();
22540 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22541 hunks
22542 .into_iter()
22543 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22544 .collect::<Vec<_>>()
22545 });
22546 assert_eq!(hunk_ranges.len(), 1);
22547
22548 cx.update_editor(|editor, _, cx| {
22549 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22550 });
22551 executor.run_until_parked();
22552
22553 let hunk_collapsed = r#"
22554 ˇb
22555 c
22556 "#
22557 .unindent();
22558
22559 cx.assert_state_with_diff(hunk_collapsed);
22560
22561 cx.update_editor(|editor, _, cx| {
22562 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22563 });
22564 executor.run_until_parked();
22565
22566 cx.assert_state_with_diff(hunk_expanded);
22567}
22568
22569#[gpui::test]
22570async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
22571 executor: BackgroundExecutor,
22572 cx: &mut TestAppContext,
22573) {
22574 init_test(cx, |_| {});
22575 let mut cx = EditorTestContext::new(cx).await;
22576
22577 cx.set_state("ˇnew\nsecond\nthird\n");
22578 cx.set_head_text("old\nsecond\nthird\n");
22579 cx.update_editor(|editor, window, cx| {
22580 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
22581 });
22582 executor.run_until_parked();
22583 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22584
22585 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
22586 cx.update_editor(|editor, window, cx| {
22587 let snapshot = editor.snapshot(window, cx);
22588 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22589 let hunks = editor
22590 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22591 .collect::<Vec<_>>();
22592 assert_eq!(hunks.len(), 1);
22593 let hunk_range = Anchor::range_in_buffer(excerpt_id, hunks[0].buffer_range.clone());
22594 editor.toggle_single_diff_hunk(hunk_range, cx)
22595 });
22596 executor.run_until_parked();
22597 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
22598
22599 // Keep the editor scrolled to the top so the full hunk remains visible.
22600 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22601}
22602
22603#[gpui::test]
22604async fn test_display_diff_hunks(cx: &mut TestAppContext) {
22605 init_test(cx, |_| {});
22606
22607 let fs = FakeFs::new(cx.executor());
22608 fs.insert_tree(
22609 path!("/test"),
22610 json!({
22611 ".git": {},
22612 "file-1": "ONE\n",
22613 "file-2": "TWO\n",
22614 "file-3": "THREE\n",
22615 }),
22616 )
22617 .await;
22618
22619 fs.set_head_for_repo(
22620 path!("/test/.git").as_ref(),
22621 &[
22622 ("file-1", "one\n".into()),
22623 ("file-2", "two\n".into()),
22624 ("file-3", "three\n".into()),
22625 ],
22626 "deadbeef",
22627 );
22628
22629 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
22630 let mut buffers = vec![];
22631 for i in 1..=3 {
22632 let buffer = project
22633 .update(cx, |project, cx| {
22634 let path = format!(path!("/test/file-{}"), i);
22635 project.open_local_buffer(path, cx)
22636 })
22637 .await
22638 .unwrap();
22639 buffers.push(buffer);
22640 }
22641
22642 let multibuffer = cx.new(|cx| {
22643 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
22644 multibuffer.set_all_diff_hunks_expanded(cx);
22645 for buffer in &buffers {
22646 let snapshot = buffer.read(cx).snapshot();
22647 multibuffer.set_excerpts_for_path(
22648 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
22649 buffer.clone(),
22650 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
22651 2,
22652 cx,
22653 );
22654 }
22655 multibuffer
22656 });
22657
22658 let editor = cx.add_window(|window, cx| {
22659 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
22660 });
22661 cx.run_until_parked();
22662
22663 let snapshot = editor
22664 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22665 .unwrap();
22666 let hunks = snapshot
22667 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
22668 .map(|hunk| match hunk {
22669 DisplayDiffHunk::Unfolded {
22670 display_row_range, ..
22671 } => display_row_range,
22672 DisplayDiffHunk::Folded { .. } => unreachable!(),
22673 })
22674 .collect::<Vec<_>>();
22675 assert_eq!(
22676 hunks,
22677 [
22678 DisplayRow(2)..DisplayRow(4),
22679 DisplayRow(7)..DisplayRow(9),
22680 DisplayRow(12)..DisplayRow(14),
22681 ]
22682 );
22683}
22684
22685#[gpui::test]
22686async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
22687 init_test(cx, |_| {});
22688
22689 let mut cx = EditorTestContext::new(cx).await;
22690 cx.set_head_text(indoc! { "
22691 one
22692 two
22693 three
22694 four
22695 five
22696 "
22697 });
22698 cx.set_index_text(indoc! { "
22699 one
22700 two
22701 three
22702 four
22703 five
22704 "
22705 });
22706 cx.set_state(indoc! {"
22707 one
22708 TWO
22709 ˇTHREE
22710 FOUR
22711 five
22712 "});
22713 cx.run_until_parked();
22714 cx.update_editor(|editor, window, cx| {
22715 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22716 });
22717 cx.run_until_parked();
22718 cx.assert_index_text(Some(indoc! {"
22719 one
22720 TWO
22721 THREE
22722 FOUR
22723 five
22724 "}));
22725 cx.set_state(indoc! { "
22726 one
22727 TWO
22728 ˇTHREE-HUNDRED
22729 FOUR
22730 five
22731 "});
22732 cx.run_until_parked();
22733 cx.update_editor(|editor, window, cx| {
22734 let snapshot = editor.snapshot(window, cx);
22735 let hunks = editor
22736 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22737 .collect::<Vec<_>>();
22738 assert_eq!(hunks.len(), 1);
22739 assert_eq!(
22740 hunks[0].status(),
22741 DiffHunkStatus {
22742 kind: DiffHunkStatusKind::Modified,
22743 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
22744 }
22745 );
22746
22747 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22748 });
22749 cx.run_until_parked();
22750 cx.assert_index_text(Some(indoc! {"
22751 one
22752 TWO
22753 THREE-HUNDRED
22754 FOUR
22755 five
22756 "}));
22757}
22758
22759#[gpui::test]
22760fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
22761 init_test(cx, |_| {});
22762
22763 let editor = cx.add_window(|window, cx| {
22764 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
22765 build_editor(buffer, window, cx)
22766 });
22767
22768 let render_args = Arc::new(Mutex::new(None));
22769 let snapshot = editor
22770 .update(cx, |editor, window, cx| {
22771 let snapshot = editor.buffer().read(cx).snapshot(cx);
22772 let range =
22773 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
22774
22775 struct RenderArgs {
22776 row: MultiBufferRow,
22777 folded: bool,
22778 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
22779 }
22780
22781 let crease = Crease::inline(
22782 range,
22783 FoldPlaceholder::test(),
22784 {
22785 let toggle_callback = render_args.clone();
22786 move |row, folded, callback, _window, _cx| {
22787 *toggle_callback.lock() = Some(RenderArgs {
22788 row,
22789 folded,
22790 callback,
22791 });
22792 div()
22793 }
22794 },
22795 |_row, _folded, _window, _cx| div(),
22796 );
22797
22798 editor.insert_creases(Some(crease), cx);
22799 let snapshot = editor.snapshot(window, cx);
22800 let _div =
22801 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
22802 snapshot
22803 })
22804 .unwrap();
22805
22806 let render_args = render_args.lock().take().unwrap();
22807 assert_eq!(render_args.row, MultiBufferRow(1));
22808 assert!(!render_args.folded);
22809 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22810
22811 cx.update_window(*editor, |_, window, cx| {
22812 (render_args.callback)(true, window, cx)
22813 })
22814 .unwrap();
22815 let snapshot = editor
22816 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22817 .unwrap();
22818 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
22819
22820 cx.update_window(*editor, |_, window, cx| {
22821 (render_args.callback)(false, window, cx)
22822 })
22823 .unwrap();
22824 let snapshot = editor
22825 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22826 .unwrap();
22827 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22828}
22829
22830#[gpui::test]
22831async fn test_input_text(cx: &mut TestAppContext) {
22832 init_test(cx, |_| {});
22833 let mut cx = EditorTestContext::new(cx).await;
22834
22835 cx.set_state(
22836 &r#"ˇone
22837 two
22838
22839 three
22840 fourˇ
22841 five
22842
22843 siˇx"#
22844 .unindent(),
22845 );
22846
22847 cx.dispatch_action(HandleInput(String::new()));
22848 cx.assert_editor_state(
22849 &r#"ˇone
22850 two
22851
22852 three
22853 fourˇ
22854 five
22855
22856 siˇx"#
22857 .unindent(),
22858 );
22859
22860 cx.dispatch_action(HandleInput("AAAA".to_string()));
22861 cx.assert_editor_state(
22862 &r#"AAAAˇone
22863 two
22864
22865 three
22866 fourAAAAˇ
22867 five
22868
22869 siAAAAˇx"#
22870 .unindent(),
22871 );
22872}
22873
22874#[gpui::test]
22875async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22876 init_test(cx, |_| {});
22877
22878 let mut cx = EditorTestContext::new(cx).await;
22879 cx.set_state(
22880 r#"let foo = 1;
22881let foo = 2;
22882let foo = 3;
22883let fooˇ = 4;
22884let foo = 5;
22885let foo = 6;
22886let foo = 7;
22887let foo = 8;
22888let foo = 9;
22889let foo = 10;
22890let foo = 11;
22891let foo = 12;
22892let foo = 13;
22893let foo = 14;
22894let foo = 15;"#,
22895 );
22896
22897 cx.update_editor(|e, window, cx| {
22898 assert_eq!(
22899 e.next_scroll_position,
22900 NextScrollCursorCenterTopBottom::Center,
22901 "Default next scroll direction is center",
22902 );
22903
22904 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22905 assert_eq!(
22906 e.next_scroll_position,
22907 NextScrollCursorCenterTopBottom::Top,
22908 "After center, next scroll direction should be top",
22909 );
22910
22911 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22912 assert_eq!(
22913 e.next_scroll_position,
22914 NextScrollCursorCenterTopBottom::Bottom,
22915 "After top, next scroll direction should be bottom",
22916 );
22917
22918 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22919 assert_eq!(
22920 e.next_scroll_position,
22921 NextScrollCursorCenterTopBottom::Center,
22922 "After bottom, scrolling should start over",
22923 );
22924
22925 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22926 assert_eq!(
22927 e.next_scroll_position,
22928 NextScrollCursorCenterTopBottom::Top,
22929 "Scrolling continues if retriggered fast enough"
22930 );
22931 });
22932
22933 cx.executor()
22934 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22935 cx.executor().run_until_parked();
22936 cx.update_editor(|e, _, _| {
22937 assert_eq!(
22938 e.next_scroll_position,
22939 NextScrollCursorCenterTopBottom::Center,
22940 "If scrolling is not triggered fast enough, it should reset"
22941 );
22942 });
22943}
22944
22945#[gpui::test]
22946async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22947 init_test(cx, |_| {});
22948 let mut cx = EditorLspTestContext::new_rust(
22949 lsp::ServerCapabilities {
22950 definition_provider: Some(lsp::OneOf::Left(true)),
22951 references_provider: Some(lsp::OneOf::Left(true)),
22952 ..lsp::ServerCapabilities::default()
22953 },
22954 cx,
22955 )
22956 .await;
22957
22958 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22959 let go_to_definition = cx
22960 .lsp
22961 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22962 move |params, _| async move {
22963 if empty_go_to_definition {
22964 Ok(None)
22965 } else {
22966 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22967 uri: params.text_document_position_params.text_document.uri,
22968 range: lsp::Range::new(
22969 lsp::Position::new(4, 3),
22970 lsp::Position::new(4, 6),
22971 ),
22972 })))
22973 }
22974 },
22975 );
22976 let references = cx
22977 .lsp
22978 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22979 Ok(Some(vec![lsp::Location {
22980 uri: params.text_document_position.text_document.uri,
22981 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22982 }]))
22983 });
22984 (go_to_definition, references)
22985 };
22986
22987 cx.set_state(
22988 &r#"fn one() {
22989 let mut a = ˇtwo();
22990 }
22991
22992 fn two() {}"#
22993 .unindent(),
22994 );
22995 set_up_lsp_handlers(false, &mut cx);
22996 let navigated = cx
22997 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22998 .await
22999 .expect("Failed to navigate to definition");
23000 assert_eq!(
23001 navigated,
23002 Navigated::Yes,
23003 "Should have navigated to definition from the GetDefinition response"
23004 );
23005 cx.assert_editor_state(
23006 &r#"fn one() {
23007 let mut a = two();
23008 }
23009
23010 fn «twoˇ»() {}"#
23011 .unindent(),
23012 );
23013
23014 let editors = cx.update_workspace(|workspace, _, cx| {
23015 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23016 });
23017 cx.update_editor(|_, _, test_editor_cx| {
23018 assert_eq!(
23019 editors.len(),
23020 1,
23021 "Initially, only one, test, editor should be open in the workspace"
23022 );
23023 assert_eq!(
23024 test_editor_cx.entity(),
23025 editors.last().expect("Asserted len is 1").clone()
23026 );
23027 });
23028
23029 set_up_lsp_handlers(true, &mut cx);
23030 let navigated = cx
23031 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
23032 .await
23033 .expect("Failed to navigate to lookup references");
23034 assert_eq!(
23035 navigated,
23036 Navigated::Yes,
23037 "Should have navigated to references as a fallback after empty GoToDefinition response"
23038 );
23039 // We should not change the selections in the existing file,
23040 // if opening another milti buffer with the references
23041 cx.assert_editor_state(
23042 &r#"fn one() {
23043 let mut a = two();
23044 }
23045
23046 fn «twoˇ»() {}"#
23047 .unindent(),
23048 );
23049 let editors = cx.update_workspace(|workspace, _, cx| {
23050 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23051 });
23052 cx.update_editor(|_, _, test_editor_cx| {
23053 assert_eq!(
23054 editors.len(),
23055 2,
23056 "After falling back to references search, we open a new editor with the results"
23057 );
23058 let references_fallback_text = editors
23059 .into_iter()
23060 .find(|new_editor| *new_editor != test_editor_cx.entity())
23061 .expect("Should have one non-test editor now")
23062 .read(test_editor_cx)
23063 .text(test_editor_cx);
23064 assert_eq!(
23065 references_fallback_text, "fn one() {\n let mut a = two();\n}",
23066 "Should use the range from the references response and not the GoToDefinition one"
23067 );
23068 });
23069}
23070
23071#[gpui::test]
23072async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
23073 init_test(cx, |_| {});
23074 cx.update(|cx| {
23075 let mut editor_settings = EditorSettings::get_global(cx).clone();
23076 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
23077 EditorSettings::override_global(editor_settings, cx);
23078 });
23079 let mut cx = EditorLspTestContext::new_rust(
23080 lsp::ServerCapabilities {
23081 definition_provider: Some(lsp::OneOf::Left(true)),
23082 references_provider: Some(lsp::OneOf::Left(true)),
23083 ..lsp::ServerCapabilities::default()
23084 },
23085 cx,
23086 )
23087 .await;
23088 let original_state = r#"fn one() {
23089 let mut a = ˇtwo();
23090 }
23091
23092 fn two() {}"#
23093 .unindent();
23094 cx.set_state(&original_state);
23095
23096 let mut go_to_definition = cx
23097 .lsp
23098 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
23099 move |_, _| async move { Ok(None) },
23100 );
23101 let _references = cx
23102 .lsp
23103 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
23104 panic!("Should not call for references with no go to definition fallback")
23105 });
23106
23107 let navigated = cx
23108 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
23109 .await
23110 .expect("Failed to navigate to lookup references");
23111 go_to_definition
23112 .next()
23113 .await
23114 .expect("Should have called the go_to_definition handler");
23115
23116 assert_eq!(
23117 navigated,
23118 Navigated::No,
23119 "Should have navigated to references as a fallback after empty GoToDefinition response"
23120 );
23121 cx.assert_editor_state(&original_state);
23122 let editors = cx.update_workspace(|workspace, _, cx| {
23123 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23124 });
23125 cx.update_editor(|_, _, _| {
23126 assert_eq!(
23127 editors.len(),
23128 1,
23129 "After unsuccessful fallback, no other editor should have been opened"
23130 );
23131 });
23132}
23133
23134#[gpui::test]
23135async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
23136 init_test(cx, |_| {});
23137 let mut cx = EditorLspTestContext::new_rust(
23138 lsp::ServerCapabilities {
23139 references_provider: Some(lsp::OneOf::Left(true)),
23140 ..lsp::ServerCapabilities::default()
23141 },
23142 cx,
23143 )
23144 .await;
23145
23146 cx.set_state(
23147 &r#"
23148 fn one() {
23149 let mut a = two();
23150 }
23151
23152 fn ˇtwo() {}"#
23153 .unindent(),
23154 );
23155 cx.lsp
23156 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23157 Ok(Some(vec![
23158 lsp::Location {
23159 uri: params.text_document_position.text_document.uri.clone(),
23160 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
23161 },
23162 lsp::Location {
23163 uri: params.text_document_position.text_document.uri,
23164 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
23165 },
23166 ]))
23167 });
23168 let navigated = cx
23169 .update_editor(|editor, window, cx| {
23170 editor.find_all_references(&FindAllReferences::default(), window, cx)
23171 })
23172 .unwrap()
23173 .await
23174 .expect("Failed to navigate to references");
23175 assert_eq!(
23176 navigated,
23177 Navigated::Yes,
23178 "Should have navigated to references from the FindAllReferences response"
23179 );
23180 cx.assert_editor_state(
23181 &r#"fn one() {
23182 let mut a = two();
23183 }
23184
23185 fn ˇtwo() {}"#
23186 .unindent(),
23187 );
23188
23189 let editors = cx.update_workspace(|workspace, _, cx| {
23190 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23191 });
23192 cx.update_editor(|_, _, _| {
23193 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
23194 });
23195
23196 cx.set_state(
23197 &r#"fn one() {
23198 let mut a = ˇtwo();
23199 }
23200
23201 fn two() {}"#
23202 .unindent(),
23203 );
23204 let navigated = cx
23205 .update_editor(|editor, window, cx| {
23206 editor.find_all_references(&FindAllReferences::default(), window, cx)
23207 })
23208 .unwrap()
23209 .await
23210 .expect("Failed to navigate to references");
23211 assert_eq!(
23212 navigated,
23213 Navigated::Yes,
23214 "Should have navigated to references from the FindAllReferences response"
23215 );
23216 cx.assert_editor_state(
23217 &r#"fn one() {
23218 let mut a = ˇtwo();
23219 }
23220
23221 fn two() {}"#
23222 .unindent(),
23223 );
23224 let editors = cx.update_workspace(|workspace, _, cx| {
23225 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23226 });
23227 cx.update_editor(|_, _, _| {
23228 assert_eq!(
23229 editors.len(),
23230 2,
23231 "should have re-used the previous multibuffer"
23232 );
23233 });
23234
23235 cx.set_state(
23236 &r#"fn one() {
23237 let mut a = ˇtwo();
23238 }
23239 fn three() {}
23240 fn two() {}"#
23241 .unindent(),
23242 );
23243 cx.lsp
23244 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23245 Ok(Some(vec![
23246 lsp::Location {
23247 uri: params.text_document_position.text_document.uri.clone(),
23248 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
23249 },
23250 lsp::Location {
23251 uri: params.text_document_position.text_document.uri,
23252 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
23253 },
23254 ]))
23255 });
23256 let navigated = cx
23257 .update_editor(|editor, window, cx| {
23258 editor.find_all_references(&FindAllReferences::default(), window, cx)
23259 })
23260 .unwrap()
23261 .await
23262 .expect("Failed to navigate to references");
23263 assert_eq!(
23264 navigated,
23265 Navigated::Yes,
23266 "Should have navigated to references from the FindAllReferences response"
23267 );
23268 cx.assert_editor_state(
23269 &r#"fn one() {
23270 let mut a = ˇtwo();
23271 }
23272 fn three() {}
23273 fn two() {}"#
23274 .unindent(),
23275 );
23276 let editors = cx.update_workspace(|workspace, _, cx| {
23277 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23278 });
23279 cx.update_editor(|_, _, _| {
23280 assert_eq!(
23281 editors.len(),
23282 3,
23283 "should have used a new multibuffer as offsets changed"
23284 );
23285 });
23286}
23287#[gpui::test]
23288async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
23289 init_test(cx, |_| {});
23290
23291 let language = Arc::new(Language::new(
23292 LanguageConfig::default(),
23293 Some(tree_sitter_rust::LANGUAGE.into()),
23294 ));
23295
23296 let text = r#"
23297 #[cfg(test)]
23298 mod tests() {
23299 #[test]
23300 fn runnable_1() {
23301 let a = 1;
23302 }
23303
23304 #[test]
23305 fn runnable_2() {
23306 let a = 1;
23307 let b = 2;
23308 }
23309 }
23310 "#
23311 .unindent();
23312
23313 let fs = FakeFs::new(cx.executor());
23314 fs.insert_file("/file.rs", Default::default()).await;
23315
23316 let project = Project::test(fs, ["/a".as_ref()], cx).await;
23317 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23318 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23319 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
23320 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
23321
23322 let editor = cx.new_window_entity(|window, cx| {
23323 Editor::new(
23324 EditorMode::full(),
23325 multi_buffer,
23326 Some(project.clone()),
23327 window,
23328 cx,
23329 )
23330 });
23331
23332 editor.update_in(cx, |editor, window, cx| {
23333 let snapshot = editor.buffer().read(cx).snapshot(cx);
23334 editor.tasks.insert(
23335 (buffer.read(cx).remote_id(), 3),
23336 RunnableTasks {
23337 templates: vec![],
23338 offset: snapshot.anchor_before(MultiBufferOffset(43)),
23339 column: 0,
23340 extra_variables: HashMap::default(),
23341 context_range: BufferOffset(43)..BufferOffset(85),
23342 },
23343 );
23344 editor.tasks.insert(
23345 (buffer.read(cx).remote_id(), 8),
23346 RunnableTasks {
23347 templates: vec![],
23348 offset: snapshot.anchor_before(MultiBufferOffset(86)),
23349 column: 0,
23350 extra_variables: HashMap::default(),
23351 context_range: BufferOffset(86)..BufferOffset(191),
23352 },
23353 );
23354
23355 // Test finding task when cursor is inside function body
23356 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23357 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
23358 });
23359 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23360 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
23361
23362 // Test finding task when cursor is on function name
23363 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23364 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
23365 });
23366 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23367 assert_eq!(row, 8, "Should find task when cursor is on function name");
23368 });
23369}
23370
23371#[gpui::test]
23372async fn test_folding_buffers(cx: &mut TestAppContext) {
23373 init_test(cx, |_| {});
23374
23375 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23376 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
23377 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
23378
23379 let fs = FakeFs::new(cx.executor());
23380 fs.insert_tree(
23381 path!("/a"),
23382 json!({
23383 "first.rs": sample_text_1,
23384 "second.rs": sample_text_2,
23385 "third.rs": sample_text_3,
23386 }),
23387 )
23388 .await;
23389 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23390 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23391 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23392 let worktree = project.update(cx, |project, cx| {
23393 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23394 assert_eq!(worktrees.len(), 1);
23395 worktrees.pop().unwrap()
23396 });
23397 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23398
23399 let buffer_1 = project
23400 .update(cx, |project, cx| {
23401 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23402 })
23403 .await
23404 .unwrap();
23405 let buffer_2 = project
23406 .update(cx, |project, cx| {
23407 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23408 })
23409 .await
23410 .unwrap();
23411 let buffer_3 = project
23412 .update(cx, |project, cx| {
23413 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23414 })
23415 .await
23416 .unwrap();
23417
23418 let multi_buffer = cx.new(|cx| {
23419 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23420 multi_buffer.push_excerpts(
23421 buffer_1.clone(),
23422 [
23423 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23424 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23425 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23426 ],
23427 cx,
23428 );
23429 multi_buffer.push_excerpts(
23430 buffer_2.clone(),
23431 [
23432 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23433 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23434 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23435 ],
23436 cx,
23437 );
23438 multi_buffer.push_excerpts(
23439 buffer_3.clone(),
23440 [
23441 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23442 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23443 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23444 ],
23445 cx,
23446 );
23447 multi_buffer
23448 });
23449 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23450 Editor::new(
23451 EditorMode::full(),
23452 multi_buffer.clone(),
23453 Some(project.clone()),
23454 window,
23455 cx,
23456 )
23457 });
23458
23459 assert_eq!(
23460 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23461 "\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",
23462 );
23463
23464 multi_buffer_editor.update(cx, |editor, cx| {
23465 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23466 });
23467 assert_eq!(
23468 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23469 "\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",
23470 "After folding the first buffer, its text should not be displayed"
23471 );
23472
23473 multi_buffer_editor.update(cx, |editor, cx| {
23474 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23475 });
23476 assert_eq!(
23477 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23478 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
23479 "After folding the second buffer, its text should not be displayed"
23480 );
23481
23482 multi_buffer_editor.update(cx, |editor, cx| {
23483 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23484 });
23485 assert_eq!(
23486 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23487 "\n\n\n\n\n",
23488 "After folding the third buffer, its text should not be displayed"
23489 );
23490
23491 // Emulate selection inside the fold logic, that should work
23492 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23493 editor
23494 .snapshot(window, cx)
23495 .next_line_boundary(Point::new(0, 4));
23496 });
23497
23498 multi_buffer_editor.update(cx, |editor, cx| {
23499 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23500 });
23501 assert_eq!(
23502 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23503 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
23504 "After unfolding the second buffer, its text should be displayed"
23505 );
23506
23507 // Typing inside of buffer 1 causes that buffer to be unfolded.
23508 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23509 assert_eq!(
23510 multi_buffer
23511 .read(cx)
23512 .snapshot(cx)
23513 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
23514 .collect::<String>(),
23515 "bbbb"
23516 );
23517 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23518 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
23519 });
23520 editor.handle_input("B", window, cx);
23521 });
23522
23523 assert_eq!(
23524 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23525 "\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",
23526 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
23527 );
23528
23529 multi_buffer_editor.update(cx, |editor, cx| {
23530 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23531 });
23532 assert_eq!(
23533 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23534 "\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",
23535 "After unfolding the all buffers, all original text should be displayed"
23536 );
23537}
23538
23539#[gpui::test]
23540async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
23541 init_test(cx, |_| {});
23542
23543 let sample_text_1 = "1111\n2222\n3333".to_string();
23544 let sample_text_2 = "4444\n5555\n6666".to_string();
23545 let sample_text_3 = "7777\n8888\n9999".to_string();
23546
23547 let fs = FakeFs::new(cx.executor());
23548 fs.insert_tree(
23549 path!("/a"),
23550 json!({
23551 "first.rs": sample_text_1,
23552 "second.rs": sample_text_2,
23553 "third.rs": sample_text_3,
23554 }),
23555 )
23556 .await;
23557 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23558 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23559 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23560 let worktree = project.update(cx, |project, cx| {
23561 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23562 assert_eq!(worktrees.len(), 1);
23563 worktrees.pop().unwrap()
23564 });
23565 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23566
23567 let buffer_1 = project
23568 .update(cx, |project, cx| {
23569 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23570 })
23571 .await
23572 .unwrap();
23573 let buffer_2 = project
23574 .update(cx, |project, cx| {
23575 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23576 })
23577 .await
23578 .unwrap();
23579 let buffer_3 = project
23580 .update(cx, |project, cx| {
23581 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23582 })
23583 .await
23584 .unwrap();
23585
23586 let multi_buffer = cx.new(|cx| {
23587 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23588 multi_buffer.push_excerpts(
23589 buffer_1.clone(),
23590 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23591 cx,
23592 );
23593 multi_buffer.push_excerpts(
23594 buffer_2.clone(),
23595 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23596 cx,
23597 );
23598 multi_buffer.push_excerpts(
23599 buffer_3.clone(),
23600 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23601 cx,
23602 );
23603 multi_buffer
23604 });
23605
23606 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23607 Editor::new(
23608 EditorMode::full(),
23609 multi_buffer,
23610 Some(project.clone()),
23611 window,
23612 cx,
23613 )
23614 });
23615
23616 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
23617 assert_eq!(
23618 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23619 full_text,
23620 );
23621
23622 multi_buffer_editor.update(cx, |editor, cx| {
23623 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23624 });
23625 assert_eq!(
23626 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23627 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
23628 "After folding the first buffer, its text should not be displayed"
23629 );
23630
23631 multi_buffer_editor.update(cx, |editor, cx| {
23632 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23633 });
23634
23635 assert_eq!(
23636 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23637 "\n\n\n\n\n\n7777\n8888\n9999",
23638 "After folding the second buffer, its text should not be displayed"
23639 );
23640
23641 multi_buffer_editor.update(cx, |editor, cx| {
23642 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23643 });
23644 assert_eq!(
23645 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23646 "\n\n\n\n\n",
23647 "After folding the third buffer, its text should not be displayed"
23648 );
23649
23650 multi_buffer_editor.update(cx, |editor, cx| {
23651 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23652 });
23653 assert_eq!(
23654 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23655 "\n\n\n\n4444\n5555\n6666\n\n",
23656 "After unfolding the second buffer, its text should be displayed"
23657 );
23658
23659 multi_buffer_editor.update(cx, |editor, cx| {
23660 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
23661 });
23662 assert_eq!(
23663 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23664 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
23665 "After unfolding the first buffer, its text should be displayed"
23666 );
23667
23668 multi_buffer_editor.update(cx, |editor, cx| {
23669 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23670 });
23671 assert_eq!(
23672 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23673 full_text,
23674 "After unfolding all buffers, all original text should be displayed"
23675 );
23676}
23677
23678#[gpui::test]
23679async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
23680 init_test(cx, |_| {});
23681
23682 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23683
23684 let fs = FakeFs::new(cx.executor());
23685 fs.insert_tree(
23686 path!("/a"),
23687 json!({
23688 "main.rs": sample_text,
23689 }),
23690 )
23691 .await;
23692 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23693 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23694 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23695 let worktree = project.update(cx, |project, cx| {
23696 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23697 assert_eq!(worktrees.len(), 1);
23698 worktrees.pop().unwrap()
23699 });
23700 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23701
23702 let buffer_1 = project
23703 .update(cx, |project, cx| {
23704 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23705 })
23706 .await
23707 .unwrap();
23708
23709 let multi_buffer = cx.new(|cx| {
23710 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23711 multi_buffer.push_excerpts(
23712 buffer_1.clone(),
23713 [ExcerptRange::new(
23714 Point::new(0, 0)
23715 ..Point::new(
23716 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
23717 0,
23718 ),
23719 )],
23720 cx,
23721 );
23722 multi_buffer
23723 });
23724 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23725 Editor::new(
23726 EditorMode::full(),
23727 multi_buffer,
23728 Some(project.clone()),
23729 window,
23730 cx,
23731 )
23732 });
23733
23734 let selection_range = Point::new(1, 0)..Point::new(2, 0);
23735 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23736 enum TestHighlight {}
23737 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
23738 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
23739 editor.highlight_text::<TestHighlight>(
23740 vec![highlight_range.clone()],
23741 HighlightStyle::color(Hsla::green()),
23742 cx,
23743 );
23744 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23745 s.select_ranges(Some(highlight_range))
23746 });
23747 });
23748
23749 let full_text = format!("\n\n{sample_text}");
23750 assert_eq!(
23751 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23752 full_text,
23753 );
23754}
23755
23756#[gpui::test]
23757async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
23758 init_test(cx, |_| {});
23759 cx.update(|cx| {
23760 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
23761 "keymaps/default-linux.json",
23762 cx,
23763 )
23764 .unwrap();
23765 cx.bind_keys(default_key_bindings);
23766 });
23767
23768 let (editor, cx) = cx.add_window_view(|window, cx| {
23769 let multi_buffer = MultiBuffer::build_multi(
23770 [
23771 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
23772 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
23773 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
23774 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
23775 ],
23776 cx,
23777 );
23778 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
23779
23780 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
23781 // fold all but the second buffer, so that we test navigating between two
23782 // adjacent folded buffers, as well as folded buffers at the start and
23783 // end the multibuffer
23784 editor.fold_buffer(buffer_ids[0], cx);
23785 editor.fold_buffer(buffer_ids[2], cx);
23786 editor.fold_buffer(buffer_ids[3], cx);
23787
23788 editor
23789 });
23790 cx.simulate_resize(size(px(1000.), px(1000.)));
23791
23792 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
23793 cx.assert_excerpts_with_selections(indoc! {"
23794 [EXCERPT]
23795 ˇ[FOLDED]
23796 [EXCERPT]
23797 a1
23798 b1
23799 [EXCERPT]
23800 [FOLDED]
23801 [EXCERPT]
23802 [FOLDED]
23803 "
23804 });
23805 cx.simulate_keystroke("down");
23806 cx.assert_excerpts_with_selections(indoc! {"
23807 [EXCERPT]
23808 [FOLDED]
23809 [EXCERPT]
23810 ˇa1
23811 b1
23812 [EXCERPT]
23813 [FOLDED]
23814 [EXCERPT]
23815 [FOLDED]
23816 "
23817 });
23818 cx.simulate_keystroke("down");
23819 cx.assert_excerpts_with_selections(indoc! {"
23820 [EXCERPT]
23821 [FOLDED]
23822 [EXCERPT]
23823 a1
23824 ˇb1
23825 [EXCERPT]
23826 [FOLDED]
23827 [EXCERPT]
23828 [FOLDED]
23829 "
23830 });
23831 cx.simulate_keystroke("down");
23832 cx.assert_excerpts_with_selections(indoc! {"
23833 [EXCERPT]
23834 [FOLDED]
23835 [EXCERPT]
23836 a1
23837 b1
23838 ˇ[EXCERPT]
23839 [FOLDED]
23840 [EXCERPT]
23841 [FOLDED]
23842 "
23843 });
23844 cx.simulate_keystroke("down");
23845 cx.assert_excerpts_with_selections(indoc! {"
23846 [EXCERPT]
23847 [FOLDED]
23848 [EXCERPT]
23849 a1
23850 b1
23851 [EXCERPT]
23852 ˇ[FOLDED]
23853 [EXCERPT]
23854 [FOLDED]
23855 "
23856 });
23857 for _ in 0..5 {
23858 cx.simulate_keystroke("down");
23859 cx.assert_excerpts_with_selections(indoc! {"
23860 [EXCERPT]
23861 [FOLDED]
23862 [EXCERPT]
23863 a1
23864 b1
23865 [EXCERPT]
23866 [FOLDED]
23867 [EXCERPT]
23868 ˇ[FOLDED]
23869 "
23870 });
23871 }
23872
23873 cx.simulate_keystroke("up");
23874 cx.assert_excerpts_with_selections(indoc! {"
23875 [EXCERPT]
23876 [FOLDED]
23877 [EXCERPT]
23878 a1
23879 b1
23880 [EXCERPT]
23881 ˇ[FOLDED]
23882 [EXCERPT]
23883 [FOLDED]
23884 "
23885 });
23886 cx.simulate_keystroke("up");
23887 cx.assert_excerpts_with_selections(indoc! {"
23888 [EXCERPT]
23889 [FOLDED]
23890 [EXCERPT]
23891 a1
23892 b1
23893 ˇ[EXCERPT]
23894 [FOLDED]
23895 [EXCERPT]
23896 [FOLDED]
23897 "
23898 });
23899 cx.simulate_keystroke("up");
23900 cx.assert_excerpts_with_selections(indoc! {"
23901 [EXCERPT]
23902 [FOLDED]
23903 [EXCERPT]
23904 a1
23905 ˇb1
23906 [EXCERPT]
23907 [FOLDED]
23908 [EXCERPT]
23909 [FOLDED]
23910 "
23911 });
23912 cx.simulate_keystroke("up");
23913 cx.assert_excerpts_with_selections(indoc! {"
23914 [EXCERPT]
23915 [FOLDED]
23916 [EXCERPT]
23917 ˇa1
23918 b1
23919 [EXCERPT]
23920 [FOLDED]
23921 [EXCERPT]
23922 [FOLDED]
23923 "
23924 });
23925 for _ in 0..5 {
23926 cx.simulate_keystroke("up");
23927 cx.assert_excerpts_with_selections(indoc! {"
23928 [EXCERPT]
23929 ˇ[FOLDED]
23930 [EXCERPT]
23931 a1
23932 b1
23933 [EXCERPT]
23934 [FOLDED]
23935 [EXCERPT]
23936 [FOLDED]
23937 "
23938 });
23939 }
23940}
23941
23942#[gpui::test]
23943async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23944 init_test(cx, |_| {});
23945
23946 // Simple insertion
23947 assert_highlighted_edits(
23948 "Hello, world!",
23949 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23950 true,
23951 cx,
23952 |highlighted_edits, cx| {
23953 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23954 assert_eq!(highlighted_edits.highlights.len(), 1);
23955 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23956 assert_eq!(
23957 highlighted_edits.highlights[0].1.background_color,
23958 Some(cx.theme().status().created_background)
23959 );
23960 },
23961 )
23962 .await;
23963
23964 // Replacement
23965 assert_highlighted_edits(
23966 "This is a test.",
23967 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23968 false,
23969 cx,
23970 |highlighted_edits, cx| {
23971 assert_eq!(highlighted_edits.text, "That is a test.");
23972 assert_eq!(highlighted_edits.highlights.len(), 1);
23973 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23974 assert_eq!(
23975 highlighted_edits.highlights[0].1.background_color,
23976 Some(cx.theme().status().created_background)
23977 );
23978 },
23979 )
23980 .await;
23981
23982 // Multiple edits
23983 assert_highlighted_edits(
23984 "Hello, world!",
23985 vec![
23986 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23987 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23988 ],
23989 false,
23990 cx,
23991 |highlighted_edits, cx| {
23992 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23993 assert_eq!(highlighted_edits.highlights.len(), 2);
23994 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23995 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23996 assert_eq!(
23997 highlighted_edits.highlights[0].1.background_color,
23998 Some(cx.theme().status().created_background)
23999 );
24000 assert_eq!(
24001 highlighted_edits.highlights[1].1.background_color,
24002 Some(cx.theme().status().created_background)
24003 );
24004 },
24005 )
24006 .await;
24007
24008 // Multiple lines with edits
24009 assert_highlighted_edits(
24010 "First line\nSecond line\nThird line\nFourth line",
24011 vec![
24012 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
24013 (
24014 Point::new(2, 0)..Point::new(2, 10),
24015 "New third line".to_string(),
24016 ),
24017 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
24018 ],
24019 false,
24020 cx,
24021 |highlighted_edits, cx| {
24022 assert_eq!(
24023 highlighted_edits.text,
24024 "Second modified\nNew third line\nFourth updated line"
24025 );
24026 assert_eq!(highlighted_edits.highlights.len(), 3);
24027 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
24028 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
24029 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
24030 for highlight in &highlighted_edits.highlights {
24031 assert_eq!(
24032 highlight.1.background_color,
24033 Some(cx.theme().status().created_background)
24034 );
24035 }
24036 },
24037 )
24038 .await;
24039}
24040
24041#[gpui::test]
24042async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
24043 init_test(cx, |_| {});
24044
24045 // Deletion
24046 assert_highlighted_edits(
24047 "Hello, world!",
24048 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
24049 true,
24050 cx,
24051 |highlighted_edits, cx| {
24052 assert_eq!(highlighted_edits.text, "Hello, world!");
24053 assert_eq!(highlighted_edits.highlights.len(), 1);
24054 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
24055 assert_eq!(
24056 highlighted_edits.highlights[0].1.background_color,
24057 Some(cx.theme().status().deleted_background)
24058 );
24059 },
24060 )
24061 .await;
24062
24063 // Insertion
24064 assert_highlighted_edits(
24065 "Hello, world!",
24066 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
24067 true,
24068 cx,
24069 |highlighted_edits, cx| {
24070 assert_eq!(highlighted_edits.highlights.len(), 1);
24071 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
24072 assert_eq!(
24073 highlighted_edits.highlights[0].1.background_color,
24074 Some(cx.theme().status().created_background)
24075 );
24076 },
24077 )
24078 .await;
24079}
24080
24081async fn assert_highlighted_edits(
24082 text: &str,
24083 edits: Vec<(Range<Point>, String)>,
24084 include_deletions: bool,
24085 cx: &mut TestAppContext,
24086 assertion_fn: impl Fn(HighlightedText, &App),
24087) {
24088 let window = cx.add_window(|window, cx| {
24089 let buffer = MultiBuffer::build_simple(text, cx);
24090 Editor::new(EditorMode::full(), buffer, None, window, cx)
24091 });
24092 let cx = &mut VisualTestContext::from_window(*window, cx);
24093
24094 let (buffer, snapshot) = window
24095 .update(cx, |editor, _window, cx| {
24096 (
24097 editor.buffer().clone(),
24098 editor.buffer().read(cx).snapshot(cx),
24099 )
24100 })
24101 .unwrap();
24102
24103 let edits = edits
24104 .into_iter()
24105 .map(|(range, edit)| {
24106 (
24107 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
24108 edit,
24109 )
24110 })
24111 .collect::<Vec<_>>();
24112
24113 let text_anchor_edits = edits
24114 .clone()
24115 .into_iter()
24116 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
24117 .collect::<Vec<_>>();
24118
24119 let edit_preview = window
24120 .update(cx, |_, _window, cx| {
24121 buffer
24122 .read(cx)
24123 .as_singleton()
24124 .unwrap()
24125 .read(cx)
24126 .preview_edits(text_anchor_edits.into(), cx)
24127 })
24128 .unwrap()
24129 .await;
24130
24131 cx.update(|_window, cx| {
24132 let highlighted_edits = edit_prediction_edit_text(
24133 snapshot.as_singleton().unwrap().2,
24134 &edits,
24135 &edit_preview,
24136 include_deletions,
24137 cx,
24138 );
24139 assertion_fn(highlighted_edits, cx)
24140 });
24141}
24142
24143#[track_caller]
24144fn assert_breakpoint(
24145 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
24146 path: &Arc<Path>,
24147 expected: Vec<(u32, Breakpoint)>,
24148) {
24149 if expected.is_empty() {
24150 assert!(!breakpoints.contains_key(path), "{}", path.display());
24151 } else {
24152 let mut breakpoint = breakpoints
24153 .get(path)
24154 .unwrap()
24155 .iter()
24156 .map(|breakpoint| {
24157 (
24158 breakpoint.row,
24159 Breakpoint {
24160 message: breakpoint.message.clone(),
24161 state: breakpoint.state,
24162 condition: breakpoint.condition.clone(),
24163 hit_condition: breakpoint.hit_condition.clone(),
24164 },
24165 )
24166 })
24167 .collect::<Vec<_>>();
24168
24169 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
24170
24171 assert_eq!(expected, breakpoint);
24172 }
24173}
24174
24175fn add_log_breakpoint_at_cursor(
24176 editor: &mut Editor,
24177 log_message: &str,
24178 window: &mut Window,
24179 cx: &mut Context<Editor>,
24180) {
24181 let (anchor, bp) = editor
24182 .breakpoints_at_cursors(window, cx)
24183 .first()
24184 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
24185 .unwrap_or_else(|| {
24186 let snapshot = editor.snapshot(window, cx);
24187 let cursor_position: Point =
24188 editor.selections.newest(&snapshot.display_snapshot).head();
24189
24190 let breakpoint_position = snapshot
24191 .buffer_snapshot()
24192 .anchor_before(Point::new(cursor_position.row, 0));
24193
24194 (breakpoint_position, Breakpoint::new_log(log_message))
24195 });
24196
24197 editor.edit_breakpoint_at_anchor(
24198 anchor,
24199 bp,
24200 BreakpointEditAction::EditLogMessage(log_message.into()),
24201 cx,
24202 );
24203}
24204
24205#[gpui::test]
24206async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
24207 init_test(cx, |_| {});
24208
24209 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24210 let fs = FakeFs::new(cx.executor());
24211 fs.insert_tree(
24212 path!("/a"),
24213 json!({
24214 "main.rs": sample_text,
24215 }),
24216 )
24217 .await;
24218 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24219 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24220 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24221
24222 let fs = FakeFs::new(cx.executor());
24223 fs.insert_tree(
24224 path!("/a"),
24225 json!({
24226 "main.rs": sample_text,
24227 }),
24228 )
24229 .await;
24230 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24231 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24232 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24233 let worktree_id = workspace
24234 .update(cx, |workspace, _window, cx| {
24235 workspace.project().update(cx, |project, cx| {
24236 project.worktrees(cx).next().unwrap().read(cx).id()
24237 })
24238 })
24239 .unwrap();
24240
24241 let buffer = project
24242 .update(cx, |project, cx| {
24243 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24244 })
24245 .await
24246 .unwrap();
24247
24248 let (editor, cx) = cx.add_window_view(|window, cx| {
24249 Editor::new(
24250 EditorMode::full(),
24251 MultiBuffer::build_from_buffer(buffer, cx),
24252 Some(project.clone()),
24253 window,
24254 cx,
24255 )
24256 });
24257
24258 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24259 let abs_path = project.read_with(cx, |project, cx| {
24260 project
24261 .absolute_path(&project_path, cx)
24262 .map(Arc::from)
24263 .unwrap()
24264 });
24265
24266 // assert we can add breakpoint on the first line
24267 editor.update_in(cx, |editor, window, cx| {
24268 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24269 editor.move_to_end(&MoveToEnd, window, cx);
24270 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24271 });
24272
24273 let breakpoints = editor.update(cx, |editor, cx| {
24274 editor
24275 .breakpoint_store()
24276 .as_ref()
24277 .unwrap()
24278 .read(cx)
24279 .all_source_breakpoints(cx)
24280 });
24281
24282 assert_eq!(1, breakpoints.len());
24283 assert_breakpoint(
24284 &breakpoints,
24285 &abs_path,
24286 vec![
24287 (0, Breakpoint::new_standard()),
24288 (3, Breakpoint::new_standard()),
24289 ],
24290 );
24291
24292 editor.update_in(cx, |editor, window, cx| {
24293 editor.move_to_beginning(&MoveToBeginning, window, cx);
24294 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24295 });
24296
24297 let breakpoints = editor.update(cx, |editor, cx| {
24298 editor
24299 .breakpoint_store()
24300 .as_ref()
24301 .unwrap()
24302 .read(cx)
24303 .all_source_breakpoints(cx)
24304 });
24305
24306 assert_eq!(1, breakpoints.len());
24307 assert_breakpoint(
24308 &breakpoints,
24309 &abs_path,
24310 vec![(3, Breakpoint::new_standard())],
24311 );
24312
24313 editor.update_in(cx, |editor, window, cx| {
24314 editor.move_to_end(&MoveToEnd, window, cx);
24315 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24316 });
24317
24318 let breakpoints = editor.update(cx, |editor, cx| {
24319 editor
24320 .breakpoint_store()
24321 .as_ref()
24322 .unwrap()
24323 .read(cx)
24324 .all_source_breakpoints(cx)
24325 });
24326
24327 assert_eq!(0, breakpoints.len());
24328 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24329}
24330
24331#[gpui::test]
24332async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
24333 init_test(cx, |_| {});
24334
24335 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24336
24337 let fs = FakeFs::new(cx.executor());
24338 fs.insert_tree(
24339 path!("/a"),
24340 json!({
24341 "main.rs": sample_text,
24342 }),
24343 )
24344 .await;
24345 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24346 let (workspace, cx) =
24347 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24348
24349 let worktree_id = workspace.update(cx, |workspace, cx| {
24350 workspace.project().update(cx, |project, cx| {
24351 project.worktrees(cx).next().unwrap().read(cx).id()
24352 })
24353 });
24354
24355 let buffer = project
24356 .update(cx, |project, cx| {
24357 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24358 })
24359 .await
24360 .unwrap();
24361
24362 let (editor, cx) = cx.add_window_view(|window, cx| {
24363 Editor::new(
24364 EditorMode::full(),
24365 MultiBuffer::build_from_buffer(buffer, cx),
24366 Some(project.clone()),
24367 window,
24368 cx,
24369 )
24370 });
24371
24372 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24373 let abs_path = project.read_with(cx, |project, cx| {
24374 project
24375 .absolute_path(&project_path, cx)
24376 .map(Arc::from)
24377 .unwrap()
24378 });
24379
24380 editor.update_in(cx, |editor, window, cx| {
24381 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24382 });
24383
24384 let breakpoints = editor.update(cx, |editor, cx| {
24385 editor
24386 .breakpoint_store()
24387 .as_ref()
24388 .unwrap()
24389 .read(cx)
24390 .all_source_breakpoints(cx)
24391 });
24392
24393 assert_breakpoint(
24394 &breakpoints,
24395 &abs_path,
24396 vec![(0, Breakpoint::new_log("hello world"))],
24397 );
24398
24399 // Removing a log message from a log breakpoint should remove it
24400 editor.update_in(cx, |editor, window, cx| {
24401 add_log_breakpoint_at_cursor(editor, "", window, cx);
24402 });
24403
24404 let breakpoints = editor.update(cx, |editor, cx| {
24405 editor
24406 .breakpoint_store()
24407 .as_ref()
24408 .unwrap()
24409 .read(cx)
24410 .all_source_breakpoints(cx)
24411 });
24412
24413 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24414
24415 editor.update_in(cx, |editor, window, cx| {
24416 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24417 editor.move_to_end(&MoveToEnd, window, cx);
24418 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24419 // Not adding a log message to a standard breakpoint shouldn't remove it
24420 add_log_breakpoint_at_cursor(editor, "", window, cx);
24421 });
24422
24423 let breakpoints = editor.update(cx, |editor, cx| {
24424 editor
24425 .breakpoint_store()
24426 .as_ref()
24427 .unwrap()
24428 .read(cx)
24429 .all_source_breakpoints(cx)
24430 });
24431
24432 assert_breakpoint(
24433 &breakpoints,
24434 &abs_path,
24435 vec![
24436 (0, Breakpoint::new_standard()),
24437 (3, Breakpoint::new_standard()),
24438 ],
24439 );
24440
24441 editor.update_in(cx, |editor, window, cx| {
24442 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24443 });
24444
24445 let breakpoints = editor.update(cx, |editor, cx| {
24446 editor
24447 .breakpoint_store()
24448 .as_ref()
24449 .unwrap()
24450 .read(cx)
24451 .all_source_breakpoints(cx)
24452 });
24453
24454 assert_breakpoint(
24455 &breakpoints,
24456 &abs_path,
24457 vec![
24458 (0, Breakpoint::new_standard()),
24459 (3, Breakpoint::new_log("hello world")),
24460 ],
24461 );
24462
24463 editor.update_in(cx, |editor, window, cx| {
24464 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
24465 });
24466
24467 let breakpoints = editor.update(cx, |editor, cx| {
24468 editor
24469 .breakpoint_store()
24470 .as_ref()
24471 .unwrap()
24472 .read(cx)
24473 .all_source_breakpoints(cx)
24474 });
24475
24476 assert_breakpoint(
24477 &breakpoints,
24478 &abs_path,
24479 vec![
24480 (0, Breakpoint::new_standard()),
24481 (3, Breakpoint::new_log("hello Earth!!")),
24482 ],
24483 );
24484}
24485
24486/// This also tests that Editor::breakpoint_at_cursor_head is working properly
24487/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
24488/// or when breakpoints were placed out of order. This tests for a regression too
24489#[gpui::test]
24490async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
24491 init_test(cx, |_| {});
24492
24493 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24494 let fs = FakeFs::new(cx.executor());
24495 fs.insert_tree(
24496 path!("/a"),
24497 json!({
24498 "main.rs": sample_text,
24499 }),
24500 )
24501 .await;
24502 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24503 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24504 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24505
24506 let fs = FakeFs::new(cx.executor());
24507 fs.insert_tree(
24508 path!("/a"),
24509 json!({
24510 "main.rs": sample_text,
24511 }),
24512 )
24513 .await;
24514 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24515 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24516 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24517 let worktree_id = workspace
24518 .update(cx, |workspace, _window, cx| {
24519 workspace.project().update(cx, |project, cx| {
24520 project.worktrees(cx).next().unwrap().read(cx).id()
24521 })
24522 })
24523 .unwrap();
24524
24525 let buffer = project
24526 .update(cx, |project, cx| {
24527 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24528 })
24529 .await
24530 .unwrap();
24531
24532 let (editor, cx) = cx.add_window_view(|window, cx| {
24533 Editor::new(
24534 EditorMode::full(),
24535 MultiBuffer::build_from_buffer(buffer, cx),
24536 Some(project.clone()),
24537 window,
24538 cx,
24539 )
24540 });
24541
24542 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24543 let abs_path = project.read_with(cx, |project, cx| {
24544 project
24545 .absolute_path(&project_path, cx)
24546 .map(Arc::from)
24547 .unwrap()
24548 });
24549
24550 // assert we can add breakpoint on the first line
24551 editor.update_in(cx, |editor, window, cx| {
24552 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24553 editor.move_to_end(&MoveToEnd, window, cx);
24554 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24555 editor.move_up(&MoveUp, window, cx);
24556 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24557 });
24558
24559 let breakpoints = editor.update(cx, |editor, cx| {
24560 editor
24561 .breakpoint_store()
24562 .as_ref()
24563 .unwrap()
24564 .read(cx)
24565 .all_source_breakpoints(cx)
24566 });
24567
24568 assert_eq!(1, breakpoints.len());
24569 assert_breakpoint(
24570 &breakpoints,
24571 &abs_path,
24572 vec![
24573 (0, Breakpoint::new_standard()),
24574 (2, Breakpoint::new_standard()),
24575 (3, Breakpoint::new_standard()),
24576 ],
24577 );
24578
24579 editor.update_in(cx, |editor, window, cx| {
24580 editor.move_to_beginning(&MoveToBeginning, window, cx);
24581 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24582 editor.move_to_end(&MoveToEnd, window, cx);
24583 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24584 // Disabling a breakpoint that doesn't exist should do nothing
24585 editor.move_up(&MoveUp, window, cx);
24586 editor.move_up(&MoveUp, window, cx);
24587 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24588 });
24589
24590 let breakpoints = editor.update(cx, |editor, cx| {
24591 editor
24592 .breakpoint_store()
24593 .as_ref()
24594 .unwrap()
24595 .read(cx)
24596 .all_source_breakpoints(cx)
24597 });
24598
24599 let disable_breakpoint = {
24600 let mut bp = Breakpoint::new_standard();
24601 bp.state = BreakpointState::Disabled;
24602 bp
24603 };
24604
24605 assert_eq!(1, breakpoints.len());
24606 assert_breakpoint(
24607 &breakpoints,
24608 &abs_path,
24609 vec![
24610 (0, disable_breakpoint.clone()),
24611 (2, Breakpoint::new_standard()),
24612 (3, disable_breakpoint.clone()),
24613 ],
24614 );
24615
24616 editor.update_in(cx, |editor, window, cx| {
24617 editor.move_to_beginning(&MoveToBeginning, window, cx);
24618 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24619 editor.move_to_end(&MoveToEnd, window, cx);
24620 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24621 editor.move_up(&MoveUp, window, cx);
24622 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24623 });
24624
24625 let breakpoints = editor.update(cx, |editor, cx| {
24626 editor
24627 .breakpoint_store()
24628 .as_ref()
24629 .unwrap()
24630 .read(cx)
24631 .all_source_breakpoints(cx)
24632 });
24633
24634 assert_eq!(1, breakpoints.len());
24635 assert_breakpoint(
24636 &breakpoints,
24637 &abs_path,
24638 vec![
24639 (0, Breakpoint::new_standard()),
24640 (2, disable_breakpoint),
24641 (3, Breakpoint::new_standard()),
24642 ],
24643 );
24644}
24645
24646#[gpui::test]
24647async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
24648 init_test(cx, |_| {});
24649 let capabilities = lsp::ServerCapabilities {
24650 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
24651 prepare_provider: Some(true),
24652 work_done_progress_options: Default::default(),
24653 })),
24654 ..Default::default()
24655 };
24656 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24657
24658 cx.set_state(indoc! {"
24659 struct Fˇoo {}
24660 "});
24661
24662 cx.update_editor(|editor, _, cx| {
24663 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24664 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24665 editor.highlight_background::<DocumentHighlightRead>(
24666 &[highlight_range],
24667 |_, theme| theme.colors().editor_document_highlight_read_background,
24668 cx,
24669 );
24670 });
24671
24672 let mut prepare_rename_handler = cx
24673 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
24674 move |_, _, _| async move {
24675 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
24676 start: lsp::Position {
24677 line: 0,
24678 character: 7,
24679 },
24680 end: lsp::Position {
24681 line: 0,
24682 character: 10,
24683 },
24684 })))
24685 },
24686 );
24687 let prepare_rename_task = cx
24688 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24689 .expect("Prepare rename was not started");
24690 prepare_rename_handler.next().await.unwrap();
24691 prepare_rename_task.await.expect("Prepare rename failed");
24692
24693 let mut rename_handler =
24694 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24695 let edit = lsp::TextEdit {
24696 range: lsp::Range {
24697 start: lsp::Position {
24698 line: 0,
24699 character: 7,
24700 },
24701 end: lsp::Position {
24702 line: 0,
24703 character: 10,
24704 },
24705 },
24706 new_text: "FooRenamed".to_string(),
24707 };
24708 Ok(Some(lsp::WorkspaceEdit::new(
24709 // Specify the same edit twice
24710 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
24711 )))
24712 });
24713 let rename_task = cx
24714 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24715 .expect("Confirm rename was not started");
24716 rename_handler.next().await.unwrap();
24717 rename_task.await.expect("Confirm rename failed");
24718 cx.run_until_parked();
24719
24720 // Despite two edits, only one is actually applied as those are identical
24721 cx.assert_editor_state(indoc! {"
24722 struct FooRenamedˇ {}
24723 "});
24724}
24725
24726#[gpui::test]
24727async fn test_rename_without_prepare(cx: &mut TestAppContext) {
24728 init_test(cx, |_| {});
24729 // These capabilities indicate that the server does not support prepare rename.
24730 let capabilities = lsp::ServerCapabilities {
24731 rename_provider: Some(lsp::OneOf::Left(true)),
24732 ..Default::default()
24733 };
24734 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24735
24736 cx.set_state(indoc! {"
24737 struct Fˇoo {}
24738 "});
24739
24740 cx.update_editor(|editor, _window, cx| {
24741 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24742 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24743 editor.highlight_background::<DocumentHighlightRead>(
24744 &[highlight_range],
24745 |_, theme| theme.colors().editor_document_highlight_read_background,
24746 cx,
24747 );
24748 });
24749
24750 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24751 .expect("Prepare rename was not started")
24752 .await
24753 .expect("Prepare rename failed");
24754
24755 let mut rename_handler =
24756 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24757 let edit = lsp::TextEdit {
24758 range: lsp::Range {
24759 start: lsp::Position {
24760 line: 0,
24761 character: 7,
24762 },
24763 end: lsp::Position {
24764 line: 0,
24765 character: 10,
24766 },
24767 },
24768 new_text: "FooRenamed".to_string(),
24769 };
24770 Ok(Some(lsp::WorkspaceEdit::new(
24771 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
24772 )))
24773 });
24774 let rename_task = cx
24775 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24776 .expect("Confirm rename was not started");
24777 rename_handler.next().await.unwrap();
24778 rename_task.await.expect("Confirm rename failed");
24779 cx.run_until_parked();
24780
24781 // Correct range is renamed, as `surrounding_word` is used to find it.
24782 cx.assert_editor_state(indoc! {"
24783 struct FooRenamedˇ {}
24784 "});
24785}
24786
24787#[gpui::test]
24788async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
24789 init_test(cx, |_| {});
24790 let mut cx = EditorTestContext::new(cx).await;
24791
24792 let language = Arc::new(
24793 Language::new(
24794 LanguageConfig::default(),
24795 Some(tree_sitter_html::LANGUAGE.into()),
24796 )
24797 .with_brackets_query(
24798 r#"
24799 ("<" @open "/>" @close)
24800 ("</" @open ">" @close)
24801 ("<" @open ">" @close)
24802 ("\"" @open "\"" @close)
24803 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
24804 "#,
24805 )
24806 .unwrap(),
24807 );
24808 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24809
24810 cx.set_state(indoc! {"
24811 <span>ˇ</span>
24812 "});
24813 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24814 cx.assert_editor_state(indoc! {"
24815 <span>
24816 ˇ
24817 </span>
24818 "});
24819
24820 cx.set_state(indoc! {"
24821 <span><span></span>ˇ</span>
24822 "});
24823 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24824 cx.assert_editor_state(indoc! {"
24825 <span><span></span>
24826 ˇ</span>
24827 "});
24828
24829 cx.set_state(indoc! {"
24830 <span>ˇ
24831 </span>
24832 "});
24833 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24834 cx.assert_editor_state(indoc! {"
24835 <span>
24836 ˇ
24837 </span>
24838 "});
24839}
24840
24841#[gpui::test(iterations = 10)]
24842async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
24843 init_test(cx, |_| {});
24844
24845 let fs = FakeFs::new(cx.executor());
24846 fs.insert_tree(
24847 path!("/dir"),
24848 json!({
24849 "a.ts": "a",
24850 }),
24851 )
24852 .await;
24853
24854 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
24855 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24856 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24857
24858 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24859 language_registry.add(Arc::new(Language::new(
24860 LanguageConfig {
24861 name: "TypeScript".into(),
24862 matcher: LanguageMatcher {
24863 path_suffixes: vec!["ts".to_string()],
24864 ..Default::default()
24865 },
24866 ..Default::default()
24867 },
24868 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24869 )));
24870 let mut fake_language_servers = language_registry.register_fake_lsp(
24871 "TypeScript",
24872 FakeLspAdapter {
24873 capabilities: lsp::ServerCapabilities {
24874 code_lens_provider: Some(lsp::CodeLensOptions {
24875 resolve_provider: Some(true),
24876 }),
24877 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24878 commands: vec!["_the/command".to_string()],
24879 ..lsp::ExecuteCommandOptions::default()
24880 }),
24881 ..lsp::ServerCapabilities::default()
24882 },
24883 ..FakeLspAdapter::default()
24884 },
24885 );
24886
24887 let editor = workspace
24888 .update(cx, |workspace, window, cx| {
24889 workspace.open_abs_path(
24890 PathBuf::from(path!("/dir/a.ts")),
24891 OpenOptions::default(),
24892 window,
24893 cx,
24894 )
24895 })
24896 .unwrap()
24897 .await
24898 .unwrap()
24899 .downcast::<Editor>()
24900 .unwrap();
24901 cx.executor().run_until_parked();
24902
24903 let fake_server = fake_language_servers.next().await.unwrap();
24904
24905 let buffer = editor.update(cx, |editor, cx| {
24906 editor
24907 .buffer()
24908 .read(cx)
24909 .as_singleton()
24910 .expect("have opened a single file by path")
24911 });
24912
24913 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24914 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24915 drop(buffer_snapshot);
24916 let actions = cx
24917 .update_window(*workspace, |_, window, cx| {
24918 project.code_actions(&buffer, anchor..anchor, window, cx)
24919 })
24920 .unwrap();
24921
24922 fake_server
24923 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24924 Ok(Some(vec![
24925 lsp::CodeLens {
24926 range: lsp::Range::default(),
24927 command: Some(lsp::Command {
24928 title: "Code lens command".to_owned(),
24929 command: "_the/command".to_owned(),
24930 arguments: None,
24931 }),
24932 data: None,
24933 },
24934 lsp::CodeLens {
24935 range: lsp::Range::default(),
24936 command: Some(lsp::Command {
24937 title: "Command not in capabilities".to_owned(),
24938 command: "not in capabilities".to_owned(),
24939 arguments: None,
24940 }),
24941 data: None,
24942 },
24943 lsp::CodeLens {
24944 range: lsp::Range {
24945 start: lsp::Position {
24946 line: 1,
24947 character: 1,
24948 },
24949 end: lsp::Position {
24950 line: 1,
24951 character: 1,
24952 },
24953 },
24954 command: Some(lsp::Command {
24955 title: "Command not in range".to_owned(),
24956 command: "_the/command".to_owned(),
24957 arguments: None,
24958 }),
24959 data: None,
24960 },
24961 ]))
24962 })
24963 .next()
24964 .await;
24965
24966 let actions = actions.await.unwrap();
24967 assert_eq!(
24968 actions.len(),
24969 1,
24970 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24971 );
24972 let action = actions[0].clone();
24973 let apply = project.update(cx, |project, cx| {
24974 project.apply_code_action(buffer.clone(), action, true, cx)
24975 });
24976
24977 // Resolving the code action does not populate its edits. In absence of
24978 // edits, we must execute the given command.
24979 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24980 |mut lens, _| async move {
24981 let lens_command = lens.command.as_mut().expect("should have a command");
24982 assert_eq!(lens_command.title, "Code lens command");
24983 lens_command.arguments = Some(vec![json!("the-argument")]);
24984 Ok(lens)
24985 },
24986 );
24987
24988 // While executing the command, the language server sends the editor
24989 // a `workspaceEdit` request.
24990 fake_server
24991 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24992 let fake = fake_server.clone();
24993 move |params, _| {
24994 assert_eq!(params.command, "_the/command");
24995 let fake = fake.clone();
24996 async move {
24997 fake.server
24998 .request::<lsp::request::ApplyWorkspaceEdit>(
24999 lsp::ApplyWorkspaceEditParams {
25000 label: None,
25001 edit: lsp::WorkspaceEdit {
25002 changes: Some(
25003 [(
25004 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
25005 vec![lsp::TextEdit {
25006 range: lsp::Range::new(
25007 lsp::Position::new(0, 0),
25008 lsp::Position::new(0, 0),
25009 ),
25010 new_text: "X".into(),
25011 }],
25012 )]
25013 .into_iter()
25014 .collect(),
25015 ),
25016 ..lsp::WorkspaceEdit::default()
25017 },
25018 },
25019 )
25020 .await
25021 .into_response()
25022 .unwrap();
25023 Ok(Some(json!(null)))
25024 }
25025 }
25026 })
25027 .next()
25028 .await;
25029
25030 // Applying the code lens command returns a project transaction containing the edits
25031 // sent by the language server in its `workspaceEdit` request.
25032 let transaction = apply.await.unwrap();
25033 assert!(transaction.0.contains_key(&buffer));
25034 buffer.update(cx, |buffer, cx| {
25035 assert_eq!(buffer.text(), "Xa");
25036 buffer.undo(cx);
25037 assert_eq!(buffer.text(), "a");
25038 });
25039
25040 let actions_after_edits = cx
25041 .update_window(*workspace, |_, window, cx| {
25042 project.code_actions(&buffer, anchor..anchor, window, cx)
25043 })
25044 .unwrap()
25045 .await
25046 .unwrap();
25047 assert_eq!(
25048 actions, actions_after_edits,
25049 "For the same selection, same code lens actions should be returned"
25050 );
25051
25052 let _responses =
25053 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
25054 panic!("No more code lens requests are expected");
25055 });
25056 editor.update_in(cx, |editor, window, cx| {
25057 editor.select_all(&SelectAll, window, cx);
25058 });
25059 cx.executor().run_until_parked();
25060 let new_actions = cx
25061 .update_window(*workspace, |_, window, cx| {
25062 project.code_actions(&buffer, anchor..anchor, window, cx)
25063 })
25064 .unwrap()
25065 .await
25066 .unwrap();
25067 assert_eq!(
25068 actions, new_actions,
25069 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
25070 );
25071}
25072
25073#[gpui::test]
25074async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
25075 init_test(cx, |_| {});
25076
25077 let fs = FakeFs::new(cx.executor());
25078 let main_text = r#"fn main() {
25079println!("1");
25080println!("2");
25081println!("3");
25082println!("4");
25083println!("5");
25084}"#;
25085 let lib_text = "mod foo {}";
25086 fs.insert_tree(
25087 path!("/a"),
25088 json!({
25089 "lib.rs": lib_text,
25090 "main.rs": main_text,
25091 }),
25092 )
25093 .await;
25094
25095 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25096 let (workspace, cx) =
25097 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25098 let worktree_id = workspace.update(cx, |workspace, cx| {
25099 workspace.project().update(cx, |project, cx| {
25100 project.worktrees(cx).next().unwrap().read(cx).id()
25101 })
25102 });
25103
25104 let expected_ranges = vec![
25105 Point::new(0, 0)..Point::new(0, 0),
25106 Point::new(1, 0)..Point::new(1, 1),
25107 Point::new(2, 0)..Point::new(2, 2),
25108 Point::new(3, 0)..Point::new(3, 3),
25109 ];
25110
25111 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25112 let editor_1 = workspace
25113 .update_in(cx, |workspace, window, cx| {
25114 workspace.open_path(
25115 (worktree_id, rel_path("main.rs")),
25116 Some(pane_1.downgrade()),
25117 true,
25118 window,
25119 cx,
25120 )
25121 })
25122 .unwrap()
25123 .await
25124 .downcast::<Editor>()
25125 .unwrap();
25126 pane_1.update(cx, |pane, cx| {
25127 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25128 open_editor.update(cx, |editor, cx| {
25129 assert_eq!(
25130 editor.display_text(cx),
25131 main_text,
25132 "Original main.rs text on initial open",
25133 );
25134 assert_eq!(
25135 editor
25136 .selections
25137 .all::<Point>(&editor.display_snapshot(cx))
25138 .into_iter()
25139 .map(|s| s.range())
25140 .collect::<Vec<_>>(),
25141 vec![Point::zero()..Point::zero()],
25142 "Default selections on initial open",
25143 );
25144 })
25145 });
25146 editor_1.update_in(cx, |editor, window, cx| {
25147 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25148 s.select_ranges(expected_ranges.clone());
25149 });
25150 });
25151
25152 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
25153 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
25154 });
25155 let editor_2 = workspace
25156 .update_in(cx, |workspace, window, cx| {
25157 workspace.open_path(
25158 (worktree_id, rel_path("main.rs")),
25159 Some(pane_2.downgrade()),
25160 true,
25161 window,
25162 cx,
25163 )
25164 })
25165 .unwrap()
25166 .await
25167 .downcast::<Editor>()
25168 .unwrap();
25169 pane_2.update(cx, |pane, cx| {
25170 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25171 open_editor.update(cx, |editor, cx| {
25172 assert_eq!(
25173 editor.display_text(cx),
25174 main_text,
25175 "Original main.rs text on initial open in another panel",
25176 );
25177 assert_eq!(
25178 editor
25179 .selections
25180 .all::<Point>(&editor.display_snapshot(cx))
25181 .into_iter()
25182 .map(|s| s.range())
25183 .collect::<Vec<_>>(),
25184 vec![Point::zero()..Point::zero()],
25185 "Default selections on initial open in another panel",
25186 );
25187 })
25188 });
25189
25190 editor_2.update_in(cx, |editor, window, cx| {
25191 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
25192 });
25193
25194 let _other_editor_1 = workspace
25195 .update_in(cx, |workspace, window, cx| {
25196 workspace.open_path(
25197 (worktree_id, rel_path("lib.rs")),
25198 Some(pane_1.downgrade()),
25199 true,
25200 window,
25201 cx,
25202 )
25203 })
25204 .unwrap()
25205 .await
25206 .downcast::<Editor>()
25207 .unwrap();
25208 pane_1
25209 .update_in(cx, |pane, window, cx| {
25210 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
25211 })
25212 .await
25213 .unwrap();
25214 drop(editor_1);
25215 pane_1.update(cx, |pane, cx| {
25216 pane.active_item()
25217 .unwrap()
25218 .downcast::<Editor>()
25219 .unwrap()
25220 .update(cx, |editor, cx| {
25221 assert_eq!(
25222 editor.display_text(cx),
25223 lib_text,
25224 "Other file should be open and active",
25225 );
25226 });
25227 assert_eq!(pane.items().count(), 1, "No other editors should be open");
25228 });
25229
25230 let _other_editor_2 = workspace
25231 .update_in(cx, |workspace, window, cx| {
25232 workspace.open_path(
25233 (worktree_id, rel_path("lib.rs")),
25234 Some(pane_2.downgrade()),
25235 true,
25236 window,
25237 cx,
25238 )
25239 })
25240 .unwrap()
25241 .await
25242 .downcast::<Editor>()
25243 .unwrap();
25244 pane_2
25245 .update_in(cx, |pane, window, cx| {
25246 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
25247 })
25248 .await
25249 .unwrap();
25250 drop(editor_2);
25251 pane_2.update(cx, |pane, cx| {
25252 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25253 open_editor.update(cx, |editor, cx| {
25254 assert_eq!(
25255 editor.display_text(cx),
25256 lib_text,
25257 "Other file should be open and active in another panel too",
25258 );
25259 });
25260 assert_eq!(
25261 pane.items().count(),
25262 1,
25263 "No other editors should be open in another pane",
25264 );
25265 });
25266
25267 let _editor_1_reopened = workspace
25268 .update_in(cx, |workspace, window, cx| {
25269 workspace.open_path(
25270 (worktree_id, rel_path("main.rs")),
25271 Some(pane_1.downgrade()),
25272 true,
25273 window,
25274 cx,
25275 )
25276 })
25277 .unwrap()
25278 .await
25279 .downcast::<Editor>()
25280 .unwrap();
25281 let _editor_2_reopened = workspace
25282 .update_in(cx, |workspace, window, cx| {
25283 workspace.open_path(
25284 (worktree_id, rel_path("main.rs")),
25285 Some(pane_2.downgrade()),
25286 true,
25287 window,
25288 cx,
25289 )
25290 })
25291 .unwrap()
25292 .await
25293 .downcast::<Editor>()
25294 .unwrap();
25295 pane_1.update(cx, |pane, cx| {
25296 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25297 open_editor.update(cx, |editor, cx| {
25298 assert_eq!(
25299 editor.display_text(cx),
25300 main_text,
25301 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
25302 );
25303 assert_eq!(
25304 editor
25305 .selections
25306 .all::<Point>(&editor.display_snapshot(cx))
25307 .into_iter()
25308 .map(|s| s.range())
25309 .collect::<Vec<_>>(),
25310 expected_ranges,
25311 "Previous editor in the 1st panel had selections and should get them restored on reopen",
25312 );
25313 })
25314 });
25315 pane_2.update(cx, |pane, cx| {
25316 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25317 open_editor.update(cx, |editor, cx| {
25318 assert_eq!(
25319 editor.display_text(cx),
25320 r#"fn main() {
25321⋯rintln!("1");
25322⋯intln!("2");
25323⋯ntln!("3");
25324println!("4");
25325println!("5");
25326}"#,
25327 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
25328 );
25329 assert_eq!(
25330 editor
25331 .selections
25332 .all::<Point>(&editor.display_snapshot(cx))
25333 .into_iter()
25334 .map(|s| s.range())
25335 .collect::<Vec<_>>(),
25336 vec![Point::zero()..Point::zero()],
25337 "Previous editor in the 2nd pane had no selections changed hence should restore none",
25338 );
25339 })
25340 });
25341}
25342
25343#[gpui::test]
25344async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
25345 init_test(cx, |_| {});
25346
25347 let fs = FakeFs::new(cx.executor());
25348 let main_text = r#"fn main() {
25349println!("1");
25350println!("2");
25351println!("3");
25352println!("4");
25353println!("5");
25354}"#;
25355 let lib_text = "mod foo {}";
25356 fs.insert_tree(
25357 path!("/a"),
25358 json!({
25359 "lib.rs": lib_text,
25360 "main.rs": main_text,
25361 }),
25362 )
25363 .await;
25364
25365 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25366 let (workspace, cx) =
25367 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25368 let worktree_id = workspace.update(cx, |workspace, cx| {
25369 workspace.project().update(cx, |project, cx| {
25370 project.worktrees(cx).next().unwrap().read(cx).id()
25371 })
25372 });
25373
25374 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25375 let editor = workspace
25376 .update_in(cx, |workspace, window, cx| {
25377 workspace.open_path(
25378 (worktree_id, rel_path("main.rs")),
25379 Some(pane.downgrade()),
25380 true,
25381 window,
25382 cx,
25383 )
25384 })
25385 .unwrap()
25386 .await
25387 .downcast::<Editor>()
25388 .unwrap();
25389 pane.update(cx, |pane, cx| {
25390 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25391 open_editor.update(cx, |editor, cx| {
25392 assert_eq!(
25393 editor.display_text(cx),
25394 main_text,
25395 "Original main.rs text on initial open",
25396 );
25397 })
25398 });
25399 editor.update_in(cx, |editor, window, cx| {
25400 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
25401 });
25402
25403 cx.update_global(|store: &mut SettingsStore, cx| {
25404 store.update_user_settings(cx, |s| {
25405 s.workspace.restore_on_file_reopen = Some(false);
25406 });
25407 });
25408 editor.update_in(cx, |editor, window, cx| {
25409 editor.fold_ranges(
25410 vec![
25411 Point::new(1, 0)..Point::new(1, 1),
25412 Point::new(2, 0)..Point::new(2, 2),
25413 Point::new(3, 0)..Point::new(3, 3),
25414 ],
25415 false,
25416 window,
25417 cx,
25418 );
25419 });
25420 pane.update_in(cx, |pane, window, cx| {
25421 pane.close_all_items(&CloseAllItems::default(), window, cx)
25422 })
25423 .await
25424 .unwrap();
25425 pane.update(cx, |pane, _| {
25426 assert!(pane.active_item().is_none());
25427 });
25428 cx.update_global(|store: &mut SettingsStore, cx| {
25429 store.update_user_settings(cx, |s| {
25430 s.workspace.restore_on_file_reopen = Some(true);
25431 });
25432 });
25433
25434 let _editor_reopened = workspace
25435 .update_in(cx, |workspace, window, cx| {
25436 workspace.open_path(
25437 (worktree_id, rel_path("main.rs")),
25438 Some(pane.downgrade()),
25439 true,
25440 window,
25441 cx,
25442 )
25443 })
25444 .unwrap()
25445 .await
25446 .downcast::<Editor>()
25447 .unwrap();
25448 pane.update(cx, |pane, cx| {
25449 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25450 open_editor.update(cx, |editor, cx| {
25451 assert_eq!(
25452 editor.display_text(cx),
25453 main_text,
25454 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
25455 );
25456 })
25457 });
25458}
25459
25460#[gpui::test]
25461async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
25462 struct EmptyModalView {
25463 focus_handle: gpui::FocusHandle,
25464 }
25465 impl EventEmitter<DismissEvent> for EmptyModalView {}
25466 impl Render for EmptyModalView {
25467 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
25468 div()
25469 }
25470 }
25471 impl Focusable for EmptyModalView {
25472 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
25473 self.focus_handle.clone()
25474 }
25475 }
25476 impl workspace::ModalView for EmptyModalView {}
25477 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
25478 EmptyModalView {
25479 focus_handle: cx.focus_handle(),
25480 }
25481 }
25482
25483 init_test(cx, |_| {});
25484
25485 let fs = FakeFs::new(cx.executor());
25486 let project = Project::test(fs, [], cx).await;
25487 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25488 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
25489 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
25490 let editor = cx.new_window_entity(|window, cx| {
25491 Editor::new(
25492 EditorMode::full(),
25493 buffer,
25494 Some(project.clone()),
25495 window,
25496 cx,
25497 )
25498 });
25499 workspace
25500 .update(cx, |workspace, window, cx| {
25501 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
25502 })
25503 .unwrap();
25504 editor.update_in(cx, |editor, window, cx| {
25505 editor.open_context_menu(&OpenContextMenu, window, cx);
25506 assert!(editor.mouse_context_menu.is_some());
25507 });
25508 workspace
25509 .update(cx, |workspace, window, cx| {
25510 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
25511 })
25512 .unwrap();
25513 cx.read(|cx| {
25514 assert!(editor.read(cx).mouse_context_menu.is_none());
25515 });
25516}
25517
25518fn set_linked_edit_ranges(
25519 opening: (Point, Point),
25520 closing: (Point, Point),
25521 editor: &mut Editor,
25522 cx: &mut Context<Editor>,
25523) {
25524 let Some((buffer, _)) = editor
25525 .buffer
25526 .read(cx)
25527 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
25528 else {
25529 panic!("Failed to get buffer for selection position");
25530 };
25531 let buffer = buffer.read(cx);
25532 let buffer_id = buffer.remote_id();
25533 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
25534 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
25535 let mut linked_ranges = HashMap::default();
25536 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
25537 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
25538}
25539
25540#[gpui::test]
25541async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
25542 init_test(cx, |_| {});
25543
25544 let fs = FakeFs::new(cx.executor());
25545 fs.insert_file(path!("/file.html"), Default::default())
25546 .await;
25547
25548 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
25549
25550 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25551 let html_language = Arc::new(Language::new(
25552 LanguageConfig {
25553 name: "HTML".into(),
25554 matcher: LanguageMatcher {
25555 path_suffixes: vec!["html".to_string()],
25556 ..LanguageMatcher::default()
25557 },
25558 brackets: BracketPairConfig {
25559 pairs: vec![BracketPair {
25560 start: "<".into(),
25561 end: ">".into(),
25562 close: true,
25563 ..Default::default()
25564 }],
25565 ..Default::default()
25566 },
25567 ..Default::default()
25568 },
25569 Some(tree_sitter_html::LANGUAGE.into()),
25570 ));
25571 language_registry.add(html_language);
25572 let mut fake_servers = language_registry.register_fake_lsp(
25573 "HTML",
25574 FakeLspAdapter {
25575 capabilities: lsp::ServerCapabilities {
25576 completion_provider: Some(lsp::CompletionOptions {
25577 resolve_provider: Some(true),
25578 ..Default::default()
25579 }),
25580 ..Default::default()
25581 },
25582 ..Default::default()
25583 },
25584 );
25585
25586 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25587 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25588
25589 let worktree_id = workspace
25590 .update(cx, |workspace, _window, cx| {
25591 workspace.project().update(cx, |project, cx| {
25592 project.worktrees(cx).next().unwrap().read(cx).id()
25593 })
25594 })
25595 .unwrap();
25596 project
25597 .update(cx, |project, cx| {
25598 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
25599 })
25600 .await
25601 .unwrap();
25602 let editor = workspace
25603 .update(cx, |workspace, window, cx| {
25604 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
25605 })
25606 .unwrap()
25607 .await
25608 .unwrap()
25609 .downcast::<Editor>()
25610 .unwrap();
25611
25612 let fake_server = fake_servers.next().await.unwrap();
25613 cx.run_until_parked();
25614 editor.update_in(cx, |editor, window, cx| {
25615 editor.set_text("<ad></ad>", window, cx);
25616 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
25617 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
25618 });
25619 set_linked_edit_ranges(
25620 (Point::new(0, 1), Point::new(0, 3)),
25621 (Point::new(0, 6), Point::new(0, 8)),
25622 editor,
25623 cx,
25624 );
25625 });
25626 let mut completion_handle =
25627 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25628 Ok(Some(lsp::CompletionResponse::Array(vec![
25629 lsp::CompletionItem {
25630 label: "head".to_string(),
25631 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25632 lsp::InsertReplaceEdit {
25633 new_text: "head".to_string(),
25634 insert: lsp::Range::new(
25635 lsp::Position::new(0, 1),
25636 lsp::Position::new(0, 3),
25637 ),
25638 replace: lsp::Range::new(
25639 lsp::Position::new(0, 1),
25640 lsp::Position::new(0, 3),
25641 ),
25642 },
25643 )),
25644 ..Default::default()
25645 },
25646 ])))
25647 });
25648 editor.update_in(cx, |editor, window, cx| {
25649 editor.show_completions(&ShowCompletions, window, cx);
25650 });
25651 cx.run_until_parked();
25652 completion_handle.next().await.unwrap();
25653 editor.update(cx, |editor, _| {
25654 assert!(
25655 editor.context_menu_visible(),
25656 "Completion menu should be visible"
25657 );
25658 });
25659 editor.update_in(cx, |editor, window, cx| {
25660 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
25661 });
25662 cx.executor().run_until_parked();
25663 editor.update(cx, |editor, cx| {
25664 assert_eq!(editor.text(cx), "<head></head>");
25665 });
25666}
25667
25668#[gpui::test]
25669async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
25670 init_test(cx, |_| {});
25671
25672 let mut cx = EditorTestContext::new(cx).await;
25673 let language = Arc::new(Language::new(
25674 LanguageConfig {
25675 name: "TSX".into(),
25676 matcher: LanguageMatcher {
25677 path_suffixes: vec!["tsx".to_string()],
25678 ..LanguageMatcher::default()
25679 },
25680 brackets: BracketPairConfig {
25681 pairs: vec![BracketPair {
25682 start: "<".into(),
25683 end: ">".into(),
25684 close: true,
25685 ..Default::default()
25686 }],
25687 ..Default::default()
25688 },
25689 linked_edit_characters: HashSet::from_iter(['.']),
25690 ..Default::default()
25691 },
25692 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
25693 ));
25694 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25695
25696 // Test typing > does not extend linked pair
25697 cx.set_state("<divˇ<div></div>");
25698 cx.update_editor(|editor, _, cx| {
25699 set_linked_edit_ranges(
25700 (Point::new(0, 1), Point::new(0, 4)),
25701 (Point::new(0, 11), Point::new(0, 14)),
25702 editor,
25703 cx,
25704 );
25705 });
25706 cx.update_editor(|editor, window, cx| {
25707 editor.handle_input(">", window, cx);
25708 });
25709 cx.assert_editor_state("<div>ˇ<div></div>");
25710
25711 // Test typing . do extend linked pair
25712 cx.set_state("<Animatedˇ></Animated>");
25713 cx.update_editor(|editor, _, cx| {
25714 set_linked_edit_ranges(
25715 (Point::new(0, 1), Point::new(0, 9)),
25716 (Point::new(0, 12), Point::new(0, 20)),
25717 editor,
25718 cx,
25719 );
25720 });
25721 cx.update_editor(|editor, window, cx| {
25722 editor.handle_input(".", window, cx);
25723 });
25724 cx.assert_editor_state("<Animated.ˇ></Animated.>");
25725 cx.update_editor(|editor, _, cx| {
25726 set_linked_edit_ranges(
25727 (Point::new(0, 1), Point::new(0, 10)),
25728 (Point::new(0, 13), Point::new(0, 21)),
25729 editor,
25730 cx,
25731 );
25732 });
25733 cx.update_editor(|editor, window, cx| {
25734 editor.handle_input("V", window, cx);
25735 });
25736 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
25737}
25738
25739#[gpui::test]
25740async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
25741 init_test(cx, |_| {});
25742
25743 let fs = FakeFs::new(cx.executor());
25744 fs.insert_tree(
25745 path!("/root"),
25746 json!({
25747 "a": {
25748 "main.rs": "fn main() {}",
25749 },
25750 "foo": {
25751 "bar": {
25752 "external_file.rs": "pub mod external {}",
25753 }
25754 }
25755 }),
25756 )
25757 .await;
25758
25759 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
25760 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25761 language_registry.add(rust_lang());
25762 let _fake_servers = language_registry.register_fake_lsp(
25763 "Rust",
25764 FakeLspAdapter {
25765 ..FakeLspAdapter::default()
25766 },
25767 );
25768 let (workspace, cx) =
25769 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25770 let worktree_id = workspace.update(cx, |workspace, cx| {
25771 workspace.project().update(cx, |project, cx| {
25772 project.worktrees(cx).next().unwrap().read(cx).id()
25773 })
25774 });
25775
25776 let assert_language_servers_count =
25777 |expected: usize, context: &str, cx: &mut VisualTestContext| {
25778 project.update(cx, |project, cx| {
25779 let current = project
25780 .lsp_store()
25781 .read(cx)
25782 .as_local()
25783 .unwrap()
25784 .language_servers
25785 .len();
25786 assert_eq!(expected, current, "{context}");
25787 });
25788 };
25789
25790 assert_language_servers_count(
25791 0,
25792 "No servers should be running before any file is open",
25793 cx,
25794 );
25795 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25796 let main_editor = workspace
25797 .update_in(cx, |workspace, window, cx| {
25798 workspace.open_path(
25799 (worktree_id, rel_path("main.rs")),
25800 Some(pane.downgrade()),
25801 true,
25802 window,
25803 cx,
25804 )
25805 })
25806 .unwrap()
25807 .await
25808 .downcast::<Editor>()
25809 .unwrap();
25810 pane.update(cx, |pane, cx| {
25811 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25812 open_editor.update(cx, |editor, cx| {
25813 assert_eq!(
25814 editor.display_text(cx),
25815 "fn main() {}",
25816 "Original main.rs text on initial open",
25817 );
25818 });
25819 assert_eq!(open_editor, main_editor);
25820 });
25821 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
25822
25823 let external_editor = workspace
25824 .update_in(cx, |workspace, window, cx| {
25825 workspace.open_abs_path(
25826 PathBuf::from("/root/foo/bar/external_file.rs"),
25827 OpenOptions::default(),
25828 window,
25829 cx,
25830 )
25831 })
25832 .await
25833 .expect("opening external file")
25834 .downcast::<Editor>()
25835 .expect("downcasted external file's open element to editor");
25836 pane.update(cx, |pane, cx| {
25837 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25838 open_editor.update(cx, |editor, cx| {
25839 assert_eq!(
25840 editor.display_text(cx),
25841 "pub mod external {}",
25842 "External file is open now",
25843 );
25844 });
25845 assert_eq!(open_editor, external_editor);
25846 });
25847 assert_language_servers_count(
25848 1,
25849 "Second, external, *.rs file should join the existing server",
25850 cx,
25851 );
25852
25853 pane.update_in(cx, |pane, window, cx| {
25854 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25855 })
25856 .await
25857 .unwrap();
25858 pane.update_in(cx, |pane, window, cx| {
25859 pane.navigate_backward(&Default::default(), window, cx);
25860 });
25861 cx.run_until_parked();
25862 pane.update(cx, |pane, cx| {
25863 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25864 open_editor.update(cx, |editor, cx| {
25865 assert_eq!(
25866 editor.display_text(cx),
25867 "pub mod external {}",
25868 "External file is open now",
25869 );
25870 });
25871 });
25872 assert_language_servers_count(
25873 1,
25874 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25875 cx,
25876 );
25877
25878 cx.update(|_, cx| {
25879 workspace::reload(cx);
25880 });
25881 assert_language_servers_count(
25882 1,
25883 "After reloading the worktree with local and external files opened, only one project should be started",
25884 cx,
25885 );
25886}
25887
25888#[gpui::test]
25889async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25890 init_test(cx, |_| {});
25891
25892 let mut cx = EditorTestContext::new(cx).await;
25893 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25894 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25895
25896 // test cursor move to start of each line on tab
25897 // for `if`, `elif`, `else`, `while`, `with` and `for`
25898 cx.set_state(indoc! {"
25899 def main():
25900 ˇ for item in items:
25901 ˇ while item.active:
25902 ˇ if item.value > 10:
25903 ˇ continue
25904 ˇ elif item.value < 0:
25905 ˇ break
25906 ˇ else:
25907 ˇ with item.context() as ctx:
25908 ˇ yield count
25909 ˇ else:
25910 ˇ log('while else')
25911 ˇ else:
25912 ˇ log('for else')
25913 "});
25914 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25915 cx.wait_for_autoindent_applied().await;
25916 cx.assert_editor_state(indoc! {"
25917 def main():
25918 ˇfor item in items:
25919 ˇwhile item.active:
25920 ˇif item.value > 10:
25921 ˇcontinue
25922 ˇelif item.value < 0:
25923 ˇbreak
25924 ˇelse:
25925 ˇwith item.context() as ctx:
25926 ˇyield count
25927 ˇelse:
25928 ˇlog('while else')
25929 ˇelse:
25930 ˇlog('for else')
25931 "});
25932 // test relative indent is preserved when tab
25933 // for `if`, `elif`, `else`, `while`, `with` and `for`
25934 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25935 cx.wait_for_autoindent_applied().await;
25936 cx.assert_editor_state(indoc! {"
25937 def main():
25938 ˇfor item in items:
25939 ˇwhile item.active:
25940 ˇif item.value > 10:
25941 ˇcontinue
25942 ˇelif item.value < 0:
25943 ˇbreak
25944 ˇelse:
25945 ˇwith item.context() as ctx:
25946 ˇyield count
25947 ˇelse:
25948 ˇlog('while else')
25949 ˇelse:
25950 ˇlog('for else')
25951 "});
25952
25953 // test cursor move to start of each line on tab
25954 // for `try`, `except`, `else`, `finally`, `match` and `def`
25955 cx.set_state(indoc! {"
25956 def main():
25957 ˇ try:
25958 ˇ fetch()
25959 ˇ except ValueError:
25960 ˇ handle_error()
25961 ˇ else:
25962 ˇ match value:
25963 ˇ case _:
25964 ˇ finally:
25965 ˇ def status():
25966 ˇ return 0
25967 "});
25968 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25969 cx.wait_for_autoindent_applied().await;
25970 cx.assert_editor_state(indoc! {"
25971 def main():
25972 ˇtry:
25973 ˇfetch()
25974 ˇexcept ValueError:
25975 ˇhandle_error()
25976 ˇelse:
25977 ˇmatch value:
25978 ˇcase _:
25979 ˇfinally:
25980 ˇdef status():
25981 ˇreturn 0
25982 "});
25983 // test relative indent is preserved when tab
25984 // for `try`, `except`, `else`, `finally`, `match` and `def`
25985 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25986 cx.wait_for_autoindent_applied().await;
25987 cx.assert_editor_state(indoc! {"
25988 def main():
25989 ˇtry:
25990 ˇfetch()
25991 ˇexcept ValueError:
25992 ˇhandle_error()
25993 ˇelse:
25994 ˇmatch value:
25995 ˇcase _:
25996 ˇfinally:
25997 ˇdef status():
25998 ˇreturn 0
25999 "});
26000}
26001
26002#[gpui::test]
26003async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
26004 init_test(cx, |_| {});
26005
26006 let mut cx = EditorTestContext::new(cx).await;
26007 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26008 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26009
26010 // test `else` auto outdents when typed inside `if` block
26011 cx.set_state(indoc! {"
26012 def main():
26013 if i == 2:
26014 return
26015 ˇ
26016 "});
26017 cx.update_editor(|editor, window, cx| {
26018 editor.handle_input("else:", window, cx);
26019 });
26020 cx.wait_for_autoindent_applied().await;
26021 cx.assert_editor_state(indoc! {"
26022 def main():
26023 if i == 2:
26024 return
26025 else:ˇ
26026 "});
26027
26028 // test `except` auto outdents when typed inside `try` block
26029 cx.set_state(indoc! {"
26030 def main():
26031 try:
26032 i = 2
26033 ˇ
26034 "});
26035 cx.update_editor(|editor, window, cx| {
26036 editor.handle_input("except:", window, cx);
26037 });
26038 cx.wait_for_autoindent_applied().await;
26039 cx.assert_editor_state(indoc! {"
26040 def main():
26041 try:
26042 i = 2
26043 except:ˇ
26044 "});
26045
26046 // test `else` auto outdents when typed inside `except` block
26047 cx.set_state(indoc! {"
26048 def main():
26049 try:
26050 i = 2
26051 except:
26052 j = 2
26053 ˇ
26054 "});
26055 cx.update_editor(|editor, window, cx| {
26056 editor.handle_input("else:", window, cx);
26057 });
26058 cx.wait_for_autoindent_applied().await;
26059 cx.assert_editor_state(indoc! {"
26060 def main():
26061 try:
26062 i = 2
26063 except:
26064 j = 2
26065 else:ˇ
26066 "});
26067
26068 // test `finally` auto outdents when typed inside `else` block
26069 cx.set_state(indoc! {"
26070 def main():
26071 try:
26072 i = 2
26073 except:
26074 j = 2
26075 else:
26076 k = 2
26077 ˇ
26078 "});
26079 cx.update_editor(|editor, window, cx| {
26080 editor.handle_input("finally:", window, cx);
26081 });
26082 cx.wait_for_autoindent_applied().await;
26083 cx.assert_editor_state(indoc! {"
26084 def main():
26085 try:
26086 i = 2
26087 except:
26088 j = 2
26089 else:
26090 k = 2
26091 finally:ˇ
26092 "});
26093
26094 // test `else` does not outdents when typed inside `except` block right after for block
26095 cx.set_state(indoc! {"
26096 def main():
26097 try:
26098 i = 2
26099 except:
26100 for i in range(n):
26101 pass
26102 ˇ
26103 "});
26104 cx.update_editor(|editor, window, cx| {
26105 editor.handle_input("else:", window, cx);
26106 });
26107 cx.wait_for_autoindent_applied().await;
26108 cx.assert_editor_state(indoc! {"
26109 def main():
26110 try:
26111 i = 2
26112 except:
26113 for i in range(n):
26114 pass
26115 else:ˇ
26116 "});
26117
26118 // test `finally` auto outdents when typed inside `else` block right after for block
26119 cx.set_state(indoc! {"
26120 def main():
26121 try:
26122 i = 2
26123 except:
26124 j = 2
26125 else:
26126 for i in range(n):
26127 pass
26128 ˇ
26129 "});
26130 cx.update_editor(|editor, window, cx| {
26131 editor.handle_input("finally:", window, cx);
26132 });
26133 cx.wait_for_autoindent_applied().await;
26134 cx.assert_editor_state(indoc! {"
26135 def main():
26136 try:
26137 i = 2
26138 except:
26139 j = 2
26140 else:
26141 for i in range(n):
26142 pass
26143 finally:ˇ
26144 "});
26145
26146 // test `except` outdents to inner "try" block
26147 cx.set_state(indoc! {"
26148 def main():
26149 try:
26150 i = 2
26151 if i == 2:
26152 try:
26153 i = 3
26154 ˇ
26155 "});
26156 cx.update_editor(|editor, window, cx| {
26157 editor.handle_input("except:", window, cx);
26158 });
26159 cx.wait_for_autoindent_applied().await;
26160 cx.assert_editor_state(indoc! {"
26161 def main():
26162 try:
26163 i = 2
26164 if i == 2:
26165 try:
26166 i = 3
26167 except:ˇ
26168 "});
26169
26170 // test `except` outdents to outer "try" block
26171 cx.set_state(indoc! {"
26172 def main():
26173 try:
26174 i = 2
26175 if i == 2:
26176 try:
26177 i = 3
26178 ˇ
26179 "});
26180 cx.update_editor(|editor, window, cx| {
26181 editor.handle_input("except:", window, cx);
26182 });
26183 cx.wait_for_autoindent_applied().await;
26184 cx.assert_editor_state(indoc! {"
26185 def main():
26186 try:
26187 i = 2
26188 if i == 2:
26189 try:
26190 i = 3
26191 except:ˇ
26192 "});
26193
26194 // test `else` stays at correct indent when typed after `for` block
26195 cx.set_state(indoc! {"
26196 def main():
26197 for i in range(10):
26198 if i == 3:
26199 break
26200 ˇ
26201 "});
26202 cx.update_editor(|editor, window, cx| {
26203 editor.handle_input("else:", window, cx);
26204 });
26205 cx.wait_for_autoindent_applied().await;
26206 cx.assert_editor_state(indoc! {"
26207 def main():
26208 for i in range(10):
26209 if i == 3:
26210 break
26211 else:ˇ
26212 "});
26213
26214 // test does not outdent on typing after line with square brackets
26215 cx.set_state(indoc! {"
26216 def f() -> list[str]:
26217 ˇ
26218 "});
26219 cx.update_editor(|editor, window, cx| {
26220 editor.handle_input("a", window, cx);
26221 });
26222 cx.wait_for_autoindent_applied().await;
26223 cx.assert_editor_state(indoc! {"
26224 def f() -> list[str]:
26225 aˇ
26226 "});
26227
26228 // test does not outdent on typing : after case keyword
26229 cx.set_state(indoc! {"
26230 match 1:
26231 caseˇ
26232 "});
26233 cx.update_editor(|editor, window, cx| {
26234 editor.handle_input(":", window, cx);
26235 });
26236 cx.wait_for_autoindent_applied().await;
26237 cx.assert_editor_state(indoc! {"
26238 match 1:
26239 case:ˇ
26240 "});
26241}
26242
26243#[gpui::test]
26244async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
26245 init_test(cx, |_| {});
26246 update_test_language_settings(cx, |settings| {
26247 settings.defaults.extend_comment_on_newline = Some(false);
26248 });
26249 let mut cx = EditorTestContext::new(cx).await;
26250 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26251 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26252
26253 // test correct indent after newline on comment
26254 cx.set_state(indoc! {"
26255 # COMMENT:ˇ
26256 "});
26257 cx.update_editor(|editor, window, cx| {
26258 editor.newline(&Newline, window, cx);
26259 });
26260 cx.wait_for_autoindent_applied().await;
26261 cx.assert_editor_state(indoc! {"
26262 # COMMENT:
26263 ˇ
26264 "});
26265
26266 // test correct indent after newline in brackets
26267 cx.set_state(indoc! {"
26268 {ˇ}
26269 "});
26270 cx.update_editor(|editor, window, cx| {
26271 editor.newline(&Newline, window, cx);
26272 });
26273 cx.wait_for_autoindent_applied().await;
26274 cx.assert_editor_state(indoc! {"
26275 {
26276 ˇ
26277 }
26278 "});
26279
26280 cx.set_state(indoc! {"
26281 (ˇ)
26282 "});
26283 cx.update_editor(|editor, window, cx| {
26284 editor.newline(&Newline, window, cx);
26285 });
26286 cx.run_until_parked();
26287 cx.assert_editor_state(indoc! {"
26288 (
26289 ˇ
26290 )
26291 "});
26292
26293 // do not indent after empty lists or dictionaries
26294 cx.set_state(indoc! {"
26295 a = []ˇ
26296 "});
26297 cx.update_editor(|editor, window, cx| {
26298 editor.newline(&Newline, window, cx);
26299 });
26300 cx.run_until_parked();
26301 cx.assert_editor_state(indoc! {"
26302 a = []
26303 ˇ
26304 "});
26305}
26306
26307#[gpui::test]
26308async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
26309 init_test(cx, |_| {});
26310
26311 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
26312 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
26313 language_registry.add(markdown_lang());
26314 language_registry.add(python_lang);
26315
26316 let mut cx = EditorTestContext::new(cx).await;
26317 cx.update_buffer(|buffer, cx| {
26318 buffer.set_language_registry(language_registry);
26319 buffer.set_language(Some(markdown_lang()), cx);
26320 });
26321
26322 // Test that `else:` correctly outdents to match `if:` inside the Python code block
26323 cx.set_state(indoc! {"
26324 # Heading
26325
26326 ```python
26327 def main():
26328 if condition:
26329 pass
26330 ˇ
26331 ```
26332 "});
26333 cx.update_editor(|editor, window, cx| {
26334 editor.handle_input("else:", window, cx);
26335 });
26336 cx.run_until_parked();
26337 cx.assert_editor_state(indoc! {"
26338 # Heading
26339
26340 ```python
26341 def main():
26342 if condition:
26343 pass
26344 else:ˇ
26345 ```
26346 "});
26347}
26348
26349#[gpui::test]
26350async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
26351 init_test(cx, |_| {});
26352
26353 let mut cx = EditorTestContext::new(cx).await;
26354 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26355 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26356
26357 // test cursor move to start of each line on tab
26358 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
26359 cx.set_state(indoc! {"
26360 function main() {
26361 ˇ for item in $items; do
26362 ˇ while [ -n \"$item\" ]; do
26363 ˇ if [ \"$value\" -gt 10 ]; then
26364 ˇ continue
26365 ˇ elif [ \"$value\" -lt 0 ]; then
26366 ˇ break
26367 ˇ else
26368 ˇ echo \"$item\"
26369 ˇ fi
26370 ˇ done
26371 ˇ done
26372 ˇ}
26373 "});
26374 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26375 cx.wait_for_autoindent_applied().await;
26376 cx.assert_editor_state(indoc! {"
26377 function main() {
26378 ˇfor item in $items; do
26379 ˇwhile [ -n \"$item\" ]; do
26380 ˇif [ \"$value\" -gt 10 ]; then
26381 ˇcontinue
26382 ˇelif [ \"$value\" -lt 0 ]; then
26383 ˇbreak
26384 ˇelse
26385 ˇecho \"$item\"
26386 ˇfi
26387 ˇdone
26388 ˇdone
26389 ˇ}
26390 "});
26391 // test relative indent is preserved when tab
26392 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26393 cx.wait_for_autoindent_applied().await;
26394 cx.assert_editor_state(indoc! {"
26395 function main() {
26396 ˇfor item in $items; do
26397 ˇwhile [ -n \"$item\" ]; do
26398 ˇif [ \"$value\" -gt 10 ]; then
26399 ˇcontinue
26400 ˇelif [ \"$value\" -lt 0 ]; then
26401 ˇbreak
26402 ˇelse
26403 ˇecho \"$item\"
26404 ˇfi
26405 ˇdone
26406 ˇdone
26407 ˇ}
26408 "});
26409
26410 // test cursor move to start of each line on tab
26411 // for `case` statement with patterns
26412 cx.set_state(indoc! {"
26413 function handle() {
26414 ˇ case \"$1\" in
26415 ˇ start)
26416 ˇ echo \"a\"
26417 ˇ ;;
26418 ˇ stop)
26419 ˇ echo \"b\"
26420 ˇ ;;
26421 ˇ *)
26422 ˇ echo \"c\"
26423 ˇ ;;
26424 ˇ esac
26425 ˇ}
26426 "});
26427 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26428 cx.wait_for_autoindent_applied().await;
26429 cx.assert_editor_state(indoc! {"
26430 function handle() {
26431 ˇcase \"$1\" in
26432 ˇstart)
26433 ˇecho \"a\"
26434 ˇ;;
26435 ˇstop)
26436 ˇecho \"b\"
26437 ˇ;;
26438 ˇ*)
26439 ˇecho \"c\"
26440 ˇ;;
26441 ˇesac
26442 ˇ}
26443 "});
26444}
26445
26446#[gpui::test]
26447async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
26448 init_test(cx, |_| {});
26449
26450 let mut cx = EditorTestContext::new(cx).await;
26451 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26452 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26453
26454 // test indents on comment insert
26455 cx.set_state(indoc! {"
26456 function main() {
26457 ˇ for item in $items; do
26458 ˇ while [ -n \"$item\" ]; do
26459 ˇ if [ \"$value\" -gt 10 ]; then
26460 ˇ continue
26461 ˇ elif [ \"$value\" -lt 0 ]; then
26462 ˇ break
26463 ˇ else
26464 ˇ echo \"$item\"
26465 ˇ fi
26466 ˇ done
26467 ˇ done
26468 ˇ}
26469 "});
26470 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
26471 cx.wait_for_autoindent_applied().await;
26472 cx.assert_editor_state(indoc! {"
26473 function main() {
26474 #ˇ for item in $items; do
26475 #ˇ while [ -n \"$item\" ]; do
26476 #ˇ if [ \"$value\" -gt 10 ]; then
26477 #ˇ continue
26478 #ˇ elif [ \"$value\" -lt 0 ]; then
26479 #ˇ break
26480 #ˇ else
26481 #ˇ echo \"$item\"
26482 #ˇ fi
26483 #ˇ done
26484 #ˇ done
26485 #ˇ}
26486 "});
26487}
26488
26489#[gpui::test]
26490async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
26491 init_test(cx, |_| {});
26492
26493 let mut cx = EditorTestContext::new(cx).await;
26494 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26495 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26496
26497 // test `else` auto outdents when typed inside `if` block
26498 cx.set_state(indoc! {"
26499 if [ \"$1\" = \"test\" ]; then
26500 echo \"foo bar\"
26501 ˇ
26502 "});
26503 cx.update_editor(|editor, window, cx| {
26504 editor.handle_input("else", window, cx);
26505 });
26506 cx.wait_for_autoindent_applied().await;
26507 cx.assert_editor_state(indoc! {"
26508 if [ \"$1\" = \"test\" ]; then
26509 echo \"foo bar\"
26510 elseˇ
26511 "});
26512
26513 // test `elif` auto outdents when typed inside `if` block
26514 cx.set_state(indoc! {"
26515 if [ \"$1\" = \"test\" ]; then
26516 echo \"foo bar\"
26517 ˇ
26518 "});
26519 cx.update_editor(|editor, window, cx| {
26520 editor.handle_input("elif", window, cx);
26521 });
26522 cx.wait_for_autoindent_applied().await;
26523 cx.assert_editor_state(indoc! {"
26524 if [ \"$1\" = \"test\" ]; then
26525 echo \"foo bar\"
26526 elifˇ
26527 "});
26528
26529 // test `fi` auto outdents when typed inside `else` block
26530 cx.set_state(indoc! {"
26531 if [ \"$1\" = \"test\" ]; then
26532 echo \"foo bar\"
26533 else
26534 echo \"bar baz\"
26535 ˇ
26536 "});
26537 cx.update_editor(|editor, window, cx| {
26538 editor.handle_input("fi", window, cx);
26539 });
26540 cx.wait_for_autoindent_applied().await;
26541 cx.assert_editor_state(indoc! {"
26542 if [ \"$1\" = \"test\" ]; then
26543 echo \"foo bar\"
26544 else
26545 echo \"bar baz\"
26546 fiˇ
26547 "});
26548
26549 // test `done` auto outdents when typed inside `while` block
26550 cx.set_state(indoc! {"
26551 while read line; do
26552 echo \"$line\"
26553 ˇ
26554 "});
26555 cx.update_editor(|editor, window, cx| {
26556 editor.handle_input("done", window, cx);
26557 });
26558 cx.wait_for_autoindent_applied().await;
26559 cx.assert_editor_state(indoc! {"
26560 while read line; do
26561 echo \"$line\"
26562 doneˇ
26563 "});
26564
26565 // test `done` auto outdents when typed inside `for` block
26566 cx.set_state(indoc! {"
26567 for file in *.txt; do
26568 cat \"$file\"
26569 ˇ
26570 "});
26571 cx.update_editor(|editor, window, cx| {
26572 editor.handle_input("done", window, cx);
26573 });
26574 cx.wait_for_autoindent_applied().await;
26575 cx.assert_editor_state(indoc! {"
26576 for file in *.txt; do
26577 cat \"$file\"
26578 doneˇ
26579 "});
26580
26581 // test `esac` auto outdents when typed inside `case` block
26582 cx.set_state(indoc! {"
26583 case \"$1\" in
26584 start)
26585 echo \"foo bar\"
26586 ;;
26587 stop)
26588 echo \"bar baz\"
26589 ;;
26590 ˇ
26591 "});
26592 cx.update_editor(|editor, window, cx| {
26593 editor.handle_input("esac", window, cx);
26594 });
26595 cx.wait_for_autoindent_applied().await;
26596 cx.assert_editor_state(indoc! {"
26597 case \"$1\" in
26598 start)
26599 echo \"foo bar\"
26600 ;;
26601 stop)
26602 echo \"bar baz\"
26603 ;;
26604 esacˇ
26605 "});
26606
26607 // test `*)` auto outdents when typed inside `case` block
26608 cx.set_state(indoc! {"
26609 case \"$1\" in
26610 start)
26611 echo \"foo bar\"
26612 ;;
26613 ˇ
26614 "});
26615 cx.update_editor(|editor, window, cx| {
26616 editor.handle_input("*)", window, cx);
26617 });
26618 cx.wait_for_autoindent_applied().await;
26619 cx.assert_editor_state(indoc! {"
26620 case \"$1\" in
26621 start)
26622 echo \"foo bar\"
26623 ;;
26624 *)ˇ
26625 "});
26626
26627 // test `fi` outdents to correct level with nested if blocks
26628 cx.set_state(indoc! {"
26629 if [ \"$1\" = \"test\" ]; then
26630 echo \"outer if\"
26631 if [ \"$2\" = \"debug\" ]; then
26632 echo \"inner if\"
26633 ˇ
26634 "});
26635 cx.update_editor(|editor, window, cx| {
26636 editor.handle_input("fi", window, cx);
26637 });
26638 cx.wait_for_autoindent_applied().await;
26639 cx.assert_editor_state(indoc! {"
26640 if [ \"$1\" = \"test\" ]; then
26641 echo \"outer if\"
26642 if [ \"$2\" = \"debug\" ]; then
26643 echo \"inner if\"
26644 fiˇ
26645 "});
26646}
26647
26648#[gpui::test]
26649async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
26650 init_test(cx, |_| {});
26651 update_test_language_settings(cx, |settings| {
26652 settings.defaults.extend_comment_on_newline = Some(false);
26653 });
26654 let mut cx = EditorTestContext::new(cx).await;
26655 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26656 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26657
26658 // test correct indent after newline on comment
26659 cx.set_state(indoc! {"
26660 # COMMENT:ˇ
26661 "});
26662 cx.update_editor(|editor, window, cx| {
26663 editor.newline(&Newline, window, cx);
26664 });
26665 cx.wait_for_autoindent_applied().await;
26666 cx.assert_editor_state(indoc! {"
26667 # COMMENT:
26668 ˇ
26669 "});
26670
26671 // test correct indent after newline after `then`
26672 cx.set_state(indoc! {"
26673
26674 if [ \"$1\" = \"test\" ]; thenˇ
26675 "});
26676 cx.update_editor(|editor, window, cx| {
26677 editor.newline(&Newline, window, cx);
26678 });
26679 cx.wait_for_autoindent_applied().await;
26680 cx.assert_editor_state(indoc! {"
26681
26682 if [ \"$1\" = \"test\" ]; then
26683 ˇ
26684 "});
26685
26686 // test correct indent after newline after `else`
26687 cx.set_state(indoc! {"
26688 if [ \"$1\" = \"test\" ]; then
26689 elseˇ
26690 "});
26691 cx.update_editor(|editor, window, cx| {
26692 editor.newline(&Newline, window, cx);
26693 });
26694 cx.wait_for_autoindent_applied().await;
26695 cx.assert_editor_state(indoc! {"
26696 if [ \"$1\" = \"test\" ]; then
26697 else
26698 ˇ
26699 "});
26700
26701 // test correct indent after newline after `elif`
26702 cx.set_state(indoc! {"
26703 if [ \"$1\" = \"test\" ]; then
26704 elifˇ
26705 "});
26706 cx.update_editor(|editor, window, cx| {
26707 editor.newline(&Newline, window, cx);
26708 });
26709 cx.wait_for_autoindent_applied().await;
26710 cx.assert_editor_state(indoc! {"
26711 if [ \"$1\" = \"test\" ]; then
26712 elif
26713 ˇ
26714 "});
26715
26716 // test correct indent after newline after `do`
26717 cx.set_state(indoc! {"
26718 for file in *.txt; doˇ
26719 "});
26720 cx.update_editor(|editor, window, cx| {
26721 editor.newline(&Newline, window, cx);
26722 });
26723 cx.wait_for_autoindent_applied().await;
26724 cx.assert_editor_state(indoc! {"
26725 for file in *.txt; do
26726 ˇ
26727 "});
26728
26729 // test correct indent after newline after case pattern
26730 cx.set_state(indoc! {"
26731 case \"$1\" in
26732 start)ˇ
26733 "});
26734 cx.update_editor(|editor, window, cx| {
26735 editor.newline(&Newline, window, cx);
26736 });
26737 cx.wait_for_autoindent_applied().await;
26738 cx.assert_editor_state(indoc! {"
26739 case \"$1\" in
26740 start)
26741 ˇ
26742 "});
26743
26744 // test correct indent after newline after case pattern
26745 cx.set_state(indoc! {"
26746 case \"$1\" in
26747 start)
26748 ;;
26749 *)ˇ
26750 "});
26751 cx.update_editor(|editor, window, cx| {
26752 editor.newline(&Newline, window, cx);
26753 });
26754 cx.wait_for_autoindent_applied().await;
26755 cx.assert_editor_state(indoc! {"
26756 case \"$1\" in
26757 start)
26758 ;;
26759 *)
26760 ˇ
26761 "});
26762
26763 // test correct indent after newline after function opening brace
26764 cx.set_state(indoc! {"
26765 function test() {ˇ}
26766 "});
26767 cx.update_editor(|editor, window, cx| {
26768 editor.newline(&Newline, window, cx);
26769 });
26770 cx.wait_for_autoindent_applied().await;
26771 cx.assert_editor_state(indoc! {"
26772 function test() {
26773 ˇ
26774 }
26775 "});
26776
26777 // test no extra indent after semicolon on same line
26778 cx.set_state(indoc! {"
26779 echo \"test\";ˇ
26780 "});
26781 cx.update_editor(|editor, window, cx| {
26782 editor.newline(&Newline, window, cx);
26783 });
26784 cx.wait_for_autoindent_applied().await;
26785 cx.assert_editor_state(indoc! {"
26786 echo \"test\";
26787 ˇ
26788 "});
26789}
26790
26791fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
26792 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
26793 point..point
26794}
26795
26796#[track_caller]
26797fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
26798 let (text, ranges) = marked_text_ranges(marked_text, true);
26799 assert_eq!(editor.text(cx), text);
26800 assert_eq!(
26801 editor.selections.ranges(&editor.display_snapshot(cx)),
26802 ranges
26803 .iter()
26804 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
26805 .collect::<Vec<_>>(),
26806 "Assert selections are {}",
26807 marked_text
26808 );
26809}
26810
26811pub fn handle_signature_help_request(
26812 cx: &mut EditorLspTestContext,
26813 mocked_response: lsp::SignatureHelp,
26814) -> impl Future<Output = ()> + use<> {
26815 let mut request =
26816 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
26817 let mocked_response = mocked_response.clone();
26818 async move { Ok(Some(mocked_response)) }
26819 });
26820
26821 async move {
26822 request.next().await;
26823 }
26824}
26825
26826#[track_caller]
26827pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
26828 cx.update_editor(|editor, _, _| {
26829 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
26830 let entries = menu.entries.borrow();
26831 let entries = entries
26832 .iter()
26833 .map(|entry| entry.string.as_str())
26834 .collect::<Vec<_>>();
26835 assert_eq!(entries, expected);
26836 } else {
26837 panic!("Expected completions menu");
26838 }
26839 });
26840}
26841
26842#[gpui::test]
26843async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
26844 init_test(cx, |_| {});
26845 let mut cx = EditorLspTestContext::new_rust(
26846 lsp::ServerCapabilities {
26847 completion_provider: Some(lsp::CompletionOptions {
26848 ..Default::default()
26849 }),
26850 ..Default::default()
26851 },
26852 cx,
26853 )
26854 .await;
26855 cx.lsp
26856 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
26857 Ok(Some(lsp::CompletionResponse::Array(vec![
26858 lsp::CompletionItem {
26859 label: "unsafe".into(),
26860 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26861 range: lsp::Range {
26862 start: lsp::Position {
26863 line: 0,
26864 character: 9,
26865 },
26866 end: lsp::Position {
26867 line: 0,
26868 character: 11,
26869 },
26870 },
26871 new_text: "unsafe".to_string(),
26872 })),
26873 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
26874 ..Default::default()
26875 },
26876 ])))
26877 });
26878
26879 cx.update_editor(|editor, _, cx| {
26880 editor.project().unwrap().update(cx, |project, cx| {
26881 project.snippets().update(cx, |snippets, _cx| {
26882 snippets.add_snippet_for_test(
26883 None,
26884 PathBuf::from("test_snippets.json"),
26885 vec![
26886 Arc::new(project::snippet_provider::Snippet {
26887 prefix: vec![
26888 "unlimited word count".to_string(),
26889 "unlimit word count".to_string(),
26890 "unlimited unknown".to_string(),
26891 ],
26892 body: "this is many words".to_string(),
26893 description: Some("description".to_string()),
26894 name: "multi-word snippet test".to_string(),
26895 }),
26896 Arc::new(project::snippet_provider::Snippet {
26897 prefix: vec!["unsnip".to_string(), "@few".to_string()],
26898 body: "fewer words".to_string(),
26899 description: Some("alt description".to_string()),
26900 name: "other name".to_string(),
26901 }),
26902 Arc::new(project::snippet_provider::Snippet {
26903 prefix: vec!["ab aa".to_string()],
26904 body: "abcd".to_string(),
26905 description: None,
26906 name: "alphabet".to_string(),
26907 }),
26908 ],
26909 );
26910 });
26911 })
26912 });
26913
26914 let get_completions = |cx: &mut EditorLspTestContext| {
26915 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
26916 Some(CodeContextMenu::Completions(context_menu)) => {
26917 let entries = context_menu.entries.borrow();
26918 entries
26919 .iter()
26920 .map(|entry| entry.string.clone())
26921 .collect_vec()
26922 }
26923 _ => vec![],
26924 })
26925 };
26926
26927 // snippets:
26928 // @foo
26929 // foo bar
26930 //
26931 // when typing:
26932 //
26933 // when typing:
26934 // - if I type a symbol "open the completions with snippets only"
26935 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
26936 //
26937 // stuff we need:
26938 // - filtering logic change?
26939 // - remember how far back the completion started.
26940
26941 let test_cases: &[(&str, &[&str])] = &[
26942 (
26943 "un",
26944 &[
26945 "unsafe",
26946 "unlimit word count",
26947 "unlimited unknown",
26948 "unlimited word count",
26949 "unsnip",
26950 ],
26951 ),
26952 (
26953 "u ",
26954 &[
26955 "unlimit word count",
26956 "unlimited unknown",
26957 "unlimited word count",
26958 ],
26959 ),
26960 ("u a", &["ab aa", "unsafe"]), // unsAfe
26961 (
26962 "u u",
26963 &[
26964 "unsafe",
26965 "unlimit word count",
26966 "unlimited unknown", // ranked highest among snippets
26967 "unlimited word count",
26968 "unsnip",
26969 ],
26970 ),
26971 ("uw c", &["unlimit word count", "unlimited word count"]),
26972 (
26973 "u w",
26974 &[
26975 "unlimit word count",
26976 "unlimited word count",
26977 "unlimited unknown",
26978 ],
26979 ),
26980 ("u w ", &["unlimit word count", "unlimited word count"]),
26981 (
26982 "u ",
26983 &[
26984 "unlimit word count",
26985 "unlimited unknown",
26986 "unlimited word count",
26987 ],
26988 ),
26989 ("wor", &[]),
26990 ("uf", &["unsafe"]),
26991 ("af", &["unsafe"]),
26992 ("afu", &[]),
26993 (
26994 "ue",
26995 &["unsafe", "unlimited unknown", "unlimited word count"],
26996 ),
26997 ("@", &["@few"]),
26998 ("@few", &["@few"]),
26999 ("@ ", &[]),
27000 ("a@", &["@few"]),
27001 ("a@f", &["@few", "unsafe"]),
27002 ("a@fw", &["@few"]),
27003 ("a", &["ab aa", "unsafe"]),
27004 ("aa", &["ab aa"]),
27005 ("aaa", &["ab aa"]),
27006 ("ab", &["ab aa"]),
27007 ("ab ", &["ab aa"]),
27008 ("ab a", &["ab aa", "unsafe"]),
27009 ("ab ab", &["ab aa"]),
27010 ("ab ab aa", &["ab aa"]),
27011 ];
27012
27013 for &(input_to_simulate, expected_completions) in test_cases {
27014 cx.set_state("fn a() { ˇ }\n");
27015 for c in input_to_simulate.split("") {
27016 cx.simulate_input(c);
27017 cx.run_until_parked();
27018 }
27019 let expected_completions = expected_completions
27020 .iter()
27021 .map(|s| s.to_string())
27022 .collect_vec();
27023 assert_eq!(
27024 get_completions(&mut cx),
27025 expected_completions,
27026 "< actual / expected >, input = {input_to_simulate:?}",
27027 );
27028 }
27029}
27030
27031/// Handle completion request passing a marked string specifying where the completion
27032/// should be triggered from using '|' character, what range should be replaced, and what completions
27033/// should be returned using '<' and '>' to delimit the range.
27034///
27035/// Also see `handle_completion_request_with_insert_and_replace`.
27036#[track_caller]
27037pub fn handle_completion_request(
27038 marked_string: &str,
27039 completions: Vec<&'static str>,
27040 is_incomplete: bool,
27041 counter: Arc<AtomicUsize>,
27042 cx: &mut EditorLspTestContext,
27043) -> impl Future<Output = ()> {
27044 let complete_from_marker: TextRangeMarker = '|'.into();
27045 let replace_range_marker: TextRangeMarker = ('<', '>').into();
27046 let (_, mut marked_ranges) = marked_text_ranges_by(
27047 marked_string,
27048 vec![complete_from_marker.clone(), replace_range_marker.clone()],
27049 );
27050
27051 let complete_from_position = cx.to_lsp(MultiBufferOffset(
27052 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
27053 ));
27054 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
27055 let replace_range =
27056 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
27057
27058 let mut request =
27059 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
27060 let completions = completions.clone();
27061 counter.fetch_add(1, atomic::Ordering::Release);
27062 async move {
27063 assert_eq!(params.text_document_position.text_document.uri, url.clone());
27064 assert_eq!(
27065 params.text_document_position.position,
27066 complete_from_position
27067 );
27068 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
27069 is_incomplete,
27070 item_defaults: None,
27071 items: completions
27072 .iter()
27073 .map(|completion_text| lsp::CompletionItem {
27074 label: completion_text.to_string(),
27075 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
27076 range: replace_range,
27077 new_text: completion_text.to_string(),
27078 })),
27079 ..Default::default()
27080 })
27081 .collect(),
27082 })))
27083 }
27084 });
27085
27086 async move {
27087 request.next().await;
27088 }
27089}
27090
27091/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
27092/// given instead, which also contains an `insert` range.
27093///
27094/// This function uses markers to define ranges:
27095/// - `|` marks the cursor position
27096/// - `<>` marks the replace range
27097/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
27098pub fn handle_completion_request_with_insert_and_replace(
27099 cx: &mut EditorLspTestContext,
27100 marked_string: &str,
27101 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
27102 counter: Arc<AtomicUsize>,
27103) -> impl Future<Output = ()> {
27104 let complete_from_marker: TextRangeMarker = '|'.into();
27105 let replace_range_marker: TextRangeMarker = ('<', '>').into();
27106 let insert_range_marker: TextRangeMarker = ('{', '}').into();
27107
27108 let (_, mut marked_ranges) = marked_text_ranges_by(
27109 marked_string,
27110 vec![
27111 complete_from_marker.clone(),
27112 replace_range_marker.clone(),
27113 insert_range_marker.clone(),
27114 ],
27115 );
27116
27117 let complete_from_position = cx.to_lsp(MultiBufferOffset(
27118 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
27119 ));
27120 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
27121 let replace_range =
27122 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
27123
27124 let insert_range = match marked_ranges.remove(&insert_range_marker) {
27125 Some(ranges) if !ranges.is_empty() => {
27126 let range1 = ranges[0].clone();
27127 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
27128 }
27129 _ => lsp::Range {
27130 start: replace_range.start,
27131 end: complete_from_position,
27132 },
27133 };
27134
27135 let mut request =
27136 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
27137 let completions = completions.clone();
27138 counter.fetch_add(1, atomic::Ordering::Release);
27139 async move {
27140 assert_eq!(params.text_document_position.text_document.uri, url.clone());
27141 assert_eq!(
27142 params.text_document_position.position, complete_from_position,
27143 "marker `|` position doesn't match",
27144 );
27145 Ok(Some(lsp::CompletionResponse::Array(
27146 completions
27147 .iter()
27148 .map(|(label, new_text)| lsp::CompletionItem {
27149 label: label.to_string(),
27150 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
27151 lsp::InsertReplaceEdit {
27152 insert: insert_range,
27153 replace: replace_range,
27154 new_text: new_text.to_string(),
27155 },
27156 )),
27157 ..Default::default()
27158 })
27159 .collect(),
27160 )))
27161 }
27162 });
27163
27164 async move {
27165 request.next().await;
27166 }
27167}
27168
27169fn handle_resolve_completion_request(
27170 cx: &mut EditorLspTestContext,
27171 edits: Option<Vec<(&'static str, &'static str)>>,
27172) -> impl Future<Output = ()> {
27173 let edits = edits.map(|edits| {
27174 edits
27175 .iter()
27176 .map(|(marked_string, new_text)| {
27177 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
27178 let replace_range = cx.to_lsp_range(
27179 MultiBufferOffset(marked_ranges[0].start)
27180 ..MultiBufferOffset(marked_ranges[0].end),
27181 );
27182 lsp::TextEdit::new(replace_range, new_text.to_string())
27183 })
27184 .collect::<Vec<_>>()
27185 });
27186
27187 let mut request =
27188 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
27189 let edits = edits.clone();
27190 async move {
27191 Ok(lsp::CompletionItem {
27192 additional_text_edits: edits,
27193 ..Default::default()
27194 })
27195 }
27196 });
27197
27198 async move {
27199 request.next().await;
27200 }
27201}
27202
27203pub(crate) fn update_test_language_settings(
27204 cx: &mut TestAppContext,
27205 f: impl Fn(&mut AllLanguageSettingsContent),
27206) {
27207 cx.update(|cx| {
27208 SettingsStore::update_global(cx, |store, cx| {
27209 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
27210 });
27211 });
27212}
27213
27214pub(crate) fn update_test_project_settings(
27215 cx: &mut TestAppContext,
27216 f: impl Fn(&mut ProjectSettingsContent),
27217) {
27218 cx.update(|cx| {
27219 SettingsStore::update_global(cx, |store, cx| {
27220 store.update_user_settings(cx, |settings| f(&mut settings.project));
27221 });
27222 });
27223}
27224
27225pub(crate) fn update_test_editor_settings(
27226 cx: &mut TestAppContext,
27227 f: impl Fn(&mut EditorSettingsContent),
27228) {
27229 cx.update(|cx| {
27230 SettingsStore::update_global(cx, |store, cx| {
27231 store.update_user_settings(cx, |settings| f(&mut settings.editor));
27232 })
27233 })
27234}
27235
27236pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
27237 cx.update(|cx| {
27238 assets::Assets.load_test_fonts(cx);
27239 let store = SettingsStore::test(cx);
27240 cx.set_global(store);
27241 theme::init(theme::LoadThemes::JustBase, cx);
27242 release_channel::init(semver::Version::new(0, 0, 0), cx);
27243 crate::init(cx);
27244 });
27245 zlog::init_test();
27246 update_test_language_settings(cx, f);
27247}
27248
27249#[track_caller]
27250fn assert_hunk_revert(
27251 not_reverted_text_with_selections: &str,
27252 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
27253 expected_reverted_text_with_selections: &str,
27254 base_text: &str,
27255 cx: &mut EditorLspTestContext,
27256) {
27257 cx.set_state(not_reverted_text_with_selections);
27258 cx.set_head_text(base_text);
27259 cx.executor().run_until_parked();
27260
27261 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
27262 let snapshot = editor.snapshot(window, cx);
27263 let reverted_hunk_statuses = snapshot
27264 .buffer_snapshot()
27265 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
27266 .map(|hunk| hunk.status().kind)
27267 .collect::<Vec<_>>();
27268
27269 editor.git_restore(&Default::default(), window, cx);
27270 reverted_hunk_statuses
27271 });
27272 cx.executor().run_until_parked();
27273 cx.assert_editor_state(expected_reverted_text_with_selections);
27274 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
27275}
27276
27277#[gpui::test(iterations = 10)]
27278async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
27279 init_test(cx, |_| {});
27280
27281 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
27282 let counter = diagnostic_requests.clone();
27283
27284 let fs = FakeFs::new(cx.executor());
27285 fs.insert_tree(
27286 path!("/a"),
27287 json!({
27288 "first.rs": "fn main() { let a = 5; }",
27289 "second.rs": "// Test file",
27290 }),
27291 )
27292 .await;
27293
27294 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27295 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27296 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27297
27298 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27299 language_registry.add(rust_lang());
27300 let mut fake_servers = language_registry.register_fake_lsp(
27301 "Rust",
27302 FakeLspAdapter {
27303 capabilities: lsp::ServerCapabilities {
27304 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
27305 lsp::DiagnosticOptions {
27306 identifier: None,
27307 inter_file_dependencies: true,
27308 workspace_diagnostics: true,
27309 work_done_progress_options: Default::default(),
27310 },
27311 )),
27312 ..Default::default()
27313 },
27314 ..Default::default()
27315 },
27316 );
27317
27318 let editor = workspace
27319 .update(cx, |workspace, window, cx| {
27320 workspace.open_abs_path(
27321 PathBuf::from(path!("/a/first.rs")),
27322 OpenOptions::default(),
27323 window,
27324 cx,
27325 )
27326 })
27327 .unwrap()
27328 .await
27329 .unwrap()
27330 .downcast::<Editor>()
27331 .unwrap();
27332 let fake_server = fake_servers.next().await.unwrap();
27333 let server_id = fake_server.server.server_id();
27334 let mut first_request = fake_server
27335 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
27336 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
27337 let result_id = Some(new_result_id.to_string());
27338 assert_eq!(
27339 params.text_document.uri,
27340 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27341 );
27342 async move {
27343 Ok(lsp::DocumentDiagnosticReportResult::Report(
27344 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
27345 related_documents: None,
27346 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
27347 items: Vec::new(),
27348 result_id,
27349 },
27350 }),
27351 ))
27352 }
27353 });
27354
27355 let ensure_result_id = |expected: Option<SharedString>, cx: &mut TestAppContext| {
27356 project.update(cx, |project, cx| {
27357 let buffer_id = editor
27358 .read(cx)
27359 .buffer()
27360 .read(cx)
27361 .as_singleton()
27362 .expect("created a singleton buffer")
27363 .read(cx)
27364 .remote_id();
27365 let buffer_result_id = project
27366 .lsp_store()
27367 .read(cx)
27368 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
27369 assert_eq!(expected, buffer_result_id);
27370 });
27371 };
27372
27373 ensure_result_id(None, cx);
27374 cx.executor().advance_clock(Duration::from_millis(60));
27375 cx.executor().run_until_parked();
27376 assert_eq!(
27377 diagnostic_requests.load(atomic::Ordering::Acquire),
27378 1,
27379 "Opening file should trigger diagnostic request"
27380 );
27381 first_request
27382 .next()
27383 .await
27384 .expect("should have sent the first diagnostics pull request");
27385 ensure_result_id(Some(SharedString::new("1")), cx);
27386
27387 // Editing should trigger diagnostics
27388 editor.update_in(cx, |editor, window, cx| {
27389 editor.handle_input("2", window, cx)
27390 });
27391 cx.executor().advance_clock(Duration::from_millis(60));
27392 cx.executor().run_until_parked();
27393 assert_eq!(
27394 diagnostic_requests.load(atomic::Ordering::Acquire),
27395 2,
27396 "Editing should trigger diagnostic request"
27397 );
27398 ensure_result_id(Some(SharedString::new("2")), cx);
27399
27400 // Moving cursor should not trigger diagnostic request
27401 editor.update_in(cx, |editor, window, cx| {
27402 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27403 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
27404 });
27405 });
27406 cx.executor().advance_clock(Duration::from_millis(60));
27407 cx.executor().run_until_parked();
27408 assert_eq!(
27409 diagnostic_requests.load(atomic::Ordering::Acquire),
27410 2,
27411 "Cursor movement should not trigger diagnostic request"
27412 );
27413 ensure_result_id(Some(SharedString::new("2")), cx);
27414 // Multiple rapid edits should be debounced
27415 for _ in 0..5 {
27416 editor.update_in(cx, |editor, window, cx| {
27417 editor.handle_input("x", window, cx)
27418 });
27419 }
27420 cx.executor().advance_clock(Duration::from_millis(60));
27421 cx.executor().run_until_parked();
27422
27423 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
27424 assert!(
27425 final_requests <= 4,
27426 "Multiple rapid edits should be debounced (got {final_requests} requests)",
27427 );
27428 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
27429}
27430
27431#[gpui::test]
27432async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
27433 // Regression test for issue #11671
27434 // Previously, adding a cursor after moving multiple cursors would reset
27435 // the cursor count instead of adding to the existing cursors.
27436 init_test(cx, |_| {});
27437 let mut cx = EditorTestContext::new(cx).await;
27438
27439 // Create a simple buffer with cursor at start
27440 cx.set_state(indoc! {"
27441 ˇaaaa
27442 bbbb
27443 cccc
27444 dddd
27445 eeee
27446 ffff
27447 gggg
27448 hhhh"});
27449
27450 // Add 2 cursors below (so we have 3 total)
27451 cx.update_editor(|editor, window, cx| {
27452 editor.add_selection_below(&Default::default(), window, cx);
27453 editor.add_selection_below(&Default::default(), window, cx);
27454 });
27455
27456 // Verify we have 3 cursors
27457 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
27458 assert_eq!(
27459 initial_count, 3,
27460 "Should have 3 cursors after adding 2 below"
27461 );
27462
27463 // Move down one line
27464 cx.update_editor(|editor, window, cx| {
27465 editor.move_down(&MoveDown, window, cx);
27466 });
27467
27468 // Add another cursor below
27469 cx.update_editor(|editor, window, cx| {
27470 editor.add_selection_below(&Default::default(), window, cx);
27471 });
27472
27473 // Should now have 4 cursors (3 original + 1 new)
27474 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
27475 assert_eq!(
27476 final_count, 4,
27477 "Should have 4 cursors after moving and adding another"
27478 );
27479}
27480
27481#[gpui::test]
27482async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
27483 init_test(cx, |_| {});
27484
27485 let mut cx = EditorTestContext::new(cx).await;
27486
27487 cx.set_state(indoc!(
27488 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
27489 Second line here"#
27490 ));
27491
27492 cx.update_editor(|editor, window, cx| {
27493 // Enable soft wrapping with a narrow width to force soft wrapping and
27494 // confirm that more than 2 rows are being displayed.
27495 editor.set_wrap_width(Some(100.0.into()), cx);
27496 assert!(editor.display_text(cx).lines().count() > 2);
27497
27498 editor.add_selection_below(
27499 &AddSelectionBelow {
27500 skip_soft_wrap: true,
27501 },
27502 window,
27503 cx,
27504 );
27505
27506 assert_eq!(
27507 display_ranges(editor, cx),
27508 &[
27509 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27510 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
27511 ]
27512 );
27513
27514 editor.add_selection_above(
27515 &AddSelectionAbove {
27516 skip_soft_wrap: true,
27517 },
27518 window,
27519 cx,
27520 );
27521
27522 assert_eq!(
27523 display_ranges(editor, cx),
27524 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27525 );
27526
27527 editor.add_selection_below(
27528 &AddSelectionBelow {
27529 skip_soft_wrap: false,
27530 },
27531 window,
27532 cx,
27533 );
27534
27535 assert_eq!(
27536 display_ranges(editor, cx),
27537 &[
27538 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27539 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
27540 ]
27541 );
27542
27543 editor.add_selection_above(
27544 &AddSelectionAbove {
27545 skip_soft_wrap: false,
27546 },
27547 window,
27548 cx,
27549 );
27550
27551 assert_eq!(
27552 display_ranges(editor, cx),
27553 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27554 );
27555 });
27556
27557 // Set up text where selections are in the middle of a soft-wrapped line.
27558 // When adding selection below with `skip_soft_wrap` set to `true`, the new
27559 // selection should be at the same buffer column, not the same pixel
27560 // position.
27561 cx.set_state(indoc!(
27562 r#"1. Very long line to show «howˇ» a wrapped line would look
27563 2. Very long line to show how a wrapped line would look"#
27564 ));
27565
27566 cx.update_editor(|editor, window, cx| {
27567 // Enable soft wrapping with a narrow width to force soft wrapping and
27568 // confirm that more than 2 rows are being displayed.
27569 editor.set_wrap_width(Some(100.0.into()), cx);
27570 assert!(editor.display_text(cx).lines().count() > 2);
27571
27572 editor.add_selection_below(
27573 &AddSelectionBelow {
27574 skip_soft_wrap: true,
27575 },
27576 window,
27577 cx,
27578 );
27579
27580 // Assert that there's now 2 selections, both selecting the same column
27581 // range in the buffer row.
27582 let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
27583 let selections = editor.selections.all::<Point>(&display_map);
27584 assert_eq!(selections.len(), 2);
27585 assert_eq!(selections[0].start.column, selections[1].start.column);
27586 assert_eq!(selections[0].end.column, selections[1].end.column);
27587 });
27588}
27589
27590#[gpui::test]
27591async fn test_insert_snippet(cx: &mut TestAppContext) {
27592 init_test(cx, |_| {});
27593 let mut cx = EditorTestContext::new(cx).await;
27594
27595 cx.update_editor(|editor, _, cx| {
27596 editor.project().unwrap().update(cx, |project, cx| {
27597 project.snippets().update(cx, |snippets, _cx| {
27598 let snippet = project::snippet_provider::Snippet {
27599 prefix: vec![], // no prefix needed!
27600 body: "an Unspecified".to_string(),
27601 description: Some("shhhh it's a secret".to_string()),
27602 name: "super secret snippet".to_string(),
27603 };
27604 snippets.add_snippet_for_test(
27605 None,
27606 PathBuf::from("test_snippets.json"),
27607 vec![Arc::new(snippet)],
27608 );
27609
27610 let snippet = project::snippet_provider::Snippet {
27611 prefix: vec![], // no prefix needed!
27612 body: " Location".to_string(),
27613 description: Some("the word 'location'".to_string()),
27614 name: "location word".to_string(),
27615 };
27616 snippets.add_snippet_for_test(
27617 Some("Markdown".to_string()),
27618 PathBuf::from("test_snippets.json"),
27619 vec![Arc::new(snippet)],
27620 );
27621 });
27622 })
27623 });
27624
27625 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
27626
27627 cx.update_editor(|editor, window, cx| {
27628 editor.insert_snippet_at_selections(
27629 &InsertSnippet {
27630 language: None,
27631 name: Some("super secret snippet".to_string()),
27632 snippet: None,
27633 },
27634 window,
27635 cx,
27636 );
27637
27638 // Language is specified in the action,
27639 // so the buffer language does not need to match
27640 editor.insert_snippet_at_selections(
27641 &InsertSnippet {
27642 language: Some("Markdown".to_string()),
27643 name: Some("location word".to_string()),
27644 snippet: None,
27645 },
27646 window,
27647 cx,
27648 );
27649
27650 editor.insert_snippet_at_selections(
27651 &InsertSnippet {
27652 language: None,
27653 name: None,
27654 snippet: Some("$0 after".to_string()),
27655 },
27656 window,
27657 cx,
27658 );
27659 });
27660
27661 cx.assert_editor_state(
27662 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
27663 );
27664}
27665
27666#[gpui::test(iterations = 10)]
27667async fn test_document_colors(cx: &mut TestAppContext) {
27668 let expected_color = Rgba {
27669 r: 0.33,
27670 g: 0.33,
27671 b: 0.33,
27672 a: 0.33,
27673 };
27674
27675 init_test(cx, |_| {});
27676
27677 let fs = FakeFs::new(cx.executor());
27678 fs.insert_tree(
27679 path!("/a"),
27680 json!({
27681 "first.rs": "fn main() { let a = 5; }",
27682 }),
27683 )
27684 .await;
27685
27686 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27687 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27688 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27689
27690 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27691 language_registry.add(rust_lang());
27692 let mut fake_servers = language_registry.register_fake_lsp(
27693 "Rust",
27694 FakeLspAdapter {
27695 capabilities: lsp::ServerCapabilities {
27696 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
27697 ..lsp::ServerCapabilities::default()
27698 },
27699 name: "rust-analyzer",
27700 ..FakeLspAdapter::default()
27701 },
27702 );
27703 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
27704 "Rust",
27705 FakeLspAdapter {
27706 capabilities: lsp::ServerCapabilities {
27707 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
27708 ..lsp::ServerCapabilities::default()
27709 },
27710 name: "not-rust-analyzer",
27711 ..FakeLspAdapter::default()
27712 },
27713 );
27714
27715 let editor = workspace
27716 .update(cx, |workspace, window, cx| {
27717 workspace.open_abs_path(
27718 PathBuf::from(path!("/a/first.rs")),
27719 OpenOptions::default(),
27720 window,
27721 cx,
27722 )
27723 })
27724 .unwrap()
27725 .await
27726 .unwrap()
27727 .downcast::<Editor>()
27728 .unwrap();
27729 let fake_language_server = fake_servers.next().await.unwrap();
27730 let fake_language_server_without_capabilities =
27731 fake_servers_without_capabilities.next().await.unwrap();
27732 let requests_made = Arc::new(AtomicUsize::new(0));
27733 let closure_requests_made = Arc::clone(&requests_made);
27734 let mut color_request_handle = fake_language_server
27735 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27736 let requests_made = Arc::clone(&closure_requests_made);
27737 async move {
27738 assert_eq!(
27739 params.text_document.uri,
27740 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27741 );
27742 requests_made.fetch_add(1, atomic::Ordering::Release);
27743 Ok(vec![
27744 lsp::ColorInformation {
27745 range: lsp::Range {
27746 start: lsp::Position {
27747 line: 0,
27748 character: 0,
27749 },
27750 end: lsp::Position {
27751 line: 0,
27752 character: 1,
27753 },
27754 },
27755 color: lsp::Color {
27756 red: 0.33,
27757 green: 0.33,
27758 blue: 0.33,
27759 alpha: 0.33,
27760 },
27761 },
27762 lsp::ColorInformation {
27763 range: lsp::Range {
27764 start: lsp::Position {
27765 line: 0,
27766 character: 0,
27767 },
27768 end: lsp::Position {
27769 line: 0,
27770 character: 1,
27771 },
27772 },
27773 color: lsp::Color {
27774 red: 0.33,
27775 green: 0.33,
27776 blue: 0.33,
27777 alpha: 0.33,
27778 },
27779 },
27780 ])
27781 }
27782 });
27783
27784 let _handle = fake_language_server_without_capabilities
27785 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
27786 panic!("Should not be called");
27787 });
27788 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27789 color_request_handle.next().await.unwrap();
27790 cx.run_until_parked();
27791 assert_eq!(
27792 1,
27793 requests_made.load(atomic::Ordering::Acquire),
27794 "Should query for colors once per editor open"
27795 );
27796 editor.update_in(cx, |editor, _, cx| {
27797 assert_eq!(
27798 vec![expected_color],
27799 extract_color_inlays(editor, cx),
27800 "Should have an initial inlay"
27801 );
27802 });
27803
27804 // opening another file in a split should not influence the LSP query counter
27805 workspace
27806 .update(cx, |workspace, window, cx| {
27807 assert_eq!(
27808 workspace.panes().len(),
27809 1,
27810 "Should have one pane with one editor"
27811 );
27812 workspace.move_item_to_pane_in_direction(
27813 &MoveItemToPaneInDirection {
27814 direction: SplitDirection::Right,
27815 focus: false,
27816 clone: true,
27817 },
27818 window,
27819 cx,
27820 );
27821 })
27822 .unwrap();
27823 cx.run_until_parked();
27824 workspace
27825 .update(cx, |workspace, _, cx| {
27826 let panes = workspace.panes();
27827 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
27828 for pane in panes {
27829 let editor = pane
27830 .read(cx)
27831 .active_item()
27832 .and_then(|item| item.downcast::<Editor>())
27833 .expect("Should have opened an editor in each split");
27834 let editor_file = editor
27835 .read(cx)
27836 .buffer()
27837 .read(cx)
27838 .as_singleton()
27839 .expect("test deals with singleton buffers")
27840 .read(cx)
27841 .file()
27842 .expect("test buffese should have a file")
27843 .path();
27844 assert_eq!(
27845 editor_file.as_ref(),
27846 rel_path("first.rs"),
27847 "Both editors should be opened for the same file"
27848 )
27849 }
27850 })
27851 .unwrap();
27852
27853 cx.executor().advance_clock(Duration::from_millis(500));
27854 let save = editor.update_in(cx, |editor, window, cx| {
27855 editor.move_to_end(&MoveToEnd, window, cx);
27856 editor.handle_input("dirty", window, cx);
27857 editor.save(
27858 SaveOptions {
27859 format: true,
27860 autosave: true,
27861 },
27862 project.clone(),
27863 window,
27864 cx,
27865 )
27866 });
27867 save.await.unwrap();
27868
27869 color_request_handle.next().await.unwrap();
27870 cx.run_until_parked();
27871 assert_eq!(
27872 2,
27873 requests_made.load(atomic::Ordering::Acquire),
27874 "Should query for colors once per save (deduplicated) and once per formatting after save"
27875 );
27876
27877 drop(editor);
27878 let close = workspace
27879 .update(cx, |workspace, window, cx| {
27880 workspace.active_pane().update(cx, |pane, cx| {
27881 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27882 })
27883 })
27884 .unwrap();
27885 close.await.unwrap();
27886 let close = workspace
27887 .update(cx, |workspace, window, cx| {
27888 workspace.active_pane().update(cx, |pane, cx| {
27889 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27890 })
27891 })
27892 .unwrap();
27893 close.await.unwrap();
27894 assert_eq!(
27895 2,
27896 requests_made.load(atomic::Ordering::Acquire),
27897 "After saving and closing all editors, no extra requests should be made"
27898 );
27899 workspace
27900 .update(cx, |workspace, _, cx| {
27901 assert!(
27902 workspace.active_item(cx).is_none(),
27903 "Should close all editors"
27904 )
27905 })
27906 .unwrap();
27907
27908 workspace
27909 .update(cx, |workspace, window, cx| {
27910 workspace.active_pane().update(cx, |pane, cx| {
27911 pane.navigate_backward(&workspace::GoBack, window, cx);
27912 })
27913 })
27914 .unwrap();
27915 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27916 cx.run_until_parked();
27917 let editor = workspace
27918 .update(cx, |workspace, _, cx| {
27919 workspace
27920 .active_item(cx)
27921 .expect("Should have reopened the editor again after navigating back")
27922 .downcast::<Editor>()
27923 .expect("Should be an editor")
27924 })
27925 .unwrap();
27926
27927 assert_eq!(
27928 2,
27929 requests_made.load(atomic::Ordering::Acquire),
27930 "Cache should be reused on buffer close and reopen"
27931 );
27932 editor.update(cx, |editor, cx| {
27933 assert_eq!(
27934 vec![expected_color],
27935 extract_color_inlays(editor, cx),
27936 "Should have an initial inlay"
27937 );
27938 });
27939
27940 drop(color_request_handle);
27941 let closure_requests_made = Arc::clone(&requests_made);
27942 let mut empty_color_request_handle = fake_language_server
27943 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27944 let requests_made = Arc::clone(&closure_requests_made);
27945 async move {
27946 assert_eq!(
27947 params.text_document.uri,
27948 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27949 );
27950 requests_made.fetch_add(1, atomic::Ordering::Release);
27951 Ok(Vec::new())
27952 }
27953 });
27954 let save = editor.update_in(cx, |editor, window, cx| {
27955 editor.move_to_end(&MoveToEnd, window, cx);
27956 editor.handle_input("dirty_again", window, cx);
27957 editor.save(
27958 SaveOptions {
27959 format: false,
27960 autosave: true,
27961 },
27962 project.clone(),
27963 window,
27964 cx,
27965 )
27966 });
27967 save.await.unwrap();
27968
27969 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27970 empty_color_request_handle.next().await.unwrap();
27971 cx.run_until_parked();
27972 assert_eq!(
27973 3,
27974 requests_made.load(atomic::Ordering::Acquire),
27975 "Should query for colors once per save only, as formatting was not requested"
27976 );
27977 editor.update(cx, |editor, cx| {
27978 assert_eq!(
27979 Vec::<Rgba>::new(),
27980 extract_color_inlays(editor, cx),
27981 "Should clear all colors when the server returns an empty response"
27982 );
27983 });
27984}
27985
27986#[gpui::test]
27987async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
27988 init_test(cx, |_| {});
27989 let (editor, cx) = cx.add_window_view(Editor::single_line);
27990 editor.update_in(cx, |editor, window, cx| {
27991 editor.set_text("oops\n\nwow\n", window, cx)
27992 });
27993 cx.run_until_parked();
27994 editor.update(cx, |editor, cx| {
27995 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
27996 });
27997 editor.update(cx, |editor, cx| {
27998 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
27999 });
28000 cx.run_until_parked();
28001 editor.update(cx, |editor, cx| {
28002 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
28003 });
28004}
28005
28006#[gpui::test]
28007async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
28008 init_test(cx, |_| {});
28009
28010 cx.update(|cx| {
28011 register_project_item::<Editor>(cx);
28012 });
28013
28014 let fs = FakeFs::new(cx.executor());
28015 fs.insert_tree("/root1", json!({})).await;
28016 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
28017 .await;
28018
28019 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
28020 let (workspace, cx) =
28021 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
28022
28023 let worktree_id = project.update(cx, |project, cx| {
28024 project.worktrees(cx).next().unwrap().read(cx).id()
28025 });
28026
28027 let handle = workspace
28028 .update_in(cx, |workspace, window, cx| {
28029 let project_path = (worktree_id, rel_path("one.pdf"));
28030 workspace.open_path(project_path, None, true, window, cx)
28031 })
28032 .await
28033 .unwrap();
28034 // The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
28035 // Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
28036 // With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
28037 assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
28038}
28039
28040#[gpui::test]
28041async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
28042 init_test(cx, |_| {});
28043
28044 let language = Arc::new(Language::new(
28045 LanguageConfig::default(),
28046 Some(tree_sitter_rust::LANGUAGE.into()),
28047 ));
28048
28049 // Test hierarchical sibling navigation
28050 let text = r#"
28051 fn outer() {
28052 if condition {
28053 let a = 1;
28054 }
28055 let b = 2;
28056 }
28057
28058 fn another() {
28059 let c = 3;
28060 }
28061 "#;
28062
28063 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
28064 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
28065 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
28066
28067 // Wait for parsing to complete
28068 editor
28069 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
28070 .await;
28071
28072 editor.update_in(cx, |editor, window, cx| {
28073 // Start by selecting "let a = 1;" inside the if block
28074 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28075 s.select_display_ranges([
28076 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
28077 ]);
28078 });
28079
28080 let initial_selection = editor
28081 .selections
28082 .display_ranges(&editor.display_snapshot(cx));
28083 assert_eq!(initial_selection.len(), 1, "Should have one selection");
28084
28085 // Test select next sibling - should move up levels to find the next sibling
28086 // Since "let a = 1;" has no siblings in the if block, it should move up
28087 // to find "let b = 2;" which is a sibling of the if block
28088 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
28089 let next_selection = editor
28090 .selections
28091 .display_ranges(&editor.display_snapshot(cx));
28092
28093 // Should have a selection and it should be different from the initial
28094 assert_eq!(
28095 next_selection.len(),
28096 1,
28097 "Should have one selection after next"
28098 );
28099 assert_ne!(
28100 next_selection[0], initial_selection[0],
28101 "Next sibling selection should be different"
28102 );
28103
28104 // Test hierarchical navigation by going to the end of the current function
28105 // and trying to navigate to the next function
28106 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28107 s.select_display_ranges([
28108 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
28109 ]);
28110 });
28111
28112 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
28113 let function_next_selection = editor
28114 .selections
28115 .display_ranges(&editor.display_snapshot(cx));
28116
28117 // Should move to the next function
28118 assert_eq!(
28119 function_next_selection.len(),
28120 1,
28121 "Should have one selection after function next"
28122 );
28123
28124 // Test select previous sibling navigation
28125 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
28126 let prev_selection = editor
28127 .selections
28128 .display_ranges(&editor.display_snapshot(cx));
28129
28130 // Should have a selection and it should be different
28131 assert_eq!(
28132 prev_selection.len(),
28133 1,
28134 "Should have one selection after prev"
28135 );
28136 assert_ne!(
28137 prev_selection[0], function_next_selection[0],
28138 "Previous sibling selection should be different from next"
28139 );
28140 });
28141}
28142
28143#[gpui::test]
28144async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
28145 init_test(cx, |_| {});
28146
28147 let mut cx = EditorTestContext::new(cx).await;
28148 cx.set_state(
28149 "let ˇvariable = 42;
28150let another = variable + 1;
28151let result = variable * 2;",
28152 );
28153
28154 // Set up document highlights manually (simulating LSP response)
28155 cx.update_editor(|editor, _window, cx| {
28156 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
28157
28158 // Create highlights for "variable" occurrences
28159 let highlight_ranges = [
28160 Point::new(0, 4)..Point::new(0, 12), // First "variable"
28161 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
28162 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
28163 ];
28164
28165 let anchor_ranges: Vec<_> = highlight_ranges
28166 .iter()
28167 .map(|range| range.clone().to_anchors(&buffer_snapshot))
28168 .collect();
28169
28170 editor.highlight_background::<DocumentHighlightRead>(
28171 &anchor_ranges,
28172 |_, theme| theme.colors().editor_document_highlight_read_background,
28173 cx,
28174 );
28175 });
28176
28177 // Go to next highlight - should move to second "variable"
28178 cx.update_editor(|editor, window, cx| {
28179 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28180 });
28181 cx.assert_editor_state(
28182 "let variable = 42;
28183let another = ˇvariable + 1;
28184let result = variable * 2;",
28185 );
28186
28187 // Go to next highlight - should move to third "variable"
28188 cx.update_editor(|editor, window, cx| {
28189 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28190 });
28191 cx.assert_editor_state(
28192 "let variable = 42;
28193let another = variable + 1;
28194let result = ˇvariable * 2;",
28195 );
28196
28197 // Go to next highlight - should stay at third "variable" (no wrap-around)
28198 cx.update_editor(|editor, window, cx| {
28199 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28200 });
28201 cx.assert_editor_state(
28202 "let variable = 42;
28203let another = variable + 1;
28204let result = ˇvariable * 2;",
28205 );
28206
28207 // Now test going backwards from third position
28208 cx.update_editor(|editor, window, cx| {
28209 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28210 });
28211 cx.assert_editor_state(
28212 "let variable = 42;
28213let another = ˇvariable + 1;
28214let result = variable * 2;",
28215 );
28216
28217 // Go to previous highlight - should move to first "variable"
28218 cx.update_editor(|editor, window, cx| {
28219 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28220 });
28221 cx.assert_editor_state(
28222 "let ˇvariable = 42;
28223let another = variable + 1;
28224let result = variable * 2;",
28225 );
28226
28227 // Go to previous highlight - should stay on first "variable"
28228 cx.update_editor(|editor, window, cx| {
28229 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28230 });
28231 cx.assert_editor_state(
28232 "let ˇvariable = 42;
28233let another = variable + 1;
28234let result = variable * 2;",
28235 );
28236}
28237
28238#[gpui::test]
28239async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
28240 cx: &mut gpui::TestAppContext,
28241) {
28242 init_test(cx, |_| {});
28243
28244 let url = "https://zed.dev";
28245
28246 let markdown_language = Arc::new(Language::new(
28247 LanguageConfig {
28248 name: "Markdown".into(),
28249 ..LanguageConfig::default()
28250 },
28251 None,
28252 ));
28253
28254 let mut cx = EditorTestContext::new(cx).await;
28255 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28256 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
28257
28258 cx.update_editor(|editor, window, cx| {
28259 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28260 editor.paste(&Paste, window, cx);
28261 });
28262
28263 cx.assert_editor_state(&format!(
28264 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
28265 ));
28266}
28267
28268#[gpui::test]
28269async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
28270 init_test(cx, |_| {});
28271
28272 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
28273 let mut cx = EditorTestContext::new(cx).await;
28274
28275 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28276
28277 // Case 1: Test if adding a character with multi cursors preserves nested list indents
28278 cx.set_state(&indoc! {"
28279 - [ ] Item 1
28280 - [ ] Item 1.a
28281 - [ˇ] Item 2
28282 - [ˇ] Item 2.a
28283 - [ˇ] Item 2.b
28284 "
28285 });
28286 cx.update_editor(|editor, window, cx| {
28287 editor.handle_input("x", window, cx);
28288 });
28289 cx.run_until_parked();
28290 cx.assert_editor_state(indoc! {"
28291 - [ ] Item 1
28292 - [ ] Item 1.a
28293 - [xˇ] Item 2
28294 - [xˇ] Item 2.a
28295 - [xˇ] Item 2.b
28296 "
28297 });
28298
28299 // Case 2: Test adding new line after nested list continues the list with unchecked task
28300 cx.set_state(&indoc! {"
28301 - [ ] Item 1
28302 - [ ] Item 1.a
28303 - [x] Item 2
28304 - [x] Item 2.a
28305 - [x] Item 2.bˇ"
28306 });
28307 cx.update_editor(|editor, window, cx| {
28308 editor.newline(&Newline, window, cx);
28309 });
28310 cx.assert_editor_state(indoc! {"
28311 - [ ] Item 1
28312 - [ ] Item 1.a
28313 - [x] Item 2
28314 - [x] Item 2.a
28315 - [x] Item 2.b
28316 - [ ] ˇ"
28317 });
28318
28319 // Case 3: Test adding content to continued list item
28320 cx.update_editor(|editor, window, cx| {
28321 editor.handle_input("Item 2.c", window, cx);
28322 });
28323 cx.run_until_parked();
28324 cx.assert_editor_state(indoc! {"
28325 - [ ] Item 1
28326 - [ ] Item 1.a
28327 - [x] Item 2
28328 - [x] Item 2.a
28329 - [x] Item 2.b
28330 - [ ] Item 2.cˇ"
28331 });
28332
28333 // Case 4: Test adding new line after nested ordered list continues with next number
28334 cx.set_state(indoc! {"
28335 1. Item 1
28336 1. Item 1.a
28337 2. Item 2
28338 1. Item 2.a
28339 2. Item 2.bˇ"
28340 });
28341 cx.update_editor(|editor, window, cx| {
28342 editor.newline(&Newline, window, cx);
28343 });
28344 cx.assert_editor_state(indoc! {"
28345 1. Item 1
28346 1. Item 1.a
28347 2. Item 2
28348 1. Item 2.a
28349 2. Item 2.b
28350 3. ˇ"
28351 });
28352
28353 // Case 5: Adding content to continued ordered list item
28354 cx.update_editor(|editor, window, cx| {
28355 editor.handle_input("Item 2.c", window, cx);
28356 });
28357 cx.run_until_parked();
28358 cx.assert_editor_state(indoc! {"
28359 1. Item 1
28360 1. Item 1.a
28361 2. Item 2
28362 1. Item 2.a
28363 2. Item 2.b
28364 3. Item 2.cˇ"
28365 });
28366
28367 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
28368 cx.set_state(indoc! {"
28369 - Item 1
28370 - Item 1.a
28371 - Item 1.a
28372 ˇ"});
28373 cx.update_editor(|editor, window, cx| {
28374 editor.handle_input("-", window, cx);
28375 });
28376 cx.run_until_parked();
28377 cx.assert_editor_state(indoc! {"
28378 - Item 1
28379 - Item 1.a
28380 - Item 1.a
28381 -ˇ"});
28382
28383 // Case 7: Test blockquote newline preserves something
28384 cx.set_state(indoc! {"
28385 > Item 1ˇ"
28386 });
28387 cx.update_editor(|editor, window, cx| {
28388 editor.newline(&Newline, window, cx);
28389 });
28390 cx.assert_editor_state(indoc! {"
28391 > Item 1
28392 ˇ"
28393 });
28394}
28395
28396#[gpui::test]
28397async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
28398 cx: &mut gpui::TestAppContext,
28399) {
28400 init_test(cx, |_| {});
28401
28402 let url = "https://zed.dev";
28403
28404 let markdown_language = Arc::new(Language::new(
28405 LanguageConfig {
28406 name: "Markdown".into(),
28407 ..LanguageConfig::default()
28408 },
28409 None,
28410 ));
28411
28412 let mut cx = EditorTestContext::new(cx).await;
28413 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28414 cx.set_state(&format!(
28415 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
28416 ));
28417
28418 cx.update_editor(|editor, window, cx| {
28419 editor.copy(&Copy, window, cx);
28420 });
28421
28422 cx.set_state(&format!(
28423 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
28424 ));
28425
28426 cx.update_editor(|editor, window, cx| {
28427 editor.paste(&Paste, window, cx);
28428 });
28429
28430 cx.assert_editor_state(&format!(
28431 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
28432 ));
28433}
28434
28435#[gpui::test]
28436async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
28437 cx: &mut gpui::TestAppContext,
28438) {
28439 init_test(cx, |_| {});
28440
28441 let url = "https://zed.dev";
28442
28443 let markdown_language = Arc::new(Language::new(
28444 LanguageConfig {
28445 name: "Markdown".into(),
28446 ..LanguageConfig::default()
28447 },
28448 None,
28449 ));
28450
28451 let mut cx = EditorTestContext::new(cx).await;
28452 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28453 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
28454
28455 cx.update_editor(|editor, window, cx| {
28456 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28457 editor.paste(&Paste, window, cx);
28458 });
28459
28460 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
28461}
28462
28463#[gpui::test]
28464async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
28465 cx: &mut gpui::TestAppContext,
28466) {
28467 init_test(cx, |_| {});
28468
28469 let text = "Awesome";
28470
28471 let markdown_language = Arc::new(Language::new(
28472 LanguageConfig {
28473 name: "Markdown".into(),
28474 ..LanguageConfig::default()
28475 },
28476 None,
28477 ));
28478
28479 let mut cx = EditorTestContext::new(cx).await;
28480 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28481 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
28482
28483 cx.update_editor(|editor, window, cx| {
28484 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
28485 editor.paste(&Paste, window, cx);
28486 });
28487
28488 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
28489}
28490
28491#[gpui::test]
28492async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
28493 cx: &mut gpui::TestAppContext,
28494) {
28495 init_test(cx, |_| {});
28496
28497 let url = "https://zed.dev";
28498
28499 let markdown_language = Arc::new(Language::new(
28500 LanguageConfig {
28501 name: "Rust".into(),
28502 ..LanguageConfig::default()
28503 },
28504 None,
28505 ));
28506
28507 let mut cx = EditorTestContext::new(cx).await;
28508 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28509 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
28510
28511 cx.update_editor(|editor, window, cx| {
28512 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28513 editor.paste(&Paste, window, cx);
28514 });
28515
28516 cx.assert_editor_state(&format!(
28517 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
28518 ));
28519}
28520
28521#[gpui::test]
28522async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
28523 cx: &mut TestAppContext,
28524) {
28525 init_test(cx, |_| {});
28526
28527 let url = "https://zed.dev";
28528
28529 let markdown_language = Arc::new(Language::new(
28530 LanguageConfig {
28531 name: "Markdown".into(),
28532 ..LanguageConfig::default()
28533 },
28534 None,
28535 ));
28536
28537 let (editor, cx) = cx.add_window_view(|window, cx| {
28538 let multi_buffer = MultiBuffer::build_multi(
28539 [
28540 ("this will embed -> link", vec![Point::row_range(0..1)]),
28541 ("this will replace -> link", vec![Point::row_range(0..1)]),
28542 ],
28543 cx,
28544 );
28545 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
28546 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28547 s.select_ranges(vec![
28548 Point::new(0, 19)..Point::new(0, 23),
28549 Point::new(1, 21)..Point::new(1, 25),
28550 ])
28551 });
28552 let first_buffer_id = multi_buffer
28553 .read(cx)
28554 .excerpt_buffer_ids()
28555 .into_iter()
28556 .next()
28557 .unwrap();
28558 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
28559 first_buffer.update(cx, |buffer, cx| {
28560 buffer.set_language(Some(markdown_language.clone()), cx);
28561 });
28562
28563 editor
28564 });
28565 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28566
28567 cx.update_editor(|editor, window, cx| {
28568 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28569 editor.paste(&Paste, window, cx);
28570 });
28571
28572 cx.assert_editor_state(&format!(
28573 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
28574 ));
28575}
28576
28577#[gpui::test]
28578async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
28579 init_test(cx, |_| {});
28580
28581 let fs = FakeFs::new(cx.executor());
28582 fs.insert_tree(
28583 path!("/project"),
28584 json!({
28585 "first.rs": "# First Document\nSome content here.",
28586 "second.rs": "Plain text content for second file.",
28587 }),
28588 )
28589 .await;
28590
28591 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
28592 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
28593 let cx = &mut VisualTestContext::from_window(*workspace, cx);
28594
28595 let language = rust_lang();
28596 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28597 language_registry.add(language.clone());
28598 let mut fake_servers = language_registry.register_fake_lsp(
28599 "Rust",
28600 FakeLspAdapter {
28601 ..FakeLspAdapter::default()
28602 },
28603 );
28604
28605 let buffer1 = project
28606 .update(cx, |project, cx| {
28607 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
28608 })
28609 .await
28610 .unwrap();
28611 let buffer2 = project
28612 .update(cx, |project, cx| {
28613 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
28614 })
28615 .await
28616 .unwrap();
28617
28618 let multi_buffer = cx.new(|cx| {
28619 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
28620 multi_buffer.set_excerpts_for_path(
28621 PathKey::for_buffer(&buffer1, cx),
28622 buffer1.clone(),
28623 [Point::zero()..buffer1.read(cx).max_point()],
28624 3,
28625 cx,
28626 );
28627 multi_buffer.set_excerpts_for_path(
28628 PathKey::for_buffer(&buffer2, cx),
28629 buffer2.clone(),
28630 [Point::zero()..buffer1.read(cx).max_point()],
28631 3,
28632 cx,
28633 );
28634 multi_buffer
28635 });
28636
28637 let (editor, cx) = cx.add_window_view(|window, cx| {
28638 Editor::new(
28639 EditorMode::full(),
28640 multi_buffer,
28641 Some(project.clone()),
28642 window,
28643 cx,
28644 )
28645 });
28646
28647 let fake_language_server = fake_servers.next().await.unwrap();
28648
28649 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
28650
28651 let save = editor.update_in(cx, |editor, window, cx| {
28652 assert!(editor.is_dirty(cx));
28653
28654 editor.save(
28655 SaveOptions {
28656 format: true,
28657 autosave: true,
28658 },
28659 project,
28660 window,
28661 cx,
28662 )
28663 });
28664 let (start_edit_tx, start_edit_rx) = oneshot::channel();
28665 let (done_edit_tx, done_edit_rx) = oneshot::channel();
28666 let mut done_edit_rx = Some(done_edit_rx);
28667 let mut start_edit_tx = Some(start_edit_tx);
28668
28669 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
28670 start_edit_tx.take().unwrap().send(()).unwrap();
28671 let done_edit_rx = done_edit_rx.take().unwrap();
28672 async move {
28673 done_edit_rx.await.unwrap();
28674 Ok(None)
28675 }
28676 });
28677
28678 start_edit_rx.await.unwrap();
28679 buffer2
28680 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
28681 .unwrap();
28682
28683 done_edit_tx.send(()).unwrap();
28684
28685 save.await.unwrap();
28686 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
28687}
28688
28689#[track_caller]
28690fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
28691 editor
28692 .all_inlays(cx)
28693 .into_iter()
28694 .filter_map(|inlay| inlay.get_color())
28695 .map(Rgba::from)
28696 .collect()
28697}
28698
28699#[gpui::test]
28700fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
28701 init_test(cx, |_| {});
28702
28703 let editor = cx.add_window(|window, cx| {
28704 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
28705 build_editor(buffer, window, cx)
28706 });
28707
28708 editor
28709 .update(cx, |editor, window, cx| {
28710 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28711 s.select_display_ranges([
28712 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
28713 ])
28714 });
28715
28716 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
28717
28718 assert_eq!(
28719 editor.display_text(cx),
28720 "line1\nline2\nline2",
28721 "Duplicating last line upward should create duplicate above, not on same line"
28722 );
28723
28724 assert_eq!(
28725 editor
28726 .selections
28727 .display_ranges(&editor.display_snapshot(cx)),
28728 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
28729 "Selection should move to the duplicated line"
28730 );
28731 })
28732 .unwrap();
28733}
28734
28735#[gpui::test]
28736async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
28737 init_test(cx, |_| {});
28738
28739 let mut cx = EditorTestContext::new(cx).await;
28740
28741 cx.set_state("line1\nline2ˇ");
28742
28743 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28744
28745 let clipboard_text = cx
28746 .read_from_clipboard()
28747 .and_then(|item| item.text().as_deref().map(str::to_string));
28748
28749 assert_eq!(
28750 clipboard_text,
28751 Some("line2\n".to_string()),
28752 "Copying a line without trailing newline should include a newline"
28753 );
28754
28755 cx.set_state("line1\nˇ");
28756
28757 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28758
28759 cx.assert_editor_state("line1\nline2\nˇ");
28760}
28761
28762#[gpui::test]
28763async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28764 init_test(cx, |_| {});
28765
28766 let mut cx = EditorTestContext::new(cx).await;
28767
28768 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28769
28770 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28771
28772 let clipboard_text = cx
28773 .read_from_clipboard()
28774 .and_then(|item| item.text().as_deref().map(str::to_string));
28775
28776 assert_eq!(
28777 clipboard_text,
28778 Some("line1\nline2\nline3\n".to_string()),
28779 "Copying multiple lines should include a single newline between lines"
28780 );
28781
28782 cx.set_state("lineA\nˇ");
28783
28784 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28785
28786 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28787}
28788
28789#[gpui::test]
28790async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28791 init_test(cx, |_| {});
28792
28793 let mut cx = EditorTestContext::new(cx).await;
28794
28795 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28796
28797 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
28798
28799 let clipboard_text = cx
28800 .read_from_clipboard()
28801 .and_then(|item| item.text().as_deref().map(str::to_string));
28802
28803 assert_eq!(
28804 clipboard_text,
28805 Some("line1\nline2\nline3\n".to_string()),
28806 "Copying multiple lines should include a single newline between lines"
28807 );
28808
28809 cx.set_state("lineA\nˇ");
28810
28811 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28812
28813 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28814}
28815
28816#[gpui::test]
28817async fn test_end_of_editor_context(cx: &mut TestAppContext) {
28818 init_test(cx, |_| {});
28819
28820 let mut cx = EditorTestContext::new(cx).await;
28821
28822 cx.set_state("line1\nline2ˇ");
28823 cx.update_editor(|e, window, cx| {
28824 e.set_mode(EditorMode::SingleLine);
28825 assert!(e.key_context(window, cx).contains("end_of_input"));
28826 });
28827 cx.set_state("ˇline1\nline2");
28828 cx.update_editor(|e, window, cx| {
28829 assert!(!e.key_context(window, cx).contains("end_of_input"));
28830 });
28831 cx.set_state("line1ˇ\nline2");
28832 cx.update_editor(|e, window, cx| {
28833 assert!(!e.key_context(window, cx).contains("end_of_input"));
28834 });
28835}
28836
28837#[gpui::test]
28838async fn test_sticky_scroll(cx: &mut TestAppContext) {
28839 init_test(cx, |_| {});
28840 let mut cx = EditorTestContext::new(cx).await;
28841
28842 let buffer = indoc! {"
28843 ˇfn foo() {
28844 let abc = 123;
28845 }
28846 struct Bar;
28847 impl Bar {
28848 fn new() -> Self {
28849 Self
28850 }
28851 }
28852 fn baz() {
28853 }
28854 "};
28855 cx.set_state(&buffer);
28856
28857 cx.update_editor(|e, _, cx| {
28858 e.buffer()
28859 .read(cx)
28860 .as_singleton()
28861 .unwrap()
28862 .update(cx, |buffer, cx| {
28863 buffer.set_language(Some(rust_lang()), cx);
28864 })
28865 });
28866
28867 let mut sticky_headers = |offset: ScrollOffset| {
28868 cx.update_editor(|e, window, cx| {
28869 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
28870 let style = e.style(cx).clone();
28871 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), &style, cx)
28872 .into_iter()
28873 .map(
28874 |StickyHeader {
28875 start_point,
28876 offset,
28877 ..
28878 }| { (start_point, offset) },
28879 )
28880 .collect::<Vec<_>>()
28881 })
28882 };
28883
28884 let fn_foo = Point { row: 0, column: 0 };
28885 let impl_bar = Point { row: 4, column: 0 };
28886 let fn_new = Point { row: 5, column: 4 };
28887
28888 assert_eq!(sticky_headers(0.0), vec![]);
28889 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
28890 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
28891 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
28892 assert_eq!(sticky_headers(2.0), vec![]);
28893 assert_eq!(sticky_headers(2.5), vec![]);
28894 assert_eq!(sticky_headers(3.0), vec![]);
28895 assert_eq!(sticky_headers(3.5), vec![]);
28896 assert_eq!(sticky_headers(4.0), vec![]);
28897 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28898 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28899 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
28900 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
28901 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
28902 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
28903 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
28904 assert_eq!(sticky_headers(8.0), vec![]);
28905 assert_eq!(sticky_headers(8.5), vec![]);
28906 assert_eq!(sticky_headers(9.0), vec![]);
28907 assert_eq!(sticky_headers(9.5), vec![]);
28908 assert_eq!(sticky_headers(10.0), vec![]);
28909}
28910
28911#[gpui::test]
28912fn test_relative_line_numbers(cx: &mut TestAppContext) {
28913 init_test(cx, |_| {});
28914
28915 let buffer_1 = cx.new(|cx| Buffer::local("aaaaaaaaaa\nbbb\n", cx));
28916 let buffer_2 = cx.new(|cx| Buffer::local("cccccccccc\nddd\n", cx));
28917 let buffer_3 = cx.new(|cx| Buffer::local("eee\nffffffffff\n", cx));
28918
28919 let multibuffer = cx.new(|cx| {
28920 let mut multibuffer = MultiBuffer::new(ReadWrite);
28921 multibuffer.push_excerpts(
28922 buffer_1.clone(),
28923 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
28924 cx,
28925 );
28926 multibuffer.push_excerpts(
28927 buffer_2.clone(),
28928 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
28929 cx,
28930 );
28931 multibuffer.push_excerpts(
28932 buffer_3.clone(),
28933 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
28934 cx,
28935 );
28936 multibuffer
28937 });
28938
28939 // wrapped contents of multibuffer:
28940 // aaa
28941 // aaa
28942 // aaa
28943 // a
28944 // bbb
28945 //
28946 // ccc
28947 // ccc
28948 // ccc
28949 // c
28950 // ddd
28951 //
28952 // eee
28953 // fff
28954 // fff
28955 // fff
28956 // f
28957
28958 let editor = cx.add_window(|window, cx| build_editor(multibuffer, window, cx));
28959 _ = editor.update(cx, |editor, window, cx| {
28960 editor.set_wrap_width(Some(30.0.into()), cx); // every 3 characters
28961
28962 // includes trailing newlines.
28963 let expected_line_numbers = [2, 6, 7, 10, 14, 15, 18, 19, 23];
28964 let expected_wrapped_line_numbers = [
28965 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23,
28966 ];
28967
28968 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28969 s.select_ranges([
28970 Point::new(7, 0)..Point::new(7, 1), // second row of `ccc`
28971 ]);
28972 });
28973
28974 let snapshot = editor.snapshot(window, cx);
28975
28976 // these are all 0-indexed
28977 let base_display_row = DisplayRow(11);
28978 let base_row = 3;
28979 let wrapped_base_row = 7;
28980
28981 // test not counting wrapped lines
28982 let expected_relative_numbers = expected_line_numbers
28983 .into_iter()
28984 .enumerate()
28985 .map(|(i, row)| (DisplayRow(row), i.abs_diff(base_row) as u32))
28986 .collect_vec();
28987 let actual_relative_numbers = snapshot
28988 .calculate_relative_line_numbers(
28989 &(DisplayRow(0)..DisplayRow(24)),
28990 base_display_row,
28991 false,
28992 )
28993 .into_iter()
28994 .sorted()
28995 .collect_vec();
28996 assert_eq!(expected_relative_numbers, actual_relative_numbers);
28997 // check `calculate_relative_line_numbers()` against `relative_line_delta()` for each line
28998 for (display_row, relative_number) in expected_relative_numbers {
28999 assert_eq!(
29000 relative_number,
29001 snapshot
29002 .relative_line_delta(display_row, base_display_row, false)
29003 .unsigned_abs() as u32,
29004 );
29005 }
29006
29007 // test counting wrapped lines
29008 let expected_wrapped_relative_numbers = expected_wrapped_line_numbers
29009 .into_iter()
29010 .enumerate()
29011 .map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32))
29012 .filter(|(row, _)| *row != base_display_row)
29013 .collect_vec();
29014 let actual_relative_numbers = snapshot
29015 .calculate_relative_line_numbers(
29016 &(DisplayRow(0)..DisplayRow(24)),
29017 base_display_row,
29018 true,
29019 )
29020 .into_iter()
29021 .sorted()
29022 .collect_vec();
29023 assert_eq!(expected_wrapped_relative_numbers, actual_relative_numbers);
29024 // check `calculate_relative_line_numbers()` against `relative_wrapped_line_delta()` for each line
29025 for (display_row, relative_number) in expected_wrapped_relative_numbers {
29026 assert_eq!(
29027 relative_number,
29028 snapshot
29029 .relative_line_delta(display_row, base_display_row, true)
29030 .unsigned_abs() as u32,
29031 );
29032 }
29033 });
29034}
29035
29036#[gpui::test]
29037async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
29038 init_test(cx, |_| {});
29039 cx.update(|cx| {
29040 SettingsStore::update_global(cx, |store, cx| {
29041 store.update_user_settings(cx, |settings| {
29042 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
29043 enabled: Some(true),
29044 })
29045 });
29046 });
29047 });
29048 let mut cx = EditorTestContext::new(cx).await;
29049
29050 let line_height = cx.update_editor(|editor, window, cx| {
29051 editor
29052 .style(cx)
29053 .text
29054 .line_height_in_pixels(window.rem_size())
29055 });
29056
29057 let buffer = indoc! {"
29058 ˇfn foo() {
29059 let abc = 123;
29060 }
29061 struct Bar;
29062 impl Bar {
29063 fn new() -> Self {
29064 Self
29065 }
29066 }
29067 fn baz() {
29068 }
29069 "};
29070 cx.set_state(&buffer);
29071
29072 cx.update_editor(|e, _, cx| {
29073 e.buffer()
29074 .read(cx)
29075 .as_singleton()
29076 .unwrap()
29077 .update(cx, |buffer, cx| {
29078 buffer.set_language(Some(rust_lang()), cx);
29079 })
29080 });
29081
29082 let fn_foo = || empty_range(0, 0);
29083 let impl_bar = || empty_range(4, 0);
29084 let fn_new = || empty_range(5, 4);
29085
29086 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
29087 cx.update_editor(|e, window, cx| {
29088 e.scroll(
29089 gpui::Point {
29090 x: 0.,
29091 y: scroll_offset,
29092 },
29093 None,
29094 window,
29095 cx,
29096 );
29097 });
29098 cx.simulate_click(
29099 gpui::Point {
29100 x: px(0.),
29101 y: click_offset as f32 * line_height,
29102 },
29103 Modifiers::none(),
29104 );
29105 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
29106 };
29107
29108 assert_eq!(
29109 scroll_and_click(
29110 4.5, // impl Bar is halfway off the screen
29111 0.0 // click top of screen
29112 ),
29113 // scrolled to impl Bar
29114 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29115 );
29116
29117 assert_eq!(
29118 scroll_and_click(
29119 4.5, // impl Bar is halfway off the screen
29120 0.25 // click middle of impl Bar
29121 ),
29122 // scrolled to impl Bar
29123 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29124 );
29125
29126 assert_eq!(
29127 scroll_and_click(
29128 4.5, // impl Bar is halfway off the screen
29129 1.5 // click below impl Bar (e.g. fn new())
29130 ),
29131 // scrolled to fn new() - this is below the impl Bar header which has persisted
29132 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
29133 );
29134
29135 assert_eq!(
29136 scroll_and_click(
29137 5.5, // fn new is halfway underneath impl Bar
29138 0.75 // click on the overlap of impl Bar and fn new()
29139 ),
29140 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29141 );
29142
29143 assert_eq!(
29144 scroll_and_click(
29145 5.5, // fn new is halfway underneath impl Bar
29146 1.25 // click on the visible part of fn new()
29147 ),
29148 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
29149 );
29150
29151 assert_eq!(
29152 scroll_and_click(
29153 1.5, // fn foo is halfway off the screen
29154 0.0 // click top of screen
29155 ),
29156 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
29157 );
29158
29159 assert_eq!(
29160 scroll_and_click(
29161 1.5, // fn foo is halfway off the screen
29162 0.75 // click visible part of let abc...
29163 )
29164 .0,
29165 // no change in scroll
29166 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
29167 (gpui::Point { x: 0., y: 1.5 })
29168 );
29169}
29170
29171#[gpui::test]
29172async fn test_next_prev_reference(cx: &mut TestAppContext) {
29173 const CYCLE_POSITIONS: &[&'static str] = &[
29174 indoc! {"
29175 fn foo() {
29176 let ˇabc = 123;
29177 let x = abc + 1;
29178 let y = abc + 2;
29179 let z = abc + 2;
29180 }
29181 "},
29182 indoc! {"
29183 fn foo() {
29184 let abc = 123;
29185 let x = ˇabc + 1;
29186 let y = abc + 2;
29187 let z = abc + 2;
29188 }
29189 "},
29190 indoc! {"
29191 fn foo() {
29192 let abc = 123;
29193 let x = abc + 1;
29194 let y = ˇabc + 2;
29195 let z = abc + 2;
29196 }
29197 "},
29198 indoc! {"
29199 fn foo() {
29200 let abc = 123;
29201 let x = abc + 1;
29202 let y = abc + 2;
29203 let z = ˇabc + 2;
29204 }
29205 "},
29206 ];
29207
29208 init_test(cx, |_| {});
29209
29210 let mut cx = EditorLspTestContext::new_rust(
29211 lsp::ServerCapabilities {
29212 references_provider: Some(lsp::OneOf::Left(true)),
29213 ..Default::default()
29214 },
29215 cx,
29216 )
29217 .await;
29218
29219 // importantly, the cursor is in the middle
29220 cx.set_state(indoc! {"
29221 fn foo() {
29222 let aˇbc = 123;
29223 let x = abc + 1;
29224 let y = abc + 2;
29225 let z = abc + 2;
29226 }
29227 "});
29228
29229 let reference_ranges = [
29230 lsp::Position::new(1, 8),
29231 lsp::Position::new(2, 12),
29232 lsp::Position::new(3, 12),
29233 lsp::Position::new(4, 12),
29234 ]
29235 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
29236
29237 cx.lsp
29238 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
29239 Ok(Some(
29240 reference_ranges
29241 .map(|range| lsp::Location {
29242 uri: params.text_document_position.text_document.uri.clone(),
29243 range,
29244 })
29245 .to_vec(),
29246 ))
29247 });
29248
29249 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
29250 cx.update_editor(|editor, window, cx| {
29251 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
29252 })
29253 .unwrap()
29254 .await
29255 .unwrap()
29256 };
29257
29258 _move(Direction::Next, 1, &mut cx).await;
29259 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29260
29261 _move(Direction::Next, 1, &mut cx).await;
29262 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29263
29264 _move(Direction::Next, 1, &mut cx).await;
29265 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29266
29267 // loops back to the start
29268 _move(Direction::Next, 1, &mut cx).await;
29269 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29270
29271 // loops back to the end
29272 _move(Direction::Prev, 1, &mut cx).await;
29273 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29274
29275 _move(Direction::Prev, 1, &mut cx).await;
29276 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29277
29278 _move(Direction::Prev, 1, &mut cx).await;
29279 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29280
29281 _move(Direction::Prev, 1, &mut cx).await;
29282 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29283
29284 _move(Direction::Next, 3, &mut cx).await;
29285 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29286
29287 _move(Direction::Prev, 2, &mut cx).await;
29288 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29289}
29290
29291#[gpui::test]
29292async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
29293 init_test(cx, |_| {});
29294
29295 let (editor, cx) = cx.add_window_view(|window, cx| {
29296 let multi_buffer = MultiBuffer::build_multi(
29297 [
29298 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29299 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29300 ],
29301 cx,
29302 );
29303 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29304 });
29305
29306 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29307 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
29308
29309 cx.assert_excerpts_with_selections(indoc! {"
29310 [EXCERPT]
29311 ˇ1
29312 2
29313 3
29314 [EXCERPT]
29315 1
29316 2
29317 3
29318 "});
29319
29320 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
29321 cx.update_editor(|editor, window, cx| {
29322 editor.change_selections(None.into(), window, cx, |s| {
29323 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29324 });
29325 });
29326 cx.assert_excerpts_with_selections(indoc! {"
29327 [EXCERPT]
29328 1
29329 2ˇ
29330 3
29331 [EXCERPT]
29332 1
29333 2
29334 3
29335 "});
29336
29337 cx.update_editor(|editor, window, cx| {
29338 editor
29339 .select_all_matches(&SelectAllMatches, window, cx)
29340 .unwrap();
29341 });
29342 cx.assert_excerpts_with_selections(indoc! {"
29343 [EXCERPT]
29344 1
29345 2ˇ
29346 3
29347 [EXCERPT]
29348 1
29349 2ˇ
29350 3
29351 "});
29352
29353 cx.update_editor(|editor, window, cx| {
29354 editor.handle_input("X", window, cx);
29355 });
29356 cx.assert_excerpts_with_selections(indoc! {"
29357 [EXCERPT]
29358 1
29359 Xˇ
29360 3
29361 [EXCERPT]
29362 1
29363 Xˇ
29364 3
29365 "});
29366
29367 // Scenario 2: Select "2", then fold second buffer before insertion
29368 cx.update_multibuffer(|mb, cx| {
29369 for buffer_id in buffer_ids.iter() {
29370 let buffer = mb.buffer(*buffer_id).unwrap();
29371 buffer.update(cx, |buffer, cx| {
29372 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29373 });
29374 }
29375 });
29376
29377 // Select "2" and select all matches
29378 cx.update_editor(|editor, window, cx| {
29379 editor.change_selections(None.into(), window, cx, |s| {
29380 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29381 });
29382 editor
29383 .select_all_matches(&SelectAllMatches, window, cx)
29384 .unwrap();
29385 });
29386
29387 // Fold second buffer - should remove selections from folded buffer
29388 cx.update_editor(|editor, _, cx| {
29389 editor.fold_buffer(buffer_ids[1], cx);
29390 });
29391 cx.assert_excerpts_with_selections(indoc! {"
29392 [EXCERPT]
29393 1
29394 2ˇ
29395 3
29396 [EXCERPT]
29397 [FOLDED]
29398 "});
29399
29400 // Insert text - should only affect first buffer
29401 cx.update_editor(|editor, window, cx| {
29402 editor.handle_input("Y", window, cx);
29403 });
29404 cx.update_editor(|editor, _, cx| {
29405 editor.unfold_buffer(buffer_ids[1], cx);
29406 });
29407 cx.assert_excerpts_with_selections(indoc! {"
29408 [EXCERPT]
29409 1
29410 Yˇ
29411 3
29412 [EXCERPT]
29413 1
29414 2
29415 3
29416 "});
29417
29418 // Scenario 3: Select "2", then fold first buffer before insertion
29419 cx.update_multibuffer(|mb, cx| {
29420 for buffer_id in buffer_ids.iter() {
29421 let buffer = mb.buffer(*buffer_id).unwrap();
29422 buffer.update(cx, |buffer, cx| {
29423 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29424 });
29425 }
29426 });
29427
29428 // Select "2" and select all matches
29429 cx.update_editor(|editor, window, cx| {
29430 editor.change_selections(None.into(), window, cx, |s| {
29431 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29432 });
29433 editor
29434 .select_all_matches(&SelectAllMatches, window, cx)
29435 .unwrap();
29436 });
29437
29438 // Fold first buffer - should remove selections from folded buffer
29439 cx.update_editor(|editor, _, cx| {
29440 editor.fold_buffer(buffer_ids[0], cx);
29441 });
29442 cx.assert_excerpts_with_selections(indoc! {"
29443 [EXCERPT]
29444 [FOLDED]
29445 [EXCERPT]
29446 1
29447 2ˇ
29448 3
29449 "});
29450
29451 // Insert text - should only affect second buffer
29452 cx.update_editor(|editor, window, cx| {
29453 editor.handle_input("Z", window, cx);
29454 });
29455 cx.update_editor(|editor, _, cx| {
29456 editor.unfold_buffer(buffer_ids[0], cx);
29457 });
29458 cx.assert_excerpts_with_selections(indoc! {"
29459 [EXCERPT]
29460 1
29461 2
29462 3
29463 [EXCERPT]
29464 1
29465 Zˇ
29466 3
29467 "});
29468
29469 // Test correct folded header is selected upon fold
29470 cx.update_editor(|editor, _, cx| {
29471 editor.fold_buffer(buffer_ids[0], cx);
29472 editor.fold_buffer(buffer_ids[1], cx);
29473 });
29474 cx.assert_excerpts_with_selections(indoc! {"
29475 [EXCERPT]
29476 [FOLDED]
29477 [EXCERPT]
29478 ˇ[FOLDED]
29479 "});
29480
29481 // Test selection inside folded buffer unfolds it on type
29482 cx.update_editor(|editor, window, cx| {
29483 editor.handle_input("W", window, cx);
29484 });
29485 cx.update_editor(|editor, _, cx| {
29486 editor.unfold_buffer(buffer_ids[0], cx);
29487 });
29488 cx.assert_excerpts_with_selections(indoc! {"
29489 [EXCERPT]
29490 1
29491 2
29492 3
29493 [EXCERPT]
29494 Wˇ1
29495 Z
29496 3
29497 "});
29498}
29499
29500#[gpui::test]
29501async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
29502 init_test(cx, |_| {});
29503
29504 let (editor, cx) = cx.add_window_view(|window, cx| {
29505 let multi_buffer = MultiBuffer::build_multi(
29506 [
29507 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29508 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
29509 ],
29510 cx,
29511 );
29512 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29513 });
29514
29515 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29516
29517 cx.assert_excerpts_with_selections(indoc! {"
29518 [EXCERPT]
29519 ˇ1
29520 2
29521 3
29522 [EXCERPT]
29523 1
29524 2
29525 3
29526 4
29527 5
29528 6
29529 7
29530 8
29531 9
29532 "});
29533
29534 cx.update_editor(|editor, window, cx| {
29535 editor.change_selections(None.into(), window, cx, |s| {
29536 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
29537 });
29538 });
29539
29540 cx.assert_excerpts_with_selections(indoc! {"
29541 [EXCERPT]
29542 1
29543 2
29544 3
29545 [EXCERPT]
29546 1
29547 2
29548 3
29549 4
29550 5
29551 6
29552 ˇ7
29553 8
29554 9
29555 "});
29556
29557 cx.update_editor(|editor, _window, cx| {
29558 editor.set_vertical_scroll_margin(0, cx);
29559 });
29560
29561 cx.update_editor(|editor, window, cx| {
29562 assert_eq!(editor.vertical_scroll_margin(), 0);
29563 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29564 assert_eq!(
29565 editor.snapshot(window, cx).scroll_position(),
29566 gpui::Point::new(0., 12.0)
29567 );
29568 });
29569
29570 cx.update_editor(|editor, _window, cx| {
29571 editor.set_vertical_scroll_margin(3, cx);
29572 });
29573
29574 cx.update_editor(|editor, window, cx| {
29575 assert_eq!(editor.vertical_scroll_margin(), 3);
29576 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29577 assert_eq!(
29578 editor.snapshot(window, cx).scroll_position(),
29579 gpui::Point::new(0., 9.0)
29580 );
29581 });
29582}
29583
29584#[gpui::test]
29585async fn test_find_references_single_case(cx: &mut TestAppContext) {
29586 init_test(cx, |_| {});
29587 let mut cx = EditorLspTestContext::new_rust(
29588 lsp::ServerCapabilities {
29589 references_provider: Some(lsp::OneOf::Left(true)),
29590 ..lsp::ServerCapabilities::default()
29591 },
29592 cx,
29593 )
29594 .await;
29595
29596 let before = indoc!(
29597 r#"
29598 fn main() {
29599 let aˇbc = 123;
29600 let xyz = abc;
29601 }
29602 "#
29603 );
29604 let after = indoc!(
29605 r#"
29606 fn main() {
29607 let abc = 123;
29608 let xyz = ˇabc;
29609 }
29610 "#
29611 );
29612
29613 cx.lsp
29614 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
29615 Ok(Some(vec![
29616 lsp::Location {
29617 uri: params.text_document_position.text_document.uri.clone(),
29618 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
29619 },
29620 lsp::Location {
29621 uri: params.text_document_position.text_document.uri,
29622 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
29623 },
29624 ]))
29625 });
29626
29627 cx.set_state(before);
29628
29629 let action = FindAllReferences {
29630 always_open_multibuffer: false,
29631 };
29632
29633 let navigated = cx
29634 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
29635 .expect("should have spawned a task")
29636 .await
29637 .unwrap();
29638
29639 assert_eq!(navigated, Navigated::No);
29640
29641 cx.run_until_parked();
29642
29643 cx.assert_editor_state(after);
29644}
29645
29646#[gpui::test]
29647async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
29648 init_test(cx, |settings| {
29649 settings.defaults.tab_size = Some(2.try_into().unwrap());
29650 });
29651
29652 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29653 let mut cx = EditorTestContext::new(cx).await;
29654 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29655
29656 // Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
29657 cx.set_state(indoc! {"
29658 - [ ] taskˇ
29659 "});
29660 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29661 cx.wait_for_autoindent_applied().await;
29662 cx.assert_editor_state(indoc! {"
29663 - [ ] task
29664 - [ ] ˇ
29665 "});
29666
29667 // Case 2: Works with checked task items too
29668 cx.set_state(indoc! {"
29669 - [x] completed taskˇ
29670 "});
29671 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29672 cx.wait_for_autoindent_applied().await;
29673 cx.assert_editor_state(indoc! {"
29674 - [x] completed task
29675 - [ ] ˇ
29676 "});
29677
29678 // Case 2.1: Works with uppercase checked marker too
29679 cx.set_state(indoc! {"
29680 - [X] completed taskˇ
29681 "});
29682 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29683 cx.wait_for_autoindent_applied().await;
29684 cx.assert_editor_state(indoc! {"
29685 - [X] completed task
29686 - [ ] ˇ
29687 "});
29688
29689 // Case 3: Cursor position doesn't matter - content after marker is what counts
29690 cx.set_state(indoc! {"
29691 - [ ] taˇsk
29692 "});
29693 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29694 cx.wait_for_autoindent_applied().await;
29695 cx.assert_editor_state(indoc! {"
29696 - [ ] ta
29697 - [ ] ˇsk
29698 "});
29699
29700 // Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
29701 cx.set_state(indoc! {"
29702 - [ ] ˇ
29703 "});
29704 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29705 cx.wait_for_autoindent_applied().await;
29706 cx.assert_editor_state(
29707 indoc! {"
29708 - [ ]$$
29709 ˇ
29710 "}
29711 .replace("$", " ")
29712 .as_str(),
29713 );
29714
29715 // Case 5: Adding newline with content adds marker preserving indentation
29716 cx.set_state(indoc! {"
29717 - [ ] task
29718 - [ ] indentedˇ
29719 "});
29720 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29721 cx.wait_for_autoindent_applied().await;
29722 cx.assert_editor_state(indoc! {"
29723 - [ ] task
29724 - [ ] indented
29725 - [ ] ˇ
29726 "});
29727
29728 // Case 6: Adding newline with cursor right after prefix, unindents
29729 cx.set_state(indoc! {"
29730 - [ ] task
29731 - [ ] sub task
29732 - [ ] ˇ
29733 "});
29734 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29735 cx.wait_for_autoindent_applied().await;
29736 cx.assert_editor_state(indoc! {"
29737 - [ ] task
29738 - [ ] sub task
29739 - [ ] ˇ
29740 "});
29741 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29742 cx.wait_for_autoindent_applied().await;
29743
29744 // Case 7: Adding newline with cursor right after prefix, removes marker
29745 cx.assert_editor_state(indoc! {"
29746 - [ ] task
29747 - [ ] sub task
29748 - [ ] ˇ
29749 "});
29750 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29751 cx.wait_for_autoindent_applied().await;
29752 cx.assert_editor_state(indoc! {"
29753 - [ ] task
29754 - [ ] sub task
29755 ˇ
29756 "});
29757
29758 // Case 8: Cursor before or inside prefix does not add marker
29759 cx.set_state(indoc! {"
29760 ˇ- [ ] task
29761 "});
29762 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29763 cx.wait_for_autoindent_applied().await;
29764 cx.assert_editor_state(indoc! {"
29765
29766 ˇ- [ ] task
29767 "});
29768
29769 cx.set_state(indoc! {"
29770 - [ˇ ] task
29771 "});
29772 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29773 cx.wait_for_autoindent_applied().await;
29774 cx.assert_editor_state(indoc! {"
29775 - [
29776 ˇ
29777 ] task
29778 "});
29779}
29780
29781#[gpui::test]
29782async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
29783 init_test(cx, |settings| {
29784 settings.defaults.tab_size = Some(2.try_into().unwrap());
29785 });
29786
29787 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29788 let mut cx = EditorTestContext::new(cx).await;
29789 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29790
29791 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
29792 cx.set_state(indoc! {"
29793 - itemˇ
29794 "});
29795 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29796 cx.wait_for_autoindent_applied().await;
29797 cx.assert_editor_state(indoc! {"
29798 - item
29799 - ˇ
29800 "});
29801
29802 // Case 2: Works with different markers
29803 cx.set_state(indoc! {"
29804 * starred itemˇ
29805 "});
29806 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29807 cx.wait_for_autoindent_applied().await;
29808 cx.assert_editor_state(indoc! {"
29809 * starred item
29810 * ˇ
29811 "});
29812
29813 cx.set_state(indoc! {"
29814 + plus itemˇ
29815 "});
29816 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29817 cx.wait_for_autoindent_applied().await;
29818 cx.assert_editor_state(indoc! {"
29819 + plus item
29820 + ˇ
29821 "});
29822
29823 // Case 3: Cursor position doesn't matter - content after marker is what counts
29824 cx.set_state(indoc! {"
29825 - itˇem
29826 "});
29827 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29828 cx.wait_for_autoindent_applied().await;
29829 cx.assert_editor_state(indoc! {"
29830 - it
29831 - ˇem
29832 "});
29833
29834 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
29835 cx.set_state(indoc! {"
29836 - ˇ
29837 "});
29838 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29839 cx.wait_for_autoindent_applied().await;
29840 cx.assert_editor_state(
29841 indoc! {"
29842 - $
29843 ˇ
29844 "}
29845 .replace("$", " ")
29846 .as_str(),
29847 );
29848
29849 // Case 5: Adding newline with content adds marker preserving indentation
29850 cx.set_state(indoc! {"
29851 - item
29852 - indentedˇ
29853 "});
29854 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29855 cx.wait_for_autoindent_applied().await;
29856 cx.assert_editor_state(indoc! {"
29857 - item
29858 - indented
29859 - ˇ
29860 "});
29861
29862 // Case 6: Adding newline with cursor right after marker, unindents
29863 cx.set_state(indoc! {"
29864 - item
29865 - sub item
29866 - ˇ
29867 "});
29868 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29869 cx.wait_for_autoindent_applied().await;
29870 cx.assert_editor_state(indoc! {"
29871 - item
29872 - sub item
29873 - ˇ
29874 "});
29875 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29876 cx.wait_for_autoindent_applied().await;
29877
29878 // Case 7: Adding newline with cursor right after marker, removes marker
29879 cx.assert_editor_state(indoc! {"
29880 - item
29881 - sub item
29882 - ˇ
29883 "});
29884 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29885 cx.wait_for_autoindent_applied().await;
29886 cx.assert_editor_state(indoc! {"
29887 - item
29888 - sub item
29889 ˇ
29890 "});
29891
29892 // Case 8: Cursor before or inside prefix does not add marker
29893 cx.set_state(indoc! {"
29894 ˇ- item
29895 "});
29896 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29897 cx.wait_for_autoindent_applied().await;
29898 cx.assert_editor_state(indoc! {"
29899
29900 ˇ- item
29901 "});
29902
29903 cx.set_state(indoc! {"
29904 -ˇ item
29905 "});
29906 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29907 cx.wait_for_autoindent_applied().await;
29908 cx.assert_editor_state(indoc! {"
29909 -
29910 ˇitem
29911 "});
29912}
29913
29914#[gpui::test]
29915async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
29916 init_test(cx, |settings| {
29917 settings.defaults.tab_size = Some(2.try_into().unwrap());
29918 });
29919
29920 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29921 let mut cx = EditorTestContext::new(cx).await;
29922 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29923
29924 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
29925 cx.set_state(indoc! {"
29926 1. first itemˇ
29927 "});
29928 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29929 cx.wait_for_autoindent_applied().await;
29930 cx.assert_editor_state(indoc! {"
29931 1. first item
29932 2. ˇ
29933 "});
29934
29935 // Case 2: Works with larger numbers
29936 cx.set_state(indoc! {"
29937 10. tenth itemˇ
29938 "});
29939 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29940 cx.wait_for_autoindent_applied().await;
29941 cx.assert_editor_state(indoc! {"
29942 10. tenth item
29943 11. ˇ
29944 "});
29945
29946 // Case 3: Cursor position doesn't matter - content after marker is what counts
29947 cx.set_state(indoc! {"
29948 1. itˇem
29949 "});
29950 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29951 cx.wait_for_autoindent_applied().await;
29952 cx.assert_editor_state(indoc! {"
29953 1. it
29954 2. ˇem
29955 "});
29956
29957 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
29958 cx.set_state(indoc! {"
29959 1. ˇ
29960 "});
29961 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29962 cx.wait_for_autoindent_applied().await;
29963 cx.assert_editor_state(
29964 indoc! {"
29965 1. $
29966 ˇ
29967 "}
29968 .replace("$", " ")
29969 .as_str(),
29970 );
29971
29972 // Case 5: Adding newline with content adds marker preserving indentation
29973 cx.set_state(indoc! {"
29974 1. item
29975 2. indentedˇ
29976 "});
29977 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29978 cx.wait_for_autoindent_applied().await;
29979 cx.assert_editor_state(indoc! {"
29980 1. item
29981 2. indented
29982 3. ˇ
29983 "});
29984
29985 // Case 6: Adding newline with cursor right after marker, unindents
29986 cx.set_state(indoc! {"
29987 1. item
29988 2. sub item
29989 3. ˇ
29990 "});
29991 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29992 cx.wait_for_autoindent_applied().await;
29993 cx.assert_editor_state(indoc! {"
29994 1. item
29995 2. sub item
29996 1. ˇ
29997 "});
29998 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29999 cx.wait_for_autoindent_applied().await;
30000
30001 // Case 7: Adding newline with cursor right after marker, removes marker
30002 cx.assert_editor_state(indoc! {"
30003 1. item
30004 2. sub item
30005 1. ˇ
30006 "});
30007 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30008 cx.wait_for_autoindent_applied().await;
30009 cx.assert_editor_state(indoc! {"
30010 1. item
30011 2. sub item
30012 ˇ
30013 "});
30014
30015 // Case 8: Cursor before or inside prefix does not add marker
30016 cx.set_state(indoc! {"
30017 ˇ1. item
30018 "});
30019 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30020 cx.wait_for_autoindent_applied().await;
30021 cx.assert_editor_state(indoc! {"
30022
30023 ˇ1. item
30024 "});
30025
30026 cx.set_state(indoc! {"
30027 1ˇ. item
30028 "});
30029 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30030 cx.wait_for_autoindent_applied().await;
30031 cx.assert_editor_state(indoc! {"
30032 1
30033 ˇ. item
30034 "});
30035}
30036
30037#[gpui::test]
30038async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
30039 init_test(cx, |settings| {
30040 settings.defaults.tab_size = Some(2.try_into().unwrap());
30041 });
30042
30043 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30044 let mut cx = EditorTestContext::new(cx).await;
30045 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30046
30047 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
30048 cx.set_state(indoc! {"
30049 1. first item
30050 1. sub first item
30051 2. sub second item
30052 3. ˇ
30053 "});
30054 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30055 cx.wait_for_autoindent_applied().await;
30056 cx.assert_editor_state(indoc! {"
30057 1. first item
30058 1. sub first item
30059 2. sub second item
30060 1. ˇ
30061 "});
30062}
30063
30064#[gpui::test]
30065async fn test_tab_list_indent(cx: &mut TestAppContext) {
30066 init_test(cx, |settings| {
30067 settings.defaults.tab_size = Some(2.try_into().unwrap());
30068 });
30069
30070 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30071 let mut cx = EditorTestContext::new(cx).await;
30072 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30073
30074 // Case 1: Unordered list - cursor after prefix, adds indent before prefix
30075 cx.set_state(indoc! {"
30076 - ˇitem
30077 "});
30078 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30079 cx.wait_for_autoindent_applied().await;
30080 let expected = indoc! {"
30081 $$- ˇitem
30082 "};
30083 cx.assert_editor_state(expected.replace("$", " ").as_str());
30084
30085 // Case 2: Task list - cursor after prefix
30086 cx.set_state(indoc! {"
30087 - [ ] ˇtask
30088 "});
30089 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30090 cx.wait_for_autoindent_applied().await;
30091 let expected = indoc! {"
30092 $$- [ ] ˇtask
30093 "};
30094 cx.assert_editor_state(expected.replace("$", " ").as_str());
30095
30096 // Case 3: Ordered list - cursor after prefix
30097 cx.set_state(indoc! {"
30098 1. ˇfirst
30099 "});
30100 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30101 cx.wait_for_autoindent_applied().await;
30102 let expected = indoc! {"
30103 $$1. ˇfirst
30104 "};
30105 cx.assert_editor_state(expected.replace("$", " ").as_str());
30106
30107 // Case 4: With existing indentation - adds more indent
30108 let initial = indoc! {"
30109 $$- ˇitem
30110 "};
30111 cx.set_state(initial.replace("$", " ").as_str());
30112 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30113 cx.wait_for_autoindent_applied().await;
30114 let expected = indoc! {"
30115 $$$$- ˇitem
30116 "};
30117 cx.assert_editor_state(expected.replace("$", " ").as_str());
30118
30119 // Case 5: Empty list item
30120 cx.set_state(indoc! {"
30121 - ˇ
30122 "});
30123 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30124 cx.wait_for_autoindent_applied().await;
30125 let expected = indoc! {"
30126 $$- ˇ
30127 "};
30128 cx.assert_editor_state(expected.replace("$", " ").as_str());
30129
30130 // Case 6: Cursor at end of line with content
30131 cx.set_state(indoc! {"
30132 - itemˇ
30133 "});
30134 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30135 cx.wait_for_autoindent_applied().await;
30136 let expected = indoc! {"
30137 $$- itemˇ
30138 "};
30139 cx.assert_editor_state(expected.replace("$", " ").as_str());
30140
30141 // Case 7: Cursor at start of list item, indents it
30142 cx.set_state(indoc! {"
30143 - item
30144 ˇ - sub item
30145 "});
30146 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30147 cx.wait_for_autoindent_applied().await;
30148 let expected = indoc! {"
30149 - item
30150 ˇ - sub item
30151 "};
30152 cx.assert_editor_state(expected);
30153
30154 // Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
30155 cx.update_editor(|_, _, cx| {
30156 SettingsStore::update_global(cx, |store, cx| {
30157 store.update_user_settings(cx, |settings| {
30158 settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
30159 });
30160 });
30161 });
30162 cx.set_state(indoc! {"
30163 - item
30164 ˇ - sub item
30165 "});
30166 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30167 cx.wait_for_autoindent_applied().await;
30168 let expected = indoc! {"
30169 - item
30170 ˇ- sub item
30171 "};
30172 cx.assert_editor_state(expected);
30173}
30174
30175#[gpui::test]
30176async fn test_local_worktree_trust(cx: &mut TestAppContext) {
30177 init_test(cx, |_| {});
30178 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), cx));
30179
30180 cx.update(|cx| {
30181 SettingsStore::update_global(cx, |store, cx| {
30182 store.update_user_settings(cx, |settings| {
30183 settings.project.all_languages.defaults.inlay_hints =
30184 Some(InlayHintSettingsContent {
30185 enabled: Some(true),
30186 ..InlayHintSettingsContent::default()
30187 });
30188 });
30189 });
30190 });
30191
30192 let fs = FakeFs::new(cx.executor());
30193 fs.insert_tree(
30194 path!("/project"),
30195 json!({
30196 ".zed": {
30197 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
30198 },
30199 "main.rs": "fn main() {}"
30200 }),
30201 )
30202 .await;
30203
30204 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
30205 let server_name = "override-rust-analyzer";
30206 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
30207
30208 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
30209 language_registry.add(rust_lang());
30210
30211 let capabilities = lsp::ServerCapabilities {
30212 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
30213 ..lsp::ServerCapabilities::default()
30214 };
30215 let mut fake_language_servers = language_registry.register_fake_lsp(
30216 "Rust",
30217 FakeLspAdapter {
30218 name: server_name,
30219 capabilities,
30220 initializer: Some(Box::new({
30221 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30222 move |fake_server| {
30223 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30224 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
30225 move |_params, _| {
30226 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
30227 async move {
30228 Ok(Some(vec![lsp::InlayHint {
30229 position: lsp::Position::new(0, 0),
30230 label: lsp::InlayHintLabel::String("hint".to_string()),
30231 kind: None,
30232 text_edits: None,
30233 tooltip: None,
30234 padding_left: None,
30235 padding_right: None,
30236 data: None,
30237 }]))
30238 }
30239 },
30240 );
30241 }
30242 })),
30243 ..FakeLspAdapter::default()
30244 },
30245 );
30246
30247 cx.run_until_parked();
30248
30249 let worktree_id = project.read_with(cx, |project, cx| {
30250 project
30251 .worktrees(cx)
30252 .next()
30253 .map(|wt| wt.read(cx).id())
30254 .expect("should have a worktree")
30255 });
30256 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
30257
30258 let trusted_worktrees =
30259 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
30260
30261 let can_trust = trusted_worktrees.update(cx, |store, cx| {
30262 store.can_trust(&worktree_store, worktree_id, cx)
30263 });
30264 assert!(!can_trust, "worktree should be restricted initially");
30265
30266 let buffer_before_approval = project
30267 .update(cx, |project, cx| {
30268 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
30269 })
30270 .await
30271 .unwrap();
30272
30273 let (editor, cx) = cx.add_window_view(|window, cx| {
30274 Editor::new(
30275 EditorMode::full(),
30276 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
30277 Some(project.clone()),
30278 window,
30279 cx,
30280 )
30281 });
30282 cx.run_until_parked();
30283 let fake_language_server = fake_language_servers.next();
30284
30285 cx.read(|cx| {
30286 let file = buffer_before_approval.read(cx).file();
30287 assert_eq!(
30288 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30289 .language_servers,
30290 ["...".to_string()],
30291 "local .zed/settings.json must not apply before trust approval"
30292 )
30293 });
30294
30295 editor.update_in(cx, |editor, window, cx| {
30296 editor.handle_input("1", window, cx);
30297 });
30298 cx.run_until_parked();
30299 cx.executor()
30300 .advance_clock(std::time::Duration::from_secs(1));
30301 assert_eq!(
30302 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
30303 0,
30304 "inlay hints must not be queried before trust approval"
30305 );
30306
30307 trusted_worktrees.update(cx, |store, cx| {
30308 store.trust(
30309 &worktree_store,
30310 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
30311 cx,
30312 );
30313 });
30314 cx.run_until_parked();
30315
30316 cx.read(|cx| {
30317 let file = buffer_before_approval.read(cx).file();
30318 assert_eq!(
30319 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30320 .language_servers,
30321 ["override-rust-analyzer".to_string()],
30322 "local .zed/settings.json should apply after trust approval"
30323 )
30324 });
30325 let _fake_language_server = fake_language_server.await.unwrap();
30326 editor.update_in(cx, |editor, window, cx| {
30327 editor.handle_input("1", window, cx);
30328 });
30329 cx.run_until_parked();
30330 cx.executor()
30331 .advance_clock(std::time::Duration::from_secs(1));
30332 assert!(
30333 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
30334 "inlay hints should be queried after trust approval"
30335 );
30336
30337 let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
30338 store.can_trust(&worktree_store, worktree_id, cx)
30339 });
30340 assert!(can_trust_after, "worktree should be trusted after trust()");
30341}
30342
30343#[gpui::test]
30344fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
30345 // This test reproduces a bug where drawing an editor at a position above the viewport
30346 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
30347 // causes an infinite loop in blocks_in_range.
30348 //
30349 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
30350 // the content mask intersection produces visible_bounds with origin at the viewport top.
30351 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
30352 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
30353 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
30354 init_test(cx, |_| {});
30355
30356 let window = cx.add_window(|_, _| gpui::Empty);
30357 let mut cx = VisualTestContext::from_window(*window, cx);
30358
30359 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
30360 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
30361
30362 // Simulate a small viewport (500x500 pixels at origin 0,0)
30363 cx.simulate_resize(gpui::size(px(500.), px(500.)));
30364
30365 // Draw the editor at a very negative Y position, simulating an editor that's been
30366 // scrolled way above the visible viewport (like in a List that has scrolled past it).
30367 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
30368 // This should NOT hang - it should just render nothing.
30369 cx.draw(
30370 gpui::point(px(0.), px(-10000.)),
30371 gpui::size(px(500.), px(3000.)),
30372 |_, _| editor.clone(),
30373 );
30374
30375 // If we get here without hanging, the test passes
30376}
30377
30378#[gpui::test]
30379async fn test_diff_review_indicator_created_on_gutter_hover(cx: &mut TestAppContext) {
30380 init_test(cx, |_| {});
30381
30382 let fs = FakeFs::new(cx.executor());
30383 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30384 .await;
30385
30386 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30387 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30388 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30389
30390 let editor = workspace
30391 .update(cx, |workspace, window, cx| {
30392 workspace.open_abs_path(
30393 PathBuf::from(path!("/root/file.txt")),
30394 OpenOptions::default(),
30395 window,
30396 cx,
30397 )
30398 })
30399 .unwrap()
30400 .await
30401 .unwrap()
30402 .downcast::<Editor>()
30403 .unwrap();
30404
30405 // Enable diff review button mode
30406 editor.update(cx, |editor, cx| {
30407 editor.set_show_diff_review_button(true, cx);
30408 });
30409
30410 // Initially, no indicator should be present
30411 editor.update(cx, |editor, _cx| {
30412 assert!(
30413 editor.gutter_diff_review_indicator.0.is_none(),
30414 "Indicator should be None initially"
30415 );
30416 });
30417}
30418
30419#[gpui::test]
30420async fn test_diff_review_button_hidden_when_ai_disabled(cx: &mut TestAppContext) {
30421 init_test(cx, |_| {});
30422
30423 // Register DisableAiSettings and set disable_ai to true
30424 cx.update(|cx| {
30425 project::DisableAiSettings::register(cx);
30426 project::DisableAiSettings::override_global(
30427 project::DisableAiSettings { disable_ai: true },
30428 cx,
30429 );
30430 });
30431
30432 let fs = FakeFs::new(cx.executor());
30433 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30434 .await;
30435
30436 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30437 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30438 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30439
30440 let editor = workspace
30441 .update(cx, |workspace, window, cx| {
30442 workspace.open_abs_path(
30443 PathBuf::from(path!("/root/file.txt")),
30444 OpenOptions::default(),
30445 window,
30446 cx,
30447 )
30448 })
30449 .unwrap()
30450 .await
30451 .unwrap()
30452 .downcast::<Editor>()
30453 .unwrap();
30454
30455 // Enable diff review button mode
30456 editor.update(cx, |editor, cx| {
30457 editor.set_show_diff_review_button(true, cx);
30458 });
30459
30460 // Verify AI is disabled
30461 cx.read(|cx| {
30462 assert!(
30463 project::DisableAiSettings::get_global(cx).disable_ai,
30464 "AI should be disabled"
30465 );
30466 });
30467
30468 // The indicator should not be created when AI is disabled
30469 // (The mouse_moved handler checks DisableAiSettings before creating the indicator)
30470 editor.update(cx, |editor, _cx| {
30471 assert!(
30472 editor.gutter_diff_review_indicator.0.is_none(),
30473 "Indicator should be None when AI is disabled"
30474 );
30475 });
30476}
30477
30478#[gpui::test]
30479async fn test_diff_review_button_shown_when_ai_enabled(cx: &mut TestAppContext) {
30480 init_test(cx, |_| {});
30481
30482 // Register DisableAiSettings and set disable_ai to false
30483 cx.update(|cx| {
30484 project::DisableAiSettings::register(cx);
30485 project::DisableAiSettings::override_global(
30486 project::DisableAiSettings { disable_ai: false },
30487 cx,
30488 );
30489 });
30490
30491 let fs = FakeFs::new(cx.executor());
30492 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30493 .await;
30494
30495 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30496 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30497 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30498
30499 let editor = workspace
30500 .update(cx, |workspace, window, cx| {
30501 workspace.open_abs_path(
30502 PathBuf::from(path!("/root/file.txt")),
30503 OpenOptions::default(),
30504 window,
30505 cx,
30506 )
30507 })
30508 .unwrap()
30509 .await
30510 .unwrap()
30511 .downcast::<Editor>()
30512 .unwrap();
30513
30514 // Enable diff review button mode
30515 editor.update(cx, |editor, cx| {
30516 editor.set_show_diff_review_button(true, cx);
30517 });
30518
30519 // Verify AI is enabled
30520 cx.read(|cx| {
30521 assert!(
30522 !project::DisableAiSettings::get_global(cx).disable_ai,
30523 "AI should be enabled"
30524 );
30525 });
30526
30527 // The show_diff_review_button flag should be true
30528 editor.update(cx, |editor, _cx| {
30529 assert!(
30530 editor.show_diff_review_button(),
30531 "show_diff_review_button should be true"
30532 );
30533 });
30534}
30535
30536/// Helper function to create a DiffHunkKey for testing.
30537/// Uses Anchor::min() as a placeholder anchor since these tests don't need
30538/// real buffer positioning.
30539fn test_hunk_key(file_path: &str) -> DiffHunkKey {
30540 DiffHunkKey {
30541 file_path: if file_path.is_empty() {
30542 Arc::from(util::rel_path::RelPath::empty())
30543 } else {
30544 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
30545 },
30546 hunk_start_anchor: Anchor::min(),
30547 }
30548}
30549
30550/// Helper function to create a DiffHunkKey with a specific anchor for testing.
30551fn test_hunk_key_with_anchor(file_path: &str, anchor: Anchor) -> DiffHunkKey {
30552 DiffHunkKey {
30553 file_path: if file_path.is_empty() {
30554 Arc::from(util::rel_path::RelPath::empty())
30555 } else {
30556 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
30557 },
30558 hunk_start_anchor: anchor,
30559 }
30560}
30561
30562/// Helper function to add a review comment with default anchors for testing.
30563fn add_test_comment(
30564 editor: &mut Editor,
30565 key: DiffHunkKey,
30566 comment: &str,
30567 cx: &mut Context<Editor>,
30568) -> usize {
30569 editor.add_review_comment(key, comment.to_string(), Anchor::min()..Anchor::max(), cx)
30570}
30571
30572#[gpui::test]
30573fn test_review_comment_add_to_hunk(cx: &mut TestAppContext) {
30574 init_test(cx, |_| {});
30575
30576 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30577
30578 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30579 let key = test_hunk_key("");
30580
30581 let id = add_test_comment(editor, key.clone(), "Test comment", cx);
30582
30583 let snapshot = editor.buffer().read(cx).snapshot(cx);
30584 assert_eq!(editor.total_review_comment_count(), 1);
30585 assert_eq!(editor.hunk_comment_count(&key, &snapshot), 1);
30586
30587 let comments = editor.comments_for_hunk(&key, &snapshot);
30588 assert_eq!(comments.len(), 1);
30589 assert_eq!(comments[0].comment, "Test comment");
30590 assert_eq!(comments[0].id, id);
30591 });
30592}
30593
30594#[gpui::test]
30595fn test_review_comments_are_per_hunk(cx: &mut TestAppContext) {
30596 init_test(cx, |_| {});
30597
30598 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30599
30600 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30601 let snapshot = editor.buffer().read(cx).snapshot(cx);
30602 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
30603 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
30604 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
30605 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
30606
30607 add_test_comment(editor, key1.clone(), "Comment for file1", cx);
30608 add_test_comment(editor, key2.clone(), "Comment for file2", cx);
30609
30610 let snapshot = editor.buffer().read(cx).snapshot(cx);
30611 assert_eq!(editor.total_review_comment_count(), 2);
30612 assert_eq!(editor.hunk_comment_count(&key1, &snapshot), 1);
30613 assert_eq!(editor.hunk_comment_count(&key2, &snapshot), 1);
30614
30615 assert_eq!(
30616 editor.comments_for_hunk(&key1, &snapshot)[0].comment,
30617 "Comment for file1"
30618 );
30619 assert_eq!(
30620 editor.comments_for_hunk(&key2, &snapshot)[0].comment,
30621 "Comment for file2"
30622 );
30623 });
30624}
30625
30626#[gpui::test]
30627fn test_review_comment_remove(cx: &mut TestAppContext) {
30628 init_test(cx, |_| {});
30629
30630 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30631
30632 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30633 let key = test_hunk_key("");
30634
30635 let id = add_test_comment(editor, key, "To be removed", cx);
30636
30637 assert_eq!(editor.total_review_comment_count(), 1);
30638
30639 let removed = editor.remove_review_comment(id, cx);
30640 assert!(removed);
30641 assert_eq!(editor.total_review_comment_count(), 0);
30642
30643 // Try to remove again
30644 let removed_again = editor.remove_review_comment(id, cx);
30645 assert!(!removed_again);
30646 });
30647}
30648
30649#[gpui::test]
30650fn test_review_comment_update(cx: &mut TestAppContext) {
30651 init_test(cx, |_| {});
30652
30653 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30654
30655 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30656 let key = test_hunk_key("");
30657
30658 let id = add_test_comment(editor, key.clone(), "Original text", cx);
30659
30660 let updated = editor.update_review_comment(id, "Updated text".to_string(), cx);
30661 assert!(updated);
30662
30663 let snapshot = editor.buffer().read(cx).snapshot(cx);
30664 let comments = editor.comments_for_hunk(&key, &snapshot);
30665 assert_eq!(comments[0].comment, "Updated text");
30666 assert!(!comments[0].is_editing); // Should clear editing flag
30667 });
30668}
30669
30670#[gpui::test]
30671fn test_review_comment_take_all(cx: &mut TestAppContext) {
30672 init_test(cx, |_| {});
30673
30674 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30675
30676 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30677 let snapshot = editor.buffer().read(cx).snapshot(cx);
30678 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
30679 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
30680 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
30681 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
30682
30683 let id1 = add_test_comment(editor, key1.clone(), "Comment 1", cx);
30684 let id2 = add_test_comment(editor, key1.clone(), "Comment 2", cx);
30685 let id3 = add_test_comment(editor, key2.clone(), "Comment 3", cx);
30686
30687 // IDs should be sequential starting from 0
30688 assert_eq!(id1, 0);
30689 assert_eq!(id2, 1);
30690 assert_eq!(id3, 2);
30691
30692 assert_eq!(editor.total_review_comment_count(), 3);
30693
30694 let taken = editor.take_all_review_comments(cx);
30695
30696 // Should have 2 entries (one per hunk)
30697 assert_eq!(taken.len(), 2);
30698
30699 // Total comments should be 3
30700 let total: usize = taken
30701 .iter()
30702 .map(|(_, comments): &(DiffHunkKey, Vec<StoredReviewComment>)| comments.len())
30703 .sum();
30704 assert_eq!(total, 3);
30705
30706 // Storage should be empty
30707 assert_eq!(editor.total_review_comment_count(), 0);
30708
30709 // After taking all comments, ID counter should reset
30710 // New comments should get IDs starting from 0 again
30711 let new_id1 = add_test_comment(editor, key1, "New Comment 1", cx);
30712 let new_id2 = add_test_comment(editor, key2, "New Comment 2", cx);
30713
30714 assert_eq!(new_id1, 0, "ID counter should reset after take_all");
30715 assert_eq!(new_id2, 1, "IDs should be sequential after reset");
30716 });
30717}
30718
30719#[gpui::test]
30720fn test_diff_review_overlay_show_and_dismiss(cx: &mut TestAppContext) {
30721 init_test(cx, |_| {});
30722
30723 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30724
30725 // Show overlay
30726 editor
30727 .update(cx, |editor, window, cx| {
30728 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
30729 })
30730 .unwrap();
30731
30732 // Verify overlay is shown
30733 editor
30734 .update(cx, |editor, _window, cx| {
30735 assert!(!editor.diff_review_overlays.is_empty());
30736 assert_eq!(editor.diff_review_line_range(cx), Some((0, 0)));
30737 assert!(editor.diff_review_prompt_editor().is_some());
30738 })
30739 .unwrap();
30740
30741 // Dismiss overlay
30742 editor
30743 .update(cx, |editor, _window, cx| {
30744 editor.dismiss_all_diff_review_overlays(cx);
30745 })
30746 .unwrap();
30747
30748 // Verify overlay is dismissed
30749 editor
30750 .update(cx, |editor, _window, cx| {
30751 assert!(editor.diff_review_overlays.is_empty());
30752 assert_eq!(editor.diff_review_line_range(cx), None);
30753 assert!(editor.diff_review_prompt_editor().is_none());
30754 })
30755 .unwrap();
30756}
30757
30758#[gpui::test]
30759fn test_diff_review_overlay_dismiss_via_cancel(cx: &mut TestAppContext) {
30760 init_test(cx, |_| {});
30761
30762 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30763
30764 // Show overlay
30765 editor
30766 .update(cx, |editor, window, cx| {
30767 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
30768 })
30769 .unwrap();
30770
30771 // Verify overlay is shown
30772 editor
30773 .update(cx, |editor, _window, _cx| {
30774 assert!(!editor.diff_review_overlays.is_empty());
30775 })
30776 .unwrap();
30777
30778 // Dismiss via dismiss_menus_and_popups (which is called by cancel action)
30779 editor
30780 .update(cx, |editor, window, cx| {
30781 editor.dismiss_menus_and_popups(true, window, cx);
30782 })
30783 .unwrap();
30784
30785 // Verify overlay is dismissed
30786 editor
30787 .update(cx, |editor, _window, _cx| {
30788 assert!(editor.diff_review_overlays.is_empty());
30789 })
30790 .unwrap();
30791}
30792
30793#[gpui::test]
30794fn test_diff_review_empty_comment_not_submitted(cx: &mut TestAppContext) {
30795 init_test(cx, |_| {});
30796
30797 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30798
30799 // Show overlay
30800 editor
30801 .update(cx, |editor, window, cx| {
30802 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
30803 })
30804 .unwrap();
30805
30806 // Try to submit without typing anything (empty comment)
30807 editor
30808 .update(cx, |editor, window, cx| {
30809 editor.submit_diff_review_comment(window, cx);
30810 })
30811 .unwrap();
30812
30813 // Verify no comment was added
30814 editor
30815 .update(cx, |editor, _window, _cx| {
30816 assert_eq!(editor.total_review_comment_count(), 0);
30817 })
30818 .unwrap();
30819
30820 // Try to submit with whitespace-only comment
30821 editor
30822 .update(cx, |editor, window, cx| {
30823 if let Some(prompt_editor) = editor.diff_review_prompt_editor().cloned() {
30824 prompt_editor.update(cx, |pe, cx| {
30825 pe.insert(" \n\t ", window, cx);
30826 });
30827 }
30828 editor.submit_diff_review_comment(window, cx);
30829 })
30830 .unwrap();
30831
30832 // Verify still no comment was added
30833 editor
30834 .update(cx, |editor, _window, _cx| {
30835 assert_eq!(editor.total_review_comment_count(), 0);
30836 })
30837 .unwrap();
30838}
30839
30840#[gpui::test]
30841fn test_diff_review_inline_edit_flow(cx: &mut TestAppContext) {
30842 init_test(cx, |_| {});
30843
30844 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30845
30846 // Add a comment directly
30847 let comment_id = editor
30848 .update(cx, |editor, _window, cx| {
30849 let key = test_hunk_key("");
30850 add_test_comment(editor, key, "Original comment", cx)
30851 })
30852 .unwrap();
30853
30854 // Set comment to editing mode
30855 editor
30856 .update(cx, |editor, _window, cx| {
30857 editor.set_comment_editing(comment_id, true, cx);
30858 })
30859 .unwrap();
30860
30861 // Verify editing flag is set
30862 editor
30863 .update(cx, |editor, _window, cx| {
30864 let key = test_hunk_key("");
30865 let snapshot = editor.buffer().read(cx).snapshot(cx);
30866 let comments = editor.comments_for_hunk(&key, &snapshot);
30867 assert_eq!(comments.len(), 1);
30868 assert!(comments[0].is_editing);
30869 })
30870 .unwrap();
30871
30872 // Update the comment
30873 editor
30874 .update(cx, |editor, _window, cx| {
30875 let updated =
30876 editor.update_review_comment(comment_id, "Updated comment".to_string(), cx);
30877 assert!(updated);
30878 })
30879 .unwrap();
30880
30881 // Verify comment was updated and editing flag is cleared
30882 editor
30883 .update(cx, |editor, _window, cx| {
30884 let key = test_hunk_key("");
30885 let snapshot = editor.buffer().read(cx).snapshot(cx);
30886 let comments = editor.comments_for_hunk(&key, &snapshot);
30887 assert_eq!(comments[0].comment, "Updated comment");
30888 assert!(!comments[0].is_editing);
30889 })
30890 .unwrap();
30891}
30892
30893#[gpui::test]
30894fn test_orphaned_comments_are_cleaned_up(cx: &mut TestAppContext) {
30895 init_test(cx, |_| {});
30896
30897 // Create an editor with some text
30898 let editor = cx.add_window(|window, cx| {
30899 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
30900 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
30901 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
30902 });
30903
30904 // Add a comment with an anchor on line 2
30905 editor
30906 .update(cx, |editor, _window, cx| {
30907 let snapshot = editor.buffer().read(cx).snapshot(cx);
30908 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
30909 let key = DiffHunkKey {
30910 file_path: Arc::from(util::rel_path::RelPath::empty()),
30911 hunk_start_anchor: anchor,
30912 };
30913 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
30914 assert_eq!(editor.total_review_comment_count(), 1);
30915 })
30916 .unwrap();
30917
30918 // Delete all content (this should orphan the comment's anchor)
30919 editor
30920 .update(cx, |editor, window, cx| {
30921 editor.select_all(&SelectAll, window, cx);
30922 editor.insert("completely new content", window, cx);
30923 })
30924 .unwrap();
30925
30926 // Trigger cleanup
30927 editor
30928 .update(cx, |editor, _window, cx| {
30929 editor.cleanup_orphaned_review_comments(cx);
30930 // Comment should be removed because its anchor is invalid
30931 assert_eq!(editor.total_review_comment_count(), 0);
30932 })
30933 .unwrap();
30934}
30935
30936#[gpui::test]
30937fn test_orphaned_comments_cleanup_called_on_buffer_edit(cx: &mut TestAppContext) {
30938 init_test(cx, |_| {});
30939
30940 // Create an editor with some text
30941 let editor = cx.add_window(|window, cx| {
30942 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
30943 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
30944 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
30945 });
30946
30947 // Add a comment with an anchor on line 2
30948 editor
30949 .update(cx, |editor, _window, cx| {
30950 let snapshot = editor.buffer().read(cx).snapshot(cx);
30951 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
30952 let key = DiffHunkKey {
30953 file_path: Arc::from(util::rel_path::RelPath::empty()),
30954 hunk_start_anchor: anchor,
30955 };
30956 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
30957 assert_eq!(editor.total_review_comment_count(), 1);
30958 })
30959 .unwrap();
30960
30961 // Edit the buffer - this should trigger cleanup via on_buffer_event
30962 // Delete all content which orphans the anchor
30963 editor
30964 .update(cx, |editor, window, cx| {
30965 editor.select_all(&SelectAll, window, cx);
30966 editor.insert("completely new content", window, cx);
30967 // The cleanup is called automatically in on_buffer_event when Edited fires
30968 })
30969 .unwrap();
30970
30971 // Verify cleanup happened automatically (not manually triggered)
30972 editor
30973 .update(cx, |editor, _window, _cx| {
30974 // Comment should be removed because its anchor became invalid
30975 // and cleanup was called automatically on buffer edit
30976 assert_eq!(editor.total_review_comment_count(), 0);
30977 })
30978 .unwrap();
30979}
30980
30981#[gpui::test]
30982fn test_comments_stored_for_multiple_hunks(cx: &mut TestAppContext) {
30983 init_test(cx, |_| {});
30984
30985 // This test verifies that comments can be stored for multiple different hunks
30986 // and that hunk_comment_count correctly identifies comments per hunk.
30987 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30988
30989 _ = editor.update(cx, |editor, _window, cx| {
30990 let snapshot = editor.buffer().read(cx).snapshot(cx);
30991
30992 // Create two different hunk keys (simulating two different files)
30993 let anchor = snapshot.anchor_before(Point::new(0, 0));
30994 let key1 = DiffHunkKey {
30995 file_path: Arc::from(util::rel_path::RelPath::unix("file1.rs").unwrap()),
30996 hunk_start_anchor: anchor,
30997 };
30998 let key2 = DiffHunkKey {
30999 file_path: Arc::from(util::rel_path::RelPath::unix("file2.rs").unwrap()),
31000 hunk_start_anchor: anchor,
31001 };
31002
31003 // Add comments to first hunk
31004 editor.add_review_comment(
31005 key1.clone(),
31006 "Comment 1 for file1".to_string(),
31007 anchor..anchor,
31008 cx,
31009 );
31010 editor.add_review_comment(
31011 key1.clone(),
31012 "Comment 2 for file1".to_string(),
31013 anchor..anchor,
31014 cx,
31015 );
31016
31017 // Add comment to second hunk
31018 editor.add_review_comment(
31019 key2.clone(),
31020 "Comment for file2".to_string(),
31021 anchor..anchor,
31022 cx,
31023 );
31024
31025 // Verify total count
31026 assert_eq!(editor.total_review_comment_count(), 3);
31027
31028 // Verify per-hunk counts
31029 let snapshot = editor.buffer().read(cx).snapshot(cx);
31030 assert_eq!(
31031 editor.hunk_comment_count(&key1, &snapshot),
31032 2,
31033 "file1 should have 2 comments"
31034 );
31035 assert_eq!(
31036 editor.hunk_comment_count(&key2, &snapshot),
31037 1,
31038 "file2 should have 1 comment"
31039 );
31040
31041 // Verify comments_for_hunk returns correct comments
31042 let file1_comments = editor.comments_for_hunk(&key1, &snapshot);
31043 assert_eq!(file1_comments.len(), 2);
31044 assert_eq!(file1_comments[0].comment, "Comment 1 for file1");
31045 assert_eq!(file1_comments[1].comment, "Comment 2 for file1");
31046
31047 let file2_comments = editor.comments_for_hunk(&key2, &snapshot);
31048 assert_eq!(file2_comments.len(), 1);
31049 assert_eq!(file2_comments[0].comment, "Comment for file2");
31050 });
31051}
31052
31053#[gpui::test]
31054fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
31055 init_test(cx, |_| {});
31056
31057 // This test verifies that hunk_keys_match correctly identifies when two
31058 // DiffHunkKeys refer to the same hunk (same file path and anchor point).
31059 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31060
31061 _ = editor.update(cx, |editor, _window, cx| {
31062 let snapshot = editor.buffer().read(cx).snapshot(cx);
31063 let anchor = snapshot.anchor_before(Point::new(0, 0));
31064
31065 // Create two keys with the same file path and anchor
31066 let key1 = DiffHunkKey {
31067 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
31068 hunk_start_anchor: anchor,
31069 };
31070 let key2 = DiffHunkKey {
31071 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
31072 hunk_start_anchor: anchor,
31073 };
31074
31075 // Add comment to first key
31076 editor.add_review_comment(key1, "Test comment".to_string(), anchor..anchor, cx);
31077
31078 // Verify second key (same hunk) finds the comment
31079 let snapshot = editor.buffer().read(cx).snapshot(cx);
31080 assert_eq!(
31081 editor.hunk_comment_count(&key2, &snapshot),
31082 1,
31083 "Same hunk should find the comment"
31084 );
31085
31086 // Create a key with different file path
31087 let different_file_key = DiffHunkKey {
31088 file_path: Arc::from(util::rel_path::RelPath::unix("other.rs").unwrap()),
31089 hunk_start_anchor: anchor,
31090 };
31091
31092 // Different file should not find the comment
31093 assert_eq!(
31094 editor.hunk_comment_count(&different_file_key, &snapshot),
31095 0,
31096 "Different file should not find the comment"
31097 );
31098 });
31099}
31100
31101#[gpui::test]
31102fn test_overlay_comments_expanded_state(cx: &mut TestAppContext) {
31103 init_test(cx, |_| {});
31104
31105 // This test verifies that set_diff_review_comments_expanded correctly
31106 // updates the expanded state of overlays.
31107 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31108
31109 // Show overlay
31110 editor
31111 .update(cx, |editor, window, cx| {
31112 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
31113 })
31114 .unwrap();
31115
31116 // Verify initially expanded (default)
31117 editor
31118 .update(cx, |editor, _window, _cx| {
31119 assert!(
31120 editor.diff_review_overlays[0].comments_expanded,
31121 "Should be expanded by default"
31122 );
31123 })
31124 .unwrap();
31125
31126 // Set to collapsed using the public method
31127 editor
31128 .update(cx, |editor, _window, cx| {
31129 editor.set_diff_review_comments_expanded(false, cx);
31130 })
31131 .unwrap();
31132
31133 // Verify collapsed
31134 editor
31135 .update(cx, |editor, _window, _cx| {
31136 assert!(
31137 !editor.diff_review_overlays[0].comments_expanded,
31138 "Should be collapsed after setting to false"
31139 );
31140 })
31141 .unwrap();
31142
31143 // Set back to expanded
31144 editor
31145 .update(cx, |editor, _window, cx| {
31146 editor.set_diff_review_comments_expanded(true, cx);
31147 })
31148 .unwrap();
31149
31150 // Verify expanded again
31151 editor
31152 .update(cx, |editor, _window, _cx| {
31153 assert!(
31154 editor.diff_review_overlays[0].comments_expanded,
31155 "Should be expanded after setting to true"
31156 );
31157 })
31158 .unwrap();
31159}
31160
31161#[gpui::test]
31162fn test_diff_review_multiline_selection(cx: &mut TestAppContext) {
31163 init_test(cx, |_| {});
31164
31165 // Create an editor with multiple lines of text
31166 let editor = cx.add_window(|window, cx| {
31167 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\nline 4\nline 5\n", cx));
31168 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31169 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31170 });
31171
31172 // Test showing overlay with a multi-line selection (lines 1-3, which are rows 0-2)
31173 editor
31174 .update(cx, |editor, window, cx| {
31175 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(2), window, cx);
31176 })
31177 .unwrap();
31178
31179 // Verify line range
31180 editor
31181 .update(cx, |editor, _window, cx| {
31182 assert!(!editor.diff_review_overlays.is_empty());
31183 assert_eq!(editor.diff_review_line_range(cx), Some((0, 2)));
31184 })
31185 .unwrap();
31186
31187 // Dismiss and test with reversed range (end < start)
31188 editor
31189 .update(cx, |editor, _window, cx| {
31190 editor.dismiss_all_diff_review_overlays(cx);
31191 })
31192 .unwrap();
31193
31194 // Show overlay with reversed range - should normalize it
31195 editor
31196 .update(cx, |editor, window, cx| {
31197 editor.show_diff_review_overlay(DisplayRow(3)..DisplayRow(1), window, cx);
31198 })
31199 .unwrap();
31200
31201 // Verify range is normalized (start <= end)
31202 editor
31203 .update(cx, |editor, _window, cx| {
31204 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
31205 })
31206 .unwrap();
31207}
31208
31209#[gpui::test]
31210fn test_diff_review_drag_state(cx: &mut TestAppContext) {
31211 init_test(cx, |_| {});
31212
31213 let editor = cx.add_window(|window, cx| {
31214 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
31215 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31216 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31217 });
31218
31219 // Initially no drag state
31220 editor
31221 .update(cx, |editor, _window, _cx| {
31222 assert!(editor.diff_review_drag_state.is_none());
31223 })
31224 .unwrap();
31225
31226 // Start drag at row 1
31227 editor
31228 .update(cx, |editor, window, cx| {
31229 editor.start_diff_review_drag(DisplayRow(1), window, cx);
31230 })
31231 .unwrap();
31232
31233 // Verify drag state is set
31234 editor
31235 .update(cx, |editor, window, cx| {
31236 assert!(editor.diff_review_drag_state.is_some());
31237 let snapshot = editor.snapshot(window, cx);
31238 let range = editor
31239 .diff_review_drag_state
31240 .as_ref()
31241 .unwrap()
31242 .row_range(&snapshot.display_snapshot);
31243 assert_eq!(*range.start(), DisplayRow(1));
31244 assert_eq!(*range.end(), DisplayRow(1));
31245 })
31246 .unwrap();
31247
31248 // Update drag to row 3
31249 editor
31250 .update(cx, |editor, window, cx| {
31251 editor.update_diff_review_drag(DisplayRow(3), window, cx);
31252 })
31253 .unwrap();
31254
31255 // Verify drag state is updated
31256 editor
31257 .update(cx, |editor, window, cx| {
31258 assert!(editor.diff_review_drag_state.is_some());
31259 let snapshot = editor.snapshot(window, cx);
31260 let range = editor
31261 .diff_review_drag_state
31262 .as_ref()
31263 .unwrap()
31264 .row_range(&snapshot.display_snapshot);
31265 assert_eq!(*range.start(), DisplayRow(1));
31266 assert_eq!(*range.end(), DisplayRow(3));
31267 })
31268 .unwrap();
31269
31270 // End drag - should show overlay
31271 editor
31272 .update(cx, |editor, window, cx| {
31273 editor.end_diff_review_drag(window, cx);
31274 })
31275 .unwrap();
31276
31277 // Verify drag state is cleared and overlay is shown
31278 editor
31279 .update(cx, |editor, _window, cx| {
31280 assert!(editor.diff_review_drag_state.is_none());
31281 assert!(!editor.diff_review_overlays.is_empty());
31282 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
31283 })
31284 .unwrap();
31285}
31286
31287#[gpui::test]
31288fn test_diff_review_drag_cancel(cx: &mut TestAppContext) {
31289 init_test(cx, |_| {});
31290
31291 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31292
31293 // Start drag
31294 editor
31295 .update(cx, |editor, window, cx| {
31296 editor.start_diff_review_drag(DisplayRow(0), window, cx);
31297 })
31298 .unwrap();
31299
31300 // Verify drag state is set
31301 editor
31302 .update(cx, |editor, _window, _cx| {
31303 assert!(editor.diff_review_drag_state.is_some());
31304 })
31305 .unwrap();
31306
31307 // Cancel drag
31308 editor
31309 .update(cx, |editor, _window, cx| {
31310 editor.cancel_diff_review_drag(cx);
31311 })
31312 .unwrap();
31313
31314 // Verify drag state is cleared and no overlay was created
31315 editor
31316 .update(cx, |editor, _window, _cx| {
31317 assert!(editor.diff_review_drag_state.is_none());
31318 assert!(editor.diff_review_overlays.is_empty());
31319 })
31320 .unwrap();
31321}
31322
31323#[gpui::test]
31324fn test_calculate_overlay_height(cx: &mut TestAppContext) {
31325 init_test(cx, |_| {});
31326
31327 // This test verifies that calculate_overlay_height returns correct heights
31328 // based on comment count and expanded state.
31329 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31330
31331 _ = editor.update(cx, |editor, _window, cx| {
31332 let snapshot = editor.buffer().read(cx).snapshot(cx);
31333 let anchor = snapshot.anchor_before(Point::new(0, 0));
31334 let key = DiffHunkKey {
31335 file_path: Arc::from(util::rel_path::RelPath::empty()),
31336 hunk_start_anchor: anchor,
31337 };
31338
31339 // No comments: base height of 2
31340 let height_no_comments = editor.calculate_overlay_height(&key, true, &snapshot);
31341 assert_eq!(
31342 height_no_comments, 2,
31343 "Base height should be 2 with no comments"
31344 );
31345
31346 // Add one comment
31347 editor.add_review_comment(key.clone(), "Comment 1".to_string(), anchor..anchor, cx);
31348
31349 let snapshot = editor.buffer().read(cx).snapshot(cx);
31350
31351 // With comments expanded: base (2) + header (1) + 2 per comment
31352 let height_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
31353 assert_eq!(
31354 height_expanded,
31355 2 + 1 + 2, // base + header + 1 comment * 2
31356 "Height with 1 comment expanded"
31357 );
31358
31359 // With comments collapsed: base (2) + header (1)
31360 let height_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
31361 assert_eq!(
31362 height_collapsed,
31363 2 + 1, // base + header only
31364 "Height with comments collapsed"
31365 );
31366
31367 // Add more comments
31368 editor.add_review_comment(key.clone(), "Comment 2".to_string(), anchor..anchor, cx);
31369 editor.add_review_comment(key.clone(), "Comment 3".to_string(), anchor..anchor, cx);
31370
31371 let snapshot = editor.buffer().read(cx).snapshot(cx);
31372
31373 // With 3 comments expanded
31374 let height_3_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
31375 assert_eq!(
31376 height_3_expanded,
31377 2 + 1 + (3 * 2), // base + header + 3 comments * 2
31378 "Height with 3 comments expanded"
31379 );
31380
31381 // Collapsed height stays the same regardless of comment count
31382 let height_3_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
31383 assert_eq!(
31384 height_3_collapsed,
31385 2 + 1, // base + header only
31386 "Height with 3 comments collapsed should be same as 1 comment collapsed"
31387 );
31388 });
31389}
31390
31391#[gpui::test]
31392async fn test_move_to_start_end_of_larger_syntax_node_single_cursor(cx: &mut TestAppContext) {
31393 init_test(cx, |_| {});
31394
31395 let language = Arc::new(Language::new(
31396 LanguageConfig::default(),
31397 Some(tree_sitter_rust::LANGUAGE.into()),
31398 ));
31399
31400 let text = r#"
31401 fn main() {
31402 let x = foo(1, 2);
31403 }
31404 "#
31405 .unindent();
31406
31407 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
31408 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31409 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
31410
31411 editor
31412 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
31413 .await;
31414
31415 // Test case 1: Move to end of syntax nodes
31416 editor.update_in(cx, |editor, window, cx| {
31417 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31418 s.select_display_ranges([
31419 DisplayPoint::new(DisplayRow(1), 16)..DisplayPoint::new(DisplayRow(1), 16)
31420 ]);
31421 });
31422 });
31423 editor.update(cx, |editor, cx| {
31424 assert_text_with_selections(
31425 editor,
31426 indoc! {r#"
31427 fn main() {
31428 let x = foo(ˇ1, 2);
31429 }
31430 "#},
31431 cx,
31432 );
31433 });
31434 editor.update_in(cx, |editor, window, cx| {
31435 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31436 });
31437 editor.update(cx, |editor, cx| {
31438 assert_text_with_selections(
31439 editor,
31440 indoc! {r#"
31441 fn main() {
31442 let x = foo(1ˇ, 2);
31443 }
31444 "#},
31445 cx,
31446 );
31447 });
31448 editor.update_in(cx, |editor, window, cx| {
31449 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31450 });
31451 editor.update(cx, |editor, cx| {
31452 assert_text_with_selections(
31453 editor,
31454 indoc! {r#"
31455 fn main() {
31456 let x = foo(1, 2)ˇ;
31457 }
31458 "#},
31459 cx,
31460 );
31461 });
31462 editor.update_in(cx, |editor, window, cx| {
31463 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31464 });
31465 editor.update(cx, |editor, cx| {
31466 assert_text_with_selections(
31467 editor,
31468 indoc! {r#"
31469 fn main() {
31470 let x = foo(1, 2);ˇ
31471 }
31472 "#},
31473 cx,
31474 );
31475 });
31476 editor.update_in(cx, |editor, window, cx| {
31477 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31478 });
31479 editor.update(cx, |editor, cx| {
31480 assert_text_with_selections(
31481 editor,
31482 indoc! {r#"
31483 fn main() {
31484 let x = foo(1, 2);
31485 }ˇ
31486 "#},
31487 cx,
31488 );
31489 });
31490
31491 // Test case 2: Move to start of syntax nodes
31492 editor.update_in(cx, |editor, window, cx| {
31493 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31494 s.select_display_ranges([
31495 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20)
31496 ]);
31497 });
31498 });
31499 editor.update(cx, |editor, cx| {
31500 assert_text_with_selections(
31501 editor,
31502 indoc! {r#"
31503 fn main() {
31504 let x = foo(1, 2ˇ);
31505 }
31506 "#},
31507 cx,
31508 );
31509 });
31510 editor.update_in(cx, |editor, window, cx| {
31511 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31512 });
31513 editor.update(cx, |editor, cx| {
31514 assert_text_with_selections(
31515 editor,
31516 indoc! {r#"
31517 fn main() {
31518 let x = fooˇ(1, 2);
31519 }
31520 "#},
31521 cx,
31522 );
31523 });
31524 editor.update_in(cx, |editor, window, cx| {
31525 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31526 });
31527 editor.update(cx, |editor, cx| {
31528 assert_text_with_selections(
31529 editor,
31530 indoc! {r#"
31531 fn main() {
31532 let x = ˇfoo(1, 2);
31533 }
31534 "#},
31535 cx,
31536 );
31537 });
31538 editor.update_in(cx, |editor, window, cx| {
31539 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31540 });
31541 editor.update(cx, |editor, cx| {
31542 assert_text_with_selections(
31543 editor,
31544 indoc! {r#"
31545 fn main() {
31546 ˇlet x = foo(1, 2);
31547 }
31548 "#},
31549 cx,
31550 );
31551 });
31552 editor.update_in(cx, |editor, window, cx| {
31553 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31554 });
31555 editor.update(cx, |editor, cx| {
31556 assert_text_with_selections(
31557 editor,
31558 indoc! {r#"
31559 fn main() ˇ{
31560 let x = foo(1, 2);
31561 }
31562 "#},
31563 cx,
31564 );
31565 });
31566 editor.update_in(cx, |editor, window, cx| {
31567 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31568 });
31569 editor.update(cx, |editor, cx| {
31570 assert_text_with_selections(
31571 editor,
31572 indoc! {r#"
31573 ˇfn main() {
31574 let x = foo(1, 2);
31575 }
31576 "#},
31577 cx,
31578 );
31579 });
31580}
31581
31582#[gpui::test]
31583async fn test_move_to_start_end_of_larger_syntax_node_two_cursors(cx: &mut TestAppContext) {
31584 init_test(cx, |_| {});
31585
31586 let language = Arc::new(Language::new(
31587 LanguageConfig::default(),
31588 Some(tree_sitter_rust::LANGUAGE.into()),
31589 ));
31590
31591 let text = r#"
31592 fn main() {
31593 let x = foo(1, 2);
31594 let y = bar(3, 4);
31595 }
31596 "#
31597 .unindent();
31598
31599 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
31600 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31601 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
31602
31603 editor
31604 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
31605 .await;
31606
31607 // Test case 1: Move to end of syntax nodes with two cursors
31608 editor.update_in(cx, |editor, window, cx| {
31609 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31610 s.select_display_ranges([
31611 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20),
31612 DisplayPoint::new(DisplayRow(2), 20)..DisplayPoint::new(DisplayRow(2), 20),
31613 ]);
31614 });
31615 });
31616 editor.update(cx, |editor, cx| {
31617 assert_text_with_selections(
31618 editor,
31619 indoc! {r#"
31620 fn main() {
31621 let x = foo(1, 2ˇ);
31622 let y = bar(3, 4ˇ);
31623 }
31624 "#},
31625 cx,
31626 );
31627 });
31628 editor.update_in(cx, |editor, window, cx| {
31629 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31630 });
31631 editor.update(cx, |editor, cx| {
31632 assert_text_with_selections(
31633 editor,
31634 indoc! {r#"
31635 fn main() {
31636 let x = foo(1, 2)ˇ;
31637 let y = bar(3, 4)ˇ;
31638 }
31639 "#},
31640 cx,
31641 );
31642 });
31643 editor.update_in(cx, |editor, window, cx| {
31644 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31645 });
31646 editor.update(cx, |editor, cx| {
31647 assert_text_with_selections(
31648 editor,
31649 indoc! {r#"
31650 fn main() {
31651 let x = foo(1, 2);ˇ
31652 let y = bar(3, 4);ˇ
31653 }
31654 "#},
31655 cx,
31656 );
31657 });
31658
31659 // Test case 2: Move to start of syntax nodes with two cursors
31660 editor.update_in(cx, |editor, window, cx| {
31661 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31662 s.select_display_ranges([
31663 DisplayPoint::new(DisplayRow(1), 19)..DisplayPoint::new(DisplayRow(1), 19),
31664 DisplayPoint::new(DisplayRow(2), 19)..DisplayPoint::new(DisplayRow(2), 19),
31665 ]);
31666 });
31667 });
31668 editor.update(cx, |editor, cx| {
31669 assert_text_with_selections(
31670 editor,
31671 indoc! {r#"
31672 fn main() {
31673 let x = foo(1, ˇ2);
31674 let y = bar(3, ˇ4);
31675 }
31676 "#},
31677 cx,
31678 );
31679 });
31680 editor.update_in(cx, |editor, window, cx| {
31681 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31682 });
31683 editor.update(cx, |editor, cx| {
31684 assert_text_with_selections(
31685 editor,
31686 indoc! {r#"
31687 fn main() {
31688 let x = fooˇ(1, 2);
31689 let y = barˇ(3, 4);
31690 }
31691 "#},
31692 cx,
31693 );
31694 });
31695 editor.update_in(cx, |editor, window, cx| {
31696 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31697 });
31698 editor.update(cx, |editor, cx| {
31699 assert_text_with_selections(
31700 editor,
31701 indoc! {r#"
31702 fn main() {
31703 let x = ˇfoo(1, 2);
31704 let y = ˇbar(3, 4);
31705 }
31706 "#},
31707 cx,
31708 );
31709 });
31710 editor.update_in(cx, |editor, window, cx| {
31711 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31712 });
31713 editor.update(cx, |editor, cx| {
31714 assert_text_with_selections(
31715 editor,
31716 indoc! {r#"
31717 fn main() {
31718 ˇlet x = foo(1, 2);
31719 ˇlet y = bar(3, 4);
31720 }
31721 "#},
31722 cx,
31723 );
31724 });
31725}
31726
31727#[gpui::test]
31728async fn test_move_to_start_end_of_larger_syntax_node_with_selections_and_strings(
31729 cx: &mut TestAppContext,
31730) {
31731 init_test(cx, |_| {});
31732
31733 let language = Arc::new(Language::new(
31734 LanguageConfig::default(),
31735 Some(tree_sitter_rust::LANGUAGE.into()),
31736 ));
31737
31738 let text = r#"
31739 fn main() {
31740 let x = foo(1, 2);
31741 let msg = "hello world";
31742 }
31743 "#
31744 .unindent();
31745
31746 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
31747 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31748 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
31749
31750 editor
31751 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
31752 .await;
31753
31754 // Test case 1: With existing selection, move_to_end keeps selection
31755 editor.update_in(cx, |editor, window, cx| {
31756 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31757 s.select_display_ranges([
31758 DisplayPoint::new(DisplayRow(1), 12)..DisplayPoint::new(DisplayRow(1), 21)
31759 ]);
31760 });
31761 });
31762 editor.update(cx, |editor, cx| {
31763 assert_text_with_selections(
31764 editor,
31765 indoc! {r#"
31766 fn main() {
31767 let x = «foo(1, 2)ˇ»;
31768 let msg = "hello world";
31769 }
31770 "#},
31771 cx,
31772 );
31773 });
31774 editor.update_in(cx, |editor, window, cx| {
31775 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31776 });
31777 editor.update(cx, |editor, cx| {
31778 assert_text_with_selections(
31779 editor,
31780 indoc! {r#"
31781 fn main() {
31782 let x = «foo(1, 2)ˇ»;
31783 let msg = "hello world";
31784 }
31785 "#},
31786 cx,
31787 );
31788 });
31789
31790 // Test case 2: Move to end within a string
31791 editor.update_in(cx, |editor, window, cx| {
31792 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31793 s.select_display_ranges([
31794 DisplayPoint::new(DisplayRow(2), 15)..DisplayPoint::new(DisplayRow(2), 15)
31795 ]);
31796 });
31797 });
31798 editor.update(cx, |editor, cx| {
31799 assert_text_with_selections(
31800 editor,
31801 indoc! {r#"
31802 fn main() {
31803 let x = foo(1, 2);
31804 let msg = "ˇhello world";
31805 }
31806 "#},
31807 cx,
31808 );
31809 });
31810 editor.update_in(cx, |editor, window, cx| {
31811 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31812 });
31813 editor.update(cx, |editor, cx| {
31814 assert_text_with_selections(
31815 editor,
31816 indoc! {r#"
31817 fn main() {
31818 let x = foo(1, 2);
31819 let msg = "hello worldˇ";
31820 }
31821 "#},
31822 cx,
31823 );
31824 });
31825
31826 // Test case 3: Move to start within a string
31827 editor.update_in(cx, |editor, window, cx| {
31828 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31829 s.select_display_ranges([
31830 DisplayPoint::new(DisplayRow(2), 21)..DisplayPoint::new(DisplayRow(2), 21)
31831 ]);
31832 });
31833 });
31834 editor.update(cx, |editor, cx| {
31835 assert_text_with_selections(
31836 editor,
31837 indoc! {r#"
31838 fn main() {
31839 let x = foo(1, 2);
31840 let msg = "hello ˇworld";
31841 }
31842 "#},
31843 cx,
31844 );
31845 });
31846 editor.update_in(cx, |editor, window, cx| {
31847 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31848 });
31849 editor.update(cx, |editor, cx| {
31850 assert_text_with_selections(
31851 editor,
31852 indoc! {r#"
31853 fn main() {
31854 let x = foo(1, 2);
31855 let msg = "ˇhello world";
31856 }
31857 "#},
31858 cx,
31859 );
31860 });
31861}