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 runnables::RunnableTasks,
9 scroll::scroll_amount::ScrollAmount,
10 test::{
11 assert_text_with_selections, build_editor, editor_content_with_blocks,
12 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
13 editor_test_context::EditorTestContext,
14 select_ranges,
15 },
16};
17use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
18use collections::HashMap;
19use futures::{StreamExt, channel::oneshot};
20use gpui::{
21 BackgroundExecutor, DismissEvent, TestAppContext, UpdateGlobal, VisualTestContext,
22 WindowBounds, WindowOptions, div,
23};
24use indoc::indoc;
25use language::{
26 BracketPairConfig,
27 Capability::ReadWrite,
28 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
29 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
30 language_settings::{
31 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
32 },
33 tree_sitter_python,
34};
35use language_settings::Formatter;
36use languages::markdown_lang;
37use languages::rust_lang;
38use lsp::{CompletionParams, DEFAULT_LSP_REQUEST_TIMEOUT};
39use multi_buffer::{IndentGuide, MultiBuffer, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey};
40use parking_lot::Mutex;
41use pretty_assertions::{assert_eq, assert_ne};
42use project::{
43 FakeFs, Project,
44 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
45 project_settings::LspSettings,
46 trusted_worktrees::{PathTrust, TrustedWorktrees},
47};
48use serde_json::{self, json};
49use settings::{
50 AllLanguageSettingsContent, DelayMs, EditorSettingsContent, GlobalLspSettingsContent,
51 IndentGuideBackgroundColoring, IndentGuideColoring, InlayHintSettingsContent,
52 ProjectSettingsContent, SearchSettingsContent, SettingsContent, SettingsStore,
53};
54use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
55use std::{
56 iter,
57 sync::atomic::{self, AtomicUsize},
58};
59use test::build_editor_with_project;
60use text::ToPoint as _;
61use unindent::Unindent;
62use util::{
63 assert_set_eq, path,
64 rel_path::rel_path,
65 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
66};
67use workspace::{
68 CloseActiveItem, CloseAllItems, CloseOtherItems, MultiWorkspace, NavigationEntry, OpenOptions,
69 ViewId,
70 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
71 register_project_item,
72};
73
74fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
75 editor
76 .selections
77 .display_ranges(&editor.display_snapshot(cx))
78}
79
80#[cfg(any(test, feature = "test-support"))]
81pub mod property_test;
82
83#[gpui::test]
84fn test_edit_events(cx: &mut TestAppContext) {
85 init_test(cx, |_| {});
86
87 let buffer = cx.new(|cx| {
88 let mut buffer = language::Buffer::local("123456", cx);
89 buffer.set_group_interval(Duration::from_secs(1));
90 buffer
91 });
92
93 let events = Rc::new(RefCell::new(Vec::new()));
94 let editor1 = cx.add_window({
95 let events = events.clone();
96 |window, cx| {
97 let entity = cx.entity();
98 cx.subscribe_in(
99 &entity,
100 window,
101 move |_, _, event: &EditorEvent, _, _| match event {
102 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
103 EditorEvent::BufferEdited => {
104 events.borrow_mut().push(("editor1", "buffer edited"))
105 }
106 _ => {}
107 },
108 )
109 .detach();
110 Editor::for_buffer(buffer.clone(), None, window, cx)
111 }
112 });
113
114 let editor2 = cx.add_window({
115 let events = events.clone();
116 |window, cx| {
117 cx.subscribe_in(
118 &cx.entity(),
119 window,
120 move |_, _, event: &EditorEvent, _, _| match event {
121 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
122 EditorEvent::BufferEdited => {
123 events.borrow_mut().push(("editor2", "buffer edited"))
124 }
125 _ => {}
126 },
127 )
128 .detach();
129 Editor::for_buffer(buffer.clone(), None, window, cx)
130 }
131 });
132
133 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
134
135 // Mutating editor 1 will emit an `Edited` event only for that editor.
136 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
137 assert_eq!(
138 mem::take(&mut *events.borrow_mut()),
139 [
140 ("editor1", "edited"),
141 ("editor1", "buffer edited"),
142 ("editor2", "buffer edited"),
143 ]
144 );
145
146 // Mutating editor 2 will emit an `Edited` event only for that editor.
147 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
148 assert_eq!(
149 mem::take(&mut *events.borrow_mut()),
150 [
151 ("editor2", "edited"),
152 ("editor1", "buffer edited"),
153 ("editor2", "buffer edited"),
154 ]
155 );
156
157 // Undoing on editor 1 will emit an `Edited` event only for that editor.
158 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
159 assert_eq!(
160 mem::take(&mut *events.borrow_mut()),
161 [
162 ("editor1", "edited"),
163 ("editor1", "buffer edited"),
164 ("editor2", "buffer edited"),
165 ]
166 );
167
168 // Redoing on editor 1 will emit an `Edited` event only for that editor.
169 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
170 assert_eq!(
171 mem::take(&mut *events.borrow_mut()),
172 [
173 ("editor1", "edited"),
174 ("editor1", "buffer edited"),
175 ("editor2", "buffer edited"),
176 ]
177 );
178
179 // Undoing on editor 2 will emit an `Edited` event only for that editor.
180 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
181 assert_eq!(
182 mem::take(&mut *events.borrow_mut()),
183 [
184 ("editor2", "edited"),
185 ("editor1", "buffer edited"),
186 ("editor2", "buffer edited"),
187 ]
188 );
189
190 // Redoing on editor 2 will emit an `Edited` event only for that editor.
191 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
192 assert_eq!(
193 mem::take(&mut *events.borrow_mut()),
194 [
195 ("editor2", "edited"),
196 ("editor1", "buffer edited"),
197 ("editor2", "buffer edited"),
198 ]
199 );
200
201 // No event is emitted when the mutation is a no-op.
202 _ = editor2.update(cx, |editor, window, cx| {
203 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
204 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
205 });
206
207 editor.backspace(&Backspace, window, cx);
208 });
209 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
210}
211
212#[gpui::test]
213fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
214 init_test(cx, |_| {});
215
216 let mut now = Instant::now();
217 let group_interval = Duration::from_millis(1);
218 let buffer = cx.new(|cx| {
219 let mut buf = language::Buffer::local("123456", cx);
220 buf.set_group_interval(group_interval);
221 buf
222 });
223 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
224 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
225
226 _ = editor.update(cx, |editor, window, cx| {
227 editor.start_transaction_at(now, window, cx);
228 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
229 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(4)])
230 });
231
232 editor.insert("cd", window, cx);
233 editor.end_transaction_at(now, cx);
234 assert_eq!(editor.text(cx), "12cd56");
235 assert_eq!(
236 editor.selections.ranges(&editor.display_snapshot(cx)),
237 vec![MultiBufferOffset(4)..MultiBufferOffset(4)]
238 );
239
240 editor.start_transaction_at(now, window, cx);
241 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
242 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(5)])
243 });
244 editor.insert("e", window, cx);
245 editor.end_transaction_at(now, cx);
246 assert_eq!(editor.text(cx), "12cde6");
247 assert_eq!(
248 editor.selections.ranges(&editor.display_snapshot(cx)),
249 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
250 );
251
252 now += group_interval + Duration::from_millis(1);
253 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
254 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)])
255 });
256
257 // Simulate an edit in another editor
258 buffer.update(cx, |buffer, cx| {
259 buffer.start_transaction_at(now, cx);
260 buffer.edit(
261 [(MultiBufferOffset(0)..MultiBufferOffset(1), "a")],
262 None,
263 cx,
264 );
265 buffer.edit(
266 [(MultiBufferOffset(1)..MultiBufferOffset(1), "b")],
267 None,
268 cx,
269 );
270 buffer.end_transaction_at(now, cx);
271 });
272
273 assert_eq!(editor.text(cx), "ab2cde6");
274 assert_eq!(
275 editor.selections.ranges(&editor.display_snapshot(cx)),
276 vec![MultiBufferOffset(3)..MultiBufferOffset(3)]
277 );
278
279 // Last transaction happened past the group interval in a different editor.
280 // Undo it individually and don't restore selections.
281 editor.undo(&Undo, window, cx);
282 assert_eq!(editor.text(cx), "12cde6");
283 assert_eq!(
284 editor.selections.ranges(&editor.display_snapshot(cx)),
285 vec![MultiBufferOffset(2)..MultiBufferOffset(2)]
286 );
287
288 // First two transactions happened within the group interval in this editor.
289 // Undo them together and restore selections.
290 editor.undo(&Undo, window, cx);
291 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
292 assert_eq!(editor.text(cx), "123456");
293 assert_eq!(
294 editor.selections.ranges(&editor.display_snapshot(cx)),
295 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
296 );
297
298 // Redo the first two transactions together.
299 editor.redo(&Redo, window, cx);
300 assert_eq!(editor.text(cx), "12cde6");
301 assert_eq!(
302 editor.selections.ranges(&editor.display_snapshot(cx)),
303 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
304 );
305
306 // Redo the last transaction on its own.
307 editor.redo(&Redo, window, cx);
308 assert_eq!(editor.text(cx), "ab2cde6");
309 assert_eq!(
310 editor.selections.ranges(&editor.display_snapshot(cx)),
311 vec![MultiBufferOffset(6)..MultiBufferOffset(6)]
312 );
313
314 // Test empty transactions.
315 editor.start_transaction_at(now, window, cx);
316 editor.end_transaction_at(now, cx);
317 editor.undo(&Undo, window, cx);
318 assert_eq!(editor.text(cx), "12cde6");
319 });
320}
321
322#[gpui::test]
323fn test_ime_composition(cx: &mut TestAppContext) {
324 init_test(cx, |_| {});
325
326 let buffer = cx.new(|cx| {
327 let mut buffer = language::Buffer::local("abcde", cx);
328 // Ensure automatic grouping doesn't occur.
329 buffer.set_group_interval(Duration::ZERO);
330 buffer
331 });
332
333 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
334 cx.add_window(|window, cx| {
335 let mut editor = build_editor(buffer.clone(), window, cx);
336
337 // Start a new IME composition.
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 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
341 assert_eq!(editor.text(cx), "äbcde");
342 assert_eq!(
343 editor.marked_text_ranges(cx),
344 Some(vec![
345 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
346 ])
347 );
348
349 // Finalize IME composition.
350 editor.replace_text_in_range(None, "ā", window, cx);
351 assert_eq!(editor.text(cx), "ābcde");
352 assert_eq!(editor.marked_text_ranges(cx), None);
353
354 // IME composition edits are grouped and are undone/redone at once.
355 editor.undo(&Default::default(), window, cx);
356 assert_eq!(editor.text(cx), "abcde");
357 assert_eq!(editor.marked_text_ranges(cx), None);
358 editor.redo(&Default::default(), window, cx);
359 assert_eq!(editor.text(cx), "ābcde");
360 assert_eq!(editor.marked_text_ranges(cx), None);
361
362 // Start a new IME composition.
363 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
364 assert_eq!(
365 editor.marked_text_ranges(cx),
366 Some(vec![
367 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
368 ])
369 );
370
371 // Undoing during an IME composition cancels it.
372 editor.undo(&Default::default(), window, cx);
373 assert_eq!(editor.text(cx), "ābcde");
374 assert_eq!(editor.marked_text_ranges(cx), None);
375
376 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
377 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
378 assert_eq!(editor.text(cx), "ābcdè");
379 assert_eq!(
380 editor.marked_text_ranges(cx),
381 Some(vec![
382 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(5))
383 ])
384 );
385
386 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
387 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
388 assert_eq!(editor.text(cx), "ābcdę");
389 assert_eq!(editor.marked_text_ranges(cx), None);
390
391 // Start a new IME composition with multiple cursors.
392 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
393 s.select_ranges([
394 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(1)),
395 MultiBufferOffsetUtf16(OffsetUtf16(3))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
396 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(5)),
397 ])
398 });
399 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
400 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
401 assert_eq!(
402 editor.marked_text_ranges(cx),
403 Some(vec![
404 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
405 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(7)),
406 MultiBufferOffsetUtf16(OffsetUtf16(8))..MultiBufferOffsetUtf16(OffsetUtf16(11))
407 ])
408 );
409
410 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
411 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
412 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
413 assert_eq!(
414 editor.marked_text_ranges(cx),
415 Some(vec![
416 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(2)),
417 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(6)),
418 MultiBufferOffsetUtf16(OffsetUtf16(9))..MultiBufferOffsetUtf16(OffsetUtf16(10))
419 ])
420 );
421
422 // Finalize IME composition with multiple cursors.
423 editor.replace_text_in_range(Some(9..10), "2", window, cx);
424 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
425 assert_eq!(editor.marked_text_ranges(cx), None);
426
427 editor
428 });
429}
430
431#[gpui::test]
432fn test_selection_with_mouse(cx: &mut TestAppContext) {
433 init_test(cx, |_| {});
434
435 let editor = cx.add_window(|window, cx| {
436 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
437 build_editor(buffer, window, cx)
438 });
439
440 _ = editor.update(cx, |editor, window, cx| {
441 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
442 });
443 assert_eq!(
444 editor
445 .update(cx, |editor, _, cx| display_ranges(editor, cx))
446 .unwrap(),
447 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
448 );
449
450 _ = editor.update(cx, |editor, window, cx| {
451 editor.update_selection(
452 DisplayPoint::new(DisplayRow(3), 3),
453 0,
454 gpui::Point::<f32>::default(),
455 window,
456 cx,
457 );
458 });
459
460 assert_eq!(
461 editor
462 .update(cx, |editor, _, cx| display_ranges(editor, cx))
463 .unwrap(),
464 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
465 );
466
467 _ = editor.update(cx, |editor, window, cx| {
468 editor.update_selection(
469 DisplayPoint::new(DisplayRow(1), 1),
470 0,
471 gpui::Point::<f32>::default(),
472 window,
473 cx,
474 );
475 });
476
477 assert_eq!(
478 editor
479 .update(cx, |editor, _, cx| display_ranges(editor, cx))
480 .unwrap(),
481 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
482 );
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.end_selection(window, cx);
486 editor.update_selection(
487 DisplayPoint::new(DisplayRow(3), 3),
488 0,
489 gpui::Point::<f32>::default(),
490 window,
491 cx,
492 );
493 });
494
495 assert_eq!(
496 editor
497 .update(cx, |editor, _, cx| display_ranges(editor, cx))
498 .unwrap(),
499 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
500 );
501
502 _ = editor.update(cx, |editor, window, cx| {
503 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
504 editor.update_selection(
505 DisplayPoint::new(DisplayRow(0), 0),
506 0,
507 gpui::Point::<f32>::default(),
508 window,
509 cx,
510 );
511 });
512
513 assert_eq!(
514 editor
515 .update(cx, |editor, _, cx| display_ranges(editor, cx))
516 .unwrap(),
517 [
518 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
519 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
520 ]
521 );
522
523 _ = editor.update(cx, |editor, window, cx| {
524 editor.end_selection(window, cx);
525 });
526
527 assert_eq!(
528 editor
529 .update(cx, |editor, _, cx| display_ranges(editor, cx))
530 .unwrap(),
531 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
532 );
533}
534
535#[gpui::test]
536fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
537 init_test(cx, |_| {});
538
539 let editor = cx.add_window(|window, cx| {
540 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
541 build_editor(buffer, window, cx)
542 });
543
544 _ = editor.update(cx, |editor, window, cx| {
545 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.end_selection(window, cx);
550 });
551
552 _ = editor.update(cx, |editor, window, cx| {
553 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
554 });
555
556 _ = editor.update(cx, |editor, window, cx| {
557 editor.end_selection(window, cx);
558 });
559
560 assert_eq!(
561 editor
562 .update(cx, |editor, _, cx| display_ranges(editor, cx))
563 .unwrap(),
564 [
565 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
566 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
567 ]
568 );
569
570 _ = editor.update(cx, |editor, window, cx| {
571 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
572 });
573
574 _ = editor.update(cx, |editor, window, cx| {
575 editor.end_selection(window, cx);
576 });
577
578 assert_eq!(
579 editor
580 .update(cx, |editor, _, cx| display_ranges(editor, cx))
581 .unwrap(),
582 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
583 );
584}
585
586#[gpui::test]
587fn test_canceling_pending_selection(cx: &mut TestAppContext) {
588 init_test(cx, |_| {});
589
590 let editor = cx.add_window(|window, cx| {
591 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
592 build_editor(buffer, window, cx)
593 });
594
595 _ = editor.update(cx, |editor, window, cx| {
596 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
597 assert_eq!(
598 display_ranges(editor, cx),
599 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
600 );
601 });
602
603 _ = editor.update(cx, |editor, window, cx| {
604 editor.update_selection(
605 DisplayPoint::new(DisplayRow(3), 3),
606 0,
607 gpui::Point::<f32>::default(),
608 window,
609 cx,
610 );
611 assert_eq!(
612 display_ranges(editor, cx),
613 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
614 );
615 });
616
617 _ = editor.update(cx, |editor, window, cx| {
618 editor.cancel(&Cancel, window, cx);
619 editor.update_selection(
620 DisplayPoint::new(DisplayRow(1), 1),
621 0,
622 gpui::Point::<f32>::default(),
623 window,
624 cx,
625 );
626 assert_eq!(
627 display_ranges(editor, cx),
628 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
629 );
630 });
631}
632
633#[gpui::test]
634fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
635 init_test(cx, |_| {});
636
637 let editor = cx.add_window(|window, cx| {
638 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
639 build_editor(buffer, window, cx)
640 });
641
642 _ = editor.update(cx, |editor, window, cx| {
643 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
644 assert_eq!(
645 display_ranges(editor, cx),
646 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
647 );
648
649 editor.move_down(&Default::default(), window, cx);
650 assert_eq!(
651 display_ranges(editor, cx),
652 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
653 );
654
655 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
656 assert_eq!(
657 display_ranges(editor, cx),
658 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
659 );
660
661 editor.move_up(&Default::default(), window, cx);
662 assert_eq!(
663 display_ranges(editor, cx),
664 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
665 );
666 });
667}
668
669#[gpui::test]
670fn test_extending_selection(cx: &mut TestAppContext) {
671 init_test(cx, |_| {});
672
673 let editor = cx.add_window(|window, cx| {
674 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
675 build_editor(buffer, window, cx)
676 });
677
678 _ = editor.update(cx, |editor, window, cx| {
679 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
680 editor.end_selection(window, cx);
681 assert_eq!(
682 display_ranges(editor, cx),
683 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
684 );
685
686 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
687 editor.end_selection(window, cx);
688 assert_eq!(
689 display_ranges(editor, cx),
690 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
691 );
692
693 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
694 editor.end_selection(window, cx);
695 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
696 assert_eq!(
697 display_ranges(editor, cx),
698 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
699 );
700
701 editor.update_selection(
702 DisplayPoint::new(DisplayRow(0), 1),
703 0,
704 gpui::Point::<f32>::default(),
705 window,
706 cx,
707 );
708 editor.end_selection(window, cx);
709 assert_eq!(
710 display_ranges(editor, cx),
711 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
712 );
713
714 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
715 editor.end_selection(window, cx);
716 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
717 editor.end_selection(window, cx);
718 assert_eq!(
719 display_ranges(editor, cx),
720 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
721 );
722
723 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
724 assert_eq!(
725 display_ranges(editor, cx),
726 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
727 );
728
729 editor.update_selection(
730 DisplayPoint::new(DisplayRow(0), 6),
731 0,
732 gpui::Point::<f32>::default(),
733 window,
734 cx,
735 );
736 assert_eq!(
737 display_ranges(editor, cx),
738 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
739 );
740
741 editor.update_selection(
742 DisplayPoint::new(DisplayRow(0), 1),
743 0,
744 gpui::Point::<f32>::default(),
745 window,
746 cx,
747 );
748 editor.end_selection(window, cx);
749 assert_eq!(
750 display_ranges(editor, cx),
751 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
752 );
753 });
754}
755
756#[gpui::test]
757fn test_clone(cx: &mut TestAppContext) {
758 init_test(cx, |_| {});
759
760 let (text, selection_ranges) = marked_text_ranges(
761 indoc! {"
762 one
763 two
764 threeˇ
765 four
766 fiveˇ
767 "},
768 true,
769 );
770
771 let editor = cx.add_window(|window, cx| {
772 let buffer = MultiBuffer::build_simple(&text, cx);
773 build_editor(buffer, window, cx)
774 });
775
776 _ = editor.update(cx, |editor, window, cx| {
777 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
778 s.select_ranges(
779 selection_ranges
780 .iter()
781 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
782 )
783 });
784 editor.fold_creases(
785 vec![
786 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
787 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
788 ],
789 true,
790 window,
791 cx,
792 );
793 });
794
795 let cloned_editor = editor
796 .update(cx, |editor, _, cx| {
797 cx.open_window(Default::default(), |window, cx| {
798 cx.new(|cx| editor.clone(window, cx))
799 })
800 })
801 .unwrap()
802 .unwrap();
803
804 let snapshot = editor
805 .update(cx, |e, window, cx| e.snapshot(window, cx))
806 .unwrap();
807 let cloned_snapshot = cloned_editor
808 .update(cx, |e, window, cx| e.snapshot(window, cx))
809 .unwrap();
810
811 assert_eq!(
812 cloned_editor
813 .update(cx, |e, _, cx| e.display_text(cx))
814 .unwrap(),
815 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
816 );
817 assert_eq!(
818 cloned_snapshot
819 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
820 .collect::<Vec<_>>(),
821 snapshot
822 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
823 .collect::<Vec<_>>(),
824 );
825 assert_set_eq!(
826 cloned_editor
827 .update(cx, |editor, _, cx| editor
828 .selections
829 .ranges::<Point>(&editor.display_snapshot(cx)))
830 .unwrap(),
831 editor
832 .update(cx, |editor, _, cx| editor
833 .selections
834 .ranges(&editor.display_snapshot(cx)))
835 .unwrap()
836 );
837 assert_set_eq!(
838 cloned_editor
839 .update(cx, |e, _window, cx| e
840 .selections
841 .display_ranges(&e.display_snapshot(cx)))
842 .unwrap(),
843 editor
844 .update(cx, |e, _, cx| e
845 .selections
846 .display_ranges(&e.display_snapshot(cx)))
847 .unwrap()
848 );
849}
850
851#[gpui::test]
852async fn test_navigation_history(cx: &mut TestAppContext) {
853 init_test(cx, |_| {});
854
855 use workspace::item::Item;
856
857 let fs = FakeFs::new(cx.executor());
858 let project = Project::test(fs, [], cx).await;
859 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
860 let workspace = window
861 .read_with(cx, |mw, _| mw.workspace().clone())
862 .unwrap();
863 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
864
865 _ = window.update(cx, |_mw, window, cx| {
866 cx.new(|cx| {
867 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
868 let mut editor = build_editor(buffer, window, cx);
869 let handle = cx.entity();
870 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
871
872 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
873 editor.nav_history.as_mut().unwrap().pop_backward(cx)
874 }
875
876 // Move the cursor a small distance.
877 // Nothing is added to the navigation history.
878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
879 s.select_display_ranges([
880 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
881 ])
882 });
883 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
884 s.select_display_ranges([
885 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
886 ])
887 });
888 assert!(pop_history(&mut editor, cx).is_none());
889
890 // Move the cursor a large distance.
891 // The history can jump back to the previous position.
892 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
893 s.select_display_ranges([
894 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
895 ])
896 });
897 let nav_entry = pop_history(&mut editor, cx).unwrap();
898 editor.navigate(nav_entry.data.unwrap(), window, cx);
899 assert_eq!(nav_entry.item.id(), cx.entity_id());
900 assert_eq!(
901 editor
902 .selections
903 .display_ranges(&editor.display_snapshot(cx)),
904 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
905 );
906 assert!(pop_history(&mut editor, cx).is_none());
907
908 // Move the cursor a small distance via the mouse.
909 // Nothing is added to the navigation history.
910 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
911 editor.end_selection(window, cx);
912 assert_eq!(
913 editor
914 .selections
915 .display_ranges(&editor.display_snapshot(cx)),
916 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
917 );
918 assert!(pop_history(&mut editor, cx).is_none());
919
920 // Move the cursor a large distance via the mouse.
921 // The history can jump back to the previous position.
922 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
923 editor.end_selection(window, cx);
924 assert_eq!(
925 editor
926 .selections
927 .display_ranges(&editor.display_snapshot(cx)),
928 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
929 );
930 let nav_entry = pop_history(&mut editor, cx).unwrap();
931 editor.navigate(nav_entry.data.unwrap(), window, cx);
932 assert_eq!(nav_entry.item.id(), cx.entity_id());
933 assert_eq!(
934 editor
935 .selections
936 .display_ranges(&editor.display_snapshot(cx)),
937 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
938 );
939 assert!(pop_history(&mut editor, cx).is_none());
940
941 // Set scroll position to check later
942 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
943 let original_scroll_position = editor
944 .scroll_manager
945 .native_anchor(&editor.display_snapshot(cx), cx);
946
947 // Jump to the end of the document and adjust scroll
948 editor.move_to_end(&MoveToEnd, window, cx);
949 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
950 assert_ne!(
951 editor
952 .scroll_manager
953 .native_anchor(&editor.display_snapshot(cx), cx),
954 original_scroll_position
955 );
956
957 let nav_entry = pop_history(&mut editor, cx).unwrap();
958 editor.navigate(nav_entry.data.unwrap(), window, cx);
959 assert_eq!(
960 editor
961 .scroll_manager
962 .native_anchor(&editor.display_snapshot(cx), cx),
963 original_scroll_position
964 );
965
966 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
967 let mut invalid_anchor = editor
968 .scroll_manager
969 .native_anchor(&editor.display_snapshot(cx), cx)
970 .anchor;
971 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
972 let invalid_point = Point::new(9999, 0);
973 editor.navigate(
974 Arc::new(NavigationData {
975 cursor_anchor: invalid_anchor,
976 cursor_position: invalid_point,
977 scroll_anchor: ScrollAnchor {
978 anchor: invalid_anchor,
979 offset: Default::default(),
980 },
981 scroll_top_row: invalid_point.row,
982 }),
983 window,
984 cx,
985 );
986 assert_eq!(
987 editor
988 .selections
989 .display_ranges(&editor.display_snapshot(cx)),
990 &[editor.max_point(cx)..editor.max_point(cx)]
991 );
992 assert_eq!(
993 editor.scroll_position(cx),
994 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
995 );
996
997 editor
998 })
999 });
1000}
1001
1002#[gpui::test]
1003fn test_cancel(cx: &mut TestAppContext) {
1004 init_test(cx, |_| {});
1005
1006 let editor = cx.add_window(|window, cx| {
1007 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
1008 build_editor(buffer, window, cx)
1009 });
1010
1011 _ = editor.update(cx, |editor, window, cx| {
1012 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
1013 editor.update_selection(
1014 DisplayPoint::new(DisplayRow(1), 1),
1015 0,
1016 gpui::Point::<f32>::default(),
1017 window,
1018 cx,
1019 );
1020 editor.end_selection(window, cx);
1021
1022 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
1023 editor.update_selection(
1024 DisplayPoint::new(DisplayRow(0), 3),
1025 0,
1026 gpui::Point::<f32>::default(),
1027 window,
1028 cx,
1029 );
1030 editor.end_selection(window, cx);
1031 assert_eq!(
1032 display_ranges(editor, cx),
1033 [
1034 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
1035 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
1036 ]
1037 );
1038 });
1039
1040 _ = editor.update(cx, |editor, window, cx| {
1041 editor.cancel(&Cancel, window, cx);
1042 assert_eq!(
1043 display_ranges(editor, cx),
1044 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1045 );
1046 });
1047
1048 _ = editor.update(cx, |editor, window, cx| {
1049 editor.cancel(&Cancel, window, cx);
1050 assert_eq!(
1051 display_ranges(editor, cx),
1052 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1053 );
1054 });
1055}
1056
1057#[gpui::test]
1058fn test_fold_action(cx: &mut TestAppContext) {
1059 init_test(cx, |_| {});
1060
1061 let editor = cx.add_window(|window, cx| {
1062 let buffer = MultiBuffer::build_simple(
1063 &"
1064 impl Foo {
1065 // Hello!
1066
1067 fn a() {
1068 1
1069 }
1070
1071 fn b() {
1072 2
1073 }
1074
1075 fn c() {
1076 3
1077 }
1078 }
1079 "
1080 .unindent(),
1081 cx,
1082 );
1083 build_editor(buffer, window, cx)
1084 });
1085
1086 _ = editor.update(cx, |editor, window, cx| {
1087 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1088 s.select_display_ranges([
1089 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1090 ]);
1091 });
1092 editor.fold(&Fold, window, cx);
1093 assert_eq!(
1094 editor.display_text(cx),
1095 "
1096 impl Foo {
1097 // Hello!
1098
1099 fn a() {
1100 1
1101 }
1102
1103 fn b() {⋯
1104 }
1105
1106 fn c() {⋯
1107 }
1108 }
1109 "
1110 .unindent(),
1111 );
1112
1113 editor.fold(&Fold, window, cx);
1114 assert_eq!(
1115 editor.display_text(cx),
1116 "
1117 impl Foo {⋯
1118 }
1119 "
1120 .unindent(),
1121 );
1122
1123 editor.unfold_lines(&UnfoldLines, window, cx);
1124 assert_eq!(
1125 editor.display_text(cx),
1126 "
1127 impl Foo {
1128 // Hello!
1129
1130 fn a() {
1131 1
1132 }
1133
1134 fn b() {⋯
1135 }
1136
1137 fn c() {⋯
1138 }
1139 }
1140 "
1141 .unindent(),
1142 );
1143
1144 editor.unfold_lines(&UnfoldLines, window, cx);
1145 assert_eq!(
1146 editor.display_text(cx),
1147 editor.buffer.read(cx).read(cx).text()
1148 );
1149 });
1150}
1151
1152#[gpui::test]
1153fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1154 init_test(cx, |_| {});
1155
1156 let editor = cx.add_window(|window, cx| {
1157 let buffer = MultiBuffer::build_simple(
1158 &"
1159 class Foo:
1160 # Hello!
1161
1162 def a():
1163 print(1)
1164
1165 def b():
1166 print(2)
1167
1168 def c():
1169 print(3)
1170 "
1171 .unindent(),
1172 cx,
1173 );
1174 build_editor(buffer, window, cx)
1175 });
1176
1177 _ = editor.update(cx, |editor, window, cx| {
1178 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1179 s.select_display_ranges([
1180 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1181 ]);
1182 });
1183 editor.fold(&Fold, window, cx);
1184 assert_eq!(
1185 editor.display_text(cx),
1186 "
1187 class Foo:
1188 # Hello!
1189
1190 def a():
1191 print(1)
1192
1193 def b():⋯
1194
1195 def c():⋯
1196 "
1197 .unindent(),
1198 );
1199
1200 editor.fold(&Fold, window, cx);
1201 assert_eq!(
1202 editor.display_text(cx),
1203 "
1204 class Foo:⋯
1205 "
1206 .unindent(),
1207 );
1208
1209 editor.unfold_lines(&UnfoldLines, window, cx);
1210 assert_eq!(
1211 editor.display_text(cx),
1212 "
1213 class Foo:
1214 # Hello!
1215
1216 def a():
1217 print(1)
1218
1219 def b():⋯
1220
1221 def c():⋯
1222 "
1223 .unindent(),
1224 );
1225
1226 editor.unfold_lines(&UnfoldLines, window, cx);
1227 assert_eq!(
1228 editor.display_text(cx),
1229 editor.buffer.read(cx).read(cx).text()
1230 );
1231 });
1232}
1233
1234#[gpui::test]
1235fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1236 init_test(cx, |_| {});
1237
1238 let editor = cx.add_window(|window, cx| {
1239 let buffer = MultiBuffer::build_simple(
1240 &"
1241 class Foo:
1242 # Hello!
1243
1244 def a():
1245 print(1)
1246
1247 def b():
1248 print(2)
1249
1250
1251 def c():
1252 print(3)
1253
1254
1255 "
1256 .unindent(),
1257 cx,
1258 );
1259 build_editor(buffer, window, cx)
1260 });
1261
1262 _ = editor.update(cx, |editor, window, cx| {
1263 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1264 s.select_display_ranges([
1265 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1266 ]);
1267 });
1268 editor.fold(&Fold, window, cx);
1269 assert_eq!(
1270 editor.display_text(cx),
1271 "
1272 class Foo:
1273 # Hello!
1274
1275 def a():
1276 print(1)
1277
1278 def b():⋯
1279
1280
1281 def c():⋯
1282
1283
1284 "
1285 .unindent(),
1286 );
1287
1288 editor.fold(&Fold, window, cx);
1289 assert_eq!(
1290 editor.display_text(cx),
1291 "
1292 class Foo:⋯
1293
1294
1295 "
1296 .unindent(),
1297 );
1298
1299 editor.unfold_lines(&UnfoldLines, window, cx);
1300 assert_eq!(
1301 editor.display_text(cx),
1302 "
1303 class Foo:
1304 # Hello!
1305
1306 def a():
1307 print(1)
1308
1309 def b():⋯
1310
1311
1312 def c():⋯
1313
1314
1315 "
1316 .unindent(),
1317 );
1318
1319 editor.unfold_lines(&UnfoldLines, window, cx);
1320 assert_eq!(
1321 editor.display_text(cx),
1322 editor.buffer.read(cx).read(cx).text()
1323 );
1324 });
1325}
1326
1327#[gpui::test]
1328fn test_fold_at_level(cx: &mut TestAppContext) {
1329 init_test(cx, |_| {});
1330
1331 let editor = cx.add_window(|window, cx| {
1332 let buffer = MultiBuffer::build_simple(
1333 &"
1334 class Foo:
1335 # Hello!
1336
1337 def a():
1338 print(1)
1339
1340 def b():
1341 print(2)
1342
1343
1344 class Bar:
1345 # World!
1346
1347 def a():
1348 print(1)
1349
1350 def b():
1351 print(2)
1352
1353
1354 "
1355 .unindent(),
1356 cx,
1357 );
1358 build_editor(buffer, window, cx)
1359 });
1360
1361 _ = editor.update(cx, |editor, window, cx| {
1362 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1363 assert_eq!(
1364 editor.display_text(cx),
1365 "
1366 class Foo:
1367 # Hello!
1368
1369 def a():⋯
1370
1371 def b():⋯
1372
1373
1374 class Bar:
1375 # World!
1376
1377 def a():⋯
1378
1379 def b():⋯
1380
1381
1382 "
1383 .unindent(),
1384 );
1385
1386 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1387 assert_eq!(
1388 editor.display_text(cx),
1389 "
1390 class Foo:⋯
1391
1392
1393 class Bar:⋯
1394
1395
1396 "
1397 .unindent(),
1398 );
1399
1400 editor.unfold_all(&UnfoldAll, window, cx);
1401 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1402 assert_eq!(
1403 editor.display_text(cx),
1404 "
1405 class Foo:
1406 # Hello!
1407
1408 def a():
1409 print(1)
1410
1411 def b():
1412 print(2)
1413
1414
1415 class Bar:
1416 # World!
1417
1418 def a():
1419 print(1)
1420
1421 def b():
1422 print(2)
1423
1424
1425 "
1426 .unindent(),
1427 );
1428
1429 assert_eq!(
1430 editor.display_text(cx),
1431 editor.buffer.read(cx).read(cx).text()
1432 );
1433 let (_, positions) = marked_text_ranges(
1434 &"
1435 class Foo:
1436 # Hello!
1437
1438 def a():
1439 print(1)
1440
1441 def b():
1442 p«riˇ»nt(2)
1443
1444
1445 class Bar:
1446 # World!
1447
1448 def a():
1449 «ˇprint(1)
1450
1451 def b():
1452 print(2)»
1453
1454
1455 "
1456 .unindent(),
1457 true,
1458 );
1459
1460 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1461 s.select_ranges(
1462 positions
1463 .iter()
1464 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
1465 )
1466 });
1467
1468 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1469 assert_eq!(
1470 editor.display_text(cx),
1471 "
1472 class Foo:
1473 # Hello!
1474
1475 def a():⋯
1476
1477 def b():
1478 print(2)
1479
1480
1481 class Bar:
1482 # World!
1483
1484 def a():
1485 print(1)
1486
1487 def b():
1488 print(2)
1489
1490
1491 "
1492 .unindent(),
1493 );
1494 });
1495}
1496
1497#[gpui::test]
1498fn test_move_cursor(cx: &mut TestAppContext) {
1499 init_test(cx, |_| {});
1500
1501 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1502 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1503
1504 buffer.update(cx, |buffer, cx| {
1505 buffer.edit(
1506 vec![
1507 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1508 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1509 ],
1510 None,
1511 cx,
1512 );
1513 });
1514 _ = editor.update(cx, |editor, window, cx| {
1515 assert_eq!(
1516 display_ranges(editor, cx),
1517 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1518 );
1519
1520 editor.move_down(&MoveDown, window, cx);
1521 assert_eq!(
1522 display_ranges(editor, cx),
1523 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1524 );
1525
1526 editor.move_right(&MoveRight, window, cx);
1527 assert_eq!(
1528 display_ranges(editor, cx),
1529 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1530 );
1531
1532 editor.move_left(&MoveLeft, window, cx);
1533 assert_eq!(
1534 display_ranges(editor, cx),
1535 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1536 );
1537
1538 editor.move_up(&MoveUp, window, cx);
1539 assert_eq!(
1540 display_ranges(editor, cx),
1541 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1542 );
1543
1544 editor.move_to_end(&MoveToEnd, window, cx);
1545 assert_eq!(
1546 display_ranges(editor, cx),
1547 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1548 );
1549
1550 editor.move_to_beginning(&MoveToBeginning, window, cx);
1551 assert_eq!(
1552 display_ranges(editor, cx),
1553 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1554 );
1555
1556 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1557 s.select_display_ranges([
1558 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1559 ]);
1560 });
1561 editor.select_to_beginning(&SelectToBeginning, window, cx);
1562 assert_eq!(
1563 display_ranges(editor, cx),
1564 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1565 );
1566
1567 editor.select_to_end(&SelectToEnd, window, cx);
1568 assert_eq!(
1569 display_ranges(editor, cx),
1570 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1571 );
1572 });
1573}
1574
1575#[gpui::test]
1576fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1577 init_test(cx, |_| {});
1578
1579 let editor = cx.add_window(|window, cx| {
1580 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1581 build_editor(buffer, window, cx)
1582 });
1583
1584 assert_eq!('🟥'.len_utf8(), 4);
1585 assert_eq!('α'.len_utf8(), 2);
1586
1587 _ = editor.update(cx, |editor, window, cx| {
1588 editor.fold_creases(
1589 vec![
1590 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1591 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1592 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1593 ],
1594 true,
1595 window,
1596 cx,
1597 );
1598 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1599
1600 editor.move_right(&MoveRight, window, cx);
1601 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1602 editor.move_right(&MoveRight, window, cx);
1603 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1604 editor.move_right(&MoveRight, window, cx);
1605 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1606
1607 editor.move_down(&MoveDown, window, cx);
1608 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1609 editor.move_left(&MoveLeft, window, cx);
1610 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".len())]);
1611 editor.move_left(&MoveLeft, window, cx);
1612 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab".len())]);
1613 editor.move_left(&MoveLeft, window, cx);
1614 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1615
1616 editor.move_down(&MoveDown, window, cx);
1617 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1618 editor.move_right(&MoveRight, window, cx);
1619 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ".len())]);
1620 editor.move_right(&MoveRight, window, cx);
1621 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯".len())]);
1622 editor.move_right(&MoveRight, window, cx);
1623 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1624
1625 editor.move_up(&MoveUp, window, cx);
1626 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1627 editor.move_down(&MoveDown, window, cx);
1628 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1629 editor.move_up(&MoveUp, window, cx);
1630 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1631
1632 editor.move_up(&MoveUp, window, cx);
1633 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1634 editor.move_left(&MoveLeft, window, cx);
1635 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1636 editor.move_left(&MoveLeft, window, cx);
1637 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1638 });
1639}
1640
1641#[gpui::test]
1642fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1643 init_test(cx, |_| {});
1644
1645 let editor = cx.add_window(|window, cx| {
1646 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1647 build_editor(buffer, window, cx)
1648 });
1649 _ = editor.update(cx, |editor, window, cx| {
1650 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1651 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1652 });
1653
1654 // moving above start of document should move selection to start of document,
1655 // but the next move down should still be at the original goal_x
1656 editor.move_up(&MoveUp, window, cx);
1657 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1658
1659 editor.move_down(&MoveDown, window, cx);
1660 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1661
1662 editor.move_down(&MoveDown, window, cx);
1663 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1664
1665 editor.move_down(&MoveDown, window, cx);
1666 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1667
1668 editor.move_down(&MoveDown, window, cx);
1669 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1670
1671 // moving past end of document should not change goal_x
1672 editor.move_down(&MoveDown, window, cx);
1673 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1674
1675 editor.move_down(&MoveDown, window, cx);
1676 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1677
1678 editor.move_up(&MoveUp, window, cx);
1679 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1680
1681 editor.move_up(&MoveUp, window, cx);
1682 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1683
1684 editor.move_up(&MoveUp, window, cx);
1685 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1686 });
1687}
1688
1689#[gpui::test]
1690fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1691 init_test(cx, |_| {});
1692 let move_to_beg = MoveToBeginningOfLine {
1693 stop_at_soft_wraps: true,
1694 stop_at_indent: true,
1695 };
1696
1697 let delete_to_beg = DeleteToBeginningOfLine {
1698 stop_at_indent: false,
1699 };
1700
1701 let move_to_end = MoveToEndOfLine {
1702 stop_at_soft_wraps: true,
1703 };
1704
1705 let editor = cx.add_window(|window, cx| {
1706 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1707 build_editor(buffer, window, cx)
1708 });
1709 _ = editor.update(cx, |editor, window, cx| {
1710 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1711 s.select_display_ranges([
1712 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1713 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1714 ]);
1715 });
1716 });
1717
1718 _ = editor.update(cx, |editor, window, cx| {
1719 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1720 assert_eq!(
1721 display_ranges(editor, cx),
1722 &[
1723 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1724 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1725 ]
1726 );
1727 });
1728
1729 _ = editor.update(cx, |editor, window, cx| {
1730 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1731 assert_eq!(
1732 display_ranges(editor, cx),
1733 &[
1734 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1735 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1736 ]
1737 );
1738 });
1739
1740 _ = editor.update(cx, |editor, window, cx| {
1741 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1742 assert_eq!(
1743 display_ranges(editor, cx),
1744 &[
1745 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1746 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1747 ]
1748 );
1749 });
1750
1751 _ = editor.update(cx, |editor, window, cx| {
1752 editor.move_to_end_of_line(&move_to_end, window, cx);
1753 assert_eq!(
1754 display_ranges(editor, cx),
1755 &[
1756 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1757 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1758 ]
1759 );
1760 });
1761
1762 // Moving to the end of line again is a no-op.
1763 _ = editor.update(cx, |editor, window, cx| {
1764 editor.move_to_end_of_line(&move_to_end, window, cx);
1765 assert_eq!(
1766 display_ranges(editor, cx),
1767 &[
1768 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1769 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1770 ]
1771 );
1772 });
1773
1774 _ = editor.update(cx, |editor, window, cx| {
1775 editor.move_left(&MoveLeft, window, cx);
1776 editor.select_to_beginning_of_line(
1777 &SelectToBeginningOfLine {
1778 stop_at_soft_wraps: true,
1779 stop_at_indent: true,
1780 },
1781 window,
1782 cx,
1783 );
1784 assert_eq!(
1785 display_ranges(editor, cx),
1786 &[
1787 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1788 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1789 ]
1790 );
1791 });
1792
1793 _ = editor.update(cx, |editor, window, cx| {
1794 editor.select_to_beginning_of_line(
1795 &SelectToBeginningOfLine {
1796 stop_at_soft_wraps: true,
1797 stop_at_indent: true,
1798 },
1799 window,
1800 cx,
1801 );
1802 assert_eq!(
1803 display_ranges(editor, cx),
1804 &[
1805 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1806 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1807 ]
1808 );
1809 });
1810
1811 _ = editor.update(cx, |editor, window, cx| {
1812 editor.select_to_beginning_of_line(
1813 &SelectToBeginningOfLine {
1814 stop_at_soft_wraps: true,
1815 stop_at_indent: 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), 0),
1824 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1825 ]
1826 );
1827 });
1828
1829 _ = editor.update(cx, |editor, window, cx| {
1830 editor.select_to_end_of_line(
1831 &SelectToEndOfLine {
1832 stop_at_soft_wraps: true,
1833 },
1834 window,
1835 cx,
1836 );
1837 assert_eq!(
1838 display_ranges(editor, cx),
1839 &[
1840 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1841 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1842 ]
1843 );
1844 });
1845
1846 _ = editor.update(cx, |editor, window, cx| {
1847 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1848 assert_eq!(editor.display_text(cx), "ab\n de");
1849 assert_eq!(
1850 display_ranges(editor, cx),
1851 &[
1852 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1853 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1854 ]
1855 );
1856 });
1857
1858 _ = editor.update(cx, |editor, window, cx| {
1859 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1860 assert_eq!(editor.display_text(cx), "\n");
1861 assert_eq!(
1862 display_ranges(editor, cx),
1863 &[
1864 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1865 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1866 ]
1867 );
1868 });
1869}
1870
1871#[gpui::test]
1872fn test_beginning_of_line_single_line_editor(cx: &mut TestAppContext) {
1873 init_test(cx, |_| {});
1874
1875 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
1876
1877 _ = editor.update(cx, |editor, window, cx| {
1878 editor.set_text(" indented text", window, cx);
1879 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1880 s.select_display_ranges([
1881 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 10)
1882 ]);
1883 });
1884
1885 editor.move_to_beginning_of_line(
1886 &MoveToBeginningOfLine {
1887 stop_at_soft_wraps: true,
1888 stop_at_indent: true,
1889 },
1890 window,
1891 cx,
1892 );
1893 assert_eq!(
1894 display_ranges(editor, cx),
1895 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1896 );
1897 });
1898
1899 _ = editor.update(cx, |editor, window, cx| {
1900 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1901 s.select_display_ranges([
1902 DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 10)
1903 ]);
1904 });
1905
1906 editor.select_to_beginning_of_line(
1907 &SelectToBeginningOfLine {
1908 stop_at_soft_wraps: true,
1909 stop_at_indent: true,
1910 },
1911 window,
1912 cx,
1913 );
1914 assert_eq!(
1915 display_ranges(editor, cx),
1916 &[DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 0)]
1917 );
1918 });
1919}
1920
1921#[gpui::test]
1922fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1923 init_test(cx, |_| {});
1924 let move_to_beg = MoveToBeginningOfLine {
1925 stop_at_soft_wraps: false,
1926 stop_at_indent: false,
1927 };
1928
1929 let move_to_end = MoveToEndOfLine {
1930 stop_at_soft_wraps: false,
1931 };
1932
1933 let editor = cx.add_window(|window, cx| {
1934 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1935 build_editor(buffer, window, cx)
1936 });
1937
1938 _ = editor.update(cx, |editor, window, cx| {
1939 editor.set_wrap_width(Some(140.0.into()), cx);
1940
1941 // We expect the following lines after wrapping
1942 // ```
1943 // thequickbrownfox
1944 // jumpedoverthelazydo
1945 // gs
1946 // ```
1947 // The final `gs` was soft-wrapped onto a new line.
1948 assert_eq!(
1949 "thequickbrownfox\njumpedoverthelaz\nydogs",
1950 editor.display_text(cx),
1951 );
1952
1953 // First, let's assert behavior on the first line, that was not soft-wrapped.
1954 // Start the cursor at the `k` on the first line
1955 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1956 s.select_display_ranges([
1957 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1958 ]);
1959 });
1960
1961 // Moving to the beginning of the line should put us at the beginning of the line.
1962 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1963 assert_eq!(
1964 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1965 display_ranges(editor, cx)
1966 );
1967
1968 // Moving to the end of the line should put us at the end of the line.
1969 editor.move_to_end_of_line(&move_to_end, window, cx);
1970 assert_eq!(
1971 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1972 display_ranges(editor, cx)
1973 );
1974
1975 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1976 // Start the cursor at the last line (`y` that was wrapped to a new line)
1977 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1978 s.select_display_ranges([
1979 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1980 ]);
1981 });
1982
1983 // Moving to the beginning of the line should put us at the start of the second line of
1984 // display text, i.e., the `j`.
1985 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1986 assert_eq!(
1987 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1988 display_ranges(editor, cx)
1989 );
1990
1991 // Moving to the beginning of the line again should be a no-op.
1992 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1993 assert_eq!(
1994 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1995 display_ranges(editor, cx)
1996 );
1997
1998 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1999 // next display line.
2000 editor.move_to_end_of_line(&move_to_end, window, cx);
2001 assert_eq!(
2002 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
2003 display_ranges(editor, cx)
2004 );
2005
2006 // Moving to the end of the line again should be a no-op.
2007 editor.move_to_end_of_line(&move_to_end, window, cx);
2008 assert_eq!(
2009 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
2010 display_ranges(editor, cx)
2011 );
2012 });
2013}
2014
2015#[gpui::test]
2016fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
2017 init_test(cx, |_| {});
2018
2019 let move_to_beg = MoveToBeginningOfLine {
2020 stop_at_soft_wraps: true,
2021 stop_at_indent: true,
2022 };
2023
2024 let select_to_beg = SelectToBeginningOfLine {
2025 stop_at_soft_wraps: true,
2026 stop_at_indent: true,
2027 };
2028
2029 let delete_to_beg = DeleteToBeginningOfLine {
2030 stop_at_indent: true,
2031 };
2032
2033 let move_to_end = MoveToEndOfLine {
2034 stop_at_soft_wraps: false,
2035 };
2036
2037 let editor = cx.add_window(|window, cx| {
2038 let buffer = MultiBuffer::build_simple("abc\n def", cx);
2039 build_editor(buffer, window, cx)
2040 });
2041
2042 _ = editor.update(cx, |editor, window, cx| {
2043 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2044 s.select_display_ranges([
2045 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
2046 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
2047 ]);
2048 });
2049
2050 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
2051 // and the second cursor at the first non-whitespace character in the line.
2052 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2053 assert_eq!(
2054 display_ranges(editor, cx),
2055 &[
2056 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2057 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2058 ]
2059 );
2060
2061 // Moving to the beginning of the line again should be a no-op for the first cursor,
2062 // and should move the second cursor to the beginning of the line.
2063 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2064 assert_eq!(
2065 display_ranges(editor, cx),
2066 &[
2067 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2068 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2069 ]
2070 );
2071
2072 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2073 // and should move the second cursor back to the first non-whitespace character in the line.
2074 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2075 assert_eq!(
2076 display_ranges(editor, cx),
2077 &[
2078 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2079 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2080 ]
2081 );
2082
2083 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2084 // and to the first non-whitespace character in the line for the second cursor.
2085 editor.move_to_end_of_line(&move_to_end, window, cx);
2086 editor.move_left(&MoveLeft, window, cx);
2087 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2088 assert_eq!(
2089 display_ranges(editor, cx),
2090 &[
2091 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2092 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2093 ]
2094 );
2095
2096 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2097 // and should select to the beginning of the line for the second cursor.
2098 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2099 assert_eq!(
2100 display_ranges(editor, cx),
2101 &[
2102 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2103 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2104 ]
2105 );
2106
2107 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2108 // and should delete to the first non-whitespace character in the line for the second cursor.
2109 editor.move_to_end_of_line(&move_to_end, window, cx);
2110 editor.move_left(&MoveLeft, window, cx);
2111 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2112 assert_eq!(editor.text(cx), "c\n f");
2113 });
2114}
2115
2116#[gpui::test]
2117fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2118 init_test(cx, |_| {});
2119
2120 let move_to_beg = MoveToBeginningOfLine {
2121 stop_at_soft_wraps: true,
2122 stop_at_indent: true,
2123 };
2124
2125 let editor = cx.add_window(|window, cx| {
2126 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2127 build_editor(buffer, window, cx)
2128 });
2129
2130 _ = editor.update(cx, |editor, window, cx| {
2131 // test cursor between line_start and indent_start
2132 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2133 s.select_display_ranges([
2134 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2135 ]);
2136 });
2137
2138 // cursor should move to line_start
2139 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2140 assert_eq!(
2141 display_ranges(editor, cx),
2142 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2143 );
2144
2145 // cursor should move to indent_start
2146 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2147 assert_eq!(
2148 display_ranges(editor, cx),
2149 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2150 );
2151
2152 // cursor should move to back to line_start
2153 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2154 assert_eq!(
2155 display_ranges(editor, cx),
2156 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2157 );
2158 });
2159}
2160
2161#[gpui::test]
2162fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2163 init_test(cx, |_| {});
2164
2165 let editor = cx.add_window(|window, cx| {
2166 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2167 build_editor(buffer, window, cx)
2168 });
2169 _ = editor.update(cx, |editor, window, cx| {
2170 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2171 s.select_display_ranges([
2172 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2173 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2174 ])
2175 });
2176 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2177 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2178
2179 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2180 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2181
2182 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2183 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2184
2185 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2186 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2187
2188 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2189 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2190
2191 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2192 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2193
2194 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2195 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2196
2197 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2198 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2199
2200 editor.move_right(&MoveRight, window, cx);
2201 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2202 assert_selection_ranges(
2203 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2204 editor,
2205 cx,
2206 );
2207
2208 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2209 assert_selection_ranges(
2210 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2211 editor,
2212 cx,
2213 );
2214
2215 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2216 assert_selection_ranges(
2217 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2218 editor,
2219 cx,
2220 );
2221 });
2222}
2223
2224#[gpui::test]
2225fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2226 init_test(cx, |_| {});
2227
2228 let editor = cx.add_window(|window, cx| {
2229 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2230 build_editor(buffer, window, cx)
2231 });
2232
2233 _ = editor.update(cx, |editor, window, cx| {
2234 editor.set_wrap_width(Some(140.0.into()), cx);
2235 assert_eq!(
2236 editor.display_text(cx),
2237 "use one::{\n two::three::\n four::five\n};"
2238 );
2239
2240 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2241 s.select_display_ranges([
2242 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2243 ]);
2244 });
2245
2246 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2247 assert_eq!(
2248 display_ranges(editor, cx),
2249 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2250 );
2251
2252 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2253 assert_eq!(
2254 display_ranges(editor, cx),
2255 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2256 );
2257
2258 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2259 assert_eq!(
2260 display_ranges(editor, cx),
2261 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2262 );
2263
2264 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2265 assert_eq!(
2266 display_ranges(editor, cx),
2267 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2268 );
2269
2270 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2271 assert_eq!(
2272 display_ranges(editor, cx),
2273 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2274 );
2275
2276 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2277 assert_eq!(
2278 display_ranges(editor, cx),
2279 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2280 );
2281 });
2282}
2283
2284#[gpui::test]
2285async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2286 init_test(cx, |_| {});
2287 let mut cx = EditorTestContext::new(cx).await;
2288
2289 let line_height = cx.update_editor(|editor, window, cx| {
2290 editor
2291 .style(cx)
2292 .text
2293 .line_height_in_pixels(window.rem_size())
2294 });
2295 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2296
2297 // The third line only contains a single space so we can later assert that the
2298 // editor's paragraph movement considers a non-blank line as a paragraph
2299 // boundary.
2300 cx.set_state(&"ˇone\ntwo\n \nthree\nfourˇ\nfive\n\nsix");
2301
2302 cx.update_editor(|editor, window, cx| {
2303 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2304 });
2305 cx.assert_editor_state(&"one\ntwo\nˇ \nthree\nfour\nfive\nˇ\nsix");
2306
2307 cx.update_editor(|editor, window, cx| {
2308 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2309 });
2310 cx.assert_editor_state(&"one\ntwo\n \nthree\nfour\nfive\nˇ\nsixˇ");
2311
2312 cx.update_editor(|editor, window, cx| {
2313 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2314 });
2315 cx.assert_editor_state(&"one\ntwo\n \nthree\nfour\nfive\n\nsixˇ");
2316
2317 cx.update_editor(|editor, window, cx| {
2318 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2319 });
2320 cx.assert_editor_state(&"one\ntwo\n \nthree\nfour\nfive\nˇ\nsix");
2321
2322 cx.update_editor(|editor, window, cx| {
2323 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2324 });
2325
2326 cx.assert_editor_state(&"one\ntwo\nˇ \nthree\nfour\nfive\n\nsix");
2327
2328 cx.update_editor(|editor, window, cx| {
2329 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2330 });
2331 cx.assert_editor_state(&"ˇone\ntwo\n \nthree\nfour\nfive\n\nsix");
2332}
2333
2334#[gpui::test]
2335async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2336 init_test(cx, |_| {});
2337 let mut cx = EditorTestContext::new(cx).await;
2338 let line_height = cx.update_editor(|editor, window, cx| {
2339 editor
2340 .style(cx)
2341 .text
2342 .line_height_in_pixels(window.rem_size())
2343 });
2344 let window = cx.window;
2345 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2346
2347 cx.set_state(
2348 r#"ˇone
2349 two
2350 three
2351 four
2352 five
2353 six
2354 seven
2355 eight
2356 nine
2357 ten
2358 "#,
2359 );
2360
2361 cx.update_editor(|editor, window, cx| {
2362 assert_eq!(
2363 editor.snapshot(window, cx).scroll_position(),
2364 gpui::Point::new(0., 0.)
2365 );
2366 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2367 assert_eq!(
2368 editor.snapshot(window, cx).scroll_position(),
2369 gpui::Point::new(0., 3.)
2370 );
2371 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2372 assert_eq!(
2373 editor.snapshot(window, cx).scroll_position(),
2374 gpui::Point::new(0., 6.)
2375 );
2376 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2377 assert_eq!(
2378 editor.snapshot(window, cx).scroll_position(),
2379 gpui::Point::new(0., 3.)
2380 );
2381
2382 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2383 assert_eq!(
2384 editor.snapshot(window, cx).scroll_position(),
2385 gpui::Point::new(0., 1.)
2386 );
2387 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2388 assert_eq!(
2389 editor.snapshot(window, cx).scroll_position(),
2390 gpui::Point::new(0., 3.)
2391 );
2392 });
2393}
2394
2395#[gpui::test]
2396async fn test_autoscroll(cx: &mut TestAppContext) {
2397 init_test(cx, |_| {});
2398 let mut cx = EditorTestContext::new(cx).await;
2399
2400 let line_height = cx.update_editor(|editor, window, cx| {
2401 editor.set_vertical_scroll_margin(2, cx);
2402 editor
2403 .style(cx)
2404 .text
2405 .line_height_in_pixels(window.rem_size())
2406 });
2407 let window = cx.window;
2408 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2409
2410 cx.set_state(
2411 r#"ˇone
2412 two
2413 three
2414 four
2415 five
2416 six
2417 seven
2418 eight
2419 nine
2420 ten
2421 "#,
2422 );
2423 cx.update_editor(|editor, window, cx| {
2424 assert_eq!(
2425 editor.snapshot(window, cx).scroll_position(),
2426 gpui::Point::new(0., 0.0)
2427 );
2428 });
2429
2430 // Add a cursor below the visible area. Since both cursors cannot fit
2431 // on screen, the editor autoscrolls to reveal the newest cursor, and
2432 // allows the vertical scroll margin below that cursor.
2433 cx.update_editor(|editor, window, cx| {
2434 editor.change_selections(Default::default(), window, cx, |selections| {
2435 selections.select_ranges([
2436 Point::new(0, 0)..Point::new(0, 0),
2437 Point::new(6, 0)..Point::new(6, 0),
2438 ]);
2439 })
2440 });
2441 cx.update_editor(|editor, window, cx| {
2442 assert_eq!(
2443 editor.snapshot(window, cx).scroll_position(),
2444 gpui::Point::new(0., 3.0)
2445 );
2446 });
2447
2448 // Move down. The editor cursor scrolls down to track the newest cursor.
2449 cx.update_editor(|editor, window, cx| {
2450 editor.move_down(&Default::default(), window, cx);
2451 });
2452 cx.update_editor(|editor, window, cx| {
2453 assert_eq!(
2454 editor.snapshot(window, cx).scroll_position(),
2455 gpui::Point::new(0., 4.0)
2456 );
2457 });
2458
2459 // Add a cursor above the visible area. Since both cursors fit on screen,
2460 // the editor scrolls to show both.
2461 cx.update_editor(|editor, window, cx| {
2462 editor.change_selections(Default::default(), window, cx, |selections| {
2463 selections.select_ranges([
2464 Point::new(1, 0)..Point::new(1, 0),
2465 Point::new(6, 0)..Point::new(6, 0),
2466 ]);
2467 })
2468 });
2469 cx.update_editor(|editor, window, cx| {
2470 assert_eq!(
2471 editor.snapshot(window, cx).scroll_position(),
2472 gpui::Point::new(0., 1.0)
2473 );
2474 });
2475}
2476
2477#[gpui::test]
2478async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2479 init_test(cx, |_| {});
2480 let mut cx = EditorTestContext::new(cx).await;
2481
2482 let line_height = cx.update_editor(|editor, window, cx| {
2483 editor
2484 .style(cx)
2485 .text
2486 .line_height_in_pixels(window.rem_size())
2487 });
2488 let window = cx.window;
2489 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2490 cx.set_state(
2491 &r#"
2492 ˇone
2493 two
2494 threeˇ
2495 four
2496 five
2497 six
2498 seven
2499 eight
2500 nine
2501 ten
2502 "#
2503 .unindent(),
2504 );
2505
2506 cx.update_editor(|editor, window, cx| {
2507 editor.move_page_down(&MovePageDown::default(), window, cx)
2508 });
2509 cx.assert_editor_state(
2510 &r#"
2511 one
2512 two
2513 three
2514 ˇfour
2515 five
2516 sixˇ
2517 seven
2518 eight
2519 nine
2520 ten
2521 "#
2522 .unindent(),
2523 );
2524
2525 cx.update_editor(|editor, window, cx| {
2526 editor.move_page_down(&MovePageDown::default(), window, cx)
2527 });
2528 cx.assert_editor_state(
2529 &r#"
2530 one
2531 two
2532 three
2533 four
2534 five
2535 six
2536 ˇseven
2537 eight
2538 nineˇ
2539 ten
2540 "#
2541 .unindent(),
2542 );
2543
2544 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2545 cx.assert_editor_state(
2546 &r#"
2547 one
2548 two
2549 three
2550 ˇfour
2551 five
2552 sixˇ
2553 seven
2554 eight
2555 nine
2556 ten
2557 "#
2558 .unindent(),
2559 );
2560
2561 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2562 cx.assert_editor_state(
2563 &r#"
2564 ˇone
2565 two
2566 threeˇ
2567 four
2568 five
2569 six
2570 seven
2571 eight
2572 nine
2573 ten
2574 "#
2575 .unindent(),
2576 );
2577
2578 // Test select collapsing
2579 cx.update_editor(|editor, window, cx| {
2580 editor.move_page_down(&MovePageDown::default(), window, cx);
2581 editor.move_page_down(&MovePageDown::default(), window, cx);
2582 editor.move_page_down(&MovePageDown::default(), window, cx);
2583 });
2584 cx.assert_editor_state(
2585 &r#"
2586 one
2587 two
2588 three
2589 four
2590 five
2591 six
2592 seven
2593 eight
2594 nine
2595 ˇten
2596 ˇ"#
2597 .unindent(),
2598 );
2599}
2600
2601#[gpui::test]
2602async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2603 init_test(cx, |_| {});
2604 let mut cx = EditorTestContext::new(cx).await;
2605 cx.set_state("one «two threeˇ» four");
2606 cx.update_editor(|editor, window, cx| {
2607 editor.delete_to_beginning_of_line(
2608 &DeleteToBeginningOfLine {
2609 stop_at_indent: false,
2610 },
2611 window,
2612 cx,
2613 );
2614 assert_eq!(editor.text(cx), " four");
2615 });
2616}
2617
2618#[gpui::test]
2619async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2620 init_test(cx, |_| {});
2621
2622 let mut cx = EditorTestContext::new(cx).await;
2623
2624 // For an empty selection, the preceding word fragment is deleted.
2625 // For non-empty selections, only selected characters are deleted.
2626 cx.set_state("onˇe two t«hreˇ»e four");
2627 cx.update_editor(|editor, window, cx| {
2628 editor.delete_to_previous_word_start(
2629 &DeleteToPreviousWordStart {
2630 ignore_newlines: false,
2631 ignore_brackets: false,
2632 },
2633 window,
2634 cx,
2635 );
2636 });
2637 cx.assert_editor_state("ˇe two tˇe four");
2638
2639 cx.set_state("e tˇwo te «fˇ»our");
2640 cx.update_editor(|editor, window, cx| {
2641 editor.delete_to_next_word_end(
2642 &DeleteToNextWordEnd {
2643 ignore_newlines: false,
2644 ignore_brackets: false,
2645 },
2646 window,
2647 cx,
2648 );
2649 });
2650 cx.assert_editor_state("e tˇ te ˇour");
2651}
2652
2653#[gpui::test]
2654async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2655 init_test(cx, |_| {});
2656
2657 let mut cx = EditorTestContext::new(cx).await;
2658
2659 cx.set_state("here is some text ˇwith a space");
2660 cx.update_editor(|editor, window, cx| {
2661 editor.delete_to_previous_word_start(
2662 &DeleteToPreviousWordStart {
2663 ignore_newlines: false,
2664 ignore_brackets: true,
2665 },
2666 window,
2667 cx,
2668 );
2669 });
2670 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2671 cx.assert_editor_state("here is some textˇwith a space");
2672
2673 cx.set_state("here is some text ˇwith a space");
2674 cx.update_editor(|editor, window, cx| {
2675 editor.delete_to_previous_word_start(
2676 &DeleteToPreviousWordStart {
2677 ignore_newlines: false,
2678 ignore_brackets: false,
2679 },
2680 window,
2681 cx,
2682 );
2683 });
2684 cx.assert_editor_state("here is some textˇwith a space");
2685
2686 cx.set_state("here is some textˇ with a space");
2687 cx.update_editor(|editor, window, cx| {
2688 editor.delete_to_next_word_end(
2689 &DeleteToNextWordEnd {
2690 ignore_newlines: false,
2691 ignore_brackets: true,
2692 },
2693 window,
2694 cx,
2695 );
2696 });
2697 // Same happens in the other direction.
2698 cx.assert_editor_state("here is some textˇwith a space");
2699
2700 cx.set_state("here is some textˇ with a space");
2701 cx.update_editor(|editor, window, cx| {
2702 editor.delete_to_next_word_end(
2703 &DeleteToNextWordEnd {
2704 ignore_newlines: false,
2705 ignore_brackets: false,
2706 },
2707 window,
2708 cx,
2709 );
2710 });
2711 cx.assert_editor_state("here is some textˇwith a space");
2712
2713 cx.set_state("here is some textˇ with a space");
2714 cx.update_editor(|editor, window, cx| {
2715 editor.delete_to_next_word_end(
2716 &DeleteToNextWordEnd {
2717 ignore_newlines: true,
2718 ignore_brackets: false,
2719 },
2720 window,
2721 cx,
2722 );
2723 });
2724 cx.assert_editor_state("here is some textˇwith a space");
2725 cx.update_editor(|editor, window, cx| {
2726 editor.delete_to_previous_word_start(
2727 &DeleteToPreviousWordStart {
2728 ignore_newlines: true,
2729 ignore_brackets: false,
2730 },
2731 window,
2732 cx,
2733 );
2734 });
2735 cx.assert_editor_state("here is some ˇwith a space");
2736 cx.update_editor(|editor, window, cx| {
2737 editor.delete_to_previous_word_start(
2738 &DeleteToPreviousWordStart {
2739 ignore_newlines: true,
2740 ignore_brackets: false,
2741 },
2742 window,
2743 cx,
2744 );
2745 });
2746 // Single whitespaces are removed with the word behind them.
2747 cx.assert_editor_state("here is ˇwith a space");
2748 cx.update_editor(|editor, window, cx| {
2749 editor.delete_to_previous_word_start(
2750 &DeleteToPreviousWordStart {
2751 ignore_newlines: true,
2752 ignore_brackets: false,
2753 },
2754 window,
2755 cx,
2756 );
2757 });
2758 cx.assert_editor_state("here ˇwith a space");
2759 cx.update_editor(|editor, window, cx| {
2760 editor.delete_to_previous_word_start(
2761 &DeleteToPreviousWordStart {
2762 ignore_newlines: true,
2763 ignore_brackets: false,
2764 },
2765 window,
2766 cx,
2767 );
2768 });
2769 cx.assert_editor_state("ˇwith a space");
2770 cx.update_editor(|editor, window, cx| {
2771 editor.delete_to_previous_word_start(
2772 &DeleteToPreviousWordStart {
2773 ignore_newlines: true,
2774 ignore_brackets: false,
2775 },
2776 window,
2777 cx,
2778 );
2779 });
2780 cx.assert_editor_state("ˇwith a space");
2781 cx.update_editor(|editor, window, cx| {
2782 editor.delete_to_next_word_end(
2783 &DeleteToNextWordEnd {
2784 ignore_newlines: true,
2785 ignore_brackets: false,
2786 },
2787 window,
2788 cx,
2789 );
2790 });
2791 // Same happens in the other direction.
2792 cx.assert_editor_state("ˇ a space");
2793 cx.update_editor(|editor, window, cx| {
2794 editor.delete_to_next_word_end(
2795 &DeleteToNextWordEnd {
2796 ignore_newlines: true,
2797 ignore_brackets: false,
2798 },
2799 window,
2800 cx,
2801 );
2802 });
2803 cx.assert_editor_state("ˇ space");
2804 cx.update_editor(|editor, window, cx| {
2805 editor.delete_to_next_word_end(
2806 &DeleteToNextWordEnd {
2807 ignore_newlines: true,
2808 ignore_brackets: false,
2809 },
2810 window,
2811 cx,
2812 );
2813 });
2814 cx.assert_editor_state("ˇ");
2815 cx.update_editor(|editor, window, cx| {
2816 editor.delete_to_next_word_end(
2817 &DeleteToNextWordEnd {
2818 ignore_newlines: true,
2819 ignore_brackets: false,
2820 },
2821 window,
2822 cx,
2823 );
2824 });
2825 cx.assert_editor_state("ˇ");
2826 cx.update_editor(|editor, window, cx| {
2827 editor.delete_to_previous_word_start(
2828 &DeleteToPreviousWordStart {
2829 ignore_newlines: true,
2830 ignore_brackets: false,
2831 },
2832 window,
2833 cx,
2834 );
2835 });
2836 cx.assert_editor_state("ˇ");
2837}
2838
2839#[gpui::test]
2840async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2841 init_test(cx, |_| {});
2842
2843 let language = Arc::new(
2844 Language::new(
2845 LanguageConfig {
2846 brackets: BracketPairConfig {
2847 pairs: vec![
2848 BracketPair {
2849 start: "\"".to_string(),
2850 end: "\"".to_string(),
2851 close: true,
2852 surround: true,
2853 newline: false,
2854 },
2855 BracketPair {
2856 start: "(".to_string(),
2857 end: ")".to_string(),
2858 close: true,
2859 surround: true,
2860 newline: true,
2861 },
2862 ],
2863 ..BracketPairConfig::default()
2864 },
2865 ..LanguageConfig::default()
2866 },
2867 Some(tree_sitter_rust::LANGUAGE.into()),
2868 )
2869 .with_brackets_query(
2870 r#"
2871 ("(" @open ")" @close)
2872 ("\"" @open "\"" @close)
2873 "#,
2874 )
2875 .unwrap(),
2876 );
2877
2878 let mut cx = EditorTestContext::new(cx).await;
2879 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2880
2881 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2882 cx.update_editor(|editor, window, cx| {
2883 editor.delete_to_previous_word_start(
2884 &DeleteToPreviousWordStart {
2885 ignore_newlines: true,
2886 ignore_brackets: false,
2887 },
2888 window,
2889 cx,
2890 );
2891 });
2892 // Deletion stops before brackets if asked to not ignore them.
2893 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2894 cx.update_editor(|editor, window, cx| {
2895 editor.delete_to_previous_word_start(
2896 &DeleteToPreviousWordStart {
2897 ignore_newlines: true,
2898 ignore_brackets: false,
2899 },
2900 window,
2901 cx,
2902 );
2903 });
2904 // Deletion has to remove a single bracket and then stop again.
2905 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2906
2907 cx.update_editor(|editor, window, cx| {
2908 editor.delete_to_previous_word_start(
2909 &DeleteToPreviousWordStart {
2910 ignore_newlines: true,
2911 ignore_brackets: false,
2912 },
2913 window,
2914 cx,
2915 );
2916 });
2917 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2918
2919 cx.update_editor(|editor, window, cx| {
2920 editor.delete_to_previous_word_start(
2921 &DeleteToPreviousWordStart {
2922 ignore_newlines: true,
2923 ignore_brackets: false,
2924 },
2925 window,
2926 cx,
2927 );
2928 });
2929 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2930
2931 cx.update_editor(|editor, window, cx| {
2932 editor.delete_to_previous_word_start(
2933 &DeleteToPreviousWordStart {
2934 ignore_newlines: true,
2935 ignore_brackets: false,
2936 },
2937 window,
2938 cx,
2939 );
2940 });
2941 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2942
2943 cx.update_editor(|editor, window, cx| {
2944 editor.delete_to_next_word_end(
2945 &DeleteToNextWordEnd {
2946 ignore_newlines: true,
2947 ignore_brackets: false,
2948 },
2949 window,
2950 cx,
2951 );
2952 });
2953 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2954 cx.assert_editor_state(r#"ˇ");"#);
2955
2956 cx.update_editor(|editor, window, cx| {
2957 editor.delete_to_next_word_end(
2958 &DeleteToNextWordEnd {
2959 ignore_newlines: true,
2960 ignore_brackets: false,
2961 },
2962 window,
2963 cx,
2964 );
2965 });
2966 cx.assert_editor_state(r#"ˇ"#);
2967
2968 cx.update_editor(|editor, window, cx| {
2969 editor.delete_to_next_word_end(
2970 &DeleteToNextWordEnd {
2971 ignore_newlines: true,
2972 ignore_brackets: false,
2973 },
2974 window,
2975 cx,
2976 );
2977 });
2978 cx.assert_editor_state(r#"ˇ"#);
2979
2980 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2981 cx.update_editor(|editor, window, cx| {
2982 editor.delete_to_previous_word_start(
2983 &DeleteToPreviousWordStart {
2984 ignore_newlines: true,
2985 ignore_brackets: true,
2986 },
2987 window,
2988 cx,
2989 );
2990 });
2991 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2992}
2993
2994#[gpui::test]
2995fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2996 init_test(cx, |_| {});
2997
2998 let editor = cx.add_window(|window, cx| {
2999 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3000 build_editor(buffer, window, cx)
3001 });
3002 let del_to_prev_word_start = DeleteToPreviousWordStart {
3003 ignore_newlines: false,
3004 ignore_brackets: false,
3005 };
3006 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3007 ignore_newlines: true,
3008 ignore_brackets: false,
3009 };
3010
3011 _ = editor.update(cx, |editor, window, cx| {
3012 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3013 s.select_display_ranges([
3014 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3015 ])
3016 });
3017 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3018 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3019 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3020 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3021 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3022 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3023 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3024 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3025 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3026 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3027 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3028 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3029 });
3030}
3031
3032#[gpui::test]
3033fn test_delete_to_previous_subword_start_or_newline(cx: &mut TestAppContext) {
3034 init_test(cx, |_| {});
3035
3036 let editor = cx.add_window(|window, cx| {
3037 let buffer = MultiBuffer::build_simple("fooBar\n\nbazQux", cx);
3038 build_editor(buffer, window, cx)
3039 });
3040 let del_to_prev_sub_word_start = DeleteToPreviousSubwordStart {
3041 ignore_newlines: false,
3042 ignore_brackets: false,
3043 };
3044 let del_to_prev_sub_word_start_ignore_newlines = DeleteToPreviousSubwordStart {
3045 ignore_newlines: true,
3046 ignore_brackets: false,
3047 };
3048
3049 _ = editor.update(cx, |editor, window, cx| {
3050 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3051 s.select_display_ranges([
3052 DisplayPoint::new(DisplayRow(2), 6)..DisplayPoint::new(DisplayRow(2), 6)
3053 ])
3054 });
3055 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3056 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n\nbaz");
3057 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3058 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n\n");
3059 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3060 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n");
3061 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3062 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar");
3063 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3064 assert_eq!(editor.buffer.read(cx).read(cx).text(), "foo");
3065 editor.delete_to_previous_subword_start(
3066 &del_to_prev_sub_word_start_ignore_newlines,
3067 window,
3068 cx,
3069 );
3070 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3071 });
3072}
3073
3074#[gpui::test]
3075fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3076 init_test(cx, |_| {});
3077
3078 let editor = cx.add_window(|window, cx| {
3079 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3080 build_editor(buffer, window, cx)
3081 });
3082 let del_to_next_word_end = DeleteToNextWordEnd {
3083 ignore_newlines: false,
3084 ignore_brackets: false,
3085 };
3086 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3087 ignore_newlines: true,
3088 ignore_brackets: false,
3089 };
3090
3091 _ = editor.update(cx, |editor, window, cx| {
3092 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3093 s.select_display_ranges([
3094 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3095 ])
3096 });
3097 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3098 assert_eq!(
3099 editor.buffer.read(cx).read(cx).text(),
3100 "one\n two\nthree\n four"
3101 );
3102 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3103 assert_eq!(
3104 editor.buffer.read(cx).read(cx).text(),
3105 "\n two\nthree\n four"
3106 );
3107 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3108 assert_eq!(
3109 editor.buffer.read(cx).read(cx).text(),
3110 "two\nthree\n four"
3111 );
3112 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3113 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3114 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3115 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3116 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3117 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3118 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3119 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3120 });
3121}
3122
3123#[gpui::test]
3124fn test_delete_to_next_subword_end_or_newline(cx: &mut TestAppContext) {
3125 init_test(cx, |_| {});
3126
3127 let editor = cx.add_window(|window, cx| {
3128 let buffer = MultiBuffer::build_simple("\nfooBar\n bazQux", cx);
3129 build_editor(buffer, window, cx)
3130 });
3131 let del_to_next_subword_end = DeleteToNextSubwordEnd {
3132 ignore_newlines: false,
3133 ignore_brackets: false,
3134 };
3135 let del_to_next_subword_end_ignore_newlines = DeleteToNextSubwordEnd {
3136 ignore_newlines: true,
3137 ignore_brackets: false,
3138 };
3139
3140 _ = editor.update(cx, |editor, window, cx| {
3141 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3142 s.select_display_ranges([
3143 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3144 ])
3145 });
3146 // Delete "\n" (empty line)
3147 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3148 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n bazQux");
3149 // Delete "foo" (subword boundary)
3150 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3151 assert_eq!(editor.buffer.read(cx).read(cx).text(), "Bar\n bazQux");
3152 // Delete "Bar"
3153 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3154 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n bazQux");
3155 // Delete "\n " (newline + leading whitespace)
3156 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3157 assert_eq!(editor.buffer.read(cx).read(cx).text(), "bazQux");
3158 // Delete "baz" (subword boundary)
3159 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3160 assert_eq!(editor.buffer.read(cx).read(cx).text(), "Qux");
3161 // With ignore_newlines, delete "Qux"
3162 editor.delete_to_next_subword_end(&del_to_next_subword_end_ignore_newlines, window, cx);
3163 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3164 });
3165}
3166
3167#[gpui::test]
3168fn test_newline(cx: &mut TestAppContext) {
3169 init_test(cx, |_| {});
3170
3171 let editor = cx.add_window(|window, cx| {
3172 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3173 build_editor(buffer, window, cx)
3174 });
3175
3176 _ = editor.update(cx, |editor, window, cx| {
3177 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3178 s.select_display_ranges([
3179 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3180 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3181 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3182 ])
3183 });
3184
3185 editor.newline(&Newline, window, cx);
3186 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3187 });
3188}
3189
3190#[gpui::test]
3191async fn test_newline_yaml(cx: &mut TestAppContext) {
3192 init_test(cx, |_| {});
3193
3194 let mut cx = EditorTestContext::new(cx).await;
3195 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3196 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3197
3198 // Object (between 2 fields)
3199 cx.set_state(indoc! {"
3200 test:ˇ
3201 hello: bye"});
3202 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3203 cx.assert_editor_state(indoc! {"
3204 test:
3205 ˇ
3206 hello: bye"});
3207
3208 // Object (first and single line)
3209 cx.set_state(indoc! {"
3210 test:ˇ"});
3211 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3212 cx.assert_editor_state(indoc! {"
3213 test:
3214 ˇ"});
3215
3216 // Array with objects (after first element)
3217 cx.set_state(indoc! {"
3218 test:
3219 - foo: barˇ"});
3220 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3221 cx.assert_editor_state(indoc! {"
3222 test:
3223 - foo: bar
3224 ˇ"});
3225
3226 // Array with objects and comment
3227 cx.set_state(indoc! {"
3228 test:
3229 - foo: bar
3230 - bar: # testˇ"});
3231 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3232 cx.assert_editor_state(indoc! {"
3233 test:
3234 - foo: bar
3235 - bar: # test
3236 ˇ"});
3237
3238 // Array with objects (after second element)
3239 cx.set_state(indoc! {"
3240 test:
3241 - foo: bar
3242 - bar: fooˇ"});
3243 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3244 cx.assert_editor_state(indoc! {"
3245 test:
3246 - foo: bar
3247 - bar: foo
3248 ˇ"});
3249
3250 // Array with strings (after first element)
3251 cx.set_state(indoc! {"
3252 test:
3253 - fooˇ"});
3254 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3255 cx.assert_editor_state(indoc! {"
3256 test:
3257 - foo
3258 ˇ"});
3259}
3260
3261#[gpui::test]
3262fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3263 init_test(cx, |_| {});
3264
3265 let editor = cx.add_window(|window, cx| {
3266 let buffer = MultiBuffer::build_simple(
3267 "
3268 a
3269 b(
3270 X
3271 )
3272 c(
3273 X
3274 )
3275 "
3276 .unindent()
3277 .as_str(),
3278 cx,
3279 );
3280 let mut editor = build_editor(buffer, window, cx);
3281 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3282 s.select_ranges([
3283 Point::new(2, 4)..Point::new(2, 5),
3284 Point::new(5, 4)..Point::new(5, 5),
3285 ])
3286 });
3287 editor
3288 });
3289
3290 _ = editor.update(cx, |editor, window, cx| {
3291 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3292 editor.buffer.update(cx, |buffer, cx| {
3293 buffer.edit(
3294 [
3295 (Point::new(1, 2)..Point::new(3, 0), ""),
3296 (Point::new(4, 2)..Point::new(6, 0), ""),
3297 ],
3298 None,
3299 cx,
3300 );
3301 assert_eq!(
3302 buffer.read(cx).text(),
3303 "
3304 a
3305 b()
3306 c()
3307 "
3308 .unindent()
3309 );
3310 });
3311 assert_eq!(
3312 editor.selections.ranges(&editor.display_snapshot(cx)),
3313 &[
3314 Point::new(1, 2)..Point::new(1, 2),
3315 Point::new(2, 2)..Point::new(2, 2),
3316 ],
3317 );
3318
3319 editor.newline(&Newline, window, cx);
3320 assert_eq!(
3321 editor.text(cx),
3322 "
3323 a
3324 b(
3325 )
3326 c(
3327 )
3328 "
3329 .unindent()
3330 );
3331
3332 // The selections are moved after the inserted newlines
3333 assert_eq!(
3334 editor.selections.ranges(&editor.display_snapshot(cx)),
3335 &[
3336 Point::new(2, 0)..Point::new(2, 0),
3337 Point::new(4, 0)..Point::new(4, 0),
3338 ],
3339 );
3340 });
3341}
3342
3343#[gpui::test]
3344async fn test_newline_above(cx: &mut TestAppContext) {
3345 init_test(cx, |settings| {
3346 settings.defaults.tab_size = NonZeroU32::new(4)
3347 });
3348
3349 let language = Arc::new(
3350 Language::new(
3351 LanguageConfig::default(),
3352 Some(tree_sitter_rust::LANGUAGE.into()),
3353 )
3354 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3355 .unwrap(),
3356 );
3357
3358 let mut cx = EditorTestContext::new(cx).await;
3359 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3360 cx.set_state(indoc! {"
3361 const a: ˇA = (
3362 (ˇ
3363 «const_functionˇ»(ˇ),
3364 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3365 )ˇ
3366 ˇ);ˇ
3367 "});
3368
3369 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3370 cx.assert_editor_state(indoc! {"
3371 ˇ
3372 const a: A = (
3373 ˇ
3374 (
3375 ˇ
3376 ˇ
3377 const_function(),
3378 ˇ
3379 ˇ
3380 ˇ
3381 ˇ
3382 something_else,
3383 ˇ
3384 )
3385 ˇ
3386 ˇ
3387 );
3388 "});
3389}
3390
3391#[gpui::test]
3392async fn test_newline_below(cx: &mut TestAppContext) {
3393 init_test(cx, |settings| {
3394 settings.defaults.tab_size = NonZeroU32::new(4)
3395 });
3396
3397 let language = Arc::new(
3398 Language::new(
3399 LanguageConfig::default(),
3400 Some(tree_sitter_rust::LANGUAGE.into()),
3401 )
3402 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3403 .unwrap(),
3404 );
3405
3406 let mut cx = EditorTestContext::new(cx).await;
3407 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3408 cx.set_state(indoc! {"
3409 const a: ˇA = (
3410 (ˇ
3411 «const_functionˇ»(ˇ),
3412 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3413 )ˇ
3414 ˇ);ˇ
3415 "});
3416
3417 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3418 cx.assert_editor_state(indoc! {"
3419 const a: A = (
3420 ˇ
3421 (
3422 ˇ
3423 const_function(),
3424 ˇ
3425 ˇ
3426 something_else,
3427 ˇ
3428 ˇ
3429 ˇ
3430 ˇ
3431 )
3432 ˇ
3433 );
3434 ˇ
3435 ˇ
3436 "});
3437}
3438
3439#[gpui::test]
3440fn test_newline_respects_read_only(cx: &mut TestAppContext) {
3441 init_test(cx, |_| {});
3442
3443 let editor = cx.add_window(|window, cx| {
3444 let buffer = MultiBuffer::build_simple("aaaa\nbbbb\n", cx);
3445 build_editor(buffer, window, cx)
3446 });
3447
3448 _ = editor.update(cx, |editor, window, cx| {
3449 editor.set_read_only(true);
3450 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3451 s.select_display_ranges([
3452 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2)
3453 ])
3454 });
3455
3456 editor.newline(&Newline, window, cx);
3457 assert_eq!(
3458 editor.text(cx),
3459 "aaaa\nbbbb\n",
3460 "newline should not modify a read-only editor"
3461 );
3462
3463 editor.newline_above(&NewlineAbove, window, cx);
3464 assert_eq!(
3465 editor.text(cx),
3466 "aaaa\nbbbb\n",
3467 "newline_above should not modify a read-only editor"
3468 );
3469
3470 editor.newline_below(&NewlineBelow, window, cx);
3471 assert_eq!(
3472 editor.text(cx),
3473 "aaaa\nbbbb\n",
3474 "newline_below should not modify a read-only editor"
3475 );
3476 });
3477}
3478
3479#[gpui::test]
3480fn test_newline_below_multibuffer(cx: &mut TestAppContext) {
3481 init_test(cx, |_| {});
3482
3483 let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
3484 let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
3485 let multibuffer = cx.new(|cx| {
3486 let mut multibuffer = MultiBuffer::new(ReadWrite);
3487 multibuffer.set_excerpts_for_path(
3488 PathKey::sorted(0),
3489 buffer_1.clone(),
3490 [Point::new(0, 0)..Point::new(2, 3)],
3491 0,
3492 cx,
3493 );
3494 multibuffer.set_excerpts_for_path(
3495 PathKey::sorted(1),
3496 buffer_2.clone(),
3497 [Point::new(0, 0)..Point::new(2, 3)],
3498 0,
3499 cx,
3500 );
3501 multibuffer
3502 });
3503
3504 cx.add_window(|window, cx| {
3505 let mut editor = build_editor(multibuffer, window, cx);
3506
3507 assert_eq!(
3508 editor.text(cx),
3509 indoc! {"
3510 aaa
3511 bbb
3512 ccc
3513 ddd
3514 eee
3515 fff"}
3516 );
3517
3518 // Cursor on the last line of the first excerpt.
3519 // The newline should be inserted within the first excerpt (buffer_1),
3520 // not in the second excerpt (buffer_2).
3521 select_ranges(
3522 &mut editor,
3523 indoc! {"
3524 aaa
3525 bbb
3526 cˇcc
3527 ddd
3528 eee
3529 fff"},
3530 window,
3531 cx,
3532 );
3533 editor.newline_below(&NewlineBelow, window, cx);
3534 assert_text_with_selections(
3535 &mut editor,
3536 indoc! {"
3537 aaa
3538 bbb
3539 ccc
3540 ˇ
3541 ddd
3542 eee
3543 fff"},
3544 cx,
3545 );
3546 buffer_1.read_with(cx, |buffer, _| {
3547 assert_eq!(buffer.text(), "aaa\nbbb\nccc\n");
3548 });
3549 buffer_2.read_with(cx, |buffer, _| {
3550 assert_eq!(buffer.text(), "ddd\neee\nfff");
3551 });
3552
3553 editor
3554 });
3555}
3556
3557#[gpui::test]
3558fn test_newline_below_multibuffer_middle_of_excerpt(cx: &mut TestAppContext) {
3559 init_test(cx, |_| {});
3560
3561 let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
3562 let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
3563 let multibuffer = cx.new(|cx| {
3564 let mut multibuffer = MultiBuffer::new(ReadWrite);
3565 multibuffer.set_excerpts_for_path(
3566 PathKey::sorted(0),
3567 buffer_1.clone(),
3568 [Point::new(0, 0)..Point::new(2, 3)],
3569 0,
3570 cx,
3571 );
3572 multibuffer.set_excerpts_for_path(
3573 PathKey::sorted(1),
3574 buffer_2.clone(),
3575 [Point::new(0, 0)..Point::new(2, 3)],
3576 0,
3577 cx,
3578 );
3579 multibuffer
3580 });
3581
3582 cx.add_window(|window, cx| {
3583 let mut editor = build_editor(multibuffer, window, cx);
3584
3585 // Cursor in the middle of the first excerpt.
3586 select_ranges(
3587 &mut editor,
3588 indoc! {"
3589 aˇaa
3590 bbb
3591 ccc
3592 ddd
3593 eee
3594 fff"},
3595 window,
3596 cx,
3597 );
3598 editor.newline_below(&NewlineBelow, window, cx);
3599 assert_text_with_selections(
3600 &mut editor,
3601 indoc! {"
3602 aaa
3603 ˇ
3604 bbb
3605 ccc
3606 ddd
3607 eee
3608 fff"},
3609 cx,
3610 );
3611 buffer_1.read_with(cx, |buffer, _| {
3612 assert_eq!(buffer.text(), "aaa\n\nbbb\nccc");
3613 });
3614 buffer_2.read_with(cx, |buffer, _| {
3615 assert_eq!(buffer.text(), "ddd\neee\nfff");
3616 });
3617
3618 editor
3619 });
3620}
3621
3622#[gpui::test]
3623fn test_newline_below_multibuffer_last_line_of_last_excerpt(cx: &mut TestAppContext) {
3624 init_test(cx, |_| {});
3625
3626 let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
3627 let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
3628 let multibuffer = cx.new(|cx| {
3629 let mut multibuffer = MultiBuffer::new(ReadWrite);
3630 multibuffer.set_excerpts_for_path(
3631 PathKey::sorted(0),
3632 buffer_1.clone(),
3633 [Point::new(0, 0)..Point::new(2, 3)],
3634 0,
3635 cx,
3636 );
3637 multibuffer.set_excerpts_for_path(
3638 PathKey::sorted(1),
3639 buffer_2.clone(),
3640 [Point::new(0, 0)..Point::new(2, 3)],
3641 0,
3642 cx,
3643 );
3644 multibuffer
3645 });
3646
3647 cx.add_window(|window, cx| {
3648 let mut editor = build_editor(multibuffer, window, cx);
3649
3650 // Cursor on the last line of the last excerpt.
3651 select_ranges(
3652 &mut editor,
3653 indoc! {"
3654 aaa
3655 bbb
3656 ccc
3657 ddd
3658 eee
3659 fˇff"},
3660 window,
3661 cx,
3662 );
3663 editor.newline_below(&NewlineBelow, window, cx);
3664 assert_text_with_selections(
3665 &mut editor,
3666 indoc! {"
3667 aaa
3668 bbb
3669 ccc
3670 ddd
3671 eee
3672 fff
3673 ˇ"},
3674 cx,
3675 );
3676 buffer_1.read_with(cx, |buffer, _| {
3677 assert_eq!(buffer.text(), "aaa\nbbb\nccc");
3678 });
3679 buffer_2.read_with(cx, |buffer, _| {
3680 assert_eq!(buffer.text(), "ddd\neee\nfff\n");
3681 });
3682
3683 editor
3684 });
3685}
3686
3687#[gpui::test]
3688fn test_newline_below_multibuffer_multiple_cursors(cx: &mut TestAppContext) {
3689 init_test(cx, |_| {});
3690
3691 let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
3692 let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
3693 let multibuffer = cx.new(|cx| {
3694 let mut multibuffer = MultiBuffer::new(ReadWrite);
3695 multibuffer.set_excerpts_for_path(
3696 PathKey::sorted(0),
3697 buffer_1.clone(),
3698 [Point::new(0, 0)..Point::new(2, 3)],
3699 0,
3700 cx,
3701 );
3702 multibuffer.set_excerpts_for_path(
3703 PathKey::sorted(1),
3704 buffer_2.clone(),
3705 [Point::new(0, 0)..Point::new(2, 3)],
3706 0,
3707 cx,
3708 );
3709 multibuffer
3710 });
3711
3712 cx.add_window(|window, cx| {
3713 let mut editor = build_editor(multibuffer, window, cx);
3714
3715 // Cursors on the last line of the first excerpt and the first line
3716 // of the second excerpt. Each newline should go into its respective buffer.
3717 select_ranges(
3718 &mut editor,
3719 indoc! {"
3720 aaa
3721 bbb
3722 cˇcc
3723 dˇdd
3724 eee
3725 fff"},
3726 window,
3727 cx,
3728 );
3729 editor.newline_below(&NewlineBelow, window, cx);
3730 assert_text_with_selections(
3731 &mut editor,
3732 indoc! {"
3733 aaa
3734 bbb
3735 ccc
3736 ˇ
3737 ddd
3738 ˇ
3739 eee
3740 fff"},
3741 cx,
3742 );
3743 buffer_1.read_with(cx, |buffer, _| {
3744 assert_eq!(buffer.text(), "aaa\nbbb\nccc\n");
3745 });
3746 buffer_2.read_with(cx, |buffer, _| {
3747 assert_eq!(buffer.text(), "ddd\n\neee\nfff");
3748 });
3749
3750 editor
3751 });
3752}
3753
3754#[gpui::test]
3755async fn test_newline_comments(cx: &mut TestAppContext) {
3756 init_test(cx, |settings| {
3757 settings.defaults.tab_size = NonZeroU32::new(4)
3758 });
3759
3760 let language = Arc::new(Language::new(
3761 LanguageConfig {
3762 line_comments: vec!["// ".into()],
3763 ..LanguageConfig::default()
3764 },
3765 None,
3766 ));
3767 {
3768 let mut cx = EditorTestContext::new(cx).await;
3769 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3770 cx.set_state(indoc! {"
3771 // Fooˇ
3772 "});
3773
3774 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3775 cx.assert_editor_state(indoc! {"
3776 // Foo
3777 // ˇ
3778 "});
3779 // Ensure that we add comment prefix when existing line contains space
3780 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3781 cx.assert_editor_state(
3782 indoc! {"
3783 // Foo
3784 //s
3785 // ˇ
3786 "}
3787 .replace("s", " ") // s is used as space placeholder to prevent format on save
3788 .as_str(),
3789 );
3790 // Ensure that we add comment prefix when existing line does not contain space
3791 cx.set_state(indoc! {"
3792 // Foo
3793 //ˇ
3794 "});
3795 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3796 cx.assert_editor_state(indoc! {"
3797 // Foo
3798 //
3799 // ˇ
3800 "});
3801 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3802 cx.set_state(indoc! {"
3803 ˇ// Foo
3804 "});
3805 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3806 cx.assert_editor_state(indoc! {"
3807
3808 ˇ// Foo
3809 "});
3810 }
3811 // Ensure that comment continuations can be disabled.
3812 update_test_language_settings(cx, &|settings| {
3813 settings.defaults.extend_comment_on_newline = Some(false);
3814 });
3815 let mut cx = EditorTestContext::new(cx).await;
3816 cx.set_state(indoc! {"
3817 // Fooˇ
3818 "});
3819 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3820 cx.assert_editor_state(indoc! {"
3821 // Foo
3822 ˇ
3823 "});
3824}
3825
3826#[gpui::test]
3827async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3828 init_test(cx, |settings| {
3829 settings.defaults.tab_size = NonZeroU32::new(4)
3830 });
3831
3832 let language = Arc::new(Language::new(
3833 LanguageConfig {
3834 line_comments: vec!["// ".into(), "/// ".into()],
3835 ..LanguageConfig::default()
3836 },
3837 None,
3838 ));
3839 {
3840 let mut cx = EditorTestContext::new(cx).await;
3841 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3842 cx.set_state(indoc! {"
3843 //ˇ
3844 "});
3845 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3846 cx.assert_editor_state(indoc! {"
3847 //
3848 // ˇ
3849 "});
3850
3851 cx.set_state(indoc! {"
3852 ///ˇ
3853 "});
3854 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3855 cx.assert_editor_state(indoc! {"
3856 ///
3857 /// ˇ
3858 "});
3859 }
3860}
3861
3862#[gpui::test]
3863async fn test_newline_comments_repl_separators(cx: &mut TestAppContext) {
3864 init_test(cx, |settings| {
3865 settings.defaults.tab_size = NonZeroU32::new(4)
3866 });
3867 let language = Arc::new(Language::new(
3868 LanguageConfig {
3869 line_comments: vec!["# ".into()],
3870 ..LanguageConfig::default()
3871 },
3872 None,
3873 ));
3874
3875 {
3876 let mut cx = EditorTestContext::new(cx).await;
3877 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3878 cx.set_state(indoc! {"
3879 # %%ˇ
3880 "});
3881 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3882 cx.assert_editor_state(indoc! {"
3883 # %%
3884 ˇ
3885 "});
3886
3887 cx.set_state(indoc! {"
3888 # %%%%%ˇ
3889 "});
3890 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3891 cx.assert_editor_state(indoc! {"
3892 # %%%%%
3893 ˇ
3894 "});
3895
3896 cx.set_state(indoc! {"
3897 # %ˇ%
3898 "});
3899 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3900 cx.assert_editor_state(indoc! {"
3901 # %
3902 # ˇ%
3903 "});
3904 }
3905}
3906
3907#[gpui::test]
3908async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3909 init_test(cx, |settings| {
3910 settings.defaults.tab_size = NonZeroU32::new(4)
3911 });
3912
3913 let language = Arc::new(
3914 Language::new(
3915 LanguageConfig {
3916 documentation_comment: Some(language::BlockCommentConfig {
3917 start: "/**".into(),
3918 end: "*/".into(),
3919 prefix: "* ".into(),
3920 tab_size: 1,
3921 }),
3922
3923 ..LanguageConfig::default()
3924 },
3925 Some(tree_sitter_rust::LANGUAGE.into()),
3926 )
3927 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3928 .unwrap(),
3929 );
3930
3931 {
3932 let mut cx = EditorTestContext::new(cx).await;
3933 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3934 cx.set_state(indoc! {"
3935 /**ˇ
3936 "});
3937
3938 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3939 cx.assert_editor_state(indoc! {"
3940 /**
3941 * ˇ
3942 "});
3943 // Ensure that if cursor is before the comment start,
3944 // we do not actually insert a comment prefix.
3945 cx.set_state(indoc! {"
3946 ˇ/**
3947 "});
3948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3949 cx.assert_editor_state(indoc! {"
3950
3951 ˇ/**
3952 "});
3953 // Ensure that if cursor is between it doesn't add comment prefix.
3954 cx.set_state(indoc! {"
3955 /*ˇ*
3956 "});
3957 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3958 cx.assert_editor_state(indoc! {"
3959 /*
3960 ˇ*
3961 "});
3962 // Ensure that if suffix exists on same line after cursor it adds new line.
3963 cx.set_state(indoc! {"
3964 /**ˇ*/
3965 "});
3966 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3967 cx.assert_editor_state(indoc! {"
3968 /**
3969 * ˇ
3970 */
3971 "});
3972 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3973 cx.set_state(indoc! {"
3974 /**ˇ */
3975 "});
3976 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3977 cx.assert_editor_state(indoc! {"
3978 /**
3979 * ˇ
3980 */
3981 "});
3982 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3983 cx.set_state(indoc! {"
3984 /** ˇ*/
3985 "});
3986 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3987 cx.assert_editor_state(
3988 indoc! {"
3989 /**s
3990 * ˇ
3991 */
3992 "}
3993 .replace("s", " ") // s is used as space placeholder to prevent format on save
3994 .as_str(),
3995 );
3996 // Ensure that delimiter space is preserved when newline on already
3997 // spaced delimiter.
3998 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3999 cx.assert_editor_state(
4000 indoc! {"
4001 /**s
4002 *s
4003 * ˇ
4004 */
4005 "}
4006 .replace("s", " ") // s is used as space placeholder to prevent format on save
4007 .as_str(),
4008 );
4009 // Ensure that delimiter space is preserved when space is not
4010 // on existing delimiter.
4011 cx.set_state(indoc! {"
4012 /**
4013 *ˇ
4014 */
4015 "});
4016 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4017 cx.assert_editor_state(indoc! {"
4018 /**
4019 *
4020 * ˇ
4021 */
4022 "});
4023 // Ensure that if suffix exists on same line after cursor it
4024 // doesn't add extra new line if prefix is not on same line.
4025 cx.set_state(indoc! {"
4026 /**
4027 ˇ*/
4028 "});
4029 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4030 cx.assert_editor_state(indoc! {"
4031 /**
4032
4033 ˇ*/
4034 "});
4035 // Ensure that it detects suffix after existing prefix.
4036 cx.set_state(indoc! {"
4037 /**ˇ/
4038 "});
4039 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4040 cx.assert_editor_state(indoc! {"
4041 /**
4042 ˇ/
4043 "});
4044 // Ensure that if suffix exists on same line before
4045 // cursor it does not add comment prefix.
4046 cx.set_state(indoc! {"
4047 /** */ˇ
4048 "});
4049 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4050 cx.assert_editor_state(indoc! {"
4051 /** */
4052 ˇ
4053 "});
4054 // Ensure that if suffix exists on same line before
4055 // cursor it does not add comment prefix.
4056 cx.set_state(indoc! {"
4057 /**
4058 *
4059 */ˇ
4060 "});
4061 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4062 cx.assert_editor_state(indoc! {"
4063 /**
4064 *
4065 */
4066 ˇ
4067 "});
4068
4069 // Ensure that inline comment followed by code
4070 // doesn't add comment prefix on newline
4071 cx.set_state(indoc! {"
4072 /** */ textˇ
4073 "});
4074 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4075 cx.assert_editor_state(indoc! {"
4076 /** */ text
4077 ˇ
4078 "});
4079
4080 // Ensure that text after comment end tag
4081 // doesn't add comment prefix on newline
4082 cx.set_state(indoc! {"
4083 /**
4084 *
4085 */ˇtext
4086 "});
4087 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4088 cx.assert_editor_state(indoc! {"
4089 /**
4090 *
4091 */
4092 ˇtext
4093 "});
4094
4095 // Ensure if not comment block it doesn't
4096 // add comment prefix on newline
4097 cx.set_state(indoc! {"
4098 * textˇ
4099 "});
4100 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4101 cx.assert_editor_state(indoc! {"
4102 * text
4103 ˇ
4104 "});
4105 }
4106 // Ensure that comment continuations can be disabled.
4107 update_test_language_settings(cx, &|settings| {
4108 settings.defaults.extend_comment_on_newline = Some(false);
4109 });
4110 let mut cx = EditorTestContext::new(cx).await;
4111 cx.set_state(indoc! {"
4112 /**ˇ
4113 "});
4114 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4115 cx.assert_editor_state(indoc! {"
4116 /**
4117 ˇ
4118 "});
4119}
4120
4121#[gpui::test]
4122async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
4123 init_test(cx, |settings| {
4124 settings.defaults.tab_size = NonZeroU32::new(4)
4125 });
4126
4127 let lua_language = Arc::new(Language::new(
4128 LanguageConfig {
4129 line_comments: vec!["--".into()],
4130 block_comment: Some(language::BlockCommentConfig {
4131 start: "--[[".into(),
4132 prefix: "".into(),
4133 end: "]]".into(),
4134 tab_size: 0,
4135 }),
4136 ..LanguageConfig::default()
4137 },
4138 None,
4139 ));
4140
4141 let mut cx = EditorTestContext::new(cx).await;
4142 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
4143
4144 // Line with line comment should extend
4145 cx.set_state(indoc! {"
4146 --ˇ
4147 "});
4148 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4149 cx.assert_editor_state(indoc! {"
4150 --
4151 --ˇ
4152 "});
4153
4154 // Line with block comment that matches line comment should not extend
4155 cx.set_state(indoc! {"
4156 --[[ˇ
4157 "});
4158 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4159 cx.assert_editor_state(indoc! {"
4160 --[[
4161 ˇ
4162 "});
4163}
4164
4165#[gpui::test]
4166fn test_insert_with_old_selections(cx: &mut TestAppContext) {
4167 init_test(cx, |_| {});
4168
4169 let editor = cx.add_window(|window, cx| {
4170 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
4171 let mut editor = build_editor(buffer, window, cx);
4172 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4173 s.select_ranges([
4174 MultiBufferOffset(3)..MultiBufferOffset(4),
4175 MultiBufferOffset(11)..MultiBufferOffset(12),
4176 MultiBufferOffset(19)..MultiBufferOffset(20),
4177 ])
4178 });
4179 editor
4180 });
4181
4182 _ = editor.update(cx, |editor, window, cx| {
4183 // Edit the buffer directly, deleting ranges surrounding the editor's selections
4184 editor.buffer.update(cx, |buffer, cx| {
4185 buffer.edit(
4186 [
4187 (MultiBufferOffset(2)..MultiBufferOffset(5), ""),
4188 (MultiBufferOffset(10)..MultiBufferOffset(13), ""),
4189 (MultiBufferOffset(18)..MultiBufferOffset(21), ""),
4190 ],
4191 None,
4192 cx,
4193 );
4194 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
4195 });
4196 assert_eq!(
4197 editor.selections.ranges(&editor.display_snapshot(cx)),
4198 &[
4199 MultiBufferOffset(2)..MultiBufferOffset(2),
4200 MultiBufferOffset(7)..MultiBufferOffset(7),
4201 MultiBufferOffset(12)..MultiBufferOffset(12)
4202 ],
4203 );
4204
4205 editor.insert("Z", window, cx);
4206 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
4207
4208 // The selections are moved after the inserted characters
4209 assert_eq!(
4210 editor.selections.ranges(&editor.display_snapshot(cx)),
4211 &[
4212 MultiBufferOffset(3)..MultiBufferOffset(3),
4213 MultiBufferOffset(9)..MultiBufferOffset(9),
4214 MultiBufferOffset(15)..MultiBufferOffset(15)
4215 ],
4216 );
4217 });
4218}
4219
4220#[gpui::test]
4221async fn test_tab(cx: &mut TestAppContext) {
4222 init_test(cx, |settings| {
4223 settings.defaults.tab_size = NonZeroU32::new(3)
4224 });
4225
4226 let mut cx = EditorTestContext::new(cx).await;
4227 cx.set_state(indoc! {"
4228 ˇabˇc
4229 ˇ🏀ˇ🏀ˇefg
4230 dˇ
4231 "});
4232 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4233 cx.assert_editor_state(indoc! {"
4234 ˇab ˇc
4235 ˇ🏀 ˇ🏀 ˇefg
4236 d ˇ
4237 "});
4238
4239 cx.set_state(indoc! {"
4240 a
4241 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
4242 "});
4243 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4244 cx.assert_editor_state(indoc! {"
4245 a
4246 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
4247 "});
4248}
4249
4250#[gpui::test]
4251async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
4252 init_test(cx, |_| {});
4253
4254 let mut cx = EditorTestContext::new(cx).await;
4255 let language = Arc::new(
4256 Language::new(
4257 LanguageConfig::default(),
4258 Some(tree_sitter_rust::LANGUAGE.into()),
4259 )
4260 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
4261 .unwrap(),
4262 );
4263 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4264
4265 // test when all cursors are not at suggested indent
4266 // then simply move to their suggested indent location
4267 cx.set_state(indoc! {"
4268 const a: B = (
4269 c(
4270 ˇ
4271 ˇ )
4272 );
4273 "});
4274 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4275 cx.assert_editor_state(indoc! {"
4276 const a: B = (
4277 c(
4278 ˇ
4279 ˇ)
4280 );
4281 "});
4282
4283 // test cursor already at suggested indent not moving when
4284 // other cursors are yet to reach their suggested indents
4285 cx.set_state(indoc! {"
4286 ˇ
4287 const a: B = (
4288 c(
4289 d(
4290 ˇ
4291 )
4292 ˇ
4293 ˇ )
4294 );
4295 "});
4296 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4297 cx.assert_editor_state(indoc! {"
4298 ˇ
4299 const a: B = (
4300 c(
4301 d(
4302 ˇ
4303 )
4304 ˇ
4305 ˇ)
4306 );
4307 "});
4308 // test when all cursors are at suggested indent then tab is inserted
4309 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4310 cx.assert_editor_state(indoc! {"
4311 ˇ
4312 const a: B = (
4313 c(
4314 d(
4315 ˇ
4316 )
4317 ˇ
4318 ˇ)
4319 );
4320 "});
4321
4322 // test when current indent is less than suggested indent,
4323 // we adjust line to match suggested indent and move cursor to it
4324 //
4325 // when no other cursor is at word boundary, all of them should move
4326 cx.set_state(indoc! {"
4327 const a: B = (
4328 c(
4329 d(
4330 ˇ
4331 ˇ )
4332 ˇ )
4333 );
4334 "});
4335 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4336 cx.assert_editor_state(indoc! {"
4337 const a: B = (
4338 c(
4339 d(
4340 ˇ
4341 ˇ)
4342 ˇ)
4343 );
4344 "});
4345
4346 // test when current indent is less than suggested indent,
4347 // we adjust line to match suggested indent and move cursor to it
4348 //
4349 // when some other cursor is at word boundary, it should not move
4350 cx.set_state(indoc! {"
4351 const a: B = (
4352 c(
4353 d(
4354 ˇ
4355 ˇ )
4356 ˇ)
4357 );
4358 "});
4359 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4360 cx.assert_editor_state(indoc! {"
4361 const a: B = (
4362 c(
4363 d(
4364 ˇ
4365 ˇ)
4366 ˇ)
4367 );
4368 "});
4369
4370 // test when current indent is more than suggested indent,
4371 // we just move cursor to current indent instead of suggested indent
4372 //
4373 // when no other cursor is at word boundary, all of them should move
4374 cx.set_state(indoc! {"
4375 const a: B = (
4376 c(
4377 d(
4378 ˇ
4379 ˇ )
4380 ˇ )
4381 );
4382 "});
4383 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4384 cx.assert_editor_state(indoc! {"
4385 const a: B = (
4386 c(
4387 d(
4388 ˇ
4389 ˇ)
4390 ˇ)
4391 );
4392 "});
4393 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4394 cx.assert_editor_state(indoc! {"
4395 const a: B = (
4396 c(
4397 d(
4398 ˇ
4399 ˇ)
4400 ˇ)
4401 );
4402 "});
4403
4404 // test when current indent is more than suggested indent,
4405 // we just move cursor to current indent instead of suggested indent
4406 //
4407 // when some other cursor is at word boundary, it doesn't move
4408 cx.set_state(indoc! {"
4409 const a: B = (
4410 c(
4411 d(
4412 ˇ
4413 ˇ )
4414 ˇ)
4415 );
4416 "});
4417 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4418 cx.assert_editor_state(indoc! {"
4419 const a: B = (
4420 c(
4421 d(
4422 ˇ
4423 ˇ)
4424 ˇ)
4425 );
4426 "});
4427
4428 // handle auto-indent when there are multiple cursors on the same line
4429 cx.set_state(indoc! {"
4430 const a: B = (
4431 c(
4432 ˇ ˇ
4433 ˇ )
4434 );
4435 "});
4436 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4437 cx.assert_editor_state(indoc! {"
4438 const a: B = (
4439 c(
4440 ˇ
4441 ˇ)
4442 );
4443 "});
4444}
4445
4446#[gpui::test]
4447async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
4448 init_test(cx, |settings| {
4449 settings.defaults.tab_size = NonZeroU32::new(3)
4450 });
4451
4452 let mut cx = EditorTestContext::new(cx).await;
4453 cx.set_state(indoc! {"
4454 ˇ
4455 \t ˇ
4456 \t ˇ
4457 \t ˇ
4458 \t \t\t \t \t\t \t\t \t \t ˇ
4459 "});
4460
4461 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4462 cx.assert_editor_state(indoc! {"
4463 ˇ
4464 \t ˇ
4465 \t ˇ
4466 \t ˇ
4467 \t \t\t \t \t\t \t\t \t \t ˇ
4468 "});
4469}
4470
4471#[gpui::test]
4472async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
4473 init_test(cx, |settings| {
4474 settings.defaults.tab_size = NonZeroU32::new(4)
4475 });
4476
4477 let language = Arc::new(
4478 Language::new(
4479 LanguageConfig::default(),
4480 Some(tree_sitter_rust::LANGUAGE.into()),
4481 )
4482 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
4483 .unwrap(),
4484 );
4485
4486 let mut cx = EditorTestContext::new(cx).await;
4487 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4488 cx.set_state(indoc! {"
4489 fn a() {
4490 if b {
4491 \t ˇc
4492 }
4493 }
4494 "});
4495
4496 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4497 cx.assert_editor_state(indoc! {"
4498 fn a() {
4499 if b {
4500 ˇc
4501 }
4502 }
4503 "});
4504}
4505
4506#[gpui::test]
4507async fn test_indent_outdent(cx: &mut TestAppContext) {
4508 init_test(cx, |settings| {
4509 settings.defaults.tab_size = NonZeroU32::new(4);
4510 });
4511
4512 let mut cx = EditorTestContext::new(cx).await;
4513
4514 cx.set_state(indoc! {"
4515 «oneˇ» «twoˇ»
4516 three
4517 four
4518 "});
4519 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4520 cx.assert_editor_state(indoc! {"
4521 «oneˇ» «twoˇ»
4522 three
4523 four
4524 "});
4525
4526 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4527 cx.assert_editor_state(indoc! {"
4528 «oneˇ» «twoˇ»
4529 three
4530 four
4531 "});
4532
4533 // select across line ending
4534 cx.set_state(indoc! {"
4535 one two
4536 t«hree
4537 ˇ» four
4538 "});
4539 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4540 cx.assert_editor_state(indoc! {"
4541 one two
4542 t«hree
4543 ˇ» four
4544 "});
4545
4546 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4547 cx.assert_editor_state(indoc! {"
4548 one two
4549 t«hree
4550 ˇ» four
4551 "});
4552
4553 // Ensure that indenting/outdenting works when the cursor is at column 0.
4554 cx.set_state(indoc! {"
4555 one two
4556 ˇthree
4557 four
4558 "});
4559 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4560 cx.assert_editor_state(indoc! {"
4561 one two
4562 ˇthree
4563 four
4564 "});
4565
4566 cx.set_state(indoc! {"
4567 one two
4568 ˇ three
4569 four
4570 "});
4571 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4572 cx.assert_editor_state(indoc! {"
4573 one two
4574 ˇthree
4575 four
4576 "});
4577}
4578
4579#[gpui::test]
4580async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4581 // This is a regression test for issue #33761
4582 init_test(cx, |_| {});
4583
4584 let mut cx = EditorTestContext::new(cx).await;
4585 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4586 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4587
4588 cx.set_state(
4589 r#"ˇ# ingress:
4590ˇ# api:
4591ˇ# enabled: false
4592ˇ# pathType: Prefix
4593ˇ# console:
4594ˇ# enabled: false
4595ˇ# pathType: Prefix
4596"#,
4597 );
4598
4599 // Press tab to indent all lines
4600 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4601
4602 cx.assert_editor_state(
4603 r#" ˇ# ingress:
4604 ˇ# api:
4605 ˇ# enabled: false
4606 ˇ# pathType: Prefix
4607 ˇ# console:
4608 ˇ# enabled: false
4609 ˇ# pathType: Prefix
4610"#,
4611 );
4612}
4613
4614#[gpui::test]
4615async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4616 // This is a test to make sure our fix for issue #33761 didn't break anything
4617 init_test(cx, |_| {});
4618
4619 let mut cx = EditorTestContext::new(cx).await;
4620 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4621 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4622
4623 cx.set_state(
4624 r#"ˇingress:
4625ˇ api:
4626ˇ enabled: false
4627ˇ pathType: Prefix
4628"#,
4629 );
4630
4631 // Press tab to indent all lines
4632 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4633
4634 cx.assert_editor_state(
4635 r#"ˇingress:
4636 ˇapi:
4637 ˇenabled: false
4638 ˇpathType: Prefix
4639"#,
4640 );
4641}
4642
4643#[gpui::test]
4644async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4645 init_test(cx, |settings| {
4646 settings.defaults.hard_tabs = Some(true);
4647 });
4648
4649 let mut cx = EditorTestContext::new(cx).await;
4650
4651 // select two ranges on one line
4652 cx.set_state(indoc! {"
4653 «oneˇ» «twoˇ»
4654 three
4655 four
4656 "});
4657 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4658 cx.assert_editor_state(indoc! {"
4659 \t«oneˇ» «twoˇ»
4660 three
4661 four
4662 "});
4663 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4664 cx.assert_editor_state(indoc! {"
4665 \t\t«oneˇ» «twoˇ»
4666 three
4667 four
4668 "});
4669 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4670 cx.assert_editor_state(indoc! {"
4671 \t«oneˇ» «twoˇ»
4672 three
4673 four
4674 "});
4675 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4676 cx.assert_editor_state(indoc! {"
4677 «oneˇ» «twoˇ»
4678 three
4679 four
4680 "});
4681
4682 // select across a line ending
4683 cx.set_state(indoc! {"
4684 one two
4685 t«hree
4686 ˇ»four
4687 "});
4688 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4689 cx.assert_editor_state(indoc! {"
4690 one two
4691 \tt«hree
4692 ˇ»four
4693 "});
4694 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4695 cx.assert_editor_state(indoc! {"
4696 one two
4697 \t\tt«hree
4698 ˇ»four
4699 "});
4700 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4701 cx.assert_editor_state(indoc! {"
4702 one two
4703 \tt«hree
4704 ˇ»four
4705 "});
4706 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4707 cx.assert_editor_state(indoc! {"
4708 one two
4709 t«hree
4710 ˇ»four
4711 "});
4712
4713 // Ensure that indenting/outdenting works when the cursor is at column 0.
4714 cx.set_state(indoc! {"
4715 one two
4716 ˇthree
4717 four
4718 "});
4719 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4720 cx.assert_editor_state(indoc! {"
4721 one two
4722 ˇthree
4723 four
4724 "});
4725 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4726 cx.assert_editor_state(indoc! {"
4727 one two
4728 \tˇthree
4729 four
4730 "});
4731 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4732 cx.assert_editor_state(indoc! {"
4733 one two
4734 ˇthree
4735 four
4736 "});
4737}
4738
4739#[gpui::test]
4740fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4741 init_test(cx, |settings| {
4742 settings.languages.0.extend([
4743 (
4744 "TOML".into(),
4745 LanguageSettingsContent {
4746 tab_size: NonZeroU32::new(2),
4747 ..Default::default()
4748 },
4749 ),
4750 (
4751 "Rust".into(),
4752 LanguageSettingsContent {
4753 tab_size: NonZeroU32::new(4),
4754 ..Default::default()
4755 },
4756 ),
4757 ]);
4758 });
4759
4760 let toml_language = Arc::new(Language::new(
4761 LanguageConfig {
4762 name: "TOML".into(),
4763 ..Default::default()
4764 },
4765 None,
4766 ));
4767 let rust_language = Arc::new(Language::new(
4768 LanguageConfig {
4769 name: "Rust".into(),
4770 ..Default::default()
4771 },
4772 None,
4773 ));
4774
4775 let toml_buffer =
4776 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4777 let rust_buffer =
4778 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4779 let multibuffer = cx.new(|cx| {
4780 let mut multibuffer = MultiBuffer::new(ReadWrite);
4781 multibuffer.set_excerpts_for_path(
4782 PathKey::sorted(0),
4783 toml_buffer.clone(),
4784 [Point::new(0, 0)..Point::new(2, 0)],
4785 0,
4786 cx,
4787 );
4788 multibuffer.set_excerpts_for_path(
4789 PathKey::sorted(1),
4790 rust_buffer.clone(),
4791 [Point::new(0, 0)..Point::new(1, 0)],
4792 0,
4793 cx,
4794 );
4795 multibuffer
4796 });
4797
4798 cx.add_window(|window, cx| {
4799 let mut editor = build_editor(multibuffer, window, cx);
4800
4801 assert_eq!(
4802 editor.text(cx),
4803 indoc! {"
4804 a = 1
4805 b = 2
4806
4807 const c: usize = 3;
4808 "}
4809 );
4810
4811 select_ranges(
4812 &mut editor,
4813 indoc! {"
4814 «aˇ» = 1
4815 b = 2
4816
4817 «const c:ˇ» usize = 3;
4818 "},
4819 window,
4820 cx,
4821 );
4822
4823 editor.tab(&Tab, window, cx);
4824 assert_text_with_selections(
4825 &mut editor,
4826 indoc! {"
4827 «aˇ» = 1
4828 b = 2
4829
4830 «const c:ˇ» usize = 3;
4831 "},
4832 cx,
4833 );
4834 editor.backtab(&Backtab, window, cx);
4835 assert_text_with_selections(
4836 &mut editor,
4837 indoc! {"
4838 «aˇ» = 1
4839 b = 2
4840
4841 «const c:ˇ» usize = 3;
4842 "},
4843 cx,
4844 );
4845
4846 editor
4847 });
4848}
4849
4850#[gpui::test]
4851async fn test_backspace(cx: &mut TestAppContext) {
4852 init_test(cx, |_| {});
4853
4854 let mut cx = EditorTestContext::new(cx).await;
4855
4856 // Basic backspace
4857 cx.set_state(indoc! {"
4858 onˇe two three
4859 fou«rˇ» five six
4860 seven «ˇeight nine
4861 »ten
4862 "});
4863 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4864 cx.assert_editor_state(indoc! {"
4865 oˇe two three
4866 fouˇ five six
4867 seven ˇten
4868 "});
4869
4870 // Test backspace inside and around indents
4871 cx.set_state(indoc! {"
4872 zero
4873 ˇone
4874 ˇtwo
4875 ˇ ˇ ˇ three
4876 ˇ ˇ four
4877 "});
4878 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4879 cx.assert_editor_state(indoc! {"
4880 zero
4881 ˇone
4882 ˇtwo
4883 ˇ threeˇ four
4884 "});
4885}
4886
4887#[gpui::test]
4888async fn test_delete(cx: &mut TestAppContext) {
4889 init_test(cx, |_| {});
4890
4891 let mut cx = EditorTestContext::new(cx).await;
4892 cx.set_state(indoc! {"
4893 onˇe two three
4894 fou«rˇ» five six
4895 seven «ˇeight nine
4896 »ten
4897 "});
4898 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4899 cx.assert_editor_state(indoc! {"
4900 onˇ two three
4901 fouˇ five six
4902 seven ˇten
4903 "});
4904}
4905
4906#[gpui::test]
4907fn test_delete_line(cx: &mut TestAppContext) {
4908 init_test(cx, |_| {});
4909
4910 let editor = cx.add_window(|window, cx| {
4911 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4912 build_editor(buffer, window, cx)
4913 });
4914 _ = editor.update(cx, |editor, window, cx| {
4915 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4916 s.select_display_ranges([
4917 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4918 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4919 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4920 ])
4921 });
4922 editor.delete_line(&DeleteLine, window, cx);
4923 assert_eq!(editor.display_text(cx), "ghi");
4924 assert_eq!(
4925 display_ranges(editor, cx),
4926 vec![
4927 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4928 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4929 ]
4930 );
4931 });
4932
4933 let editor = cx.add_window(|window, cx| {
4934 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4935 build_editor(buffer, window, cx)
4936 });
4937 _ = editor.update(cx, |editor, window, cx| {
4938 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4939 s.select_display_ranges([
4940 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4941 ])
4942 });
4943 editor.delete_line(&DeleteLine, window, cx);
4944 assert_eq!(editor.display_text(cx), "ghi\n");
4945 assert_eq!(
4946 display_ranges(editor, cx),
4947 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4948 );
4949 });
4950
4951 let editor = cx.add_window(|window, cx| {
4952 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4953 build_editor(buffer, window, cx)
4954 });
4955 _ = editor.update(cx, |editor, window, cx| {
4956 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4957 s.select_display_ranges([
4958 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4959 ])
4960 });
4961 editor.delete_line(&DeleteLine, window, cx);
4962 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4963 assert_eq!(
4964 display_ranges(editor, cx),
4965 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4966 );
4967 });
4968}
4969
4970#[gpui::test]
4971fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4972 init_test(cx, |_| {});
4973
4974 cx.add_window(|window, cx| {
4975 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4976 let mut editor = build_editor(buffer.clone(), window, cx);
4977 let buffer = buffer.read(cx).as_singleton().unwrap();
4978
4979 assert_eq!(
4980 editor
4981 .selections
4982 .ranges::<Point>(&editor.display_snapshot(cx)),
4983 &[Point::new(0, 0)..Point::new(0, 0)]
4984 );
4985
4986 // When on single line, replace newline at end by space
4987 editor.join_lines(&JoinLines, window, cx);
4988 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4989 assert_eq!(
4990 editor
4991 .selections
4992 .ranges::<Point>(&editor.display_snapshot(cx)),
4993 &[Point::new(0, 3)..Point::new(0, 3)]
4994 );
4995
4996 editor.undo(&Undo, window, cx);
4997 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n\n");
4998
4999 // Select a full line, i.e. start of the first line to the start of the second line
5000 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5001 s.select_ranges([Point::new(0, 0)..Point::new(1, 0)])
5002 });
5003 editor.join_lines(&JoinLines, window, cx);
5004 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
5005
5006 editor.undo(&Undo, window, cx);
5007 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n\n");
5008
5009 // Select two full lines
5010 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5011 s.select_ranges([Point::new(0, 0)..Point::new(2, 0)])
5012 });
5013 editor.join_lines(&JoinLines, window, cx);
5014
5015 // Only the selected lines should be joined, not the third.
5016 assert_eq!(
5017 buffer.read(cx).text(),
5018 "aaa bbb\nccc\nddd\n\n",
5019 "only the two selected lines (a and b) should be joined"
5020 );
5021
5022 // When multiple lines are selected, remove newlines that are spanned by the selection
5023 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5024 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
5025 });
5026 editor.join_lines(&JoinLines, window, cx);
5027 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
5028 assert_eq!(
5029 editor
5030 .selections
5031 .ranges::<Point>(&editor.display_snapshot(cx)),
5032 &[Point::new(0, 11)..Point::new(0, 11)]
5033 );
5034
5035 // Undo should be transactional
5036 editor.undo(&Undo, window, cx);
5037 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
5038 assert_eq!(
5039 editor
5040 .selections
5041 .ranges::<Point>(&editor.display_snapshot(cx)),
5042 &[Point::new(0, 5)..Point::new(2, 2)]
5043 );
5044
5045 // When joining an empty line don't insert a space
5046 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5047 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
5048 });
5049 editor.join_lines(&JoinLines, window, cx);
5050 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
5051 assert_eq!(
5052 editor
5053 .selections
5054 .ranges::<Point>(&editor.display_snapshot(cx)),
5055 [Point::new(2, 3)..Point::new(2, 3)]
5056 );
5057
5058 // We can remove trailing newlines
5059 editor.join_lines(&JoinLines, window, cx);
5060 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
5061 assert_eq!(
5062 editor
5063 .selections
5064 .ranges::<Point>(&editor.display_snapshot(cx)),
5065 [Point::new(2, 3)..Point::new(2, 3)]
5066 );
5067
5068 // We don't blow up on the last line
5069 editor.join_lines(&JoinLines, window, cx);
5070 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
5071 assert_eq!(
5072 editor
5073 .selections
5074 .ranges::<Point>(&editor.display_snapshot(cx)),
5075 [Point::new(2, 3)..Point::new(2, 3)]
5076 );
5077
5078 // reset to test indentation
5079 editor.buffer.update(cx, |buffer, cx| {
5080 buffer.edit(
5081 [
5082 (Point::new(1, 0)..Point::new(1, 2), " "),
5083 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
5084 ],
5085 None,
5086 cx,
5087 )
5088 });
5089
5090 // We remove any leading spaces
5091 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
5092 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5093 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
5094 });
5095 editor.join_lines(&JoinLines, window, cx);
5096 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
5097
5098 // We don't insert a space for a line containing only spaces
5099 editor.join_lines(&JoinLines, window, cx);
5100 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
5101
5102 // We ignore any leading tabs
5103 editor.join_lines(&JoinLines, window, cx);
5104 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
5105
5106 editor
5107 });
5108}
5109
5110#[gpui::test]
5111fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
5112 init_test(cx, |_| {});
5113
5114 cx.add_window(|window, cx| {
5115 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
5116 let mut editor = build_editor(buffer.clone(), window, cx);
5117 let buffer = buffer.read(cx).as_singleton().unwrap();
5118
5119 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5120 s.select_ranges([
5121 Point::new(0, 2)..Point::new(1, 1),
5122 Point::new(1, 2)..Point::new(1, 2),
5123 Point::new(3, 1)..Point::new(3, 2),
5124 ])
5125 });
5126
5127 editor.join_lines(&JoinLines, window, cx);
5128 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
5129
5130 assert_eq!(
5131 editor
5132 .selections
5133 .ranges::<Point>(&editor.display_snapshot(cx)),
5134 [
5135 Point::new(0, 7)..Point::new(0, 7),
5136 Point::new(1, 3)..Point::new(1, 3)
5137 ]
5138 );
5139 editor
5140 });
5141}
5142
5143#[gpui::test]
5144async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
5145 init_test(cx, |_| {});
5146
5147 let mut cx = EditorTestContext::new(cx).await;
5148
5149 let diff_base = r#"
5150 Line 0
5151 Line 1
5152 Line 2
5153 Line 3
5154 "#
5155 .unindent();
5156
5157 cx.set_state(
5158 &r#"
5159 ˇLine 0
5160 Line 1
5161 Line 2
5162 Line 3
5163 "#
5164 .unindent(),
5165 );
5166
5167 cx.set_head_text(&diff_base);
5168 executor.run_until_parked();
5169
5170 // Join lines
5171 cx.update_editor(|editor, window, cx| {
5172 editor.join_lines(&JoinLines, window, cx);
5173 });
5174 executor.run_until_parked();
5175
5176 cx.assert_editor_state(
5177 &r#"
5178 Line 0ˇ Line 1
5179 Line 2
5180 Line 3
5181 "#
5182 .unindent(),
5183 );
5184 // Join again
5185 cx.update_editor(|editor, window, cx| {
5186 editor.join_lines(&JoinLines, window, cx);
5187 });
5188 executor.run_until_parked();
5189
5190 cx.assert_editor_state(
5191 &r#"
5192 Line 0 Line 1ˇ Line 2
5193 Line 3
5194 "#
5195 .unindent(),
5196 );
5197}
5198
5199#[gpui::test]
5200async fn test_join_lines_strips_comment_prefix(cx: &mut TestAppContext) {
5201 init_test(cx, |_| {});
5202
5203 {
5204 let language = Arc::new(Language::new(
5205 LanguageConfig {
5206 line_comments: vec!["// ".into(), "/// ".into()],
5207 documentation_comment: Some(BlockCommentConfig {
5208 start: "/*".into(),
5209 end: "*/".into(),
5210 prefix: "* ".into(),
5211 tab_size: 1,
5212 }),
5213 ..LanguageConfig::default()
5214 },
5215 None,
5216 ));
5217
5218 let mut cx = EditorTestContext::new(cx).await;
5219 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5220
5221 // Strips the comment prefix (with trailing space) from the joined-in line.
5222 cx.set_state(indoc! {"
5223 // ˇfoo
5224 // bar
5225 "});
5226 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5227 cx.assert_editor_state(indoc! {"
5228 // fooˇ bar
5229 "});
5230
5231 // Strips the longer doc-comment prefix when both `//` and `///` match.
5232 cx.set_state(indoc! {"
5233 /// ˇfoo
5234 /// bar
5235 "});
5236 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5237 cx.assert_editor_state(indoc! {"
5238 /// fooˇ bar
5239 "});
5240
5241 // Does not strip when the second line is a regular line (no comment prefix).
5242 cx.set_state(indoc! {"
5243 // ˇfoo
5244 bar
5245 "});
5246 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5247 cx.assert_editor_state(indoc! {"
5248 // fooˇ bar
5249 "});
5250
5251 // No-whitespace join also strips the comment prefix.
5252 cx.set_state(indoc! {"
5253 // ˇfoo
5254 // bar
5255 "});
5256 cx.update_editor(|e, window, cx| e.join_lines_impl(false, window, cx));
5257 cx.assert_editor_state(indoc! {"
5258 // fooˇbar
5259 "});
5260
5261 // Strips even when the joined-in line is just the bare prefix (no trailing space).
5262 cx.set_state(indoc! {"
5263 // ˇfoo
5264 //
5265 "});
5266 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5267 cx.assert_editor_state(indoc! {"
5268 // fooˇ
5269 "});
5270
5271 // Mixed line comment prefix types: the longer matching prefix is stripped.
5272 cx.set_state(indoc! {"
5273 // ˇfoo
5274 /// bar
5275 "});
5276 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5277 cx.assert_editor_state(indoc! {"
5278 // fooˇ bar
5279 "});
5280
5281 // Strips block comment body prefix (`* `) from the joined-in line.
5282 cx.set_state(indoc! {"
5283 * ˇfoo
5284 * bar
5285 "});
5286 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5287 cx.assert_editor_state(indoc! {"
5288 * fooˇ bar
5289 "});
5290
5291 // Strips bare block comment body prefix (`*` without trailing space).
5292 cx.set_state(indoc! {"
5293 * ˇfoo
5294 *
5295 "});
5296 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5297 cx.assert_editor_state(indoc! {"
5298 * fooˇ
5299 "});
5300 }
5301
5302 {
5303 let markdown_language = Arc::new(Language::new(
5304 LanguageConfig {
5305 unordered_list: vec!["- ".into(), "* ".into(), "+ ".into()],
5306 ..LanguageConfig::default()
5307 },
5308 None,
5309 ));
5310
5311 let mut cx = EditorTestContext::new(cx).await;
5312 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
5313
5314 // Strips the `- ` list marker from the joined-in line.
5315 cx.set_state(indoc! {"
5316 - ˇfoo
5317 - bar
5318 "});
5319 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5320 cx.assert_editor_state(indoc! {"
5321 - fooˇ bar
5322 "});
5323
5324 // Strips the `* ` list marker from the joined-in line.
5325 cx.set_state(indoc! {"
5326 * ˇfoo
5327 * bar
5328 "});
5329 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5330 cx.assert_editor_state(indoc! {"
5331 * fooˇ bar
5332 "});
5333
5334 // Strips the `+ ` list marker from the joined-in line.
5335 cx.set_state(indoc! {"
5336 + ˇfoo
5337 + bar
5338 "});
5339 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5340 cx.assert_editor_state(indoc! {"
5341 + fooˇ bar
5342 "});
5343
5344 // No-whitespace join also strips the list marker.
5345 cx.set_state(indoc! {"
5346 - ˇfoo
5347 - bar
5348 "});
5349 cx.update_editor(|e, window, cx| e.join_lines_impl(false, window, cx));
5350 cx.assert_editor_state(indoc! {"
5351 - fooˇbar
5352 "});
5353 }
5354}
5355
5356#[gpui::test]
5357async fn test_custom_newlines_cause_no_false_positive_diffs(
5358 executor: BackgroundExecutor,
5359 cx: &mut TestAppContext,
5360) {
5361 init_test(cx, |_| {});
5362 let mut cx = EditorTestContext::new(cx).await;
5363 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
5364 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
5365 executor.run_until_parked();
5366
5367 cx.update_editor(|editor, window, cx| {
5368 let snapshot = editor.snapshot(window, cx);
5369 assert_eq!(
5370 snapshot
5371 .buffer_snapshot()
5372 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
5373 .collect::<Vec<_>>(),
5374 Vec::new(),
5375 "Should not have any diffs for files with custom newlines"
5376 );
5377 });
5378}
5379
5380#[gpui::test]
5381async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
5382 init_test(cx, |_| {});
5383
5384 let mut cx = EditorTestContext::new(cx).await;
5385
5386 // Test sort_lines_case_insensitive()
5387 cx.set_state(indoc! {"
5388 «z
5389 y
5390 x
5391 Z
5392 Y
5393 Xˇ»
5394 "});
5395 cx.update_editor(|e, window, cx| {
5396 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
5397 });
5398 cx.assert_editor_state(indoc! {"
5399 «x
5400 X
5401 y
5402 Y
5403 z
5404 Zˇ»
5405 "});
5406
5407 // Test sort_lines_by_length()
5408 //
5409 // Demonstrates:
5410 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
5411 // - sort is stable
5412 cx.set_state(indoc! {"
5413 «123
5414 æ
5415 12
5416 ∞
5417 1
5418 æˇ»
5419 "});
5420 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
5421 cx.assert_editor_state(indoc! {"
5422 «æ
5423 ∞
5424 1
5425 æ
5426 12
5427 123ˇ»
5428 "});
5429
5430 // Test reverse_lines()
5431 cx.set_state(indoc! {"
5432 «5
5433 4
5434 3
5435 2
5436 1ˇ»
5437 "});
5438 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
5439 cx.assert_editor_state(indoc! {"
5440 «1
5441 2
5442 3
5443 4
5444 5ˇ»
5445 "});
5446
5447 // Skip testing shuffle_line()
5448
5449 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
5450 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
5451
5452 // Don't manipulate when cursor is on single line, but expand the selection
5453 cx.set_state(indoc! {"
5454 ddˇdd
5455 ccc
5456 bb
5457 a
5458 "});
5459 cx.update_editor(|e, window, cx| {
5460 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5461 });
5462 cx.assert_editor_state(indoc! {"
5463 «ddddˇ»
5464 ccc
5465 bb
5466 a
5467 "});
5468
5469 // Basic manipulate case
5470 // Start selection moves to column 0
5471 // End of selection shrinks to fit shorter line
5472 cx.set_state(indoc! {"
5473 dd«d
5474 ccc
5475 bb
5476 aaaaaˇ»
5477 "});
5478 cx.update_editor(|e, window, cx| {
5479 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5480 });
5481 cx.assert_editor_state(indoc! {"
5482 «aaaaa
5483 bb
5484 ccc
5485 dddˇ»
5486 "});
5487
5488 // Manipulate case with newlines
5489 cx.set_state(indoc! {"
5490 dd«d
5491 ccc
5492
5493 bb
5494 aaaaa
5495
5496 ˇ»
5497 "});
5498 cx.update_editor(|e, window, cx| {
5499 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5500 });
5501 cx.assert_editor_state(indoc! {"
5502 «
5503
5504 aaaaa
5505 bb
5506 ccc
5507 dddˇ»
5508
5509 "});
5510
5511 // Adding new line
5512 cx.set_state(indoc! {"
5513 aa«a
5514 bbˇ»b
5515 "});
5516 cx.update_editor(|e, window, cx| {
5517 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
5518 });
5519 cx.assert_editor_state(indoc! {"
5520 «aaa
5521 bbb
5522 added_lineˇ»
5523 "});
5524
5525 // Removing line
5526 cx.set_state(indoc! {"
5527 aa«a
5528 bbbˇ»
5529 "});
5530 cx.update_editor(|e, window, cx| {
5531 e.manipulate_immutable_lines(window, cx, |lines| {
5532 lines.pop();
5533 })
5534 });
5535 cx.assert_editor_state(indoc! {"
5536 «aaaˇ»
5537 "});
5538
5539 // Removing all lines
5540 cx.set_state(indoc! {"
5541 aa«a
5542 bbbˇ»
5543 "});
5544 cx.update_editor(|e, window, cx| {
5545 e.manipulate_immutable_lines(window, cx, |lines| {
5546 lines.drain(..);
5547 })
5548 });
5549 cx.assert_editor_state(indoc! {"
5550 ˇ
5551 "});
5552}
5553
5554#[gpui::test]
5555async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
5556 init_test(cx, |_| {});
5557
5558 let mut cx = EditorTestContext::new(cx).await;
5559
5560 // Consider continuous selection as single selection
5561 cx.set_state(indoc! {"
5562 Aaa«aa
5563 cˇ»c«c
5564 bb
5565 aaaˇ»aa
5566 "});
5567 cx.update_editor(|e, window, cx| {
5568 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5569 });
5570 cx.assert_editor_state(indoc! {"
5571 «Aaaaa
5572 ccc
5573 bb
5574 aaaaaˇ»
5575 "});
5576
5577 cx.set_state(indoc! {"
5578 Aaa«aa
5579 cˇ»c«c
5580 bb
5581 aaaˇ»aa
5582 "});
5583 cx.update_editor(|e, window, cx| {
5584 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5585 });
5586 cx.assert_editor_state(indoc! {"
5587 «Aaaaa
5588 ccc
5589 bbˇ»
5590 "});
5591
5592 // Consider non continuous selection as distinct dedup operations
5593 cx.set_state(indoc! {"
5594 «aaaaa
5595 bb
5596 aaaaa
5597 aaaaaˇ»
5598
5599 aaa«aaˇ»
5600 "});
5601 cx.update_editor(|e, window, cx| {
5602 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5603 });
5604 cx.assert_editor_state(indoc! {"
5605 «aaaaa
5606 bbˇ»
5607
5608 «aaaaaˇ»
5609 "});
5610}
5611
5612#[gpui::test]
5613async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
5614 init_test(cx, |_| {});
5615
5616 let mut cx = EditorTestContext::new(cx).await;
5617
5618 cx.set_state(indoc! {"
5619 «Aaa
5620 aAa
5621 Aaaˇ»
5622 "});
5623 cx.update_editor(|e, window, cx| {
5624 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5625 });
5626 cx.assert_editor_state(indoc! {"
5627 «Aaa
5628 aAaˇ»
5629 "});
5630
5631 cx.set_state(indoc! {"
5632 «Aaa
5633 aAa
5634 aaAˇ»
5635 "});
5636 cx.update_editor(|e, window, cx| {
5637 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5638 });
5639 cx.assert_editor_state(indoc! {"
5640 «Aaaˇ»
5641 "});
5642}
5643
5644#[gpui::test]
5645async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
5646 init_test(cx, |_| {});
5647
5648 let mut cx = EditorTestContext::new(cx).await;
5649
5650 let js_language = Arc::new(Language::new(
5651 LanguageConfig {
5652 name: "JavaScript".into(),
5653 wrap_characters: Some(language::WrapCharactersConfig {
5654 start_prefix: "<".into(),
5655 start_suffix: ">".into(),
5656 end_prefix: "</".into(),
5657 end_suffix: ">".into(),
5658 }),
5659 ..LanguageConfig::default()
5660 },
5661 None,
5662 ));
5663
5664 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5665
5666 cx.set_state(indoc! {"
5667 «testˇ»
5668 "});
5669 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5670 cx.assert_editor_state(indoc! {"
5671 <«ˇ»>test</«ˇ»>
5672 "});
5673
5674 cx.set_state(indoc! {"
5675 «test
5676 testˇ»
5677 "});
5678 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5679 cx.assert_editor_state(indoc! {"
5680 <«ˇ»>test
5681 test</«ˇ»>
5682 "});
5683
5684 cx.set_state(indoc! {"
5685 teˇst
5686 "});
5687 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5688 cx.assert_editor_state(indoc! {"
5689 te<«ˇ»></«ˇ»>st
5690 "});
5691}
5692
5693#[gpui::test]
5694async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5695 init_test(cx, |_| {});
5696
5697 let mut cx = EditorTestContext::new(cx).await;
5698
5699 let js_language = Arc::new(Language::new(
5700 LanguageConfig {
5701 name: "JavaScript".into(),
5702 wrap_characters: Some(language::WrapCharactersConfig {
5703 start_prefix: "<".into(),
5704 start_suffix: ">".into(),
5705 end_prefix: "</".into(),
5706 end_suffix: ">".into(),
5707 }),
5708 ..LanguageConfig::default()
5709 },
5710 None,
5711 ));
5712
5713 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5714
5715 cx.set_state(indoc! {"
5716 «testˇ»
5717 «testˇ» «testˇ»
5718 «testˇ»
5719 "});
5720 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5721 cx.assert_editor_state(indoc! {"
5722 <«ˇ»>test</«ˇ»>
5723 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5724 <«ˇ»>test</«ˇ»>
5725 "});
5726
5727 cx.set_state(indoc! {"
5728 «test
5729 testˇ»
5730 «test
5731 testˇ»
5732 "});
5733 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5734 cx.assert_editor_state(indoc! {"
5735 <«ˇ»>test
5736 test</«ˇ»>
5737 <«ˇ»>test
5738 test</«ˇ»>
5739 "});
5740}
5741
5742#[gpui::test]
5743async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5744 init_test(cx, |_| {});
5745
5746 let mut cx = EditorTestContext::new(cx).await;
5747
5748 let plaintext_language = Arc::new(Language::new(
5749 LanguageConfig {
5750 name: "Plain Text".into(),
5751 ..LanguageConfig::default()
5752 },
5753 None,
5754 ));
5755
5756 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5757
5758 cx.set_state(indoc! {"
5759 «testˇ»
5760 "});
5761 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5762 cx.assert_editor_state(indoc! {"
5763 «testˇ»
5764 "});
5765}
5766
5767#[gpui::test]
5768async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5769 init_test(cx, |_| {});
5770
5771 let mut cx = EditorTestContext::new(cx).await;
5772
5773 // Manipulate with multiple selections on a single line
5774 cx.set_state(indoc! {"
5775 dd«dd
5776 cˇ»c«c
5777 bb
5778 aaaˇ»aa
5779 "});
5780 cx.update_editor(|e, window, cx| {
5781 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5782 });
5783 cx.assert_editor_state(indoc! {"
5784 «aaaaa
5785 bb
5786 ccc
5787 ddddˇ»
5788 "});
5789
5790 // Manipulate with multiple disjoin selections
5791 cx.set_state(indoc! {"
5792 5«
5793 4
5794 3
5795 2
5796 1ˇ»
5797
5798 dd«dd
5799 ccc
5800 bb
5801 aaaˇ»aa
5802 "});
5803 cx.update_editor(|e, window, cx| {
5804 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5805 });
5806 cx.assert_editor_state(indoc! {"
5807 «1
5808 2
5809 3
5810 4
5811 5ˇ»
5812
5813 «aaaaa
5814 bb
5815 ccc
5816 ddddˇ»
5817 "});
5818
5819 // Adding lines on each selection
5820 cx.set_state(indoc! {"
5821 2«
5822 1ˇ»
5823
5824 bb«bb
5825 aaaˇ»aa
5826 "});
5827 cx.update_editor(|e, window, cx| {
5828 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5829 });
5830 cx.assert_editor_state(indoc! {"
5831 «2
5832 1
5833 added lineˇ»
5834
5835 «bbbb
5836 aaaaa
5837 added lineˇ»
5838 "});
5839
5840 // Removing lines on each selection
5841 cx.set_state(indoc! {"
5842 2«
5843 1ˇ»
5844
5845 bb«bb
5846 aaaˇ»aa
5847 "});
5848 cx.update_editor(|e, window, cx| {
5849 e.manipulate_immutable_lines(window, cx, |lines| {
5850 lines.pop();
5851 })
5852 });
5853 cx.assert_editor_state(indoc! {"
5854 «2ˇ»
5855
5856 «bbbbˇ»
5857 "});
5858}
5859
5860#[gpui::test]
5861async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5862 init_test(cx, |settings| {
5863 settings.defaults.tab_size = NonZeroU32::new(3)
5864 });
5865
5866 let mut cx = EditorTestContext::new(cx).await;
5867
5868 // MULTI SELECTION
5869 // Ln.1 "«" tests empty lines
5870 // Ln.9 tests just leading whitespace
5871 cx.set_state(indoc! {"
5872 «
5873 abc // No indentationˇ»
5874 «\tabc // 1 tabˇ»
5875 \t\tabc « ˇ» // 2 tabs
5876 \t ab«c // Tab followed by space
5877 \tabc // Space followed by tab (3 spaces should be the result)
5878 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5879 abˇ»ˇc ˇ ˇ // Already space indented«
5880 \t
5881 \tabc\tdef // Only the leading tab is manipulatedˇ»
5882 "});
5883 cx.update_editor(|e, window, cx| {
5884 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5885 });
5886 cx.assert_editor_state(
5887 indoc! {"
5888 «
5889 abc // No indentation
5890 abc // 1 tab
5891 abc // 2 tabs
5892 abc // Tab followed by space
5893 abc // Space followed by tab (3 spaces should be the result)
5894 abc // Mixed indentation (tab conversion depends on the column)
5895 abc // Already space indented
5896 ·
5897 abc\tdef // Only the leading tab is manipulatedˇ»
5898 "}
5899 .replace("·", "")
5900 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5901 );
5902
5903 // Test on just a few lines, the others should remain unchanged
5904 // Only lines (3, 5, 10, 11) should change
5905 cx.set_state(
5906 indoc! {"
5907 ·
5908 abc // No indentation
5909 \tabcˇ // 1 tab
5910 \t\tabc // 2 tabs
5911 \t abcˇ // Tab followed by space
5912 \tabc // Space followed by tab (3 spaces should be the result)
5913 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5914 abc // Already space indented
5915 «\t
5916 \tabc\tdef // Only the leading tab is manipulatedˇ»
5917 "}
5918 .replace("·", "")
5919 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5920 );
5921 cx.update_editor(|e, window, cx| {
5922 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5923 });
5924 cx.assert_editor_state(
5925 indoc! {"
5926 ·
5927 abc // No indentation
5928 « abc // 1 tabˇ»
5929 \t\tabc // 2 tabs
5930 « abc // Tab followed by spaceˇ»
5931 \tabc // Space followed by tab (3 spaces should be the result)
5932 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5933 abc // Already space indented
5934 « ·
5935 abc\tdef // Only the leading tab is manipulatedˇ»
5936 "}
5937 .replace("·", "")
5938 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5939 );
5940
5941 // SINGLE SELECTION
5942 // Ln.1 "«" tests empty lines
5943 // Ln.9 tests just leading whitespace
5944 cx.set_state(indoc! {"
5945 «
5946 abc // No indentation
5947 \tabc // 1 tab
5948 \t\tabc // 2 tabs
5949 \t abc // Tab followed by space
5950 \tabc // Space followed by tab (3 spaces should be the result)
5951 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5952 abc // Already space indented
5953 \t
5954 \tabc\tdef // Only the leading tab is manipulatedˇ»
5955 "});
5956 cx.update_editor(|e, window, cx| {
5957 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5958 });
5959 cx.assert_editor_state(
5960 indoc! {"
5961 «
5962 abc // No indentation
5963 abc // 1 tab
5964 abc // 2 tabs
5965 abc // Tab followed by space
5966 abc // Space followed by tab (3 spaces should be the result)
5967 abc // Mixed indentation (tab conversion depends on the column)
5968 abc // Already space indented
5969 ·
5970 abc\tdef // Only the leading tab is manipulatedˇ»
5971 "}
5972 .replace("·", "")
5973 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5974 );
5975}
5976
5977#[gpui::test]
5978async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5979 init_test(cx, |settings| {
5980 settings.defaults.tab_size = NonZeroU32::new(3)
5981 });
5982
5983 let mut cx = EditorTestContext::new(cx).await;
5984
5985 // MULTI SELECTION
5986 // Ln.1 "«" tests empty lines
5987 // Ln.11 tests just leading whitespace
5988 cx.set_state(indoc! {"
5989 «
5990 abˇ»ˇc // No indentation
5991 abc ˇ ˇ // 1 space (< 3 so dont convert)
5992 abc « // 2 spaces (< 3 so dont convert)
5993 abc // 3 spaces (convert)
5994 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5995 «\tˇ»\t«\tˇ»abc // Already tab indented
5996 «\t abc // Tab followed by space
5997 \tabc // Space followed by tab (should be consumed due to tab)
5998 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5999 \tˇ» «\t
6000 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
6001 "});
6002 cx.update_editor(|e, window, cx| {
6003 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
6004 });
6005 cx.assert_editor_state(indoc! {"
6006 «
6007 abc // No indentation
6008 abc // 1 space (< 3 so dont convert)
6009 abc // 2 spaces (< 3 so dont convert)
6010 \tabc // 3 spaces (convert)
6011 \t abc // 5 spaces (1 tab + 2 spaces)
6012 \t\t\tabc // Already tab indented
6013 \t abc // Tab followed by space
6014 \tabc // Space followed by tab (should be consumed due to tab)
6015 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
6016 \t\t\t
6017 \tabc \t // Only the leading spaces should be convertedˇ»
6018 "});
6019
6020 // Test on just a few lines, the other should remain unchanged
6021 // Only lines (4, 8, 11, 12) should change
6022 cx.set_state(
6023 indoc! {"
6024 ·
6025 abc // No indentation
6026 abc // 1 space (< 3 so dont convert)
6027 abc // 2 spaces (< 3 so dont convert)
6028 « abc // 3 spaces (convert)ˇ»
6029 abc // 5 spaces (1 tab + 2 spaces)
6030 \t\t\tabc // Already tab indented
6031 \t abc // Tab followed by space
6032 \tabc ˇ // Space followed by tab (should be consumed due to tab)
6033 \t\t \tabc // Mixed indentation
6034 \t \t \t \tabc // Mixed indentation
6035 \t \tˇ
6036 « abc \t // Only the leading spaces should be convertedˇ»
6037 "}
6038 .replace("·", "")
6039 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
6040 );
6041 cx.update_editor(|e, window, cx| {
6042 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
6043 });
6044 cx.assert_editor_state(
6045 indoc! {"
6046 ·
6047 abc // No indentation
6048 abc // 1 space (< 3 so dont convert)
6049 abc // 2 spaces (< 3 so dont convert)
6050 «\tabc // 3 spaces (convert)ˇ»
6051 abc // 5 spaces (1 tab + 2 spaces)
6052 \t\t\tabc // Already tab indented
6053 \t abc // Tab followed by space
6054 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
6055 \t\t \tabc // Mixed indentation
6056 \t \t \t \tabc // Mixed indentation
6057 «\t\t\t
6058 \tabc \t // Only the leading spaces should be convertedˇ»
6059 "}
6060 .replace("·", "")
6061 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
6062 );
6063
6064 // SINGLE SELECTION
6065 // Ln.1 "«" tests empty lines
6066 // Ln.11 tests just leading whitespace
6067 cx.set_state(indoc! {"
6068 «
6069 abc // No indentation
6070 abc // 1 space (< 3 so dont convert)
6071 abc // 2 spaces (< 3 so dont convert)
6072 abc // 3 spaces (convert)
6073 abc // 5 spaces (1 tab + 2 spaces)
6074 \t\t\tabc // Already tab indented
6075 \t abc // Tab followed by space
6076 \tabc // Space followed by tab (should be consumed due to tab)
6077 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
6078 \t \t
6079 abc \t // Only the leading spaces should be convertedˇ»
6080 "});
6081 cx.update_editor(|e, window, cx| {
6082 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
6083 });
6084 cx.assert_editor_state(indoc! {"
6085 «
6086 abc // No indentation
6087 abc // 1 space (< 3 so dont convert)
6088 abc // 2 spaces (< 3 so dont convert)
6089 \tabc // 3 spaces (convert)
6090 \t abc // 5 spaces (1 tab + 2 spaces)
6091 \t\t\tabc // Already tab indented
6092 \t abc // Tab followed by space
6093 \tabc // Space followed by tab (should be consumed due to tab)
6094 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
6095 \t\t\t
6096 \tabc \t // Only the leading spaces should be convertedˇ»
6097 "});
6098}
6099
6100#[gpui::test]
6101async fn test_toggle_case(cx: &mut TestAppContext) {
6102 init_test(cx, |_| {});
6103
6104 let mut cx = EditorTestContext::new(cx).await;
6105
6106 // If all lower case -> upper case
6107 cx.set_state(indoc! {"
6108 «hello worldˇ»
6109 "});
6110 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
6111 cx.assert_editor_state(indoc! {"
6112 «HELLO WORLDˇ»
6113 "});
6114
6115 // If all upper case -> lower case
6116 cx.set_state(indoc! {"
6117 «HELLO WORLDˇ»
6118 "});
6119 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
6120 cx.assert_editor_state(indoc! {"
6121 «hello worldˇ»
6122 "});
6123
6124 // If any upper case characters are identified -> lower case
6125 // This matches JetBrains IDEs
6126 cx.set_state(indoc! {"
6127 «hEllo worldˇ»
6128 "});
6129 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
6130 cx.assert_editor_state(indoc! {"
6131 «hello worldˇ»
6132 "});
6133}
6134
6135#[gpui::test]
6136async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
6137 init_test(cx, |_| {});
6138
6139 let mut cx = EditorTestContext::new(cx).await;
6140
6141 cx.set_state(indoc! {"
6142 «implement-windows-supportˇ»
6143 "});
6144 cx.update_editor(|e, window, cx| {
6145 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
6146 });
6147 cx.assert_editor_state(indoc! {"
6148 «Implement windows supportˇ»
6149 "});
6150}
6151
6152#[gpui::test]
6153async fn test_manipulate_text(cx: &mut TestAppContext) {
6154 init_test(cx, |_| {});
6155
6156 let mut cx = EditorTestContext::new(cx).await;
6157
6158 // Test convert_to_upper_case()
6159 cx.set_state(indoc! {"
6160 «hello worldˇ»
6161 "});
6162 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
6163 cx.assert_editor_state(indoc! {"
6164 «HELLO WORLDˇ»
6165 "});
6166
6167 // Test convert_to_lower_case()
6168 cx.set_state(indoc! {"
6169 «HELLO WORLDˇ»
6170 "});
6171 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
6172 cx.assert_editor_state(indoc! {"
6173 «hello worldˇ»
6174 "});
6175
6176 // Test multiple line, single selection case
6177 cx.set_state(indoc! {"
6178 «The quick brown
6179 fox jumps over
6180 the lazy dogˇ»
6181 "});
6182 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
6183 cx.assert_editor_state(indoc! {"
6184 «The Quick Brown
6185 Fox Jumps Over
6186 The Lazy Dogˇ»
6187 "});
6188
6189 // Test multiple line, single selection case
6190 cx.set_state(indoc! {"
6191 «The quick brown
6192 fox jumps over
6193 the lazy dogˇ»
6194 "});
6195 cx.update_editor(|e, window, cx| {
6196 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
6197 });
6198 cx.assert_editor_state(indoc! {"
6199 «TheQuickBrown
6200 FoxJumpsOver
6201 TheLazyDogˇ»
6202 "});
6203
6204 // From here on out, test more complex cases of manipulate_text()
6205
6206 // Test no selection case - should affect words cursors are in
6207 // Cursor at beginning, middle, and end of word
6208 cx.set_state(indoc! {"
6209 ˇhello big beauˇtiful worldˇ
6210 "});
6211 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
6212 cx.assert_editor_state(indoc! {"
6213 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
6214 "});
6215
6216 // Test multiple selections on a single line and across multiple lines
6217 cx.set_state(indoc! {"
6218 «Theˇ» quick «brown
6219 foxˇ» jumps «overˇ»
6220 the «lazyˇ» dog
6221 "});
6222 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
6223 cx.assert_editor_state(indoc! {"
6224 «THEˇ» quick «BROWN
6225 FOXˇ» jumps «OVERˇ»
6226 the «LAZYˇ» dog
6227 "});
6228
6229 // Test case where text length grows
6230 cx.set_state(indoc! {"
6231 «tschüߡ»
6232 "});
6233 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
6234 cx.assert_editor_state(indoc! {"
6235 «TSCHÜSSˇ»
6236 "});
6237
6238 // Test to make sure we don't crash when text shrinks
6239 cx.set_state(indoc! {"
6240 aaa_bbbˇ
6241 "});
6242 cx.update_editor(|e, window, cx| {
6243 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
6244 });
6245 cx.assert_editor_state(indoc! {"
6246 «aaaBbbˇ»
6247 "});
6248
6249 // Test to make sure we all aware of the fact that each word can grow and shrink
6250 // Final selections should be aware of this fact
6251 cx.set_state(indoc! {"
6252 aaa_bˇbb bbˇb_ccc ˇccc_ddd
6253 "});
6254 cx.update_editor(|e, window, cx| {
6255 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
6256 });
6257 cx.assert_editor_state(indoc! {"
6258 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
6259 "});
6260
6261 cx.set_state(indoc! {"
6262 «hElLo, WoRld!ˇ»
6263 "});
6264 cx.update_editor(|e, window, cx| {
6265 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
6266 });
6267 cx.assert_editor_state(indoc! {"
6268 «HeLlO, wOrLD!ˇ»
6269 "});
6270
6271 // Test that case conversions backed by `to_case` preserve leading/trailing whitespace.
6272 cx.set_state(indoc! {"
6273 « hello worldˇ»
6274 "});
6275 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
6276 cx.assert_editor_state(indoc! {"
6277 « Hello Worldˇ»
6278 "});
6279
6280 cx.set_state(indoc! {"
6281 « hello worldˇ»
6282 "});
6283 cx.update_editor(|e, window, cx| {
6284 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
6285 });
6286 cx.assert_editor_state(indoc! {"
6287 « HelloWorldˇ»
6288 "});
6289
6290 cx.set_state(indoc! {"
6291 « hello worldˇ»
6292 "});
6293 cx.update_editor(|e, window, cx| {
6294 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
6295 });
6296 cx.assert_editor_state(indoc! {"
6297 « helloWorldˇ»
6298 "});
6299
6300 cx.set_state(indoc! {"
6301 « hello worldˇ»
6302 "});
6303 cx.update_editor(|e, window, cx| e.convert_to_snake_case(&ConvertToSnakeCase, window, cx));
6304 cx.assert_editor_state(indoc! {"
6305 « hello_worldˇ»
6306 "});
6307
6308 cx.set_state(indoc! {"
6309 « hello worldˇ»
6310 "});
6311 cx.update_editor(|e, window, cx| e.convert_to_kebab_case(&ConvertToKebabCase, window, cx));
6312 cx.assert_editor_state(indoc! {"
6313 « hello-worldˇ»
6314 "});
6315
6316 cx.set_state(indoc! {"
6317 « hello worldˇ»
6318 "});
6319 cx.update_editor(|e, window, cx| {
6320 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
6321 });
6322 cx.assert_editor_state(indoc! {"
6323 « Hello worldˇ»
6324 "});
6325
6326 cx.set_state(indoc! {"
6327 « hello world\t\tˇ»
6328 "});
6329 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
6330 cx.assert_editor_state(indoc! {"
6331 « Hello World\t\tˇ»
6332 "});
6333
6334 cx.set_state(indoc! {"
6335 « hello world\t\tˇ»
6336 "});
6337 cx.update_editor(|e, window, cx| e.convert_to_snake_case(&ConvertToSnakeCase, window, cx));
6338 cx.assert_editor_state(indoc! {"
6339 « hello_world\t\tˇ»
6340 "});
6341
6342 // Test selections with `line_mode() = true`.
6343 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
6344 cx.set_state(indoc! {"
6345 «The quick brown
6346 fox jumps over
6347 tˇ»he lazy dog
6348 "});
6349 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
6350 cx.assert_editor_state(indoc! {"
6351 «THE QUICK BROWN
6352 FOX JUMPS OVER
6353 THE LAZY DOGˇ»
6354 "});
6355}
6356
6357#[gpui::test]
6358fn test_duplicate_line(cx: &mut TestAppContext) {
6359 init_test(cx, |_| {});
6360
6361 let editor = cx.add_window(|window, cx| {
6362 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
6363 build_editor(buffer, window, cx)
6364 });
6365 _ = editor.update(cx, |editor, window, cx| {
6366 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6367 s.select_display_ranges([
6368 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6369 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6370 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6371 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
6372 ])
6373 });
6374 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
6375 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
6376 assert_eq!(
6377 display_ranges(editor, cx),
6378 vec![
6379 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6380 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
6381 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
6382 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
6383 ]
6384 );
6385 });
6386
6387 let editor = cx.add_window(|window, cx| {
6388 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
6389 build_editor(buffer, window, cx)
6390 });
6391 _ = editor.update(cx, |editor, window, cx| {
6392 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6393 s.select_display_ranges([
6394 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
6395 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
6396 ])
6397 });
6398 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
6399 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
6400 assert_eq!(
6401 display_ranges(editor, cx),
6402 vec![
6403 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
6404 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
6405 ]
6406 );
6407 });
6408
6409 // With `duplicate_line_up` the selections move to the duplicated lines,
6410 // which are inserted above the original lines
6411 let editor = cx.add_window(|window, cx| {
6412 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
6413 build_editor(buffer, window, cx)
6414 });
6415 _ = editor.update(cx, |editor, window, cx| {
6416 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6417 s.select_display_ranges([
6418 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6419 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6420 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6421 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
6422 ])
6423 });
6424 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
6425 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
6426 assert_eq!(
6427 display_ranges(editor, cx),
6428 vec![
6429 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6430 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6431 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
6432 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
6433 ]
6434 );
6435 });
6436
6437 let editor = cx.add_window(|window, cx| {
6438 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
6439 build_editor(buffer, window, cx)
6440 });
6441 _ = editor.update(cx, |editor, window, cx| {
6442 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6443 s.select_display_ranges([
6444 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
6445 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
6446 ])
6447 });
6448 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
6449 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
6450 assert_eq!(
6451 display_ranges(editor, cx),
6452 vec![
6453 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
6454 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
6455 ]
6456 );
6457 });
6458
6459 let editor = cx.add_window(|window, cx| {
6460 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
6461 build_editor(buffer, window, cx)
6462 });
6463 _ = editor.update(cx, |editor, window, cx| {
6464 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6465 s.select_display_ranges([
6466 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
6467 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
6468 ])
6469 });
6470 editor.duplicate_selection(&DuplicateSelection, window, cx);
6471 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
6472 assert_eq!(
6473 display_ranges(editor, cx),
6474 vec![
6475 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
6476 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
6477 ]
6478 );
6479 });
6480}
6481
6482#[gpui::test]
6483async fn test_rotate_selections(cx: &mut TestAppContext) {
6484 init_test(cx, |_| {});
6485
6486 let mut cx = EditorTestContext::new(cx).await;
6487
6488 // Rotate text selections (horizontal)
6489 cx.set_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
6490 cx.update_editor(|e, window, cx| {
6491 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
6492 });
6493 cx.assert_editor_state("x=«3ˇ», y=«1ˇ», z=«2ˇ»");
6494 cx.update_editor(|e, window, cx| {
6495 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
6496 });
6497 cx.assert_editor_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
6498
6499 // Rotate text selections (vertical)
6500 cx.set_state(indoc! {"
6501 x=«1ˇ»
6502 y=«2ˇ»
6503 z=«3ˇ»
6504 "});
6505 cx.update_editor(|e, window, cx| {
6506 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
6507 });
6508 cx.assert_editor_state(indoc! {"
6509 x=«3ˇ»
6510 y=«1ˇ»
6511 z=«2ˇ»
6512 "});
6513 cx.update_editor(|e, window, cx| {
6514 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
6515 });
6516 cx.assert_editor_state(indoc! {"
6517 x=«1ˇ»
6518 y=«2ˇ»
6519 z=«3ˇ»
6520 "});
6521
6522 // Rotate text selections (vertical, different lengths)
6523 cx.set_state(indoc! {"
6524 x=\"«ˇ»\"
6525 y=\"«aˇ»\"
6526 z=\"«aaˇ»\"
6527 "});
6528 cx.update_editor(|e, window, cx| {
6529 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
6530 });
6531 cx.assert_editor_state(indoc! {"
6532 x=\"«aaˇ»\"
6533 y=\"«ˇ»\"
6534 z=\"«aˇ»\"
6535 "});
6536 cx.update_editor(|e, window, cx| {
6537 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
6538 });
6539 cx.assert_editor_state(indoc! {"
6540 x=\"«ˇ»\"
6541 y=\"«aˇ»\"
6542 z=\"«aaˇ»\"
6543 "});
6544
6545 // Rotate whole lines (cursor positions preserved)
6546 cx.set_state(indoc! {"
6547 ˇline123
6548 liˇne23
6549 line3ˇ
6550 "});
6551 cx.update_editor(|e, window, cx| {
6552 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
6553 });
6554 cx.assert_editor_state(indoc! {"
6555 line3ˇ
6556 ˇline123
6557 liˇne23
6558 "});
6559 cx.update_editor(|e, window, cx| {
6560 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
6561 });
6562 cx.assert_editor_state(indoc! {"
6563 ˇline123
6564 liˇne23
6565 line3ˇ
6566 "});
6567
6568 // Rotate whole lines, multiple cursors per line (positions preserved)
6569 cx.set_state(indoc! {"
6570 ˇliˇne123
6571 ˇline23
6572 ˇline3
6573 "});
6574 cx.update_editor(|e, window, cx| {
6575 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
6576 });
6577 cx.assert_editor_state(indoc! {"
6578 ˇline3
6579 ˇliˇne123
6580 ˇline23
6581 "});
6582 cx.update_editor(|e, window, cx| {
6583 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
6584 });
6585 cx.assert_editor_state(indoc! {"
6586 ˇliˇne123
6587 ˇline23
6588 ˇline3
6589 "});
6590}
6591
6592#[gpui::test]
6593fn test_move_line_up_down(cx: &mut TestAppContext) {
6594 init_test(cx, |_| {});
6595
6596 let editor = cx.add_window(|window, cx| {
6597 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
6598 build_editor(buffer, window, cx)
6599 });
6600 _ = editor.update(cx, |editor, window, cx| {
6601 editor.fold_creases(
6602 vec![
6603 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6604 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6605 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6606 ],
6607 true,
6608 window,
6609 cx,
6610 );
6611 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6612 s.select_display_ranges([
6613 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6614 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
6615 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
6616 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
6617 ])
6618 });
6619 assert_eq!(
6620 editor.display_text(cx),
6621 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
6622 );
6623
6624 editor.move_line_up(&MoveLineUp, window, cx);
6625 assert_eq!(
6626 editor.display_text(cx),
6627 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
6628 );
6629 assert_eq!(
6630 display_ranges(editor, cx),
6631 vec![
6632 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6633 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6634 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
6635 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
6636 ]
6637 );
6638 });
6639
6640 _ = editor.update(cx, |editor, window, cx| {
6641 editor.move_line_down(&MoveLineDown, window, cx);
6642 assert_eq!(
6643 editor.display_text(cx),
6644 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
6645 );
6646 assert_eq!(
6647 display_ranges(editor, cx),
6648 vec![
6649 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6650 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
6651 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
6652 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
6653 ]
6654 );
6655 });
6656
6657 _ = editor.update(cx, |editor, window, cx| {
6658 editor.move_line_down(&MoveLineDown, window, cx);
6659 assert_eq!(
6660 editor.display_text(cx),
6661 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
6662 );
6663 assert_eq!(
6664 display_ranges(editor, cx),
6665 vec![
6666 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6667 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
6668 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
6669 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
6670 ]
6671 );
6672 });
6673
6674 _ = editor.update(cx, |editor, window, cx| {
6675 editor.move_line_up(&MoveLineUp, window, cx);
6676 assert_eq!(
6677 editor.display_text(cx),
6678 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
6679 );
6680 assert_eq!(
6681 display_ranges(editor, cx),
6682 vec![
6683 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6684 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6685 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
6686 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
6687 ]
6688 );
6689 });
6690}
6691
6692#[gpui::test]
6693fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
6694 init_test(cx, |_| {});
6695 let editor = cx.add_window(|window, cx| {
6696 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
6697 build_editor(buffer, window, cx)
6698 });
6699 _ = editor.update(cx, |editor, window, cx| {
6700 editor.fold_creases(
6701 vec![Crease::simple(
6702 Point::new(6, 4)..Point::new(7, 4),
6703 FoldPlaceholder::test(),
6704 )],
6705 true,
6706 window,
6707 cx,
6708 );
6709 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6710 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
6711 });
6712 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
6713 editor.move_line_up(&MoveLineUp, window, cx);
6714 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
6715 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
6716 });
6717}
6718
6719#[gpui::test]
6720fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
6721 init_test(cx, |_| {});
6722
6723 let editor = cx.add_window(|window, cx| {
6724 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
6725 build_editor(buffer, window, cx)
6726 });
6727 _ = editor.update(cx, |editor, window, cx| {
6728 let snapshot = editor.buffer.read(cx).snapshot(cx);
6729 editor.insert_blocks(
6730 [BlockProperties {
6731 style: BlockStyle::Fixed,
6732 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
6733 height: Some(1),
6734 render: Arc::new(|_| div().into_any()),
6735 priority: 0,
6736 }],
6737 Some(Autoscroll::fit()),
6738 cx,
6739 );
6740 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6741 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
6742 });
6743 editor.move_line_down(&MoveLineDown, window, cx);
6744 });
6745}
6746
6747#[gpui::test]
6748async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
6749 init_test(cx, |_| {});
6750
6751 let mut cx = EditorTestContext::new(cx).await;
6752 cx.set_state(
6753 &"
6754 ˇzero
6755 one
6756 two
6757 three
6758 four
6759 five
6760 "
6761 .unindent(),
6762 );
6763
6764 // Create a four-line block that replaces three lines of text.
6765 cx.update_editor(|editor, window, cx| {
6766 let snapshot = editor.snapshot(window, cx);
6767 let snapshot = &snapshot.buffer_snapshot();
6768 let placement = BlockPlacement::Replace(
6769 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
6770 );
6771 editor.insert_blocks(
6772 [BlockProperties {
6773 placement,
6774 height: Some(4),
6775 style: BlockStyle::Sticky,
6776 render: Arc::new(|_| gpui::div().into_any_element()),
6777 priority: 0,
6778 }],
6779 None,
6780 cx,
6781 );
6782 });
6783
6784 // Move down so that the cursor touches the block.
6785 cx.update_editor(|editor, window, cx| {
6786 editor.move_down(&Default::default(), window, cx);
6787 });
6788 cx.assert_editor_state(
6789 &"
6790 zero
6791 «one
6792 two
6793 threeˇ»
6794 four
6795 five
6796 "
6797 .unindent(),
6798 );
6799
6800 // Move down past the block.
6801 cx.update_editor(|editor, window, cx| {
6802 editor.move_down(&Default::default(), window, cx);
6803 });
6804 cx.assert_editor_state(
6805 &"
6806 zero
6807 one
6808 two
6809 three
6810 ˇfour
6811 five
6812 "
6813 .unindent(),
6814 );
6815}
6816
6817#[gpui::test]
6818fn test_transpose(cx: &mut TestAppContext) {
6819 init_test(cx, |_| {});
6820
6821 _ = cx.add_window(|window, cx| {
6822 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
6823 editor.set_style(EditorStyle::default(), window, cx);
6824 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6825 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
6826 });
6827 editor.transpose(&Default::default(), window, cx);
6828 assert_eq!(editor.text(cx), "bac");
6829 assert_eq!(
6830 editor.selections.ranges(&editor.display_snapshot(cx)),
6831 [MultiBufferOffset(2)..MultiBufferOffset(2)]
6832 );
6833
6834 editor.transpose(&Default::default(), window, cx);
6835 assert_eq!(editor.text(cx), "bca");
6836 assert_eq!(
6837 editor.selections.ranges(&editor.display_snapshot(cx)),
6838 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6839 );
6840
6841 editor.transpose(&Default::default(), window, cx);
6842 assert_eq!(editor.text(cx), "bac");
6843 assert_eq!(
6844 editor.selections.ranges(&editor.display_snapshot(cx)),
6845 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6846 );
6847
6848 editor
6849 });
6850
6851 _ = cx.add_window(|window, cx| {
6852 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6853 editor.set_style(EditorStyle::default(), window, cx);
6854 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6855 s.select_ranges([MultiBufferOffset(3)..MultiBufferOffset(3)])
6856 });
6857 editor.transpose(&Default::default(), window, cx);
6858 assert_eq!(editor.text(cx), "acb\nde");
6859 assert_eq!(
6860 editor.selections.ranges(&editor.display_snapshot(cx)),
6861 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6862 );
6863
6864 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6865 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6866 });
6867 editor.transpose(&Default::default(), window, cx);
6868 assert_eq!(editor.text(cx), "acbd\ne");
6869 assert_eq!(
6870 editor.selections.ranges(&editor.display_snapshot(cx)),
6871 [MultiBufferOffset(5)..MultiBufferOffset(5)]
6872 );
6873
6874 editor.transpose(&Default::default(), window, cx);
6875 assert_eq!(editor.text(cx), "acbde\n");
6876 assert_eq!(
6877 editor.selections.ranges(&editor.display_snapshot(cx)),
6878 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6879 );
6880
6881 editor.transpose(&Default::default(), window, cx);
6882 assert_eq!(editor.text(cx), "acbd\ne");
6883 assert_eq!(
6884 editor.selections.ranges(&editor.display_snapshot(cx)),
6885 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6886 );
6887
6888 editor
6889 });
6890
6891 _ = cx.add_window(|window, cx| {
6892 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6893 editor.set_style(EditorStyle::default(), window, cx);
6894 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6895 s.select_ranges([
6896 MultiBufferOffset(1)..MultiBufferOffset(1),
6897 MultiBufferOffset(2)..MultiBufferOffset(2),
6898 MultiBufferOffset(4)..MultiBufferOffset(4),
6899 ])
6900 });
6901 editor.transpose(&Default::default(), window, cx);
6902 assert_eq!(editor.text(cx), "bacd\ne");
6903 assert_eq!(
6904 editor.selections.ranges(&editor.display_snapshot(cx)),
6905 [
6906 MultiBufferOffset(2)..MultiBufferOffset(2),
6907 MultiBufferOffset(3)..MultiBufferOffset(3),
6908 MultiBufferOffset(5)..MultiBufferOffset(5)
6909 ]
6910 );
6911
6912 editor.transpose(&Default::default(), window, cx);
6913 assert_eq!(editor.text(cx), "bcade\n");
6914 assert_eq!(
6915 editor.selections.ranges(&editor.display_snapshot(cx)),
6916 [
6917 MultiBufferOffset(3)..MultiBufferOffset(3),
6918 MultiBufferOffset(4)..MultiBufferOffset(4),
6919 MultiBufferOffset(6)..MultiBufferOffset(6)
6920 ]
6921 );
6922
6923 editor.transpose(&Default::default(), window, cx);
6924 assert_eq!(editor.text(cx), "bcda\ne");
6925 assert_eq!(
6926 editor.selections.ranges(&editor.display_snapshot(cx)),
6927 [
6928 MultiBufferOffset(4)..MultiBufferOffset(4),
6929 MultiBufferOffset(6)..MultiBufferOffset(6)
6930 ]
6931 );
6932
6933 editor.transpose(&Default::default(), window, cx);
6934 assert_eq!(editor.text(cx), "bcade\n");
6935 assert_eq!(
6936 editor.selections.ranges(&editor.display_snapshot(cx)),
6937 [
6938 MultiBufferOffset(4)..MultiBufferOffset(4),
6939 MultiBufferOffset(6)..MultiBufferOffset(6)
6940 ]
6941 );
6942
6943 editor.transpose(&Default::default(), window, cx);
6944 assert_eq!(editor.text(cx), "bcaed\n");
6945 assert_eq!(
6946 editor.selections.ranges(&editor.display_snapshot(cx)),
6947 [
6948 MultiBufferOffset(5)..MultiBufferOffset(5),
6949 MultiBufferOffset(6)..MultiBufferOffset(6)
6950 ]
6951 );
6952
6953 editor
6954 });
6955
6956 _ = cx.add_window(|window, cx| {
6957 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6958 editor.set_style(EditorStyle::default(), window, cx);
6959 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6960 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6961 });
6962 editor.transpose(&Default::default(), window, cx);
6963 assert_eq!(editor.text(cx), "🏀🍐✋");
6964 assert_eq!(
6965 editor.selections.ranges(&editor.display_snapshot(cx)),
6966 [MultiBufferOffset(8)..MultiBufferOffset(8)]
6967 );
6968
6969 editor.transpose(&Default::default(), window, cx);
6970 assert_eq!(editor.text(cx), "🏀✋🍐");
6971 assert_eq!(
6972 editor.selections.ranges(&editor.display_snapshot(cx)),
6973 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6974 );
6975
6976 editor.transpose(&Default::default(), window, cx);
6977 assert_eq!(editor.text(cx), "🏀🍐✋");
6978 assert_eq!(
6979 editor.selections.ranges(&editor.display_snapshot(cx)),
6980 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6981 );
6982
6983 editor
6984 });
6985}
6986
6987#[gpui::test]
6988async fn test_rewrap(cx: &mut TestAppContext) {
6989 init_test(cx, |settings| {
6990 settings.languages.0.extend([
6991 (
6992 "Markdown".into(),
6993 LanguageSettingsContent {
6994 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6995 preferred_line_length: Some(40),
6996 ..Default::default()
6997 },
6998 ),
6999 (
7000 "Plain Text".into(),
7001 LanguageSettingsContent {
7002 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
7003 preferred_line_length: Some(40),
7004 ..Default::default()
7005 },
7006 ),
7007 (
7008 "C++".into(),
7009 LanguageSettingsContent {
7010 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
7011 preferred_line_length: Some(40),
7012 ..Default::default()
7013 },
7014 ),
7015 (
7016 "Python".into(),
7017 LanguageSettingsContent {
7018 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
7019 preferred_line_length: Some(40),
7020 ..Default::default()
7021 },
7022 ),
7023 (
7024 "Rust".into(),
7025 LanguageSettingsContent {
7026 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
7027 preferred_line_length: Some(40),
7028 ..Default::default()
7029 },
7030 ),
7031 ])
7032 });
7033
7034 let mut cx = EditorTestContext::new(cx).await;
7035
7036 let cpp_language = Arc::new(Language::new(
7037 LanguageConfig {
7038 name: "C++".into(),
7039 line_comments: vec!["// ".into()],
7040 ..LanguageConfig::default()
7041 },
7042 None,
7043 ));
7044 let python_language = Arc::new(Language::new(
7045 LanguageConfig {
7046 name: "Python".into(),
7047 line_comments: vec!["# ".into()],
7048 ..LanguageConfig::default()
7049 },
7050 None,
7051 ));
7052 let markdown_language = Arc::new(Language::new(
7053 LanguageConfig {
7054 name: "Markdown".into(),
7055 rewrap_prefixes: vec![
7056 regex::Regex::new("\\d+\\.\\s+").unwrap(),
7057 regex::Regex::new("[-*+]\\s+").unwrap(),
7058 ],
7059 ..LanguageConfig::default()
7060 },
7061 None,
7062 ));
7063 let rust_language = Arc::new(
7064 Language::new(
7065 LanguageConfig {
7066 name: "Rust".into(),
7067 line_comments: vec!["// ".into(), "/// ".into()],
7068 ..LanguageConfig::default()
7069 },
7070 Some(tree_sitter_rust::LANGUAGE.into()),
7071 )
7072 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
7073 .unwrap(),
7074 );
7075
7076 let plaintext_language = Arc::new(Language::new(
7077 LanguageConfig {
7078 name: "Plain Text".into(),
7079 ..LanguageConfig::default()
7080 },
7081 None,
7082 ));
7083
7084 // Test basic rewrapping of a long line with a cursor
7085 assert_rewrap(
7086 indoc! {"
7087 // ˇThis is a long comment that needs to be wrapped.
7088 "},
7089 indoc! {"
7090 // ˇThis is a long comment that needs to
7091 // be wrapped.
7092 "},
7093 cpp_language.clone(),
7094 &mut cx,
7095 );
7096
7097 // Test rewrapping a full selection
7098 assert_rewrap(
7099 indoc! {"
7100 «// This selected long comment needs to be wrapped.ˇ»"
7101 },
7102 indoc! {"
7103 «// This selected long comment needs to
7104 // be wrapped.ˇ»"
7105 },
7106 cpp_language.clone(),
7107 &mut cx,
7108 );
7109
7110 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
7111 assert_rewrap(
7112 indoc! {"
7113 // ˇThis is the first line.
7114 // Thisˇ is the second line.
7115 // This is the thirdˇ line, all part of one paragraph.
7116 "},
7117 indoc! {"
7118 // ˇThis is the first line. Thisˇ is the
7119 // second line. This is the thirdˇ line,
7120 // all part of one paragraph.
7121 "},
7122 cpp_language.clone(),
7123 &mut cx,
7124 );
7125
7126 // Test multiple cursors in different paragraphs trigger separate rewraps
7127 assert_rewrap(
7128 indoc! {"
7129 // ˇThis is the first paragraph, first line.
7130 // ˇThis is the first paragraph, second line.
7131
7132 // ˇThis is the second paragraph, first line.
7133 // ˇThis is the second paragraph, second line.
7134 "},
7135 indoc! {"
7136 // ˇThis is the first paragraph, first
7137 // line. ˇThis is the first paragraph,
7138 // second line.
7139
7140 // ˇThis is the second paragraph, first
7141 // line. ˇThis is the second paragraph,
7142 // second line.
7143 "},
7144 cpp_language.clone(),
7145 &mut cx,
7146 );
7147
7148 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
7149 assert_rewrap(
7150 indoc! {"
7151 «// A regular long long comment to be wrapped.
7152 /// A documentation long comment to be wrapped.ˇ»
7153 "},
7154 indoc! {"
7155 «// A regular long long comment to be
7156 // wrapped.
7157 /// A documentation long comment to be
7158 /// wrapped.ˇ»
7159 "},
7160 rust_language.clone(),
7161 &mut cx,
7162 );
7163
7164 // Test that change in indentation level trigger seperate rewraps
7165 assert_rewrap(
7166 indoc! {"
7167 fn foo() {
7168 «// This is a long comment at the base indent.
7169 // This is a long comment at the next indent.ˇ»
7170 }
7171 "},
7172 indoc! {"
7173 fn foo() {
7174 «// This is a long comment at the
7175 // base indent.
7176 // This is a long comment at the
7177 // next indent.ˇ»
7178 }
7179 "},
7180 rust_language.clone(),
7181 &mut cx,
7182 );
7183
7184 // Test that different comment prefix characters (e.g., '#') are handled correctly
7185 assert_rewrap(
7186 indoc! {"
7187 # ˇThis is a long comment using a pound sign.
7188 "},
7189 indoc! {"
7190 # ˇThis is a long comment using a pound
7191 # sign.
7192 "},
7193 python_language,
7194 &mut cx,
7195 );
7196
7197 // Test rewrapping only affects comments, not code even when selected
7198 assert_rewrap(
7199 indoc! {"
7200 «/// This doc comment is long and should be wrapped.
7201 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
7202 "},
7203 indoc! {"
7204 «/// This doc comment is long and should
7205 /// be wrapped.
7206 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
7207 "},
7208 rust_language.clone(),
7209 &mut cx,
7210 );
7211
7212 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
7213 assert_rewrap(
7214 indoc! {"
7215 # Header
7216
7217 A long long long line of markdown text to wrap.ˇ
7218 "},
7219 indoc! {"
7220 # Header
7221
7222 A long long long line of markdown text
7223 to wrap.ˇ
7224 "},
7225 markdown_language.clone(),
7226 &mut cx,
7227 );
7228
7229 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
7230 assert_rewrap(
7231 indoc! {"
7232 «1. This is a numbered list item that is very long and needs to be wrapped properly.
7233 2. This is a numbered list item that is very long and needs to be wrapped properly.
7234 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
7235 "},
7236 indoc! {"
7237 «1. This is a numbered list item that is
7238 very long and needs to be wrapped
7239 properly.
7240 2. This is a numbered list item that is
7241 very long and needs to be wrapped
7242 properly.
7243 - This is an unordered list item that is
7244 also very long and should not merge
7245 with the numbered item.ˇ»
7246 "},
7247 markdown_language.clone(),
7248 &mut cx,
7249 );
7250
7251 // Test that rewrapping add indents for rewrapping boundary if not exists already.
7252 assert_rewrap(
7253 indoc! {"
7254 «1. This is a numbered list item that is
7255 very long and needs to be wrapped
7256 properly.
7257 2. This is a numbered list item that is
7258 very long and needs to be wrapped
7259 properly.
7260 - This is an unordered list item that is
7261 also very long and should not merge with
7262 the numbered item.ˇ»
7263 "},
7264 indoc! {"
7265 «1. This is a numbered list item that is
7266 very long and needs to be wrapped
7267 properly.
7268 2. This is a numbered list item that is
7269 very long and needs to be wrapped
7270 properly.
7271 - This is an unordered list item that is
7272 also very long and should not merge
7273 with the numbered item.ˇ»
7274 "},
7275 markdown_language.clone(),
7276 &mut cx,
7277 );
7278
7279 // Test that rewrapping maintain indents even when they already exists.
7280 assert_rewrap(
7281 indoc! {"
7282 «1. This is a numbered list
7283 item that is very long and needs to be wrapped properly.
7284 2. This is a numbered list
7285 item that is very long and needs to be wrapped properly.
7286 - This is an unordered list item that is also very long and
7287 should not merge with the numbered item.ˇ»
7288 "},
7289 indoc! {"
7290 «1. This is a numbered list item that is
7291 very long and needs to be wrapped
7292 properly.
7293 2. This is a numbered list item that is
7294 very long and needs to be wrapped
7295 properly.
7296 - This is an unordered list item that is
7297 also very long and should not merge
7298 with the numbered item.ˇ»
7299 "},
7300 markdown_language,
7301 &mut cx,
7302 );
7303
7304 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
7305 assert_rewrap(
7306 indoc! {"
7307 ˇThis is a very long line of plain text that will be wrapped.
7308 "},
7309 indoc! {"
7310 ˇThis is a very long line of plain text
7311 that will be wrapped.
7312 "},
7313 plaintext_language.clone(),
7314 &mut cx,
7315 );
7316
7317 // Test that non-commented code acts as a paragraph boundary within a selection
7318 assert_rewrap(
7319 indoc! {"
7320 «// This is the first long comment block to be wrapped.
7321 fn my_func(a: u32);
7322 // This is the second long comment block to be wrapped.ˇ»
7323 "},
7324 indoc! {"
7325 «// This is the first long comment block
7326 // to be wrapped.
7327 fn my_func(a: u32);
7328 // This is the second long comment block
7329 // to be wrapped.ˇ»
7330 "},
7331 rust_language,
7332 &mut cx,
7333 );
7334
7335 // Test rewrapping multiple selections, including ones with blank lines or tabs
7336 assert_rewrap(
7337 indoc! {"
7338 «ˇThis is a very long line that will be wrapped.
7339
7340 This is another paragraph in the same selection.»
7341
7342 «\tThis is a very long indented line that will be wrapped.ˇ»
7343 "},
7344 indoc! {"
7345 «ˇThis is a very long line that will be
7346 wrapped.
7347
7348 This is another paragraph in the same
7349 selection.»
7350
7351 «\tThis is a very long indented line
7352 \tthat will be wrapped.ˇ»
7353 "},
7354 plaintext_language,
7355 &mut cx,
7356 );
7357
7358 // Test that an empty comment line acts as a paragraph boundary
7359 assert_rewrap(
7360 indoc! {"
7361 // ˇThis is a long comment that will be wrapped.
7362 //
7363 // And this is another long comment that will also be wrapped.ˇ
7364 "},
7365 indoc! {"
7366 // ˇThis is a long comment that will be
7367 // wrapped.
7368 //
7369 // And this is another long comment that
7370 // will also be wrapped.ˇ
7371 "},
7372 cpp_language,
7373 &mut cx,
7374 );
7375
7376 #[track_caller]
7377 fn assert_rewrap(
7378 unwrapped_text: &str,
7379 wrapped_text: &str,
7380 language: Arc<Language>,
7381 cx: &mut EditorTestContext,
7382 ) {
7383 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
7384 cx.set_state(unwrapped_text);
7385 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
7386 cx.assert_editor_state(wrapped_text);
7387 }
7388}
7389
7390#[gpui::test]
7391async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
7392 init_test(cx, |settings| {
7393 settings.languages.0.extend([(
7394 "Rust".into(),
7395 LanguageSettingsContent {
7396 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
7397 preferred_line_length: Some(40),
7398 ..Default::default()
7399 },
7400 )])
7401 });
7402
7403 let mut cx = EditorTestContext::new(cx).await;
7404
7405 let rust_lang = Arc::new(
7406 Language::new(
7407 LanguageConfig {
7408 name: "Rust".into(),
7409 line_comments: vec!["// ".into()],
7410 block_comment: Some(BlockCommentConfig {
7411 start: "/*".into(),
7412 end: "*/".into(),
7413 prefix: "* ".into(),
7414 tab_size: 1,
7415 }),
7416 documentation_comment: Some(BlockCommentConfig {
7417 start: "/**".into(),
7418 end: "*/".into(),
7419 prefix: "* ".into(),
7420 tab_size: 1,
7421 }),
7422
7423 ..LanguageConfig::default()
7424 },
7425 Some(tree_sitter_rust::LANGUAGE.into()),
7426 )
7427 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
7428 .unwrap(),
7429 );
7430
7431 // regular block comment
7432 assert_rewrap(
7433 indoc! {"
7434 /*
7435 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7436 */
7437 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7438 "},
7439 indoc! {"
7440 /*
7441 *ˇ Lorem ipsum dolor sit amet,
7442 * consectetur adipiscing elit.
7443 */
7444 /*
7445 *ˇ Lorem ipsum dolor sit amet,
7446 * consectetur adipiscing elit.
7447 */
7448 "},
7449 rust_lang.clone(),
7450 &mut cx,
7451 );
7452
7453 // indent is respected
7454 assert_rewrap(
7455 indoc! {"
7456 {}
7457 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7458 "},
7459 indoc! {"
7460 {}
7461 /*
7462 *ˇ Lorem ipsum dolor sit amet,
7463 * consectetur adipiscing elit.
7464 */
7465 "},
7466 rust_lang.clone(),
7467 &mut cx,
7468 );
7469
7470 // short block comments with inline delimiters
7471 assert_rewrap(
7472 indoc! {"
7473 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7474 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7475 */
7476 /*
7477 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7478 "},
7479 indoc! {"
7480 /*
7481 *ˇ Lorem ipsum dolor sit amet,
7482 * consectetur adipiscing elit.
7483 */
7484 /*
7485 *ˇ Lorem ipsum dolor sit amet,
7486 * consectetur adipiscing elit.
7487 */
7488 /*
7489 *ˇ Lorem ipsum dolor sit amet,
7490 * consectetur adipiscing elit.
7491 */
7492 "},
7493 rust_lang.clone(),
7494 &mut cx,
7495 );
7496
7497 // multiline block comment with inline start/end delimiters
7498 assert_rewrap(
7499 indoc! {"
7500 /*ˇ Lorem ipsum dolor sit amet,
7501 * consectetur adipiscing elit. */
7502 "},
7503 indoc! {"
7504 /*
7505 *ˇ Lorem ipsum dolor sit amet,
7506 * consectetur adipiscing elit.
7507 */
7508 "},
7509 rust_lang.clone(),
7510 &mut cx,
7511 );
7512
7513 // block comment rewrap still respects paragraph bounds
7514 assert_rewrap(
7515 indoc! {"
7516 /*
7517 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7518 *
7519 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7520 */
7521 "},
7522 indoc! {"
7523 /*
7524 *ˇ Lorem ipsum dolor sit amet,
7525 * consectetur adipiscing elit.
7526 *
7527 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7528 */
7529 "},
7530 rust_lang.clone(),
7531 &mut cx,
7532 );
7533
7534 // documentation comments
7535 assert_rewrap(
7536 indoc! {"
7537 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7538 /**
7539 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7540 */
7541 "},
7542 indoc! {"
7543 /**
7544 *ˇ Lorem ipsum dolor sit amet,
7545 * consectetur adipiscing elit.
7546 */
7547 /**
7548 *ˇ Lorem ipsum dolor sit amet,
7549 * consectetur adipiscing elit.
7550 */
7551 "},
7552 rust_lang.clone(),
7553 &mut cx,
7554 );
7555
7556 // different, adjacent comments
7557 assert_rewrap(
7558 indoc! {"
7559 /**
7560 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7561 */
7562 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7563 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7564 "},
7565 indoc! {"
7566 /**
7567 *ˇ Lorem ipsum dolor sit amet,
7568 * consectetur adipiscing elit.
7569 */
7570 /*
7571 *ˇ Lorem ipsum dolor sit amet,
7572 * consectetur adipiscing elit.
7573 */
7574 //ˇ Lorem ipsum dolor sit amet,
7575 // consectetur adipiscing elit.
7576 "},
7577 rust_lang.clone(),
7578 &mut cx,
7579 );
7580
7581 // selection w/ single short block comment
7582 assert_rewrap(
7583 indoc! {"
7584 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7585 "},
7586 indoc! {"
7587 «/*
7588 * Lorem ipsum dolor sit amet,
7589 * consectetur adipiscing elit.
7590 */ˇ»
7591 "},
7592 rust_lang.clone(),
7593 &mut cx,
7594 );
7595
7596 // rewrapping a single comment w/ abutting comments
7597 assert_rewrap(
7598 indoc! {"
7599 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
7600 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7601 "},
7602 indoc! {"
7603 /*
7604 * ˇLorem ipsum dolor sit amet,
7605 * consectetur adipiscing elit.
7606 */
7607 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7608 "},
7609 rust_lang.clone(),
7610 &mut cx,
7611 );
7612
7613 // selection w/ non-abutting short block comments
7614 assert_rewrap(
7615 indoc! {"
7616 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7617
7618 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7619 "},
7620 indoc! {"
7621 «/*
7622 * Lorem ipsum dolor sit amet,
7623 * consectetur adipiscing elit.
7624 */
7625
7626 /*
7627 * Lorem ipsum dolor sit amet,
7628 * consectetur adipiscing elit.
7629 */ˇ»
7630 "},
7631 rust_lang.clone(),
7632 &mut cx,
7633 );
7634
7635 // selection of multiline block comments
7636 assert_rewrap(
7637 indoc! {"
7638 «/* Lorem ipsum dolor sit amet,
7639 * consectetur adipiscing elit. */ˇ»
7640 "},
7641 indoc! {"
7642 «/*
7643 * Lorem ipsum dolor sit amet,
7644 * consectetur adipiscing elit.
7645 */ˇ»
7646 "},
7647 rust_lang.clone(),
7648 &mut cx,
7649 );
7650
7651 // partial selection of multiline block comments
7652 assert_rewrap(
7653 indoc! {"
7654 «/* Lorem ipsum dolor sit amet,ˇ»
7655 * consectetur adipiscing elit. */
7656 /* Lorem ipsum dolor sit amet,
7657 «* consectetur adipiscing elit. */ˇ»
7658 "},
7659 indoc! {"
7660 «/*
7661 * Lorem ipsum dolor sit amet,ˇ»
7662 * consectetur adipiscing elit. */
7663 /* Lorem ipsum dolor sit amet,
7664 «* consectetur adipiscing elit.
7665 */ˇ»
7666 "},
7667 rust_lang.clone(),
7668 &mut cx,
7669 );
7670
7671 // selection w/ abutting short block comments
7672 // TODO: should not be combined; should rewrap as 2 comments
7673 assert_rewrap(
7674 indoc! {"
7675 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7676 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7677 "},
7678 // desired behavior:
7679 // indoc! {"
7680 // «/*
7681 // * Lorem ipsum dolor sit amet,
7682 // * consectetur adipiscing elit.
7683 // */
7684 // /*
7685 // * Lorem ipsum dolor sit amet,
7686 // * consectetur adipiscing elit.
7687 // */ˇ»
7688 // "},
7689 // actual behaviour:
7690 indoc! {"
7691 «/*
7692 * Lorem ipsum dolor sit amet,
7693 * consectetur adipiscing elit. Lorem
7694 * ipsum dolor sit amet, consectetur
7695 * adipiscing elit.
7696 */ˇ»
7697 "},
7698 rust_lang.clone(),
7699 &mut cx,
7700 );
7701
7702 // TODO: same as above, but with delimiters on separate line
7703 // assert_rewrap(
7704 // indoc! {"
7705 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7706 // */
7707 // /*
7708 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7709 // "},
7710 // // desired:
7711 // // indoc! {"
7712 // // «/*
7713 // // * Lorem ipsum dolor sit amet,
7714 // // * consectetur adipiscing elit.
7715 // // */
7716 // // /*
7717 // // * Lorem ipsum dolor sit amet,
7718 // // * consectetur adipiscing elit.
7719 // // */ˇ»
7720 // // "},
7721 // // actual: (but with trailing w/s on the empty lines)
7722 // indoc! {"
7723 // «/*
7724 // * Lorem ipsum dolor sit amet,
7725 // * consectetur adipiscing elit.
7726 // *
7727 // */
7728 // /*
7729 // *
7730 // * Lorem ipsum dolor sit amet,
7731 // * consectetur adipiscing elit.
7732 // */ˇ»
7733 // "},
7734 // rust_lang.clone(),
7735 // &mut cx,
7736 // );
7737
7738 // TODO these are unhandled edge cases; not correct, just documenting known issues
7739 assert_rewrap(
7740 indoc! {"
7741 /*
7742 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7743 */
7744 /*
7745 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7746 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
7747 "},
7748 // desired:
7749 // indoc! {"
7750 // /*
7751 // *ˇ Lorem ipsum dolor sit amet,
7752 // * consectetur adipiscing elit.
7753 // */
7754 // /*
7755 // *ˇ Lorem ipsum dolor sit amet,
7756 // * consectetur adipiscing elit.
7757 // */
7758 // /*
7759 // *ˇ Lorem ipsum dolor sit amet
7760 // */ /* consectetur adipiscing elit. */
7761 // "},
7762 // actual:
7763 indoc! {"
7764 /*
7765 //ˇ Lorem ipsum dolor sit amet,
7766 // consectetur adipiscing elit.
7767 */
7768 /*
7769 * //ˇ Lorem ipsum dolor sit amet,
7770 * consectetur adipiscing elit.
7771 */
7772 /*
7773 *ˇ Lorem ipsum dolor sit amet */ /*
7774 * consectetur adipiscing elit.
7775 */
7776 "},
7777 rust_lang,
7778 &mut cx,
7779 );
7780
7781 #[track_caller]
7782 fn assert_rewrap(
7783 unwrapped_text: &str,
7784 wrapped_text: &str,
7785 language: Arc<Language>,
7786 cx: &mut EditorTestContext,
7787 ) {
7788 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
7789 cx.set_state(unwrapped_text);
7790 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
7791 cx.assert_editor_state(wrapped_text);
7792 }
7793}
7794
7795#[gpui::test]
7796async fn test_hard_wrap(cx: &mut TestAppContext) {
7797 init_test(cx, |_| {});
7798 let mut cx = EditorTestContext::new(cx).await;
7799
7800 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
7801 cx.update_editor(|editor, _, cx| {
7802 editor.set_hard_wrap(Some(14), cx);
7803 });
7804
7805 cx.set_state(indoc!(
7806 "
7807 one two three ˇ
7808 "
7809 ));
7810 cx.simulate_input("four");
7811 cx.run_until_parked();
7812
7813 cx.assert_editor_state(indoc!(
7814 "
7815 one two three
7816 fourˇ
7817 "
7818 ));
7819
7820 cx.update_editor(|editor, window, cx| {
7821 editor.newline(&Default::default(), window, cx);
7822 });
7823 cx.run_until_parked();
7824 cx.assert_editor_state(indoc!(
7825 "
7826 one two three
7827 four
7828 ˇ
7829 "
7830 ));
7831
7832 cx.simulate_input("five");
7833 cx.run_until_parked();
7834 cx.assert_editor_state(indoc!(
7835 "
7836 one two three
7837 four
7838 fiveˇ
7839 "
7840 ));
7841
7842 cx.update_editor(|editor, window, cx| {
7843 editor.newline(&Default::default(), window, cx);
7844 });
7845 cx.run_until_parked();
7846 cx.simulate_input("# ");
7847 cx.run_until_parked();
7848 cx.assert_editor_state(indoc!(
7849 "
7850 one two three
7851 four
7852 five
7853 # ˇ
7854 "
7855 ));
7856
7857 cx.update_editor(|editor, window, cx| {
7858 editor.newline(&Default::default(), window, cx);
7859 });
7860 cx.run_until_parked();
7861 cx.assert_editor_state(indoc!(
7862 "
7863 one two three
7864 four
7865 five
7866 #\x20
7867 #ˇ
7868 "
7869 ));
7870
7871 cx.simulate_input(" 6");
7872 cx.run_until_parked();
7873 cx.assert_editor_state(indoc!(
7874 "
7875 one two three
7876 four
7877 five
7878 #
7879 # 6ˇ
7880 "
7881 ));
7882}
7883
7884#[gpui::test]
7885async fn test_cut_line_ends(cx: &mut TestAppContext) {
7886 init_test(cx, |_| {});
7887
7888 let mut cx = EditorTestContext::new(cx).await;
7889
7890 cx.set_state(indoc! {"The quick brownˇ"});
7891 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7892 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7893
7894 cx.set_state(indoc! {"The emacs foxˇ"});
7895 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7896 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7897
7898 cx.set_state(indoc! {"
7899 The quick« brownˇ»
7900 fox jumps overˇ
7901 the lazy dog"});
7902 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7903 cx.assert_editor_state(indoc! {"
7904 The quickˇ
7905 ˇthe lazy dog"});
7906
7907 cx.set_state(indoc! {"
7908 The quick« brownˇ»
7909 fox jumps overˇ
7910 the lazy dog"});
7911 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7912 cx.assert_editor_state(indoc! {"
7913 The quickˇ
7914 fox jumps overˇthe lazy dog"});
7915
7916 cx.set_state(indoc! {"
7917 The quick« brownˇ»
7918 fox jumps overˇ
7919 the lazy dog"});
7920 cx.update_editor(|e, window, cx| {
7921 e.cut_to_end_of_line(
7922 &CutToEndOfLine {
7923 stop_at_newlines: true,
7924 },
7925 window,
7926 cx,
7927 )
7928 });
7929 cx.assert_editor_state(indoc! {"
7930 The quickˇ
7931 fox jumps overˇ
7932 the lazy dog"});
7933
7934 cx.set_state(indoc! {"
7935 The quick« brownˇ»
7936 fox jumps overˇ
7937 the lazy dog"});
7938 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7939 cx.assert_editor_state(indoc! {"
7940 The quickˇ
7941 fox jumps overˇthe lazy dog"});
7942}
7943
7944#[gpui::test]
7945async fn test_clipboard(cx: &mut TestAppContext) {
7946 init_test(cx, |_| {});
7947
7948 let mut cx = EditorTestContext::new(cx).await;
7949
7950 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7951 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7952 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7953
7954 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7955 cx.set_state("two ˇfour ˇsix ˇ");
7956 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7957 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7958
7959 // Paste again but with only two cursors. Since the number of cursors doesn't
7960 // match the number of slices in the clipboard, the entire clipboard text
7961 // is pasted at each cursor.
7962 cx.set_state("ˇtwo one✅ four three six five ˇ");
7963 cx.update_editor(|e, window, cx| {
7964 e.handle_input("( ", window, cx);
7965 e.paste(&Paste, window, cx);
7966 e.handle_input(") ", window, cx);
7967 });
7968 cx.assert_editor_state(
7969 &([
7970 "( one✅ ",
7971 "three ",
7972 "five ) ˇtwo one✅ four three six five ( one✅ ",
7973 "three ",
7974 "five ) ˇ",
7975 ]
7976 .join("\n")),
7977 );
7978
7979 // Cut with three selections, one of which is full-line.
7980 cx.set_state(indoc! {"
7981 1«2ˇ»3
7982 4ˇ567
7983 «8ˇ»9"});
7984 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7985 cx.assert_editor_state(indoc! {"
7986 1ˇ3
7987 ˇ9"});
7988
7989 // Paste with three selections, noticing how the copied selection that was full-line
7990 // gets inserted before the second cursor.
7991 cx.set_state(indoc! {"
7992 1ˇ3
7993 9ˇ
7994 «oˇ»ne"});
7995 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7996 cx.assert_editor_state(indoc! {"
7997 12ˇ3
7998 4567
7999 9ˇ
8000 8ˇne"});
8001
8002 // Copy with a single cursor only, which writes the whole line into the clipboard.
8003 cx.set_state(indoc! {"
8004 The quick brown
8005 fox juˇmps over
8006 the lazy dog"});
8007 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
8008 assert_eq!(
8009 cx.read_from_clipboard()
8010 .and_then(|item| item.text().as_deref().map(str::to_string)),
8011 Some("fox jumps over\n".to_string())
8012 );
8013
8014 // Paste with three selections, noticing how the copied full-line selection is inserted
8015 // before the empty selections but replaces the selection that is non-empty.
8016 cx.set_state(indoc! {"
8017 Tˇhe quick brown
8018 «foˇ»x jumps over
8019 tˇhe lazy dog"});
8020 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8021 cx.assert_editor_state(indoc! {"
8022 fox jumps over
8023 Tˇhe quick brown
8024 fox jumps over
8025 ˇx jumps over
8026 fox jumps over
8027 tˇhe lazy dog"});
8028}
8029
8030#[gpui::test]
8031async fn test_copy_trim(cx: &mut TestAppContext) {
8032 init_test(cx, |_| {});
8033
8034 let mut cx = EditorTestContext::new(cx).await;
8035 cx.set_state(
8036 r#" «for selection in selections.iter() {
8037 let mut start = selection.start;
8038 let mut end = selection.end;
8039 let is_entire_line = selection.is_empty();
8040 if is_entire_line {
8041 start = Point::new(start.row, 0);ˇ»
8042 end = cmp::min(max_point, Point::new(end.row + 1, 0));
8043 }
8044 "#,
8045 );
8046 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
8047 assert_eq!(
8048 cx.read_from_clipboard()
8049 .and_then(|item| item.text().as_deref().map(str::to_string)),
8050 Some(
8051 "for selection in selections.iter() {
8052 let mut start = selection.start;
8053 let mut end = selection.end;
8054 let is_entire_line = selection.is_empty();
8055 if is_entire_line {
8056 start = Point::new(start.row, 0);"
8057 .to_string()
8058 ),
8059 "Regular copying preserves all indentation selected",
8060 );
8061 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
8062 assert_eq!(
8063 cx.read_from_clipboard()
8064 .and_then(|item| item.text().as_deref().map(str::to_string)),
8065 Some(
8066 "for selection in selections.iter() {
8067let mut start = selection.start;
8068let mut end = selection.end;
8069let is_entire_line = selection.is_empty();
8070if is_entire_line {
8071 start = Point::new(start.row, 0);"
8072 .to_string()
8073 ),
8074 "Copying with stripping should strip all leading whitespaces"
8075 );
8076
8077 cx.set_state(
8078 r#" « for selection in selections.iter() {
8079 let mut start = selection.start;
8080 let mut end = selection.end;
8081 let is_entire_line = selection.is_empty();
8082 if is_entire_line {
8083 start = Point::new(start.row, 0);ˇ»
8084 end = cmp::min(max_point, Point::new(end.row + 1, 0));
8085 }
8086 "#,
8087 );
8088 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
8089 assert_eq!(
8090 cx.read_from_clipboard()
8091 .and_then(|item| item.text().as_deref().map(str::to_string)),
8092 Some(
8093 " for selection in selections.iter() {
8094 let mut start = selection.start;
8095 let mut end = selection.end;
8096 let is_entire_line = selection.is_empty();
8097 if is_entire_line {
8098 start = Point::new(start.row, 0);"
8099 .to_string()
8100 ),
8101 "Regular copying preserves all indentation selected",
8102 );
8103 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
8104 assert_eq!(
8105 cx.read_from_clipboard()
8106 .and_then(|item| item.text().as_deref().map(str::to_string)),
8107 Some(
8108 "for selection in selections.iter() {
8109let mut start = selection.start;
8110let mut end = selection.end;
8111let is_entire_line = selection.is_empty();
8112if is_entire_line {
8113 start = Point::new(start.row, 0);"
8114 .to_string()
8115 ),
8116 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
8117 );
8118
8119 cx.set_state(
8120 r#" «ˇ for selection in selections.iter() {
8121 let mut start = selection.start;
8122 let mut end = selection.end;
8123 let is_entire_line = selection.is_empty();
8124 if is_entire_line {
8125 start = Point::new(start.row, 0);»
8126 end = cmp::min(max_point, Point::new(end.row + 1, 0));
8127 }
8128 "#,
8129 );
8130 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
8131 assert_eq!(
8132 cx.read_from_clipboard()
8133 .and_then(|item| item.text().as_deref().map(str::to_string)),
8134 Some(
8135 " for selection in selections.iter() {
8136 let mut start = selection.start;
8137 let mut end = selection.end;
8138 let is_entire_line = selection.is_empty();
8139 if is_entire_line {
8140 start = Point::new(start.row, 0);"
8141 .to_string()
8142 ),
8143 "Regular copying for reverse selection works the same",
8144 );
8145 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
8146 assert_eq!(
8147 cx.read_from_clipboard()
8148 .and_then(|item| item.text().as_deref().map(str::to_string)),
8149 Some(
8150 "for selection in selections.iter() {
8151let mut start = selection.start;
8152let mut end = selection.end;
8153let is_entire_line = selection.is_empty();
8154if is_entire_line {
8155 start = Point::new(start.row, 0);"
8156 .to_string()
8157 ),
8158 "Copying with stripping for reverse selection works the same"
8159 );
8160
8161 cx.set_state(
8162 r#" for selection «in selections.iter() {
8163 let mut start = selection.start;
8164 let mut end = selection.end;
8165 let is_entire_line = selection.is_empty();
8166 if is_entire_line {
8167 start = Point::new(start.row, 0);ˇ»
8168 end = cmp::min(max_point, Point::new(end.row + 1, 0));
8169 }
8170 "#,
8171 );
8172 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
8173 assert_eq!(
8174 cx.read_from_clipboard()
8175 .and_then(|item| item.text().as_deref().map(str::to_string)),
8176 Some(
8177 "in selections.iter() {
8178 let mut start = selection.start;
8179 let mut end = selection.end;
8180 let is_entire_line = selection.is_empty();
8181 if is_entire_line {
8182 start = Point::new(start.row, 0);"
8183 .to_string()
8184 ),
8185 "When selecting past the indent, the copying works as usual",
8186 );
8187 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
8188 assert_eq!(
8189 cx.read_from_clipboard()
8190 .and_then(|item| item.text().as_deref().map(str::to_string)),
8191 Some(
8192 "in selections.iter() {
8193 let mut start = selection.start;
8194 let mut end = selection.end;
8195 let is_entire_line = selection.is_empty();
8196 if is_entire_line {
8197 start = Point::new(start.row, 0);"
8198 .to_string()
8199 ),
8200 "When selecting past the indent, nothing is trimmed"
8201 );
8202
8203 cx.set_state(
8204 r#" «for selection in selections.iter() {
8205 let mut start = selection.start;
8206
8207 let mut end = selection.end;
8208 let is_entire_line = selection.is_empty();
8209 if is_entire_line {
8210 start = Point::new(start.row, 0);
8211ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
8212 }
8213 "#,
8214 );
8215 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
8216 assert_eq!(
8217 cx.read_from_clipboard()
8218 .and_then(|item| item.text().as_deref().map(str::to_string)),
8219 Some(
8220 "for selection in selections.iter() {
8221let mut start = selection.start;
8222
8223let mut end = selection.end;
8224let is_entire_line = selection.is_empty();
8225if is_entire_line {
8226 start = Point::new(start.row, 0);
8227"
8228 .to_string()
8229 ),
8230 "Copying with stripping should ignore empty lines"
8231 );
8232}
8233
8234#[gpui::test]
8235async fn test_copy_trim_line_mode(cx: &mut TestAppContext) {
8236 init_test(cx, |_| {});
8237
8238 let mut cx = EditorTestContext::new(cx).await;
8239
8240 cx.set_state(indoc! {"
8241 « fn main() {
8242 1
8243 }ˇ»
8244 "});
8245 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
8246 cx.update_editor(|editor, window, cx| editor.copy_and_trim(&CopyAndTrim, window, cx));
8247
8248 assert_eq!(
8249 cx.read_from_clipboard().and_then(|item| item.text()),
8250 Some("fn main() {\n 1\n}\n".to_string())
8251 );
8252
8253 let clipboard_selections: Vec<ClipboardSelection> = cx
8254 .read_from_clipboard()
8255 .and_then(|item| item.entries().first().cloned())
8256 .and_then(|entry| match entry {
8257 gpui::ClipboardEntry::String(text) => text.metadata_json(),
8258 _ => None,
8259 })
8260 .expect("should have clipboard selections");
8261
8262 assert_eq!(clipboard_selections.len(), 1);
8263 assert!(clipboard_selections[0].is_entire_line);
8264
8265 cx.set_state(indoc! {"
8266 «fn main() {
8267 1
8268 }ˇ»
8269 "});
8270 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
8271 cx.update_editor(|editor, window, cx| editor.copy_and_trim(&CopyAndTrim, window, cx));
8272
8273 assert_eq!(
8274 cx.read_from_clipboard().and_then(|item| item.text()),
8275 Some("fn main() {\n 1\n}\n".to_string())
8276 );
8277
8278 let clipboard_selections: Vec<ClipboardSelection> = cx
8279 .read_from_clipboard()
8280 .and_then(|item| item.entries().first().cloned())
8281 .and_then(|entry| match entry {
8282 gpui::ClipboardEntry::String(text) => text.metadata_json(),
8283 _ => None,
8284 })
8285 .expect("should have clipboard selections");
8286
8287 assert_eq!(clipboard_selections.len(), 1);
8288 assert!(clipboard_selections[0].is_entire_line);
8289}
8290
8291#[gpui::test]
8292async fn test_clipboard_line_numbers_from_multibuffer(cx: &mut TestAppContext) {
8293 init_test(cx, |_| {});
8294
8295 let fs = FakeFs::new(cx.executor());
8296 fs.insert_file(
8297 path!("/file.txt"),
8298 "first line\nsecond line\nthird line\nfourth line\nfifth line\n".into(),
8299 )
8300 .await;
8301
8302 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
8303
8304 let buffer = project
8305 .update(cx, |project, cx| {
8306 project.open_local_buffer(path!("/file.txt"), cx)
8307 })
8308 .await
8309 .unwrap();
8310
8311 let multibuffer = cx.new(|cx| {
8312 let mut multibuffer = MultiBuffer::new(ReadWrite);
8313 multibuffer.set_excerpts_for_path(
8314 PathKey::sorted(0),
8315 buffer.clone(),
8316 [Point::new(2, 0)..Point::new(5, 0)],
8317 0,
8318 cx,
8319 );
8320 multibuffer
8321 });
8322
8323 let (editor, cx) = cx.add_window_view(|window, cx| {
8324 build_editor_with_project(project.clone(), multibuffer, window, cx)
8325 });
8326
8327 editor.update_in(cx, |editor, window, cx| {
8328 assert_eq!(editor.text(cx), "third line\nfourth line\nfifth line\n");
8329
8330 editor.select_all(&SelectAll, window, cx);
8331 editor.copy(&Copy, window, cx);
8332 });
8333
8334 let clipboard_selections: Option<Vec<ClipboardSelection>> = cx
8335 .read_from_clipboard()
8336 .and_then(|item| item.entries().first().cloned())
8337 .and_then(|entry| match entry {
8338 gpui::ClipboardEntry::String(text) => text.metadata_json(),
8339 _ => None,
8340 });
8341
8342 let selections = clipboard_selections.expect("should have clipboard selections");
8343 assert_eq!(selections.len(), 1);
8344 let selection = &selections[0];
8345 assert_eq!(
8346 selection.line_range,
8347 Some(2..=5),
8348 "line range should be from original file (rows 2-5), not multibuffer rows (0-2)"
8349 );
8350}
8351
8352#[gpui::test]
8353async fn test_paste_multiline(cx: &mut TestAppContext) {
8354 init_test(cx, |_| {});
8355
8356 let mut cx = EditorTestContext::new(cx).await;
8357 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8358
8359 // Cut an indented block, without the leading whitespace.
8360 cx.set_state(indoc! {"
8361 const a: B = (
8362 c(),
8363 «d(
8364 e,
8365 f
8366 )ˇ»
8367 );
8368 "});
8369 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
8370 cx.assert_editor_state(indoc! {"
8371 const a: B = (
8372 c(),
8373 ˇ
8374 );
8375 "});
8376
8377 // Paste it at the same position.
8378 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8379 cx.assert_editor_state(indoc! {"
8380 const a: B = (
8381 c(),
8382 d(
8383 e,
8384 f
8385 )ˇ
8386 );
8387 "});
8388
8389 // Paste it at a line with a lower indent level.
8390 cx.set_state(indoc! {"
8391 ˇ
8392 const a: B = (
8393 c(),
8394 );
8395 "});
8396 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8397 cx.assert_editor_state(indoc! {"
8398 d(
8399 e,
8400 f
8401 )ˇ
8402 const a: B = (
8403 c(),
8404 );
8405 "});
8406
8407 // Cut an indented block, with the leading whitespace.
8408 cx.set_state(indoc! {"
8409 const a: B = (
8410 c(),
8411 « d(
8412 e,
8413 f
8414 )
8415 ˇ»);
8416 "});
8417 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
8418 cx.assert_editor_state(indoc! {"
8419 const a: B = (
8420 c(),
8421 ˇ);
8422 "});
8423
8424 // Paste it at the same position.
8425 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8426 cx.assert_editor_state(indoc! {"
8427 const a: B = (
8428 c(),
8429 d(
8430 e,
8431 f
8432 )
8433 ˇ);
8434 "});
8435
8436 // Paste it at a line with a higher indent level.
8437 cx.set_state(indoc! {"
8438 const a: B = (
8439 c(),
8440 d(
8441 e,
8442 fˇ
8443 )
8444 );
8445 "});
8446 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8447 cx.assert_editor_state(indoc! {"
8448 const a: B = (
8449 c(),
8450 d(
8451 e,
8452 f d(
8453 e,
8454 f
8455 )
8456 ˇ
8457 )
8458 );
8459 "});
8460
8461 // Copy an indented block, starting mid-line
8462 cx.set_state(indoc! {"
8463 const a: B = (
8464 c(),
8465 somethin«g(
8466 e,
8467 f
8468 )ˇ»
8469 );
8470 "});
8471 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
8472
8473 // Paste it on a line with a lower indent level
8474 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
8475 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8476 cx.assert_editor_state(indoc! {"
8477 const a: B = (
8478 c(),
8479 something(
8480 e,
8481 f
8482 )
8483 );
8484 g(
8485 e,
8486 f
8487 )ˇ"});
8488}
8489
8490#[gpui::test]
8491async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
8492 init_test(cx, |_| {});
8493
8494 cx.write_to_clipboard(ClipboardItem::new_string(
8495 " d(\n e\n );\n".into(),
8496 ));
8497
8498 let mut cx = EditorTestContext::new(cx).await;
8499 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8500 cx.run_until_parked();
8501
8502 cx.set_state(indoc! {"
8503 fn a() {
8504 b();
8505 if c() {
8506 ˇ
8507 }
8508 }
8509 "});
8510
8511 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8512 cx.assert_editor_state(indoc! {"
8513 fn a() {
8514 b();
8515 if c() {
8516 d(
8517 e
8518 );
8519 ˇ
8520 }
8521 }
8522 "});
8523
8524 cx.set_state(indoc! {"
8525 fn a() {
8526 b();
8527 ˇ
8528 }
8529 "});
8530
8531 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8532 cx.assert_editor_state(indoc! {"
8533 fn a() {
8534 b();
8535 d(
8536 e
8537 );
8538 ˇ
8539 }
8540 "});
8541}
8542
8543#[gpui::test]
8544async fn test_paste_multiline_from_other_app_into_matching_cursors(cx: &mut TestAppContext) {
8545 init_test(cx, |_| {});
8546
8547 cx.write_to_clipboard(ClipboardItem::new_string("alpha\nbeta\ngamma".into()));
8548
8549 let mut cx = EditorTestContext::new(cx).await;
8550
8551 // Paste into 3 cursors: each cursor should receive one line.
8552 cx.set_state("ˇ one ˇ two ˇ three");
8553 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8554 cx.assert_editor_state("alphaˇ one betaˇ two gammaˇ three");
8555
8556 // Paste into 2 cursors: line count doesn't match, so paste entire text at each cursor.
8557 cx.write_to_clipboard(ClipboardItem::new_string("alpha\nbeta\ngamma".into()));
8558 cx.set_state("ˇ one ˇ two");
8559 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8560 cx.assert_editor_state("alpha\nbeta\ngammaˇ one alpha\nbeta\ngammaˇ two");
8561
8562 // Paste into a single cursor: should paste everything as-is.
8563 cx.write_to_clipboard(ClipboardItem::new_string("alpha\nbeta\ngamma".into()));
8564 cx.set_state("ˇ one");
8565 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8566 cx.assert_editor_state("alpha\nbeta\ngammaˇ one");
8567
8568 // Paste with selections: each selection is replaced with its corresponding line.
8569 cx.write_to_clipboard(ClipboardItem::new_string("xx\nyy\nzz".into()));
8570 cx.set_state("«aˇ» one «bˇ» two «cˇ» three");
8571 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8572 cx.assert_editor_state("xxˇ one yyˇ two zzˇ three");
8573}
8574
8575#[gpui::test]
8576fn test_select_all(cx: &mut TestAppContext) {
8577 init_test(cx, |_| {});
8578
8579 let editor = cx.add_window(|window, cx| {
8580 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
8581 build_editor(buffer, window, cx)
8582 });
8583 _ = editor.update(cx, |editor, window, cx| {
8584 editor.select_all(&SelectAll, window, cx);
8585 assert_eq!(
8586 display_ranges(editor, cx),
8587 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
8588 );
8589 });
8590}
8591
8592#[gpui::test]
8593fn test_select_line(cx: &mut TestAppContext) {
8594 init_test(cx, |_| {});
8595
8596 let editor = cx.add_window(|window, cx| {
8597 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
8598 build_editor(buffer, window, cx)
8599 });
8600 _ = editor.update(cx, |editor, window, cx| {
8601 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8602 s.select_display_ranges([
8603 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8604 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
8605 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
8606 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
8607 ])
8608 });
8609 editor.select_line(&SelectLine, window, cx);
8610 // Adjacent line selections should NOT merge (only overlapping ones do)
8611 assert_eq!(
8612 display_ranges(editor, cx),
8613 vec![
8614 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0),
8615 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(2), 0),
8616 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
8617 ]
8618 );
8619 });
8620
8621 _ = editor.update(cx, |editor, window, cx| {
8622 editor.select_line(&SelectLine, window, cx);
8623 assert_eq!(
8624 display_ranges(editor, cx),
8625 vec![
8626 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
8627 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
8628 ]
8629 );
8630 });
8631
8632 _ = editor.update(cx, |editor, window, cx| {
8633 editor.select_line(&SelectLine, window, cx);
8634 // Adjacent but not overlapping, so they stay separate
8635 assert_eq!(
8636 display_ranges(editor, cx),
8637 vec![
8638 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(4), 0),
8639 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
8640 ]
8641 );
8642 });
8643}
8644
8645#[gpui::test]
8646async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
8647 init_test(cx, |_| {});
8648 let mut cx = EditorTestContext::new(cx).await;
8649
8650 #[track_caller]
8651 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
8652 cx.set_state(initial_state);
8653 cx.update_editor(|e, window, cx| {
8654 e.split_selection_into_lines(&Default::default(), window, cx)
8655 });
8656 cx.assert_editor_state(expected_state);
8657 }
8658
8659 // Selection starts and ends at the middle of lines, left-to-right
8660 test(
8661 &mut cx,
8662 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
8663 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
8664 );
8665 // Same thing, right-to-left
8666 test(
8667 &mut cx,
8668 "aa\nb«b\ncc\ndd\neˇ»e\nff",
8669 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
8670 );
8671
8672 // Whole buffer, left-to-right, last line *doesn't* end with newline
8673 test(
8674 &mut cx,
8675 "«ˇaa\nbb\ncc\ndd\nee\nff»",
8676 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
8677 );
8678 // Same thing, right-to-left
8679 test(
8680 &mut cx,
8681 "«aa\nbb\ncc\ndd\nee\nffˇ»",
8682 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
8683 );
8684
8685 // Whole buffer, left-to-right, last line ends with newline
8686 test(
8687 &mut cx,
8688 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
8689 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
8690 );
8691 // Same thing, right-to-left
8692 test(
8693 &mut cx,
8694 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
8695 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
8696 );
8697
8698 // Starts at the end of a line, ends at the start of another
8699 test(
8700 &mut cx,
8701 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
8702 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
8703 );
8704}
8705
8706#[gpui::test]
8707async fn test_split_selection_into_lines_does_not_scroll(cx: &mut TestAppContext) {
8708 init_test(cx, |_| {});
8709 let mut cx = EditorTestContext::new(cx).await;
8710
8711 let large_body = "\nline".repeat(300);
8712 cx.set_state(&format!("«ˇstart{large_body}\nend»"));
8713 let initial_scroll_position = cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8714
8715 cx.update_editor(|editor, window, cx| {
8716 editor.split_selection_into_lines(&Default::default(), window, cx);
8717 });
8718
8719 let scroll_position_after_split = cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8720 assert_eq!(
8721 initial_scroll_position, scroll_position_after_split,
8722 "Scroll position should not change after splitting selection into lines"
8723 );
8724}
8725
8726#[gpui::test]
8727async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
8728 init_test(cx, |_| {});
8729
8730 let editor = cx.add_window(|window, cx| {
8731 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
8732 build_editor(buffer, window, cx)
8733 });
8734
8735 // setup
8736 _ = editor.update(cx, |editor, window, cx| {
8737 editor.fold_creases(
8738 vec![
8739 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
8740 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
8741 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
8742 ],
8743 true,
8744 window,
8745 cx,
8746 );
8747 assert_eq!(
8748 editor.display_text(cx),
8749 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
8750 );
8751 });
8752
8753 _ = editor.update(cx, |editor, window, cx| {
8754 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8755 s.select_display_ranges([
8756 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8757 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
8758 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
8759 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
8760 ])
8761 });
8762 editor.split_selection_into_lines(&Default::default(), window, cx);
8763 assert_eq!(
8764 editor.display_text(cx),
8765 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
8766 );
8767 });
8768 EditorTestContext::for_editor(editor, cx)
8769 .await
8770 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
8771
8772 _ = editor.update(cx, |editor, window, cx| {
8773 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8774 s.select_display_ranges([
8775 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
8776 ])
8777 });
8778 editor.split_selection_into_lines(&Default::default(), window, cx);
8779 assert_eq!(
8780 editor.display_text(cx),
8781 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
8782 );
8783 assert_eq!(
8784 display_ranges(editor, cx),
8785 [
8786 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
8787 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
8788 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
8789 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
8790 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
8791 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
8792 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
8793 ]
8794 );
8795 });
8796 EditorTestContext::for_editor(editor, cx)
8797 .await
8798 .assert_editor_state(
8799 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
8800 );
8801}
8802
8803#[gpui::test]
8804async fn test_add_selection_above_below(cx: &mut TestAppContext) {
8805 init_test(cx, |_| {});
8806
8807 let mut cx = EditorTestContext::new(cx).await;
8808
8809 cx.set_state(indoc!(
8810 r#"abc
8811 defˇghi
8812
8813 jk
8814 nlmo
8815 "#
8816 ));
8817
8818 cx.update_editor(|editor, window, cx| {
8819 editor.add_selection_above(&Default::default(), window, cx);
8820 });
8821
8822 cx.assert_editor_state(indoc!(
8823 r#"abcˇ
8824 defˇghi
8825
8826 jk
8827 nlmo
8828 "#
8829 ));
8830
8831 cx.update_editor(|editor, window, cx| {
8832 editor.add_selection_above(&Default::default(), window, cx);
8833 });
8834
8835 cx.assert_editor_state(indoc!(
8836 r#"abcˇ
8837 defˇghi
8838
8839 jk
8840 nlmo
8841 "#
8842 ));
8843
8844 cx.update_editor(|editor, window, cx| {
8845 editor.add_selection_below(&Default::default(), window, cx);
8846 });
8847
8848 cx.assert_editor_state(indoc!(
8849 r#"abc
8850 defˇghi
8851
8852 jk
8853 nlmo
8854 "#
8855 ));
8856
8857 cx.update_editor(|editor, window, cx| {
8858 editor.undo_selection(&Default::default(), window, cx);
8859 });
8860
8861 cx.assert_editor_state(indoc!(
8862 r#"abcˇ
8863 defˇghi
8864
8865 jk
8866 nlmo
8867 "#
8868 ));
8869
8870 cx.update_editor(|editor, window, cx| {
8871 editor.redo_selection(&Default::default(), window, cx);
8872 });
8873
8874 cx.assert_editor_state(indoc!(
8875 r#"abc
8876 defˇghi
8877
8878 jk
8879 nlmo
8880 "#
8881 ));
8882
8883 cx.update_editor(|editor, window, cx| {
8884 editor.add_selection_below(&Default::default(), window, cx);
8885 });
8886
8887 cx.assert_editor_state(indoc!(
8888 r#"abc
8889 defˇghi
8890 ˇ
8891 jk
8892 nlmo
8893 "#
8894 ));
8895
8896 cx.update_editor(|editor, window, cx| {
8897 editor.add_selection_below(&Default::default(), window, cx);
8898 });
8899
8900 cx.assert_editor_state(indoc!(
8901 r#"abc
8902 defˇghi
8903 ˇ
8904 jkˇ
8905 nlmo
8906 "#
8907 ));
8908
8909 cx.update_editor(|editor, window, cx| {
8910 editor.add_selection_below(&Default::default(), window, cx);
8911 });
8912
8913 cx.assert_editor_state(indoc!(
8914 r#"abc
8915 defˇghi
8916 ˇ
8917 jkˇ
8918 nlmˇo
8919 "#
8920 ));
8921
8922 cx.update_editor(|editor, window, cx| {
8923 editor.add_selection_below(&Default::default(), window, cx);
8924 });
8925
8926 cx.assert_editor_state(indoc!(
8927 r#"abc
8928 defˇghi
8929 ˇ
8930 jkˇ
8931 nlmˇo
8932 ˇ"#
8933 ));
8934
8935 // change selections
8936 cx.set_state(indoc!(
8937 r#"abc
8938 def«ˇg»hi
8939
8940 jk
8941 nlmo
8942 "#
8943 ));
8944
8945 cx.update_editor(|editor, window, cx| {
8946 editor.add_selection_below(&Default::default(), window, cx);
8947 });
8948
8949 cx.assert_editor_state(indoc!(
8950 r#"abc
8951 def«ˇg»hi
8952
8953 jk
8954 nlm«ˇo»
8955 "#
8956 ));
8957
8958 cx.update_editor(|editor, window, cx| {
8959 editor.add_selection_below(&Default::default(), window, cx);
8960 });
8961
8962 cx.assert_editor_state(indoc!(
8963 r#"abc
8964 def«ˇg»hi
8965
8966 jk
8967 nlm«ˇo»
8968 "#
8969 ));
8970
8971 cx.update_editor(|editor, window, cx| {
8972 editor.add_selection_above(&Default::default(), window, cx);
8973 });
8974
8975 cx.assert_editor_state(indoc!(
8976 r#"abc
8977 def«ˇg»hi
8978
8979 jk
8980 nlmo
8981 "#
8982 ));
8983
8984 cx.update_editor(|editor, window, cx| {
8985 editor.add_selection_above(&Default::default(), window, cx);
8986 });
8987
8988 cx.assert_editor_state(indoc!(
8989 r#"abc
8990 def«ˇg»hi
8991
8992 jk
8993 nlmo
8994 "#
8995 ));
8996
8997 // Change selections again
8998 cx.set_state(indoc!(
8999 r#"a«bc
9000 defgˇ»hi
9001
9002 jk
9003 nlmo
9004 "#
9005 ));
9006
9007 cx.update_editor(|editor, window, cx| {
9008 editor.add_selection_below(&Default::default(), window, cx);
9009 });
9010
9011 cx.assert_editor_state(indoc!(
9012 r#"a«bcˇ»
9013 d«efgˇ»hi
9014
9015 j«kˇ»
9016 nlmo
9017 "#
9018 ));
9019
9020 cx.update_editor(|editor, window, cx| {
9021 editor.add_selection_below(&Default::default(), window, cx);
9022 });
9023 cx.assert_editor_state(indoc!(
9024 r#"a«bcˇ»
9025 d«efgˇ»hi
9026
9027 j«kˇ»
9028 n«lmoˇ»
9029 "#
9030 ));
9031 cx.update_editor(|editor, window, cx| {
9032 editor.add_selection_above(&Default::default(), window, cx);
9033 });
9034
9035 cx.assert_editor_state(indoc!(
9036 r#"a«bcˇ»
9037 d«efgˇ»hi
9038
9039 j«kˇ»
9040 nlmo
9041 "#
9042 ));
9043
9044 // Change selections again
9045 cx.set_state(indoc!(
9046 r#"abc
9047 d«ˇefghi
9048
9049 jk
9050 nlm»o
9051 "#
9052 ));
9053
9054 cx.update_editor(|editor, window, cx| {
9055 editor.add_selection_above(&Default::default(), window, cx);
9056 });
9057
9058 cx.assert_editor_state(indoc!(
9059 r#"a«ˇbc»
9060 d«ˇef»ghi
9061
9062 j«ˇk»
9063 n«ˇlm»o
9064 "#
9065 ));
9066
9067 cx.update_editor(|editor, window, cx| {
9068 editor.add_selection_below(&Default::default(), window, cx);
9069 });
9070
9071 cx.assert_editor_state(indoc!(
9072 r#"abc
9073 d«ˇef»ghi
9074
9075 j«ˇk»
9076 n«ˇlm»o
9077 "#
9078 ));
9079
9080 // Assert that the oldest selection's goal column is used when adding more
9081 // selections, not the most recently added selection's actual column.
9082 cx.set_state(indoc! {"
9083 foo bar bazˇ
9084 foo
9085 foo bar
9086 "});
9087
9088 cx.update_editor(|editor, window, cx| {
9089 editor.add_selection_below(
9090 &AddSelectionBelow {
9091 skip_soft_wrap: true,
9092 },
9093 window,
9094 cx,
9095 );
9096 });
9097
9098 cx.assert_editor_state(indoc! {"
9099 foo bar bazˇ
9100 fooˇ
9101 foo bar
9102 "});
9103
9104 cx.update_editor(|editor, window, cx| {
9105 editor.add_selection_below(
9106 &AddSelectionBelow {
9107 skip_soft_wrap: true,
9108 },
9109 window,
9110 cx,
9111 );
9112 });
9113
9114 cx.assert_editor_state(indoc! {"
9115 foo bar bazˇ
9116 fooˇ
9117 foo barˇ
9118 "});
9119
9120 cx.set_state(indoc! {"
9121 foo bar baz
9122 foo
9123 foo barˇ
9124 "});
9125
9126 cx.update_editor(|editor, window, cx| {
9127 editor.add_selection_above(
9128 &AddSelectionAbove {
9129 skip_soft_wrap: true,
9130 },
9131 window,
9132 cx,
9133 );
9134 });
9135
9136 cx.assert_editor_state(indoc! {"
9137 foo bar baz
9138 fooˇ
9139 foo barˇ
9140 "});
9141
9142 cx.update_editor(|editor, window, cx| {
9143 editor.add_selection_above(
9144 &AddSelectionAbove {
9145 skip_soft_wrap: true,
9146 },
9147 window,
9148 cx,
9149 );
9150 });
9151
9152 cx.assert_editor_state(indoc! {"
9153 foo barˇ baz
9154 fooˇ
9155 foo barˇ
9156 "});
9157}
9158
9159#[gpui::test]
9160async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
9161 init_test(cx, |_| {});
9162 let mut cx = EditorTestContext::new(cx).await;
9163
9164 cx.set_state(indoc!(
9165 r#"line onˇe
9166 liˇne two
9167 line three
9168 line four"#
9169 ));
9170
9171 cx.update_editor(|editor, window, cx| {
9172 editor.add_selection_below(&Default::default(), window, cx);
9173 });
9174
9175 // test multiple cursors expand in the same direction
9176 cx.assert_editor_state(indoc!(
9177 r#"line onˇe
9178 liˇne twˇo
9179 liˇne three
9180 line four"#
9181 ));
9182
9183 cx.update_editor(|editor, window, cx| {
9184 editor.add_selection_below(&Default::default(), window, cx);
9185 });
9186
9187 cx.update_editor(|editor, window, cx| {
9188 editor.add_selection_below(&Default::default(), window, cx);
9189 });
9190
9191 // test multiple cursors expand below overflow
9192 cx.assert_editor_state(indoc!(
9193 r#"line onˇe
9194 liˇne twˇo
9195 liˇne thˇree
9196 liˇne foˇur"#
9197 ));
9198
9199 cx.update_editor(|editor, window, cx| {
9200 editor.add_selection_above(&Default::default(), window, cx);
9201 });
9202
9203 // test multiple cursors retrieves back correctly
9204 cx.assert_editor_state(indoc!(
9205 r#"line onˇe
9206 liˇne twˇo
9207 liˇne thˇree
9208 line four"#
9209 ));
9210
9211 cx.update_editor(|editor, window, cx| {
9212 editor.add_selection_above(&Default::default(), window, cx);
9213 });
9214
9215 cx.update_editor(|editor, window, cx| {
9216 editor.add_selection_above(&Default::default(), window, cx);
9217 });
9218
9219 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
9220 cx.assert_editor_state(indoc!(
9221 r#"liˇne onˇe
9222 liˇne two
9223 line three
9224 line four"#
9225 ));
9226
9227 cx.update_editor(|editor, window, cx| {
9228 editor.undo_selection(&Default::default(), window, cx);
9229 });
9230
9231 // test undo
9232 cx.assert_editor_state(indoc!(
9233 r#"line onˇe
9234 liˇne twˇo
9235 line three
9236 line four"#
9237 ));
9238
9239 cx.update_editor(|editor, window, cx| {
9240 editor.redo_selection(&Default::default(), window, cx);
9241 });
9242
9243 // test redo
9244 cx.assert_editor_state(indoc!(
9245 r#"liˇne onˇe
9246 liˇne two
9247 line three
9248 line four"#
9249 ));
9250
9251 cx.set_state(indoc!(
9252 r#"abcd
9253 ef«ghˇ»
9254 ijkl
9255 «mˇ»nop"#
9256 ));
9257
9258 cx.update_editor(|editor, window, cx| {
9259 editor.add_selection_above(&Default::default(), window, cx);
9260 });
9261
9262 // test multiple selections expand in the same direction
9263 cx.assert_editor_state(indoc!(
9264 r#"ab«cdˇ»
9265 ef«ghˇ»
9266 «iˇ»jkl
9267 «mˇ»nop"#
9268 ));
9269
9270 cx.update_editor(|editor, window, cx| {
9271 editor.add_selection_above(&Default::default(), window, cx);
9272 });
9273
9274 // test multiple selection upward overflow
9275 cx.assert_editor_state(indoc!(
9276 r#"ab«cdˇ»
9277 «eˇ»f«ghˇ»
9278 «iˇ»jkl
9279 «mˇ»nop"#
9280 ));
9281
9282 cx.update_editor(|editor, window, cx| {
9283 editor.add_selection_below(&Default::default(), window, cx);
9284 });
9285
9286 // test multiple selection retrieves back correctly
9287 cx.assert_editor_state(indoc!(
9288 r#"abcd
9289 ef«ghˇ»
9290 «iˇ»jkl
9291 «mˇ»nop"#
9292 ));
9293
9294 cx.update_editor(|editor, window, cx| {
9295 editor.add_selection_below(&Default::default(), window, cx);
9296 });
9297
9298 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
9299 cx.assert_editor_state(indoc!(
9300 r#"abcd
9301 ef«ghˇ»
9302 ij«klˇ»
9303 «mˇ»nop"#
9304 ));
9305
9306 cx.update_editor(|editor, window, cx| {
9307 editor.undo_selection(&Default::default(), window, cx);
9308 });
9309
9310 // test undo
9311 cx.assert_editor_state(indoc!(
9312 r#"abcd
9313 ef«ghˇ»
9314 «iˇ»jkl
9315 «mˇ»nop"#
9316 ));
9317
9318 cx.update_editor(|editor, window, cx| {
9319 editor.redo_selection(&Default::default(), window, cx);
9320 });
9321
9322 // test redo
9323 cx.assert_editor_state(indoc!(
9324 r#"abcd
9325 ef«ghˇ»
9326 ij«klˇ»
9327 «mˇ»nop"#
9328 ));
9329}
9330
9331#[gpui::test]
9332async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
9333 init_test(cx, |_| {});
9334 let mut cx = EditorTestContext::new(cx).await;
9335
9336 cx.set_state(indoc!(
9337 r#"line onˇe
9338 liˇne two
9339 line three
9340 line four"#
9341 ));
9342
9343 cx.update_editor(|editor, window, cx| {
9344 editor.add_selection_below(&Default::default(), window, cx);
9345 editor.add_selection_below(&Default::default(), window, cx);
9346 editor.add_selection_below(&Default::default(), window, cx);
9347 });
9348
9349 // initial state with two multi cursor groups
9350 cx.assert_editor_state(indoc!(
9351 r#"line onˇe
9352 liˇne twˇo
9353 liˇne thˇree
9354 liˇne foˇur"#
9355 ));
9356
9357 // add single cursor in middle - simulate opt click
9358 cx.update_editor(|editor, window, cx| {
9359 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
9360 editor.begin_selection(new_cursor_point, true, 1, window, cx);
9361 editor.end_selection(window, cx);
9362 });
9363
9364 cx.assert_editor_state(indoc!(
9365 r#"line onˇe
9366 liˇne twˇo
9367 liˇneˇ thˇree
9368 liˇne foˇur"#
9369 ));
9370
9371 cx.update_editor(|editor, window, cx| {
9372 editor.add_selection_above(&Default::default(), window, cx);
9373 });
9374
9375 // test new added selection expands above and existing selection shrinks
9376 cx.assert_editor_state(indoc!(
9377 r#"line onˇe
9378 liˇneˇ twˇo
9379 liˇneˇ thˇree
9380 line four"#
9381 ));
9382
9383 cx.update_editor(|editor, window, cx| {
9384 editor.add_selection_above(&Default::default(), window, cx);
9385 });
9386
9387 // test new added selection expands above and existing selection shrinks
9388 cx.assert_editor_state(indoc!(
9389 r#"lineˇ onˇe
9390 liˇneˇ twˇo
9391 lineˇ three
9392 line four"#
9393 ));
9394
9395 // intial state with two selection groups
9396 cx.set_state(indoc!(
9397 r#"abcd
9398 ef«ghˇ»
9399 ijkl
9400 «mˇ»nop"#
9401 ));
9402
9403 cx.update_editor(|editor, window, cx| {
9404 editor.add_selection_above(&Default::default(), window, cx);
9405 editor.add_selection_above(&Default::default(), window, cx);
9406 });
9407
9408 cx.assert_editor_state(indoc!(
9409 r#"ab«cdˇ»
9410 «eˇ»f«ghˇ»
9411 «iˇ»jkl
9412 «mˇ»nop"#
9413 ));
9414
9415 // add single selection in middle - simulate opt drag
9416 cx.update_editor(|editor, window, cx| {
9417 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
9418 editor.begin_selection(new_cursor_point, true, 1, window, cx);
9419 editor.update_selection(
9420 DisplayPoint::new(DisplayRow(2), 4),
9421 0,
9422 gpui::Point::<f32>::default(),
9423 window,
9424 cx,
9425 );
9426 editor.end_selection(window, cx);
9427 });
9428
9429 cx.assert_editor_state(indoc!(
9430 r#"ab«cdˇ»
9431 «eˇ»f«ghˇ»
9432 «iˇ»jk«lˇ»
9433 «mˇ»nop"#
9434 ));
9435
9436 cx.update_editor(|editor, window, cx| {
9437 editor.add_selection_below(&Default::default(), window, cx);
9438 });
9439
9440 // test new added selection expands below, others shrinks from above
9441 cx.assert_editor_state(indoc!(
9442 r#"abcd
9443 ef«ghˇ»
9444 «iˇ»jk«lˇ»
9445 «mˇ»no«pˇ»"#
9446 ));
9447}
9448
9449#[gpui::test]
9450async fn test_select_next(cx: &mut TestAppContext) {
9451 init_test(cx, |_| {});
9452 let mut cx = EditorTestContext::new(cx).await;
9453
9454 // Enable case sensitive search.
9455 update_test_editor_settings(&mut cx, &|settings| {
9456 let mut search_settings = SearchSettingsContent::default();
9457 search_settings.case_sensitive = Some(true);
9458 settings.search = Some(search_settings);
9459 });
9460
9461 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
9462
9463 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9464 .unwrap();
9465 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9466
9467 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9468 .unwrap();
9469 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
9470
9471 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9472 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9473
9474 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9475 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
9476
9477 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9478 .unwrap();
9479 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
9480
9481 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9482 .unwrap();
9483 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
9484
9485 // Test selection direction should be preserved
9486 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
9487
9488 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9489 .unwrap();
9490 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
9491
9492 // Test case sensitivity
9493 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
9494 cx.update_editor(|e, window, cx| {
9495 e.select_next(&SelectNext::default(), window, cx).unwrap();
9496 });
9497 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
9498
9499 // Disable case sensitive search.
9500 update_test_editor_settings(&mut cx, &|settings| {
9501 let mut search_settings = SearchSettingsContent::default();
9502 search_settings.case_sensitive = Some(false);
9503 settings.search = Some(search_settings);
9504 });
9505
9506 cx.set_state("«ˇfoo»\nFOO\nFoo");
9507 cx.update_editor(|e, window, cx| {
9508 e.select_next(&SelectNext::default(), window, cx).unwrap();
9509 e.select_next(&SelectNext::default(), window, cx).unwrap();
9510 });
9511 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
9512}
9513
9514#[gpui::test]
9515async fn test_select_all_matches(cx: &mut TestAppContext) {
9516 init_test(cx, |_| {});
9517 let mut cx = EditorTestContext::new(cx).await;
9518
9519 // Enable case sensitive search.
9520 update_test_editor_settings(&mut cx, &|settings| {
9521 let mut search_settings = SearchSettingsContent::default();
9522 search_settings.case_sensitive = Some(true);
9523 settings.search = Some(search_settings);
9524 });
9525
9526 // Test caret-only selections
9527 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
9528 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9529 .unwrap();
9530 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
9531
9532 // Test left-to-right selections
9533 cx.set_state("abc\n«abcˇ»\nabc");
9534 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9535 .unwrap();
9536 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
9537
9538 // Test right-to-left selections
9539 cx.set_state("abc\n«ˇabc»\nabc");
9540 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9541 .unwrap();
9542 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
9543
9544 // Test selecting whitespace with caret selection
9545 cx.set_state("abc\nˇ abc\nabc");
9546 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9547 .unwrap();
9548 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
9549
9550 // Test selecting whitespace with left-to-right selection
9551 cx.set_state("abc\n«ˇ »abc\nabc");
9552 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9553 .unwrap();
9554 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
9555
9556 // Test no matches with right-to-left selection
9557 cx.set_state("abc\n« ˇ»abc\nabc");
9558 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9559 .unwrap();
9560 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
9561
9562 // Test with a single word and clip_at_line_ends=true (#29823)
9563 cx.set_state("aˇbc");
9564 cx.update_editor(|e, window, cx| {
9565 e.set_clip_at_line_ends(true, cx);
9566 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
9567 e.set_clip_at_line_ends(false, cx);
9568 });
9569 cx.assert_editor_state("«abcˇ»");
9570
9571 // Test case sensitivity
9572 cx.set_state("fˇoo\nFOO\nFoo");
9573 cx.update_editor(|e, window, cx| {
9574 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
9575 });
9576 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
9577
9578 // Disable case sensitive search.
9579 update_test_editor_settings(&mut cx, &|settings| {
9580 let mut search_settings = SearchSettingsContent::default();
9581 search_settings.case_sensitive = Some(false);
9582 settings.search = Some(search_settings);
9583 });
9584
9585 cx.set_state("fˇoo\nFOO\nFoo");
9586 cx.update_editor(|e, window, cx| {
9587 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
9588 });
9589 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
9590}
9591
9592#[gpui::test]
9593async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
9594 init_test(cx, |_| {});
9595
9596 let mut cx = EditorTestContext::new(cx).await;
9597
9598 let large_body_1 = "\nd".repeat(200);
9599 let large_body_2 = "\ne".repeat(200);
9600
9601 cx.set_state(&format!(
9602 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
9603 ));
9604 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
9605 let scroll_position = editor.scroll_position(cx);
9606 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
9607 scroll_position
9608 });
9609
9610 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9611 .unwrap();
9612 cx.assert_editor_state(&format!(
9613 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
9614 ));
9615 let scroll_position_after_selection =
9616 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
9617 assert_eq!(
9618 initial_scroll_position, scroll_position_after_selection,
9619 "Scroll position should not change after selecting all matches"
9620 );
9621}
9622
9623#[gpui::test]
9624async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
9625 init_test(cx, |_| {});
9626
9627 let mut cx = EditorLspTestContext::new_rust(
9628 lsp::ServerCapabilities {
9629 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9630 ..Default::default()
9631 },
9632 cx,
9633 )
9634 .await;
9635
9636 cx.set_state(indoc! {"
9637 line 1
9638 line 2
9639 linˇe 3
9640 line 4
9641 line 5
9642 "});
9643
9644 // Make an edit
9645 cx.update_editor(|editor, window, cx| {
9646 editor.handle_input("X", window, cx);
9647 });
9648
9649 // Move cursor to a different position
9650 cx.update_editor(|editor, window, cx| {
9651 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9652 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
9653 });
9654 });
9655
9656 cx.assert_editor_state(indoc! {"
9657 line 1
9658 line 2
9659 linXe 3
9660 line 4
9661 liˇne 5
9662 "});
9663
9664 cx.lsp
9665 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
9666 Ok(Some(vec![lsp::TextEdit::new(
9667 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9668 "PREFIX ".to_string(),
9669 )]))
9670 });
9671
9672 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
9673 .unwrap()
9674 .await
9675 .unwrap();
9676
9677 cx.assert_editor_state(indoc! {"
9678 PREFIX line 1
9679 line 2
9680 linXe 3
9681 line 4
9682 liˇne 5
9683 "});
9684
9685 // Undo formatting
9686 cx.update_editor(|editor, window, cx| {
9687 editor.undo(&Default::default(), window, cx);
9688 });
9689
9690 // Verify cursor moved back to position after edit
9691 cx.assert_editor_state(indoc! {"
9692 line 1
9693 line 2
9694 linXˇe 3
9695 line 4
9696 line 5
9697 "});
9698}
9699
9700#[gpui::test]
9701async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
9702 init_test(cx, |_| {});
9703
9704 let mut cx = EditorTestContext::new(cx).await;
9705
9706 let provider = cx.new(|_| FakeEditPredictionDelegate::default());
9707 cx.update_editor(|editor, window, cx| {
9708 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
9709 });
9710
9711 cx.set_state(indoc! {"
9712 line 1
9713 line 2
9714 linˇe 3
9715 line 4
9716 line 5
9717 line 6
9718 line 7
9719 line 8
9720 line 9
9721 line 10
9722 "});
9723
9724 let snapshot = cx.buffer_snapshot();
9725 let edit_position = snapshot.anchor_after(Point::new(2, 4));
9726
9727 cx.update(|_, cx| {
9728 provider.update(cx, |provider, _| {
9729 provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
9730 id: None,
9731 edits: vec![(edit_position..edit_position, "X".into())],
9732 cursor_position: None,
9733 edit_preview: None,
9734 }))
9735 })
9736 });
9737
9738 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
9739 cx.update_editor(|editor, window, cx| {
9740 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
9741 });
9742
9743 cx.assert_editor_state(indoc! {"
9744 line 1
9745 line 2
9746 lineXˇ 3
9747 line 4
9748 line 5
9749 line 6
9750 line 7
9751 line 8
9752 line 9
9753 line 10
9754 "});
9755
9756 cx.update_editor(|editor, window, cx| {
9757 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9758 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
9759 });
9760 });
9761
9762 cx.assert_editor_state(indoc! {"
9763 line 1
9764 line 2
9765 lineX 3
9766 line 4
9767 line 5
9768 line 6
9769 line 7
9770 line 8
9771 line 9
9772 liˇne 10
9773 "});
9774
9775 cx.update_editor(|editor, window, cx| {
9776 editor.undo(&Default::default(), window, cx);
9777 });
9778
9779 cx.assert_editor_state(indoc! {"
9780 line 1
9781 line 2
9782 lineˇ 3
9783 line 4
9784 line 5
9785 line 6
9786 line 7
9787 line 8
9788 line 9
9789 line 10
9790 "});
9791}
9792
9793#[gpui::test]
9794async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
9795 init_test(cx, |_| {});
9796
9797 let mut cx = EditorTestContext::new(cx).await;
9798 cx.set_state(
9799 r#"let foo = 2;
9800lˇet foo = 2;
9801let fooˇ = 2;
9802let foo = 2;
9803let foo = ˇ2;"#,
9804 );
9805
9806 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9807 .unwrap();
9808 cx.assert_editor_state(
9809 r#"let foo = 2;
9810«letˇ» foo = 2;
9811let «fooˇ» = 2;
9812let foo = 2;
9813let foo = «2ˇ»;"#,
9814 );
9815
9816 // noop for multiple selections with different contents
9817 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9818 .unwrap();
9819 cx.assert_editor_state(
9820 r#"let foo = 2;
9821«letˇ» foo = 2;
9822let «fooˇ» = 2;
9823let foo = 2;
9824let foo = «2ˇ»;"#,
9825 );
9826
9827 // Test last selection direction should be preserved
9828 cx.set_state(
9829 r#"let foo = 2;
9830let foo = 2;
9831let «fooˇ» = 2;
9832let «ˇfoo» = 2;
9833let foo = 2;"#,
9834 );
9835
9836 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9837 .unwrap();
9838 cx.assert_editor_state(
9839 r#"let foo = 2;
9840let foo = 2;
9841let «fooˇ» = 2;
9842let «ˇfoo» = 2;
9843let «ˇfoo» = 2;"#,
9844 );
9845}
9846
9847#[gpui::test]
9848async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
9849 init_test(cx, |_| {});
9850
9851 let mut cx =
9852 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc»\nddd", "aaa\n«bbb\nccc»\nddd"]);
9853
9854 cx.assert_editor_state(indoc! {"
9855 ˇbbb
9856 ccc
9857 bbb
9858 ccc"});
9859 cx.dispatch_action(SelectPrevious::default());
9860 cx.assert_editor_state(indoc! {"
9861 «bbbˇ»
9862 ccc
9863 bbb
9864 ccc"});
9865 cx.dispatch_action(SelectPrevious::default());
9866 cx.assert_editor_state(indoc! {"
9867 «bbbˇ»
9868 ccc
9869 «bbbˇ»
9870 ccc"});
9871}
9872
9873#[gpui::test]
9874async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
9875 init_test(cx, |_| {});
9876
9877 let mut cx = EditorTestContext::new(cx).await;
9878 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
9879
9880 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9881 .unwrap();
9882 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9883
9884 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9885 .unwrap();
9886 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
9887
9888 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9889 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9890
9891 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9892 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
9893
9894 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9895 .unwrap();
9896 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
9897
9898 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9899 .unwrap();
9900 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
9901}
9902
9903#[gpui::test]
9904async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
9905 init_test(cx, |_| {});
9906
9907 let mut cx = EditorTestContext::new(cx).await;
9908 cx.set_state("aˇ");
9909
9910 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9911 .unwrap();
9912 cx.assert_editor_state("«aˇ»");
9913 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9914 .unwrap();
9915 cx.assert_editor_state("«aˇ»");
9916}
9917
9918#[gpui::test]
9919async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
9920 init_test(cx, |_| {});
9921
9922 let mut cx = EditorTestContext::new(cx).await;
9923 cx.set_state(
9924 r#"let foo = 2;
9925lˇet foo = 2;
9926let fooˇ = 2;
9927let foo = 2;
9928let foo = ˇ2;"#,
9929 );
9930
9931 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9932 .unwrap();
9933 cx.assert_editor_state(
9934 r#"let foo = 2;
9935«letˇ» foo = 2;
9936let «fooˇ» = 2;
9937let foo = 2;
9938let foo = «2ˇ»;"#,
9939 );
9940
9941 // noop for multiple selections with different contents
9942 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9943 .unwrap();
9944 cx.assert_editor_state(
9945 r#"let foo = 2;
9946«letˇ» foo = 2;
9947let «fooˇ» = 2;
9948let foo = 2;
9949let foo = «2ˇ»;"#,
9950 );
9951}
9952
9953#[gpui::test]
9954async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
9955 init_test(cx, |_| {});
9956 let mut cx = EditorTestContext::new(cx).await;
9957
9958 // Enable case sensitive search.
9959 update_test_editor_settings(&mut cx, &|settings| {
9960 let mut search_settings = SearchSettingsContent::default();
9961 search_settings.case_sensitive = Some(true);
9962 settings.search = Some(search_settings);
9963 });
9964
9965 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
9966
9967 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9968 .unwrap();
9969 // selection direction is preserved
9970 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9971
9972 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9973 .unwrap();
9974 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9975
9976 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9977 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9978
9979 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9980 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9981
9982 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9983 .unwrap();
9984 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
9985
9986 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9987 .unwrap();
9988 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
9989
9990 // Test case sensitivity
9991 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
9992 cx.update_editor(|e, window, cx| {
9993 e.select_previous(&SelectPrevious::default(), window, cx)
9994 .unwrap();
9995 e.select_previous(&SelectPrevious::default(), window, cx)
9996 .unwrap();
9997 });
9998 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
9999
10000 // Disable case sensitive search.
10001 update_test_editor_settings(&mut cx, &|settings| {
10002 let mut search_settings = SearchSettingsContent::default();
10003 search_settings.case_sensitive = Some(false);
10004 settings.search = Some(search_settings);
10005 });
10006
10007 cx.set_state("foo\nFOO\n«ˇFoo»");
10008 cx.update_editor(|e, window, cx| {
10009 e.select_previous(&SelectPrevious::default(), window, cx)
10010 .unwrap();
10011 e.select_previous(&SelectPrevious::default(), window, cx)
10012 .unwrap();
10013 });
10014 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
10015}
10016
10017#[gpui::test]
10018async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
10019 init_test(cx, |_| {});
10020
10021 let language = Arc::new(Language::new(
10022 LanguageConfig::default(),
10023 Some(tree_sitter_rust::LANGUAGE.into()),
10024 ));
10025
10026 let text = r#"
10027 use mod1::mod2::{mod3, mod4};
10028
10029 fn fn_1(param1: bool, param2: &str) {
10030 let var1 = "text";
10031 }
10032 "#
10033 .unindent();
10034
10035 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10036 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10037 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10038
10039 editor
10040 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10041 .await;
10042
10043 editor.update_in(cx, |editor, window, cx| {
10044 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10045 s.select_display_ranges([
10046 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
10047 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
10048 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
10049 ]);
10050 });
10051 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10052 });
10053 editor.update(cx, |editor, cx| {
10054 assert_text_with_selections(
10055 editor,
10056 indoc! {r#"
10057 use mod1::mod2::{mod3, «mod4ˇ»};
10058
10059 fn fn_1«ˇ(param1: bool, param2: &str)» {
10060 let var1 = "«textˇ»";
10061 }
10062 "#},
10063 cx,
10064 );
10065 });
10066
10067 editor.update_in(cx, |editor, window, cx| {
10068 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10069 });
10070 editor.update(cx, |editor, cx| {
10071 assert_text_with_selections(
10072 editor,
10073 indoc! {r#"
10074 use mod1::mod2::«{mod3, mod4}ˇ»;
10075
10076 «ˇfn fn_1(param1: bool, param2: &str) {
10077 let var1 = "text";
10078 }»
10079 "#},
10080 cx,
10081 );
10082 });
10083
10084 editor.update_in(cx, |editor, window, cx| {
10085 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10086 });
10087 assert_eq!(
10088 editor.update(cx, |editor, cx| editor
10089 .selections
10090 .display_ranges(&editor.display_snapshot(cx))),
10091 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
10092 );
10093
10094 // Trying to expand the selected syntax node one more time has no effect.
10095 editor.update_in(cx, |editor, window, cx| {
10096 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10097 });
10098 assert_eq!(
10099 editor.update(cx, |editor, cx| editor
10100 .selections
10101 .display_ranges(&editor.display_snapshot(cx))),
10102 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
10103 );
10104
10105 editor.update_in(cx, |editor, window, cx| {
10106 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
10107 });
10108 editor.update(cx, |editor, cx| {
10109 assert_text_with_selections(
10110 editor,
10111 indoc! {r#"
10112 use mod1::mod2::«{mod3, mod4}ˇ»;
10113
10114 «ˇfn fn_1(param1: bool, param2: &str) {
10115 let var1 = "text";
10116 }»
10117 "#},
10118 cx,
10119 );
10120 });
10121
10122 editor.update_in(cx, |editor, window, cx| {
10123 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
10124 });
10125 editor.update(cx, |editor, cx| {
10126 assert_text_with_selections(
10127 editor,
10128 indoc! {r#"
10129 use mod1::mod2::{mod3, «mod4ˇ»};
10130
10131 fn fn_1«ˇ(param1: bool, param2: &str)» {
10132 let var1 = "«textˇ»";
10133 }
10134 "#},
10135 cx,
10136 );
10137 });
10138
10139 editor.update_in(cx, |editor, window, cx| {
10140 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
10141 });
10142 editor.update(cx, |editor, cx| {
10143 assert_text_with_selections(
10144 editor,
10145 indoc! {r#"
10146 use mod1::mod2::{mod3, moˇd4};
10147
10148 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
10149 let var1 = "teˇxt";
10150 }
10151 "#},
10152 cx,
10153 );
10154 });
10155
10156 // Trying to shrink the selected syntax node one more time has no effect.
10157 editor.update_in(cx, |editor, window, cx| {
10158 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
10159 });
10160 editor.update_in(cx, |editor, _, cx| {
10161 assert_text_with_selections(
10162 editor,
10163 indoc! {r#"
10164 use mod1::mod2::{mod3, moˇd4};
10165
10166 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
10167 let var1 = "teˇxt";
10168 }
10169 "#},
10170 cx,
10171 );
10172 });
10173
10174 // Ensure that we keep expanding the selection if the larger selection starts or ends within
10175 // a fold.
10176 editor.update_in(cx, |editor, window, cx| {
10177 editor.fold_creases(
10178 vec![
10179 Crease::simple(
10180 Point::new(0, 21)..Point::new(0, 24),
10181 FoldPlaceholder::test(),
10182 ),
10183 Crease::simple(
10184 Point::new(3, 20)..Point::new(3, 22),
10185 FoldPlaceholder::test(),
10186 ),
10187 ],
10188 true,
10189 window,
10190 cx,
10191 );
10192 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10193 });
10194 editor.update(cx, |editor, cx| {
10195 assert_text_with_selections(
10196 editor,
10197 indoc! {r#"
10198 use mod1::mod2::«{mod3, mod4}ˇ»;
10199
10200 fn fn_1«ˇ(param1: bool, param2: &str)» {
10201 let var1 = "«textˇ»";
10202 }
10203 "#},
10204 cx,
10205 );
10206 });
10207
10208 // Ensure multiple cursors have consistent direction after expanding
10209 editor.update_in(cx, |editor, window, cx| {
10210 editor.unfold_all(&UnfoldAll, window, cx);
10211 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10212 s.select_display_ranges([
10213 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
10214 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
10215 ]);
10216 });
10217 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10218 });
10219 editor.update(cx, |editor, cx| {
10220 assert_text_with_selections(
10221 editor,
10222 indoc! {r#"
10223 use mod1::mod2::{mod3, «mod4ˇ»};
10224
10225 fn fn_1(param1: bool, param2: &str) {
10226 let var1 = "«textˇ»";
10227 }
10228 "#},
10229 cx,
10230 );
10231 });
10232}
10233
10234#[gpui::test]
10235async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
10236 init_test(cx, |_| {});
10237
10238 let language = Arc::new(Language::new(
10239 LanguageConfig::default(),
10240 Some(tree_sitter_rust::LANGUAGE.into()),
10241 ));
10242
10243 let text = "let a = 2;";
10244
10245 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10246 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10247 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10248
10249 editor
10250 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10251 .await;
10252
10253 // Test case 1: Cursor at end of word
10254 editor.update_in(cx, |editor, window, cx| {
10255 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10256 s.select_display_ranges([
10257 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
10258 ]);
10259 });
10260 });
10261 editor.update(cx, |editor, cx| {
10262 assert_text_with_selections(editor, "let aˇ = 2;", cx);
10263 });
10264 editor.update_in(cx, |editor, window, cx| {
10265 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10266 });
10267 editor.update(cx, |editor, cx| {
10268 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
10269 });
10270 editor.update_in(cx, |editor, window, cx| {
10271 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10272 });
10273 editor.update(cx, |editor, cx| {
10274 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
10275 });
10276
10277 // Test case 2: Cursor at end of statement
10278 editor.update_in(cx, |editor, window, cx| {
10279 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10280 s.select_display_ranges([
10281 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
10282 ]);
10283 });
10284 });
10285 editor.update(cx, |editor, cx| {
10286 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
10287 });
10288 editor.update_in(cx, |editor, window, cx| {
10289 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10290 });
10291 editor.update(cx, |editor, cx| {
10292 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
10293 });
10294}
10295
10296#[gpui::test]
10297async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
10298 init_test(cx, |_| {});
10299
10300 let language = Arc::new(Language::new(
10301 LanguageConfig {
10302 name: "JavaScript".into(),
10303 ..Default::default()
10304 },
10305 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10306 ));
10307
10308 let text = r#"
10309 let a = {
10310 key: "value",
10311 };
10312 "#
10313 .unindent();
10314
10315 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10316 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10317 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10318
10319 editor
10320 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10321 .await;
10322
10323 // Test case 1: Cursor after '{'
10324 editor.update_in(cx, |editor, window, cx| {
10325 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10326 s.select_display_ranges([
10327 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
10328 ]);
10329 });
10330 });
10331 editor.update(cx, |editor, cx| {
10332 assert_text_with_selections(
10333 editor,
10334 indoc! {r#"
10335 let a = {ˇ
10336 key: "value",
10337 };
10338 "#},
10339 cx,
10340 );
10341 });
10342 editor.update_in(cx, |editor, window, cx| {
10343 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10344 });
10345 editor.update(cx, |editor, cx| {
10346 assert_text_with_selections(
10347 editor,
10348 indoc! {r#"
10349 let a = «ˇ{
10350 key: "value",
10351 }»;
10352 "#},
10353 cx,
10354 );
10355 });
10356
10357 // Test case 2: Cursor after ':'
10358 editor.update_in(cx, |editor, window, cx| {
10359 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10360 s.select_display_ranges([
10361 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
10362 ]);
10363 });
10364 });
10365 editor.update(cx, |editor, cx| {
10366 assert_text_with_selections(
10367 editor,
10368 indoc! {r#"
10369 let a = {
10370 key:ˇ "value",
10371 };
10372 "#},
10373 cx,
10374 );
10375 });
10376 editor.update_in(cx, |editor, window, cx| {
10377 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10378 });
10379 editor.update(cx, |editor, cx| {
10380 assert_text_with_selections(
10381 editor,
10382 indoc! {r#"
10383 let a = {
10384 «ˇkey: "value"»,
10385 };
10386 "#},
10387 cx,
10388 );
10389 });
10390 editor.update_in(cx, |editor, window, cx| {
10391 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10392 });
10393 editor.update(cx, |editor, cx| {
10394 assert_text_with_selections(
10395 editor,
10396 indoc! {r#"
10397 let a = «ˇ{
10398 key: "value",
10399 }»;
10400 "#},
10401 cx,
10402 );
10403 });
10404
10405 // Test case 3: Cursor after ','
10406 editor.update_in(cx, |editor, window, cx| {
10407 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10408 s.select_display_ranges([
10409 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
10410 ]);
10411 });
10412 });
10413 editor.update(cx, |editor, cx| {
10414 assert_text_with_selections(
10415 editor,
10416 indoc! {r#"
10417 let a = {
10418 key: "value",ˇ
10419 };
10420 "#},
10421 cx,
10422 );
10423 });
10424 editor.update_in(cx, |editor, window, cx| {
10425 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10426 });
10427 editor.update(cx, |editor, cx| {
10428 assert_text_with_selections(
10429 editor,
10430 indoc! {r#"
10431 let a = «ˇ{
10432 key: "value",
10433 }»;
10434 "#},
10435 cx,
10436 );
10437 });
10438
10439 // Test case 4: Cursor after ';'
10440 editor.update_in(cx, |editor, window, cx| {
10441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10442 s.select_display_ranges([
10443 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
10444 ]);
10445 });
10446 });
10447 editor.update(cx, |editor, cx| {
10448 assert_text_with_selections(
10449 editor,
10450 indoc! {r#"
10451 let a = {
10452 key: "value",
10453 };ˇ
10454 "#},
10455 cx,
10456 );
10457 });
10458 editor.update_in(cx, |editor, window, cx| {
10459 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10460 });
10461 editor.update(cx, |editor, cx| {
10462 assert_text_with_selections(
10463 editor,
10464 indoc! {r#"
10465 «ˇlet a = {
10466 key: "value",
10467 };
10468 »"#},
10469 cx,
10470 );
10471 });
10472}
10473
10474#[gpui::test]
10475async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
10476 init_test(cx, |_| {});
10477
10478 let language = Arc::new(Language::new(
10479 LanguageConfig::default(),
10480 Some(tree_sitter_rust::LANGUAGE.into()),
10481 ));
10482
10483 let text = r#"
10484 use mod1::mod2::{mod3, mod4};
10485
10486 fn fn_1(param1: bool, param2: &str) {
10487 let var1 = "hello world";
10488 }
10489 "#
10490 .unindent();
10491
10492 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10493 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10494 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10495
10496 editor
10497 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10498 .await;
10499
10500 // Test 1: Cursor on a letter of a string word
10501 editor.update_in(cx, |editor, window, cx| {
10502 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10503 s.select_display_ranges([
10504 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
10505 ]);
10506 });
10507 });
10508 editor.update_in(cx, |editor, window, cx| {
10509 assert_text_with_selections(
10510 editor,
10511 indoc! {r#"
10512 use mod1::mod2::{mod3, mod4};
10513
10514 fn fn_1(param1: bool, param2: &str) {
10515 let var1 = "hˇello world";
10516 }
10517 "#},
10518 cx,
10519 );
10520 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10521 assert_text_with_selections(
10522 editor,
10523 indoc! {r#"
10524 use mod1::mod2::{mod3, mod4};
10525
10526 fn fn_1(param1: bool, param2: &str) {
10527 let var1 = "«ˇhello» world";
10528 }
10529 "#},
10530 cx,
10531 );
10532 });
10533
10534 // Test 2: Partial selection within a word
10535 editor.update_in(cx, |editor, window, cx| {
10536 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10537 s.select_display_ranges([
10538 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
10539 ]);
10540 });
10541 });
10542 editor.update_in(cx, |editor, window, cx| {
10543 assert_text_with_selections(
10544 editor,
10545 indoc! {r#"
10546 use mod1::mod2::{mod3, mod4};
10547
10548 fn fn_1(param1: bool, param2: &str) {
10549 let var1 = "h«elˇ»lo world";
10550 }
10551 "#},
10552 cx,
10553 );
10554 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10555 assert_text_with_selections(
10556 editor,
10557 indoc! {r#"
10558 use mod1::mod2::{mod3, mod4};
10559
10560 fn fn_1(param1: bool, param2: &str) {
10561 let var1 = "«ˇhello» world";
10562 }
10563 "#},
10564 cx,
10565 );
10566 });
10567
10568 // Test 3: Complete word already selected
10569 editor.update_in(cx, |editor, window, cx| {
10570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10571 s.select_display_ranges([
10572 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
10573 ]);
10574 });
10575 });
10576 editor.update_in(cx, |editor, window, cx| {
10577 assert_text_with_selections(
10578 editor,
10579 indoc! {r#"
10580 use mod1::mod2::{mod3, mod4};
10581
10582 fn fn_1(param1: bool, param2: &str) {
10583 let var1 = "«helloˇ» world";
10584 }
10585 "#},
10586 cx,
10587 );
10588 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10589 assert_text_with_selections(
10590 editor,
10591 indoc! {r#"
10592 use mod1::mod2::{mod3, mod4};
10593
10594 fn fn_1(param1: bool, param2: &str) {
10595 let var1 = "«hello worldˇ»";
10596 }
10597 "#},
10598 cx,
10599 );
10600 });
10601
10602 // Test 4: Selection spanning across words
10603 editor.update_in(cx, |editor, window, cx| {
10604 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10605 s.select_display_ranges([
10606 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
10607 ]);
10608 });
10609 });
10610 editor.update_in(cx, |editor, window, cx| {
10611 assert_text_with_selections(
10612 editor,
10613 indoc! {r#"
10614 use mod1::mod2::{mod3, mod4};
10615
10616 fn fn_1(param1: bool, param2: &str) {
10617 let var1 = "hel«lo woˇ»rld";
10618 }
10619 "#},
10620 cx,
10621 );
10622 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10623 assert_text_with_selections(
10624 editor,
10625 indoc! {r#"
10626 use mod1::mod2::{mod3, mod4};
10627
10628 fn fn_1(param1: bool, param2: &str) {
10629 let var1 = "«ˇhello world»";
10630 }
10631 "#},
10632 cx,
10633 );
10634 });
10635
10636 // Test 5: Expansion beyond string
10637 editor.update_in(cx, |editor, window, cx| {
10638 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10639 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10640 assert_text_with_selections(
10641 editor,
10642 indoc! {r#"
10643 use mod1::mod2::{mod3, mod4};
10644
10645 fn fn_1(param1: bool, param2: &str) {
10646 «ˇlet var1 = "hello world";»
10647 }
10648 "#},
10649 cx,
10650 );
10651 });
10652}
10653
10654#[gpui::test]
10655async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
10656 init_test(cx, |_| {});
10657
10658 let mut cx = EditorTestContext::new(cx).await;
10659
10660 let language = Arc::new(Language::new(
10661 LanguageConfig::default(),
10662 Some(tree_sitter_rust::LANGUAGE.into()),
10663 ));
10664
10665 cx.update_buffer(|buffer, cx| {
10666 buffer.set_language(Some(language), cx);
10667 });
10668
10669 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
10670 cx.update_editor(|editor, window, cx| {
10671 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
10672 });
10673
10674 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
10675
10676 cx.set_state(indoc! { r#"fn a() {
10677 // what
10678 // a
10679 // ˇlong
10680 // method
10681 // I
10682 // sure
10683 // hope
10684 // it
10685 // works
10686 }"# });
10687
10688 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
10689 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
10690 cx.update(|_, cx| {
10691 multi_buffer.update(cx, |multi_buffer, cx| {
10692 multi_buffer.set_excerpts_for_path(
10693 PathKey::for_buffer(&buffer, cx),
10694 buffer,
10695 [Point::new(1, 0)..Point::new(1, 0)],
10696 3,
10697 cx,
10698 );
10699 });
10700 });
10701
10702 let editor2 = cx.new_window_entity(|window, cx| {
10703 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
10704 });
10705
10706 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
10707 cx.update_editor(|editor, window, cx| {
10708 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
10709 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
10710 })
10711 });
10712
10713 cx.assert_editor_state(indoc! { "
10714 fn a() {
10715 // what
10716 // a
10717 ˇ // long
10718 // method"});
10719
10720 cx.update_editor(|editor, window, cx| {
10721 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
10722 });
10723
10724 // Although we could potentially make the action work when the syntax node
10725 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
10726 // did. Maybe we could also expand the excerpt to contain the range?
10727 cx.assert_editor_state(indoc! { "
10728 fn a() {
10729 // what
10730 // a
10731 ˇ // long
10732 // method"});
10733}
10734
10735#[gpui::test]
10736async fn test_fold_function_bodies(cx: &mut TestAppContext) {
10737 init_test(cx, |_| {});
10738
10739 let base_text = r#"
10740 impl A {
10741 // this is an uncommitted comment
10742
10743 fn b() {
10744 c();
10745 }
10746
10747 // this is another uncommitted comment
10748
10749 fn d() {
10750 // e
10751 // f
10752 }
10753 }
10754
10755 fn g() {
10756 // h
10757 }
10758 "#
10759 .unindent();
10760
10761 let text = r#"
10762 ˇimpl A {
10763
10764 fn b() {
10765 c();
10766 }
10767
10768 fn d() {
10769 // e
10770 // f
10771 }
10772 }
10773
10774 fn g() {
10775 // h
10776 }
10777 "#
10778 .unindent();
10779
10780 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10781 cx.set_state(&text);
10782 cx.set_head_text(&base_text);
10783 cx.update_editor(|editor, window, cx| {
10784 editor.expand_all_diff_hunks(&Default::default(), window, cx);
10785 });
10786
10787 cx.assert_state_with_diff(
10788 "
10789 ˇimpl A {
10790 - // this is an uncommitted comment
10791
10792 fn b() {
10793 c();
10794 }
10795
10796 - // this is another uncommitted comment
10797 -
10798 fn d() {
10799 // e
10800 // f
10801 }
10802 }
10803
10804 fn g() {
10805 // h
10806 }
10807 "
10808 .unindent(),
10809 );
10810
10811 let expected_display_text = "
10812 impl A {
10813 // this is an uncommitted comment
10814
10815 fn b() {
10816 ⋯
10817 }
10818
10819 // this is another uncommitted comment
10820
10821 fn d() {
10822 ⋯
10823 }
10824 }
10825
10826 fn g() {
10827 ⋯
10828 }
10829 "
10830 .unindent();
10831
10832 cx.update_editor(|editor, window, cx| {
10833 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
10834 assert_eq!(editor.display_text(cx), expected_display_text);
10835 });
10836}
10837
10838#[gpui::test]
10839async fn test_autoindent(cx: &mut TestAppContext) {
10840 init_test(cx, |_| {});
10841
10842 let language = Arc::new(
10843 Language::new(
10844 LanguageConfig {
10845 brackets: BracketPairConfig {
10846 pairs: vec![
10847 BracketPair {
10848 start: "{".to_string(),
10849 end: "}".to_string(),
10850 close: false,
10851 surround: false,
10852 newline: true,
10853 },
10854 BracketPair {
10855 start: "(".to_string(),
10856 end: ")".to_string(),
10857 close: false,
10858 surround: false,
10859 newline: true,
10860 },
10861 ],
10862 ..Default::default()
10863 },
10864 ..Default::default()
10865 },
10866 Some(tree_sitter_rust::LANGUAGE.into()),
10867 )
10868 .with_indents_query(
10869 r#"
10870 (_ "(" ")" @end) @indent
10871 (_ "{" "}" @end) @indent
10872 "#,
10873 )
10874 .unwrap(),
10875 );
10876
10877 let text = "fn a() {}";
10878
10879 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10880 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10881 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10882 editor
10883 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10884 .await;
10885
10886 editor.update_in(cx, |editor, window, cx| {
10887 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10888 s.select_ranges([
10889 MultiBufferOffset(5)..MultiBufferOffset(5),
10890 MultiBufferOffset(8)..MultiBufferOffset(8),
10891 MultiBufferOffset(9)..MultiBufferOffset(9),
10892 ])
10893 });
10894 editor.newline(&Newline, window, cx);
10895 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
10896 assert_eq!(
10897 editor.selections.ranges(&editor.display_snapshot(cx)),
10898 &[
10899 Point::new(1, 4)..Point::new(1, 4),
10900 Point::new(3, 4)..Point::new(3, 4),
10901 Point::new(5, 0)..Point::new(5, 0)
10902 ]
10903 );
10904 });
10905}
10906
10907#[gpui::test]
10908async fn test_autoindent_disabled(cx: &mut TestAppContext) {
10909 init_test(cx, |settings| {
10910 settings.defaults.auto_indent = Some(settings::AutoIndentMode::None)
10911 });
10912
10913 let language = Arc::new(
10914 Language::new(
10915 LanguageConfig {
10916 brackets: BracketPairConfig {
10917 pairs: vec![
10918 BracketPair {
10919 start: "{".to_string(),
10920 end: "}".to_string(),
10921 close: false,
10922 surround: false,
10923 newline: true,
10924 },
10925 BracketPair {
10926 start: "(".to_string(),
10927 end: ")".to_string(),
10928 close: false,
10929 surround: false,
10930 newline: true,
10931 },
10932 ],
10933 ..Default::default()
10934 },
10935 ..Default::default()
10936 },
10937 Some(tree_sitter_rust::LANGUAGE.into()),
10938 )
10939 .with_indents_query(
10940 r#"
10941 (_ "(" ")" @end) @indent
10942 (_ "{" "}" @end) @indent
10943 "#,
10944 )
10945 .unwrap(),
10946 );
10947
10948 let text = "fn a() {}";
10949
10950 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10951 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10952 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10953 editor
10954 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10955 .await;
10956
10957 editor.update_in(cx, |editor, window, cx| {
10958 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10959 s.select_ranges([
10960 MultiBufferOffset(5)..MultiBufferOffset(5),
10961 MultiBufferOffset(8)..MultiBufferOffset(8),
10962 MultiBufferOffset(9)..MultiBufferOffset(9),
10963 ])
10964 });
10965 editor.newline(&Newline, window, cx);
10966 assert_eq!(
10967 editor.text(cx),
10968 indoc!(
10969 "
10970 fn a(
10971
10972 ) {
10973
10974 }
10975 "
10976 )
10977 );
10978 assert_eq!(
10979 editor.selections.ranges(&editor.display_snapshot(cx)),
10980 &[
10981 Point::new(1, 0)..Point::new(1, 0),
10982 Point::new(3, 0)..Point::new(3, 0),
10983 Point::new(5, 0)..Point::new(5, 0)
10984 ]
10985 );
10986 });
10987}
10988
10989#[gpui::test]
10990async fn test_autoindent_none_does_not_preserve_indentation_on_newline(cx: &mut TestAppContext) {
10991 init_test(cx, |settings| {
10992 settings.defaults.auto_indent = Some(settings::AutoIndentMode::None)
10993 });
10994
10995 let mut cx = EditorTestContext::new(cx).await;
10996
10997 cx.set_state(indoc! {"
10998 hello
10999 indented lineˇ
11000 world
11001 "});
11002
11003 cx.update_editor(|editor, window, cx| {
11004 editor.newline(&Newline, window, cx);
11005 });
11006
11007 cx.assert_editor_state(indoc! {"
11008 hello
11009 indented line
11010 ˇ
11011 world
11012 "});
11013}
11014
11015#[gpui::test]
11016async fn test_autoindent_preserve_indent_maintains_indentation_on_newline(cx: &mut TestAppContext) {
11017 // When auto_indent is "preserve_indent", pressing Enter on an indented line
11018 // should preserve the indentation but not adjust based on syntax.
11019 init_test(cx, |settings| {
11020 settings.defaults.auto_indent = Some(settings::AutoIndentMode::PreserveIndent)
11021 });
11022
11023 let mut cx = EditorTestContext::new(cx).await;
11024
11025 cx.set_state(indoc! {"
11026 hello
11027 indented lineˇ
11028 world
11029 "});
11030
11031 cx.update_editor(|editor, window, cx| {
11032 editor.newline(&Newline, window, cx);
11033 });
11034
11035 // The new line SHOULD have the same indentation as the previous line
11036 cx.assert_editor_state(indoc! {"
11037 hello
11038 indented line
11039 ˇ
11040 world
11041 "});
11042}
11043
11044#[gpui::test]
11045async fn test_autoindent_preserve_indent_does_not_apply_syntax_indent(cx: &mut TestAppContext) {
11046 init_test(cx, |settings| {
11047 settings.defaults.auto_indent = Some(settings::AutoIndentMode::PreserveIndent)
11048 });
11049
11050 let language = Arc::new(
11051 Language::new(
11052 LanguageConfig {
11053 brackets: BracketPairConfig {
11054 pairs: vec![BracketPair {
11055 start: "{".to_string(),
11056 end: "}".to_string(),
11057 close: false,
11058 surround: false,
11059 newline: false, // Disable extra newline behavior to isolate syntax indent test
11060 }],
11061 ..Default::default()
11062 },
11063 ..Default::default()
11064 },
11065 Some(tree_sitter_rust::LANGUAGE.into()),
11066 )
11067 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
11068 .unwrap(),
11069 );
11070
11071 let buffer =
11072 cx.new(|cx| Buffer::local("fn foo() {\n}", cx).with_language(language.clone(), cx));
11073 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11074 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11075 editor
11076 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11077 .await;
11078
11079 // Position cursor at end of line containing `{`
11080 editor.update_in(cx, |editor, window, cx| {
11081 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11082 s.select_ranges([MultiBufferOffset(10)..MultiBufferOffset(10)]) // After "fn foo() {"
11083 });
11084 editor.newline(&Newline, window, cx);
11085
11086 // With PreserveIndent, the new line should have 0 indentation (same as the fn line)
11087 // NOT 4 spaces (which tree-sitter would add for being inside `{}`)
11088 assert_eq!(editor.text(cx), "fn foo() {\n\n}");
11089 });
11090}
11091
11092#[gpui::test]
11093async fn test_autoindent_syntax_aware_applies_syntax_indent(cx: &mut TestAppContext) {
11094 // Companion test to show that SyntaxAware DOES apply tree-sitter indentation
11095 init_test(cx, |settings| {
11096 settings.defaults.auto_indent = Some(settings::AutoIndentMode::SyntaxAware)
11097 });
11098
11099 let language = Arc::new(
11100 Language::new(
11101 LanguageConfig {
11102 brackets: BracketPairConfig {
11103 pairs: vec![BracketPair {
11104 start: "{".to_string(),
11105 end: "}".to_string(),
11106 close: false,
11107 surround: false,
11108 newline: false, // Disable extra newline behavior to isolate syntax indent test
11109 }],
11110 ..Default::default()
11111 },
11112 ..Default::default()
11113 },
11114 Some(tree_sitter_rust::LANGUAGE.into()),
11115 )
11116 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
11117 .unwrap(),
11118 );
11119
11120 let buffer =
11121 cx.new(|cx| Buffer::local("fn foo() {\n}", cx).with_language(language.clone(), cx));
11122 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11123 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11124 editor
11125 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11126 .await;
11127
11128 // Position cursor at end of line containing `{`
11129 editor.update_in(cx, |editor, window, cx| {
11130 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11131 s.select_ranges([MultiBufferOffset(10)..MultiBufferOffset(10)]) // After "fn foo() {"
11132 });
11133 editor.newline(&Newline, window, cx);
11134
11135 // With SyntaxAware, tree-sitter adds indentation for being inside `{}`
11136 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
11137 });
11138}
11139
11140#[gpui::test]
11141async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
11142 init_test(cx, |settings| {
11143 settings.defaults.auto_indent = Some(settings::AutoIndentMode::SyntaxAware);
11144 settings.languages.0.insert(
11145 "python".into(),
11146 LanguageSettingsContent {
11147 auto_indent: Some(settings::AutoIndentMode::None),
11148 ..Default::default()
11149 },
11150 );
11151 });
11152
11153 let mut cx = EditorTestContext::new(cx).await;
11154
11155 let injected_language = Arc::new(
11156 Language::new(
11157 LanguageConfig {
11158 brackets: BracketPairConfig {
11159 pairs: vec![
11160 BracketPair {
11161 start: "{".to_string(),
11162 end: "}".to_string(),
11163 close: false,
11164 surround: false,
11165 newline: true,
11166 },
11167 BracketPair {
11168 start: "(".to_string(),
11169 end: ")".to_string(),
11170 close: true,
11171 surround: false,
11172 newline: true,
11173 },
11174 ],
11175 ..Default::default()
11176 },
11177 name: "python".into(),
11178 ..Default::default()
11179 },
11180 Some(tree_sitter_python::LANGUAGE.into()),
11181 )
11182 .with_indents_query(
11183 r#"
11184 (_ "(" ")" @end) @indent
11185 (_ "{" "}" @end) @indent
11186 "#,
11187 )
11188 .unwrap(),
11189 );
11190
11191 let language = Arc::new(
11192 Language::new(
11193 LanguageConfig {
11194 brackets: BracketPairConfig {
11195 pairs: vec![
11196 BracketPair {
11197 start: "{".to_string(),
11198 end: "}".to_string(),
11199 close: false,
11200 surround: false,
11201 newline: true,
11202 },
11203 BracketPair {
11204 start: "(".to_string(),
11205 end: ")".to_string(),
11206 close: true,
11207 surround: false,
11208 newline: true,
11209 },
11210 ],
11211 ..Default::default()
11212 },
11213 name: LanguageName::new_static("rust"),
11214 ..Default::default()
11215 },
11216 Some(tree_sitter_rust::LANGUAGE.into()),
11217 )
11218 .with_indents_query(
11219 r#"
11220 (_ "(" ")" @end) @indent
11221 (_ "{" "}" @end) @indent
11222 "#,
11223 )
11224 .unwrap()
11225 .with_injection_query(
11226 r#"
11227 (macro_invocation
11228 macro: (identifier) @_macro_name
11229 (token_tree) @injection.content
11230 (#set! injection.language "python"))
11231 "#,
11232 )
11233 .unwrap(),
11234 );
11235
11236 cx.language_registry().add(injected_language);
11237 cx.language_registry().add(language.clone());
11238
11239 cx.update_buffer(|buffer, cx| {
11240 buffer.set_language(Some(language), cx);
11241 });
11242
11243 cx.set_state(r#"struct A {ˇ}"#);
11244
11245 cx.update_editor(|editor, window, cx| {
11246 editor.newline(&Default::default(), window, cx);
11247 });
11248
11249 cx.assert_editor_state(indoc!(
11250 "struct A {
11251 ˇ
11252 }"
11253 ));
11254
11255 cx.set_state(r#"select_biased!(ˇ)"#);
11256
11257 cx.update_editor(|editor, window, cx| {
11258 editor.newline(&Default::default(), window, cx);
11259 editor.handle_input("def ", window, cx);
11260 editor.handle_input("(", window, cx);
11261 editor.newline(&Default::default(), window, cx);
11262 editor.handle_input("a", window, cx);
11263 });
11264
11265 cx.assert_editor_state(indoc!(
11266 "select_biased!(
11267 def (
11268 aˇ
11269 )
11270 )"
11271 ));
11272}
11273
11274#[gpui::test]
11275async fn test_autoindent_selections(cx: &mut TestAppContext) {
11276 init_test(cx, |_| {});
11277
11278 {
11279 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
11280 cx.set_state(indoc! {"
11281 impl A {
11282
11283 fn b() {}
11284
11285 «fn c() {
11286
11287 }ˇ»
11288 }
11289 "});
11290
11291 cx.update_editor(|editor, window, cx| {
11292 editor.autoindent(&Default::default(), window, cx);
11293 });
11294 cx.wait_for_autoindent_applied().await;
11295
11296 cx.assert_editor_state(indoc! {"
11297 impl A {
11298
11299 fn b() {}
11300
11301 «fn c() {
11302
11303 }ˇ»
11304 }
11305 "});
11306 }
11307
11308 {
11309 let mut cx = EditorTestContext::new_multibuffer(
11310 cx,
11311 [indoc! { "
11312 impl A {
11313 «
11314 // a
11315 fn b(){}
11316 »
11317 «
11318 }
11319 fn c(){}
11320 »
11321 "}],
11322 );
11323
11324 let buffer = cx.update_editor(|editor, _, cx| {
11325 let buffer = editor.buffer().update(cx, |buffer, _| {
11326 buffer.all_buffers().iter().next().unwrap().clone()
11327 });
11328 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
11329 buffer
11330 });
11331
11332 cx.run_until_parked();
11333 cx.update_editor(|editor, window, cx| {
11334 editor.select_all(&Default::default(), window, cx);
11335 editor.autoindent(&Default::default(), window, cx)
11336 });
11337 cx.run_until_parked();
11338
11339 cx.update(|_, cx| {
11340 assert_eq!(
11341 buffer.read(cx).text(),
11342 indoc! { "
11343 impl A {
11344
11345 // a
11346 fn b(){}
11347
11348
11349 }
11350 fn c(){}
11351
11352 " }
11353 )
11354 });
11355 }
11356}
11357
11358#[gpui::test]
11359async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
11360 init_test(cx, |_| {});
11361
11362 let mut cx = EditorTestContext::new(cx).await;
11363
11364 let language = Arc::new(Language::new(
11365 LanguageConfig {
11366 brackets: BracketPairConfig {
11367 pairs: vec![
11368 BracketPair {
11369 start: "{".to_string(),
11370 end: "}".to_string(),
11371 close: true,
11372 surround: true,
11373 newline: true,
11374 },
11375 BracketPair {
11376 start: "(".to_string(),
11377 end: ")".to_string(),
11378 close: true,
11379 surround: true,
11380 newline: true,
11381 },
11382 BracketPair {
11383 start: "/*".to_string(),
11384 end: " */".to_string(),
11385 close: true,
11386 surround: true,
11387 newline: true,
11388 },
11389 BracketPair {
11390 start: "[".to_string(),
11391 end: "]".to_string(),
11392 close: false,
11393 surround: false,
11394 newline: true,
11395 },
11396 BracketPair {
11397 start: "\"".to_string(),
11398 end: "\"".to_string(),
11399 close: true,
11400 surround: true,
11401 newline: false,
11402 },
11403 BracketPair {
11404 start: "<".to_string(),
11405 end: ">".to_string(),
11406 close: false,
11407 surround: true,
11408 newline: true,
11409 },
11410 ],
11411 ..Default::default()
11412 },
11413 autoclose_before: "})]".to_string(),
11414 ..Default::default()
11415 },
11416 Some(tree_sitter_rust::LANGUAGE.into()),
11417 ));
11418
11419 cx.language_registry().add(language.clone());
11420 cx.update_buffer(|buffer, cx| {
11421 buffer.set_language(Some(language), cx);
11422 });
11423
11424 cx.set_state(
11425 &r#"
11426 🏀ˇ
11427 εˇ
11428 ❤️ˇ
11429 "#
11430 .unindent(),
11431 );
11432
11433 // autoclose multiple nested brackets at multiple cursors
11434 cx.update_editor(|editor, window, cx| {
11435 editor.handle_input("{", window, cx);
11436 editor.handle_input("{", window, cx);
11437 editor.handle_input("{", window, cx);
11438 });
11439 cx.assert_editor_state(
11440 &"
11441 🏀{{{ˇ}}}
11442 ε{{{ˇ}}}
11443 ❤️{{{ˇ}}}
11444 "
11445 .unindent(),
11446 );
11447
11448 // insert a different closing bracket
11449 cx.update_editor(|editor, window, cx| {
11450 editor.handle_input(")", window, cx);
11451 });
11452 cx.assert_editor_state(
11453 &"
11454 🏀{{{)ˇ}}}
11455 ε{{{)ˇ}}}
11456 ❤️{{{)ˇ}}}
11457 "
11458 .unindent(),
11459 );
11460
11461 // skip over the auto-closed brackets when typing a closing bracket
11462 cx.update_editor(|editor, window, cx| {
11463 editor.move_right(&MoveRight, window, cx);
11464 editor.handle_input("}", window, cx);
11465 editor.handle_input("}", window, cx);
11466 editor.handle_input("}", window, cx);
11467 });
11468 cx.assert_editor_state(
11469 &"
11470 🏀{{{)}}}}ˇ
11471 ε{{{)}}}}ˇ
11472 ❤️{{{)}}}}ˇ
11473 "
11474 .unindent(),
11475 );
11476
11477 // autoclose multi-character pairs
11478 cx.set_state(
11479 &"
11480 ˇ
11481 ˇ
11482 "
11483 .unindent(),
11484 );
11485 cx.update_editor(|editor, window, cx| {
11486 editor.handle_input("/", window, cx);
11487 editor.handle_input("*", window, cx);
11488 });
11489 cx.assert_editor_state(
11490 &"
11491 /*ˇ */
11492 /*ˇ */
11493 "
11494 .unindent(),
11495 );
11496
11497 // one cursor autocloses a multi-character pair, one cursor
11498 // does not autoclose.
11499 cx.set_state(
11500 &"
11501 /ˇ
11502 ˇ
11503 "
11504 .unindent(),
11505 );
11506 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
11507 cx.assert_editor_state(
11508 &"
11509 /*ˇ */
11510 *ˇ
11511 "
11512 .unindent(),
11513 );
11514
11515 // Don't autoclose if the next character isn't whitespace and isn't
11516 // listed in the language's "autoclose_before" section.
11517 cx.set_state("ˇa b");
11518 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
11519 cx.assert_editor_state("{ˇa b");
11520
11521 // Don't autoclose if `close` is false for the bracket pair
11522 cx.set_state("ˇ");
11523 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
11524 cx.assert_editor_state("[ˇ");
11525
11526 // Surround with brackets if text is selected
11527 cx.set_state("«aˇ» b");
11528 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
11529 cx.assert_editor_state("{«aˇ»} b");
11530
11531 // Autoclose when not immediately after a word character
11532 cx.set_state("a ˇ");
11533 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
11534 cx.assert_editor_state("a \"ˇ\"");
11535
11536 // Autoclose pair where the start and end characters are the same
11537 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
11538 cx.assert_editor_state("a \"\"ˇ");
11539
11540 // Don't autoclose when immediately after a word character
11541 cx.set_state("aˇ");
11542 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
11543 cx.assert_editor_state("a\"ˇ");
11544
11545 // Do autoclose when after a non-word character
11546 cx.set_state("{ˇ");
11547 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
11548 cx.assert_editor_state("{\"ˇ\"");
11549
11550 // Non identical pairs autoclose regardless of preceding character
11551 cx.set_state("aˇ");
11552 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
11553 cx.assert_editor_state("a{ˇ}");
11554
11555 // Don't autoclose pair if autoclose is disabled
11556 cx.set_state("ˇ");
11557 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
11558 cx.assert_editor_state("<ˇ");
11559
11560 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
11561 cx.set_state("«aˇ» b");
11562 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
11563 cx.assert_editor_state("<«aˇ»> b");
11564}
11565
11566#[gpui::test]
11567async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
11568 init_test(cx, |settings| {
11569 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11570 });
11571
11572 let mut cx = EditorTestContext::new(cx).await;
11573
11574 let language = Arc::new(Language::new(
11575 LanguageConfig {
11576 brackets: BracketPairConfig {
11577 pairs: vec![
11578 BracketPair {
11579 start: "{".to_string(),
11580 end: "}".to_string(),
11581 close: true,
11582 surround: true,
11583 newline: true,
11584 },
11585 BracketPair {
11586 start: "(".to_string(),
11587 end: ")".to_string(),
11588 close: true,
11589 surround: true,
11590 newline: true,
11591 },
11592 BracketPair {
11593 start: "[".to_string(),
11594 end: "]".to_string(),
11595 close: false,
11596 surround: false,
11597 newline: true,
11598 },
11599 ],
11600 ..Default::default()
11601 },
11602 autoclose_before: "})]".to_string(),
11603 ..Default::default()
11604 },
11605 Some(tree_sitter_rust::LANGUAGE.into()),
11606 ));
11607
11608 cx.language_registry().add(language.clone());
11609 cx.update_buffer(|buffer, cx| {
11610 buffer.set_language(Some(language), cx);
11611 });
11612
11613 cx.set_state(
11614 &"
11615 ˇ
11616 ˇ
11617 ˇ
11618 "
11619 .unindent(),
11620 );
11621
11622 // ensure only matching closing brackets are skipped over
11623 cx.update_editor(|editor, window, cx| {
11624 editor.handle_input("}", window, cx);
11625 editor.move_left(&MoveLeft, window, cx);
11626 editor.handle_input(")", window, cx);
11627 editor.move_left(&MoveLeft, window, cx);
11628 });
11629 cx.assert_editor_state(
11630 &"
11631 ˇ)}
11632 ˇ)}
11633 ˇ)}
11634 "
11635 .unindent(),
11636 );
11637
11638 // skip-over closing brackets at multiple cursors
11639 cx.update_editor(|editor, window, cx| {
11640 editor.handle_input(")", window, cx);
11641 editor.handle_input("}", window, cx);
11642 });
11643 cx.assert_editor_state(
11644 &"
11645 )}ˇ
11646 )}ˇ
11647 )}ˇ
11648 "
11649 .unindent(),
11650 );
11651
11652 // ignore non-close brackets
11653 cx.update_editor(|editor, window, cx| {
11654 editor.handle_input("]", window, cx);
11655 editor.move_left(&MoveLeft, window, cx);
11656 editor.handle_input("]", window, cx);
11657 });
11658 cx.assert_editor_state(
11659 &"
11660 )}]ˇ]
11661 )}]ˇ]
11662 )}]ˇ]
11663 "
11664 .unindent(),
11665 );
11666}
11667
11668#[gpui::test]
11669async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
11670 init_test(cx, |_| {});
11671
11672 let mut cx = EditorTestContext::new(cx).await;
11673
11674 let html_language = Arc::new(
11675 Language::new(
11676 LanguageConfig {
11677 name: "HTML".into(),
11678 brackets: BracketPairConfig {
11679 pairs: vec![
11680 BracketPair {
11681 start: "<".into(),
11682 end: ">".into(),
11683 close: true,
11684 ..Default::default()
11685 },
11686 BracketPair {
11687 start: "{".into(),
11688 end: "}".into(),
11689 close: true,
11690 ..Default::default()
11691 },
11692 BracketPair {
11693 start: "(".into(),
11694 end: ")".into(),
11695 close: true,
11696 ..Default::default()
11697 },
11698 ],
11699 ..Default::default()
11700 },
11701 autoclose_before: "})]>".into(),
11702 ..Default::default()
11703 },
11704 Some(tree_sitter_html::LANGUAGE.into()),
11705 )
11706 .with_injection_query(
11707 r#"
11708 (script_element
11709 (raw_text) @injection.content
11710 (#set! injection.language "javascript"))
11711 "#,
11712 )
11713 .unwrap(),
11714 );
11715
11716 let javascript_language = Arc::new(Language::new(
11717 LanguageConfig {
11718 name: "JavaScript".into(),
11719 brackets: BracketPairConfig {
11720 pairs: vec![
11721 BracketPair {
11722 start: "/*".into(),
11723 end: " */".into(),
11724 close: true,
11725 ..Default::default()
11726 },
11727 BracketPair {
11728 start: "{".into(),
11729 end: "}".into(),
11730 close: true,
11731 ..Default::default()
11732 },
11733 BracketPair {
11734 start: "(".into(),
11735 end: ")".into(),
11736 close: true,
11737 ..Default::default()
11738 },
11739 ],
11740 ..Default::default()
11741 },
11742 autoclose_before: "})]>".into(),
11743 ..Default::default()
11744 },
11745 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11746 ));
11747
11748 cx.language_registry().add(html_language.clone());
11749 cx.language_registry().add(javascript_language);
11750 cx.executor().run_until_parked();
11751
11752 cx.update_buffer(|buffer, cx| {
11753 buffer.set_language(Some(html_language), cx);
11754 });
11755
11756 cx.set_state(
11757 &r#"
11758 <body>ˇ
11759 <script>
11760 var x = 1;ˇ
11761 </script>
11762 </body>ˇ
11763 "#
11764 .unindent(),
11765 );
11766
11767 // Precondition: different languages are active at different locations.
11768 cx.update_editor(|editor, window, cx| {
11769 let snapshot = editor.snapshot(window, cx);
11770 let cursors = editor
11771 .selections
11772 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
11773 let languages = cursors
11774 .iter()
11775 .map(|c| snapshot.language_at(c.start).unwrap().name())
11776 .collect::<Vec<_>>();
11777 assert_eq!(
11778 languages,
11779 &[
11780 LanguageName::from("HTML"),
11781 LanguageName::from("JavaScript"),
11782 LanguageName::from("HTML"),
11783 ]
11784 );
11785 });
11786
11787 // Angle brackets autoclose in HTML, but not JavaScript.
11788 cx.update_editor(|editor, window, cx| {
11789 editor.handle_input("<", window, cx);
11790 editor.handle_input("a", window, cx);
11791 });
11792 cx.assert_editor_state(
11793 &r#"
11794 <body><aˇ>
11795 <script>
11796 var x = 1;<aˇ
11797 </script>
11798 </body><aˇ>
11799 "#
11800 .unindent(),
11801 );
11802
11803 // Curly braces and parens autoclose in both HTML and JavaScript.
11804 cx.update_editor(|editor, window, cx| {
11805 editor.handle_input(" b=", window, cx);
11806 editor.handle_input("{", window, cx);
11807 editor.handle_input("c", window, cx);
11808 editor.handle_input("(", window, cx);
11809 });
11810 cx.assert_editor_state(
11811 &r#"
11812 <body><a b={c(ˇ)}>
11813 <script>
11814 var x = 1;<a b={c(ˇ)}
11815 </script>
11816 </body><a b={c(ˇ)}>
11817 "#
11818 .unindent(),
11819 );
11820
11821 // Brackets that were already autoclosed are skipped.
11822 cx.update_editor(|editor, window, cx| {
11823 editor.handle_input(")", window, cx);
11824 editor.handle_input("d", window, cx);
11825 editor.handle_input("}", window, cx);
11826 });
11827 cx.assert_editor_state(
11828 &r#"
11829 <body><a b={c()d}ˇ>
11830 <script>
11831 var x = 1;<a b={c()d}ˇ
11832 </script>
11833 </body><a b={c()d}ˇ>
11834 "#
11835 .unindent(),
11836 );
11837 cx.update_editor(|editor, window, cx| {
11838 editor.handle_input(">", window, cx);
11839 });
11840 cx.assert_editor_state(
11841 &r#"
11842 <body><a b={c()d}>ˇ
11843 <script>
11844 var x = 1;<a b={c()d}>ˇ
11845 </script>
11846 </body><a b={c()d}>ˇ
11847 "#
11848 .unindent(),
11849 );
11850
11851 // Reset
11852 cx.set_state(
11853 &r#"
11854 <body>ˇ
11855 <script>
11856 var x = 1;ˇ
11857 </script>
11858 </body>ˇ
11859 "#
11860 .unindent(),
11861 );
11862
11863 cx.update_editor(|editor, window, cx| {
11864 editor.handle_input("<", window, cx);
11865 });
11866 cx.assert_editor_state(
11867 &r#"
11868 <body><ˇ>
11869 <script>
11870 var x = 1;<ˇ
11871 </script>
11872 </body><ˇ>
11873 "#
11874 .unindent(),
11875 );
11876
11877 // When backspacing, the closing angle brackets are removed.
11878 cx.update_editor(|editor, window, cx| {
11879 editor.backspace(&Backspace, window, cx);
11880 });
11881 cx.assert_editor_state(
11882 &r#"
11883 <body>ˇ
11884 <script>
11885 var x = 1;ˇ
11886 </script>
11887 </body>ˇ
11888 "#
11889 .unindent(),
11890 );
11891
11892 // Block comments autoclose in JavaScript, but not HTML.
11893 cx.update_editor(|editor, window, cx| {
11894 editor.handle_input("/", window, cx);
11895 editor.handle_input("*", window, cx);
11896 });
11897 cx.assert_editor_state(
11898 &r#"
11899 <body>/*ˇ
11900 <script>
11901 var x = 1;/*ˇ */
11902 </script>
11903 </body>/*ˇ
11904 "#
11905 .unindent(),
11906 );
11907}
11908
11909#[gpui::test]
11910async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
11911 init_test(cx, |_| {});
11912
11913 let mut cx = EditorTestContext::new(cx).await;
11914
11915 let rust_language = Arc::new(
11916 Language::new(
11917 LanguageConfig {
11918 name: "Rust".into(),
11919 brackets: serde_json::from_value(json!([
11920 { "start": "{", "end": "}", "close": true, "newline": true },
11921 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
11922 ]))
11923 .unwrap(),
11924 autoclose_before: "})]>".into(),
11925 ..Default::default()
11926 },
11927 Some(tree_sitter_rust::LANGUAGE.into()),
11928 )
11929 .with_override_query("(string_literal) @string")
11930 .unwrap(),
11931 );
11932
11933 cx.language_registry().add(rust_language.clone());
11934 cx.update_buffer(|buffer, cx| {
11935 buffer.set_language(Some(rust_language), cx);
11936 });
11937
11938 cx.set_state(
11939 &r#"
11940 let x = ˇ
11941 "#
11942 .unindent(),
11943 );
11944
11945 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
11946 cx.update_editor(|editor, window, cx| {
11947 editor.handle_input("\"", window, cx);
11948 });
11949 cx.assert_editor_state(
11950 &r#"
11951 let x = "ˇ"
11952 "#
11953 .unindent(),
11954 );
11955
11956 // Inserting another quotation mark. The cursor moves across the existing
11957 // automatically-inserted quotation mark.
11958 cx.update_editor(|editor, window, cx| {
11959 editor.handle_input("\"", window, cx);
11960 });
11961 cx.assert_editor_state(
11962 &r#"
11963 let x = ""ˇ
11964 "#
11965 .unindent(),
11966 );
11967
11968 // Reset
11969 cx.set_state(
11970 &r#"
11971 let x = ˇ
11972 "#
11973 .unindent(),
11974 );
11975
11976 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
11977 cx.update_editor(|editor, window, cx| {
11978 editor.handle_input("\"", window, cx);
11979 editor.handle_input(" ", window, cx);
11980 editor.move_left(&Default::default(), window, cx);
11981 editor.handle_input("\\", window, cx);
11982 editor.handle_input("\"", window, cx);
11983 });
11984 cx.assert_editor_state(
11985 &r#"
11986 let x = "\"ˇ "
11987 "#
11988 .unindent(),
11989 );
11990
11991 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
11992 // mark. Nothing is inserted.
11993 cx.update_editor(|editor, window, cx| {
11994 editor.move_right(&Default::default(), window, cx);
11995 editor.handle_input("\"", window, cx);
11996 });
11997 cx.assert_editor_state(
11998 &r#"
11999 let x = "\" "ˇ
12000 "#
12001 .unindent(),
12002 );
12003}
12004
12005#[gpui::test]
12006async fn test_autoclose_quotes_with_scope_awareness(cx: &mut TestAppContext) {
12007 init_test(cx, |_| {});
12008
12009 let mut cx = EditorTestContext::new(cx).await;
12010 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
12011
12012 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12013
12014 // Double quote inside single-quoted string
12015 cx.set_state(indoc! {r#"
12016 def main():
12017 items = ['"', ˇ]
12018 "#});
12019 cx.update_editor(|editor, window, cx| {
12020 editor.handle_input("\"", window, cx);
12021 });
12022 cx.assert_editor_state(indoc! {r#"
12023 def main():
12024 items = ['"', "ˇ"]
12025 "#});
12026
12027 // Two double quotes inside single-quoted string
12028 cx.set_state(indoc! {r#"
12029 def main():
12030 items = ['""', ˇ]
12031 "#});
12032 cx.update_editor(|editor, window, cx| {
12033 editor.handle_input("\"", window, cx);
12034 });
12035 cx.assert_editor_state(indoc! {r#"
12036 def main():
12037 items = ['""', "ˇ"]
12038 "#});
12039
12040 // Single quote inside double-quoted string
12041 cx.set_state(indoc! {r#"
12042 def main():
12043 items = ["'", ˇ]
12044 "#});
12045 cx.update_editor(|editor, window, cx| {
12046 editor.handle_input("'", window, cx);
12047 });
12048 cx.assert_editor_state(indoc! {r#"
12049 def main():
12050 items = ["'", 'ˇ']
12051 "#});
12052
12053 // Two single quotes inside double-quoted string
12054 cx.set_state(indoc! {r#"
12055 def main():
12056 items = ["''", ˇ]
12057 "#});
12058 cx.update_editor(|editor, window, cx| {
12059 editor.handle_input("'", window, cx);
12060 });
12061 cx.assert_editor_state(indoc! {r#"
12062 def main():
12063 items = ["''", 'ˇ']
12064 "#});
12065
12066 // Mixed quotes on same line
12067 cx.set_state(indoc! {r#"
12068 def main():
12069 items = ['"""', "'''''", ˇ]
12070 "#});
12071 cx.update_editor(|editor, window, cx| {
12072 editor.handle_input("\"", window, cx);
12073 });
12074 cx.assert_editor_state(indoc! {r#"
12075 def main():
12076 items = ['"""', "'''''", "ˇ"]
12077 "#});
12078 cx.update_editor(|editor, window, cx| {
12079 editor.move_right(&MoveRight, window, cx);
12080 });
12081 cx.update_editor(|editor, window, cx| {
12082 editor.handle_input(", ", window, cx);
12083 });
12084 cx.update_editor(|editor, window, cx| {
12085 editor.handle_input("'", window, cx);
12086 });
12087 cx.assert_editor_state(indoc! {r#"
12088 def main():
12089 items = ['"""', "'''''", "", 'ˇ']
12090 "#});
12091}
12092
12093#[gpui::test]
12094async fn test_autoclose_quotes_with_multibyte_characters(cx: &mut TestAppContext) {
12095 init_test(cx, |_| {});
12096
12097 let mut cx = EditorTestContext::new(cx).await;
12098 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
12099 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12100
12101 cx.set_state(indoc! {r#"
12102 def main():
12103 items = ["🎉", ˇ]
12104 "#});
12105 cx.update_editor(|editor, window, cx| {
12106 editor.handle_input("\"", window, cx);
12107 });
12108 cx.assert_editor_state(indoc! {r#"
12109 def main():
12110 items = ["🎉", "ˇ"]
12111 "#});
12112}
12113
12114#[gpui::test]
12115async fn test_surround_with_pair(cx: &mut TestAppContext) {
12116 init_test(cx, |_| {});
12117
12118 let language = Arc::new(Language::new(
12119 LanguageConfig {
12120 brackets: BracketPairConfig {
12121 pairs: vec![
12122 BracketPair {
12123 start: "{".to_string(),
12124 end: "}".to_string(),
12125 close: true,
12126 surround: true,
12127 newline: true,
12128 },
12129 BracketPair {
12130 start: "/* ".to_string(),
12131 end: "*/".to_string(),
12132 close: true,
12133 surround: true,
12134 ..Default::default()
12135 },
12136 ],
12137 ..Default::default()
12138 },
12139 ..Default::default()
12140 },
12141 Some(tree_sitter_rust::LANGUAGE.into()),
12142 ));
12143
12144 let text = r#"
12145 a
12146 b
12147 c
12148 "#
12149 .unindent();
12150
12151 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12152 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12153 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12154 editor
12155 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12156 .await;
12157
12158 editor.update_in(cx, |editor, window, cx| {
12159 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12160 s.select_display_ranges([
12161 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
12162 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
12163 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
12164 ])
12165 });
12166
12167 editor.handle_input("{", window, cx);
12168 editor.handle_input("{", window, cx);
12169 editor.handle_input("{", window, cx);
12170 assert_eq!(
12171 editor.text(cx),
12172 "
12173 {{{a}}}
12174 {{{b}}}
12175 {{{c}}}
12176 "
12177 .unindent()
12178 );
12179 assert_eq!(
12180 display_ranges(editor, cx),
12181 [
12182 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
12183 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
12184 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
12185 ]
12186 );
12187
12188 editor.undo(&Undo, window, cx);
12189 editor.undo(&Undo, window, cx);
12190 editor.undo(&Undo, window, cx);
12191 assert_eq!(
12192 editor.text(cx),
12193 "
12194 a
12195 b
12196 c
12197 "
12198 .unindent()
12199 );
12200 assert_eq!(
12201 display_ranges(editor, cx),
12202 [
12203 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
12204 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
12205 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
12206 ]
12207 );
12208
12209 // Ensure inserting the first character of a multi-byte bracket pair
12210 // doesn't surround the selections with the bracket.
12211 editor.handle_input("/", window, cx);
12212 assert_eq!(
12213 editor.text(cx),
12214 "
12215 /
12216 /
12217 /
12218 "
12219 .unindent()
12220 );
12221 assert_eq!(
12222 display_ranges(editor, cx),
12223 [
12224 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
12225 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
12226 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
12227 ]
12228 );
12229
12230 editor.undo(&Undo, window, cx);
12231 assert_eq!(
12232 editor.text(cx),
12233 "
12234 a
12235 b
12236 c
12237 "
12238 .unindent()
12239 );
12240 assert_eq!(
12241 display_ranges(editor, cx),
12242 [
12243 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
12244 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
12245 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
12246 ]
12247 );
12248
12249 // Ensure inserting the last character of a multi-byte bracket pair
12250 // doesn't surround the selections with the bracket.
12251 editor.handle_input("*", window, cx);
12252 assert_eq!(
12253 editor.text(cx),
12254 "
12255 *
12256 *
12257 *
12258 "
12259 .unindent()
12260 );
12261 assert_eq!(
12262 display_ranges(editor, cx),
12263 [
12264 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
12265 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
12266 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
12267 ]
12268 );
12269 });
12270}
12271
12272#[gpui::test]
12273async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
12274 init_test(cx, |_| {});
12275
12276 let language = Arc::new(Language::new(
12277 LanguageConfig {
12278 brackets: BracketPairConfig {
12279 pairs: vec![BracketPair {
12280 start: "{".to_string(),
12281 end: "}".to_string(),
12282 close: true,
12283 surround: true,
12284 newline: true,
12285 }],
12286 ..Default::default()
12287 },
12288 autoclose_before: "}".to_string(),
12289 ..Default::default()
12290 },
12291 Some(tree_sitter_rust::LANGUAGE.into()),
12292 ));
12293
12294 let text = r#"
12295 a
12296 b
12297 c
12298 "#
12299 .unindent();
12300
12301 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12302 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12303 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12304 editor
12305 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12306 .await;
12307
12308 editor.update_in(cx, |editor, window, cx| {
12309 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12310 s.select_ranges([
12311 Point::new(0, 1)..Point::new(0, 1),
12312 Point::new(1, 1)..Point::new(1, 1),
12313 Point::new(2, 1)..Point::new(2, 1),
12314 ])
12315 });
12316
12317 editor.handle_input("{", window, cx);
12318 editor.handle_input("{", window, cx);
12319 editor.handle_input("_", window, cx);
12320 assert_eq!(
12321 editor.text(cx),
12322 "
12323 a{{_}}
12324 b{{_}}
12325 c{{_}}
12326 "
12327 .unindent()
12328 );
12329 assert_eq!(
12330 editor
12331 .selections
12332 .ranges::<Point>(&editor.display_snapshot(cx)),
12333 [
12334 Point::new(0, 4)..Point::new(0, 4),
12335 Point::new(1, 4)..Point::new(1, 4),
12336 Point::new(2, 4)..Point::new(2, 4)
12337 ]
12338 );
12339
12340 editor.backspace(&Default::default(), window, cx);
12341 editor.backspace(&Default::default(), window, cx);
12342 assert_eq!(
12343 editor.text(cx),
12344 "
12345 a{}
12346 b{}
12347 c{}
12348 "
12349 .unindent()
12350 );
12351 assert_eq!(
12352 editor
12353 .selections
12354 .ranges::<Point>(&editor.display_snapshot(cx)),
12355 [
12356 Point::new(0, 2)..Point::new(0, 2),
12357 Point::new(1, 2)..Point::new(1, 2),
12358 Point::new(2, 2)..Point::new(2, 2)
12359 ]
12360 );
12361
12362 editor.delete_to_previous_word_start(&Default::default(), window, cx);
12363 assert_eq!(
12364 editor.text(cx),
12365 "
12366 a
12367 b
12368 c
12369 "
12370 .unindent()
12371 );
12372 assert_eq!(
12373 editor
12374 .selections
12375 .ranges::<Point>(&editor.display_snapshot(cx)),
12376 [
12377 Point::new(0, 1)..Point::new(0, 1),
12378 Point::new(1, 1)..Point::new(1, 1),
12379 Point::new(2, 1)..Point::new(2, 1)
12380 ]
12381 );
12382 });
12383}
12384
12385#[gpui::test]
12386async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
12387 init_test(cx, |settings| {
12388 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
12389 });
12390
12391 let mut cx = EditorTestContext::new(cx).await;
12392
12393 let language = Arc::new(Language::new(
12394 LanguageConfig {
12395 brackets: BracketPairConfig {
12396 pairs: vec![
12397 BracketPair {
12398 start: "{".to_string(),
12399 end: "}".to_string(),
12400 close: true,
12401 surround: true,
12402 newline: true,
12403 },
12404 BracketPair {
12405 start: "(".to_string(),
12406 end: ")".to_string(),
12407 close: true,
12408 surround: true,
12409 newline: true,
12410 },
12411 BracketPair {
12412 start: "[".to_string(),
12413 end: "]".to_string(),
12414 close: false,
12415 surround: true,
12416 newline: true,
12417 },
12418 ],
12419 ..Default::default()
12420 },
12421 autoclose_before: "})]".to_string(),
12422 ..Default::default()
12423 },
12424 Some(tree_sitter_rust::LANGUAGE.into()),
12425 ));
12426
12427 cx.language_registry().add(language.clone());
12428 cx.update_buffer(|buffer, cx| {
12429 buffer.set_language(Some(language), cx);
12430 });
12431
12432 cx.set_state(
12433 &"
12434 {(ˇ)}
12435 [[ˇ]]
12436 {(ˇ)}
12437 "
12438 .unindent(),
12439 );
12440
12441 cx.update_editor(|editor, window, cx| {
12442 editor.backspace(&Default::default(), window, cx);
12443 editor.backspace(&Default::default(), window, cx);
12444 });
12445
12446 cx.assert_editor_state(
12447 &"
12448 ˇ
12449 ˇ]]
12450 ˇ
12451 "
12452 .unindent(),
12453 );
12454
12455 cx.update_editor(|editor, window, cx| {
12456 editor.handle_input("{", window, cx);
12457 editor.handle_input("{", window, cx);
12458 editor.move_right(&MoveRight, window, cx);
12459 editor.move_right(&MoveRight, window, cx);
12460 editor.move_left(&MoveLeft, window, cx);
12461 editor.move_left(&MoveLeft, window, cx);
12462 editor.backspace(&Default::default(), window, cx);
12463 });
12464
12465 cx.assert_editor_state(
12466 &"
12467 {ˇ}
12468 {ˇ}]]
12469 {ˇ}
12470 "
12471 .unindent(),
12472 );
12473
12474 cx.update_editor(|editor, window, cx| {
12475 editor.backspace(&Default::default(), window, cx);
12476 });
12477
12478 cx.assert_editor_state(
12479 &"
12480 ˇ
12481 ˇ]]
12482 ˇ
12483 "
12484 .unindent(),
12485 );
12486}
12487
12488#[gpui::test]
12489async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
12490 init_test(cx, |_| {});
12491
12492 let language = Arc::new(Language::new(
12493 LanguageConfig::default(),
12494 Some(tree_sitter_rust::LANGUAGE.into()),
12495 ));
12496
12497 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
12498 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12499 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12500 editor
12501 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12502 .await;
12503
12504 editor.update_in(cx, |editor, window, cx| {
12505 editor.set_auto_replace_emoji_shortcode(true);
12506
12507 editor.handle_input("Hello ", window, cx);
12508 editor.handle_input(":wave", window, cx);
12509 assert_eq!(editor.text(cx), "Hello :wave".unindent());
12510
12511 editor.handle_input(":", window, cx);
12512 assert_eq!(editor.text(cx), "Hello 👋".unindent());
12513
12514 editor.handle_input(" :smile", window, cx);
12515 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
12516
12517 editor.handle_input(":", window, cx);
12518 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
12519
12520 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
12521 editor.handle_input(":wave", window, cx);
12522 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
12523
12524 editor.handle_input(":", window, cx);
12525 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
12526
12527 editor.handle_input(":1", window, cx);
12528 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
12529
12530 editor.handle_input(":", window, cx);
12531 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
12532
12533 // Ensure shortcode does not get replaced when it is part of a word
12534 editor.handle_input(" Test:wave", window, cx);
12535 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
12536
12537 editor.handle_input(":", window, cx);
12538 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
12539
12540 editor.set_auto_replace_emoji_shortcode(false);
12541
12542 // Ensure shortcode does not get replaced when auto replace is off
12543 editor.handle_input(" :wave", window, cx);
12544 assert_eq!(
12545 editor.text(cx),
12546 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
12547 );
12548
12549 editor.handle_input(":", window, cx);
12550 assert_eq!(
12551 editor.text(cx),
12552 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
12553 );
12554 });
12555}
12556
12557#[gpui::test]
12558async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
12559 init_test(cx, |_| {});
12560
12561 let (text, insertion_ranges) = marked_text_ranges(
12562 indoc! {"
12563 ˇ
12564 "},
12565 false,
12566 );
12567
12568 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
12569 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12570
12571 _ = editor.update_in(cx, |editor, window, cx| {
12572 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
12573
12574 editor
12575 .insert_snippet(
12576 &insertion_ranges
12577 .iter()
12578 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
12579 .collect::<Vec<_>>(),
12580 snippet,
12581 window,
12582 cx,
12583 )
12584 .unwrap();
12585
12586 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
12587 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
12588 assert_eq!(editor.text(cx), expected_text);
12589 assert_eq!(
12590 editor.selections.ranges(&editor.display_snapshot(cx)),
12591 selection_ranges
12592 .iter()
12593 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
12594 .collect::<Vec<_>>()
12595 );
12596 }
12597
12598 assert(
12599 editor,
12600 cx,
12601 indoc! {"
12602 type «» =•
12603 "},
12604 );
12605
12606 assert!(editor.context_menu_visible(), "There should be a matches");
12607 });
12608}
12609
12610#[gpui::test]
12611async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
12612 init_test(cx, |_| {});
12613
12614 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
12615 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
12616 assert_eq!(editor.text(cx), expected_text);
12617 assert_eq!(
12618 editor.selections.ranges(&editor.display_snapshot(cx)),
12619 selection_ranges
12620 .iter()
12621 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
12622 .collect::<Vec<_>>()
12623 );
12624 }
12625
12626 let (text, insertion_ranges) = marked_text_ranges(
12627 indoc! {"
12628 ˇ
12629 "},
12630 false,
12631 );
12632
12633 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
12634 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12635
12636 _ = editor.update_in(cx, |editor, window, cx| {
12637 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
12638
12639 editor
12640 .insert_snippet(
12641 &insertion_ranges
12642 .iter()
12643 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
12644 .collect::<Vec<_>>(),
12645 snippet,
12646 window,
12647 cx,
12648 )
12649 .unwrap();
12650
12651 assert_state(
12652 editor,
12653 cx,
12654 indoc! {"
12655 type «» = ;•
12656 "},
12657 );
12658
12659 assert!(
12660 editor.context_menu_visible(),
12661 "Context menu should be visible for placeholder choices"
12662 );
12663
12664 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
12665
12666 assert_state(
12667 editor,
12668 cx,
12669 indoc! {"
12670 type = «»;•
12671 "},
12672 );
12673
12674 assert!(
12675 !editor.context_menu_visible(),
12676 "Context menu should be hidden after moving to next tabstop"
12677 );
12678
12679 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
12680
12681 assert_state(
12682 editor,
12683 cx,
12684 indoc! {"
12685 type = ; ˇ
12686 "},
12687 );
12688
12689 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
12690
12691 assert_state(
12692 editor,
12693 cx,
12694 indoc! {"
12695 type = ; ˇ
12696 "},
12697 );
12698 });
12699
12700 _ = editor.update_in(cx, |editor, window, cx| {
12701 editor.select_all(&SelectAll, window, cx);
12702 editor.backspace(&Backspace, window, cx);
12703
12704 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
12705 let insertion_ranges = editor
12706 .selections
12707 .all(&editor.display_snapshot(cx))
12708 .iter()
12709 .map(|s| s.range())
12710 .collect::<Vec<_>>();
12711
12712 editor
12713 .insert_snippet(&insertion_ranges, snippet, window, cx)
12714 .unwrap();
12715
12716 assert_state(editor, cx, "fn «» = value;•");
12717
12718 assert!(
12719 editor.context_menu_visible(),
12720 "Context menu should be visible for placeholder choices"
12721 );
12722
12723 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
12724
12725 assert_state(editor, cx, "fn = «valueˇ»;•");
12726
12727 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
12728
12729 assert_state(editor, cx, "fn «» = value;•");
12730
12731 assert!(
12732 editor.context_menu_visible(),
12733 "Context menu should be visible again after returning to first tabstop"
12734 );
12735
12736 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
12737
12738 assert_state(editor, cx, "fn «» = value;•");
12739 });
12740}
12741
12742#[gpui::test]
12743async fn test_snippets(cx: &mut TestAppContext) {
12744 init_test(cx, |_| {});
12745
12746 let mut cx = EditorTestContext::new(cx).await;
12747
12748 cx.set_state(indoc! {"
12749 a.ˇ b
12750 a.ˇ b
12751 a.ˇ b
12752 "});
12753
12754 cx.update_editor(|editor, window, cx| {
12755 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
12756 let insertion_ranges = editor
12757 .selections
12758 .all(&editor.display_snapshot(cx))
12759 .iter()
12760 .map(|s| s.range())
12761 .collect::<Vec<_>>();
12762 editor
12763 .insert_snippet(&insertion_ranges, snippet, window, cx)
12764 .unwrap();
12765 });
12766
12767 cx.assert_editor_state(indoc! {"
12768 a.f(«oneˇ», two, «threeˇ») b
12769 a.f(«oneˇ», two, «threeˇ») b
12770 a.f(«oneˇ», two, «threeˇ») b
12771 "});
12772
12773 // Can't move earlier than the first tab stop
12774 cx.update_editor(|editor, window, cx| {
12775 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
12776 });
12777 cx.assert_editor_state(indoc! {"
12778 a.f(«oneˇ», two, «threeˇ») b
12779 a.f(«oneˇ», two, «threeˇ») b
12780 a.f(«oneˇ», two, «threeˇ») b
12781 "});
12782
12783 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
12784 cx.assert_editor_state(indoc! {"
12785 a.f(one, «twoˇ», three) b
12786 a.f(one, «twoˇ», three) b
12787 a.f(one, «twoˇ», three) b
12788 "});
12789
12790 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
12791 cx.assert_editor_state(indoc! {"
12792 a.f(«oneˇ», two, «threeˇ») b
12793 a.f(«oneˇ», two, «threeˇ») b
12794 a.f(«oneˇ», two, «threeˇ») b
12795 "});
12796
12797 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
12798 cx.assert_editor_state(indoc! {"
12799 a.f(one, «twoˇ», three) b
12800 a.f(one, «twoˇ», three) b
12801 a.f(one, «twoˇ», three) b
12802 "});
12803 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
12804 cx.assert_editor_state(indoc! {"
12805 a.f(one, two, three)ˇ b
12806 a.f(one, two, three)ˇ b
12807 a.f(one, two, three)ˇ b
12808 "});
12809
12810 // As soon as the last tab stop is reached, snippet state is gone
12811 cx.update_editor(|editor, window, cx| {
12812 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
12813 });
12814 cx.assert_editor_state(indoc! {"
12815 a.f(one, two, three)ˇ b
12816 a.f(one, two, three)ˇ b
12817 a.f(one, two, three)ˇ b
12818 "});
12819}
12820
12821#[gpui::test]
12822async fn test_snippet_indentation(cx: &mut TestAppContext) {
12823 init_test(cx, |_| {});
12824
12825 let mut cx = EditorTestContext::new(cx).await;
12826
12827 cx.update_editor(|editor, window, cx| {
12828 let snippet = Snippet::parse(indoc! {"
12829 /*
12830 * Multiline comment with leading indentation
12831 *
12832 * $1
12833 */
12834 $0"})
12835 .unwrap();
12836 let insertion_ranges = editor
12837 .selections
12838 .all(&editor.display_snapshot(cx))
12839 .iter()
12840 .map(|s| s.range())
12841 .collect::<Vec<_>>();
12842 editor
12843 .insert_snippet(&insertion_ranges, snippet, window, cx)
12844 .unwrap();
12845 });
12846
12847 cx.assert_editor_state(indoc! {"
12848 /*
12849 * Multiline comment with leading indentation
12850 *
12851 * ˇ
12852 */
12853 "});
12854
12855 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
12856 cx.assert_editor_state(indoc! {"
12857 /*
12858 * Multiline comment with leading indentation
12859 *
12860 *•
12861 */
12862 ˇ"});
12863}
12864
12865#[gpui::test]
12866async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
12867 init_test(cx, |_| {});
12868
12869 let mut cx = EditorTestContext::new(cx).await;
12870 cx.update_editor(|editor, _, cx| {
12871 editor.project().unwrap().update(cx, |project, cx| {
12872 project.snippets().update(cx, |snippets, _cx| {
12873 let snippet = project::snippet_provider::Snippet {
12874 prefix: vec!["multi word".to_string()],
12875 body: "this is many words".to_string(),
12876 description: Some("description".to_string()),
12877 name: "multi-word snippet test".to_string(),
12878 };
12879 snippets.add_snippet_for_test(
12880 None,
12881 PathBuf::from("test_snippets.json"),
12882 vec![Arc::new(snippet)],
12883 );
12884 });
12885 })
12886 });
12887
12888 for (input_to_simulate, should_match_snippet) in [
12889 ("m", true),
12890 ("m ", true),
12891 ("m w", true),
12892 ("aa m w", true),
12893 ("aa m g", false),
12894 ] {
12895 cx.set_state("ˇ");
12896 cx.simulate_input(input_to_simulate); // fails correctly
12897
12898 cx.update_editor(|editor, _, _| {
12899 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
12900 else {
12901 assert!(!should_match_snippet); // no completions! don't even show the menu
12902 return;
12903 };
12904 assert!(context_menu.visible());
12905 let completions = context_menu.completions.borrow();
12906
12907 assert_eq!(!completions.is_empty(), should_match_snippet);
12908 });
12909 }
12910}
12911
12912#[gpui::test]
12913async fn test_document_format_during_save(cx: &mut TestAppContext) {
12914 init_test(cx, |_| {});
12915
12916 let fs = FakeFs::new(cx.executor());
12917 fs.insert_file(path!("/file.rs"), Default::default()).await;
12918
12919 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
12920
12921 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12922 language_registry.add(rust_lang());
12923 let mut fake_servers = language_registry.register_fake_lsp(
12924 "Rust",
12925 FakeLspAdapter {
12926 capabilities: lsp::ServerCapabilities {
12927 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12928 ..Default::default()
12929 },
12930 ..Default::default()
12931 },
12932 );
12933
12934 let buffer = project
12935 .update(cx, |project, cx| {
12936 project.open_local_buffer(path!("/file.rs"), cx)
12937 })
12938 .await
12939 .unwrap();
12940
12941 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12942 let (editor, cx) = cx.add_window_view(|window, cx| {
12943 build_editor_with_project(project.clone(), buffer, window, cx)
12944 });
12945 editor.update_in(cx, |editor, window, cx| {
12946 editor.set_text("one\ntwo\nthree\n", window, cx)
12947 });
12948 assert!(cx.read(|cx| editor.is_dirty(cx)));
12949
12950 let fake_server = fake_servers.next().await.unwrap();
12951
12952 {
12953 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12954 move |params, _| async move {
12955 assert_eq!(
12956 params.text_document.uri,
12957 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12958 );
12959 assert_eq!(params.options.tab_size, 4);
12960 Ok(Some(vec![lsp::TextEdit::new(
12961 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12962 ", ".to_string(),
12963 )]))
12964 },
12965 );
12966 let save = editor
12967 .update_in(cx, |editor, window, cx| {
12968 editor.save(
12969 SaveOptions {
12970 format: true,
12971 autosave: false,
12972 },
12973 project.clone(),
12974 window,
12975 cx,
12976 )
12977 })
12978 .unwrap();
12979 save.await;
12980
12981 assert_eq!(
12982 editor.update(cx, |editor, cx| editor.text(cx)),
12983 "one, two\nthree\n"
12984 );
12985 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12986 }
12987
12988 {
12989 editor.update_in(cx, |editor, window, cx| {
12990 editor.set_text("one\ntwo\nthree\n", window, cx)
12991 });
12992 assert!(cx.read(|cx| editor.is_dirty(cx)));
12993
12994 // Ensure we can still save even if formatting hangs.
12995 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12996 move |params, _| async move {
12997 assert_eq!(
12998 params.text_document.uri,
12999 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13000 );
13001 futures::future::pending::<()>().await;
13002 unreachable!()
13003 },
13004 );
13005 let save = editor
13006 .update_in(cx, |editor, window, cx| {
13007 editor.save(
13008 SaveOptions {
13009 format: true,
13010 autosave: false,
13011 },
13012 project.clone(),
13013 window,
13014 cx,
13015 )
13016 })
13017 .unwrap();
13018 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
13019 save.await;
13020 assert_eq!(
13021 editor.update(cx, |editor, cx| editor.text(cx)),
13022 "one\ntwo\nthree\n"
13023 );
13024 }
13025
13026 // Set rust language override and assert overridden tabsize is sent to language server
13027 update_test_language_settings(cx, &|settings| {
13028 settings.languages.0.insert(
13029 "Rust".into(),
13030 LanguageSettingsContent {
13031 tab_size: NonZeroU32::new(8),
13032 ..Default::default()
13033 },
13034 );
13035 });
13036
13037 {
13038 editor.update_in(cx, |editor, window, cx| {
13039 editor.set_text("somehting_new\n", window, cx)
13040 });
13041 assert!(cx.read(|cx| editor.is_dirty(cx)));
13042 let _formatting_request_signal = fake_server
13043 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
13044 assert_eq!(
13045 params.text_document.uri,
13046 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13047 );
13048 assert_eq!(params.options.tab_size, 8);
13049 Ok(Some(vec![]))
13050 });
13051 let save = editor
13052 .update_in(cx, |editor, window, cx| {
13053 editor.save(
13054 SaveOptions {
13055 format: true,
13056 autosave: false,
13057 },
13058 project.clone(),
13059 window,
13060 cx,
13061 )
13062 })
13063 .unwrap();
13064 save.await;
13065 }
13066}
13067
13068#[gpui::test]
13069async fn test_auto_formatter_skips_server_without_formatting(cx: &mut TestAppContext) {
13070 init_test(cx, |_| {});
13071
13072 let fs = FakeFs::new(cx.executor());
13073 fs.insert_file(path!("/file.rs"), Default::default()).await;
13074
13075 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
13076
13077 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13078 language_registry.add(rust_lang());
13079
13080 // First server: no formatting capability
13081 let mut no_format_servers = language_registry.register_fake_lsp(
13082 "Rust",
13083 FakeLspAdapter {
13084 name: "no-format-server",
13085 capabilities: lsp::ServerCapabilities {
13086 completion_provider: Some(lsp::CompletionOptions::default()),
13087 ..Default::default()
13088 },
13089 ..Default::default()
13090 },
13091 );
13092
13093 // Second server: has formatting capability
13094 let mut format_servers = language_registry.register_fake_lsp(
13095 "Rust",
13096 FakeLspAdapter {
13097 name: "format-server",
13098 capabilities: lsp::ServerCapabilities {
13099 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13100 ..Default::default()
13101 },
13102 ..Default::default()
13103 },
13104 );
13105
13106 let buffer = project
13107 .update(cx, |project, cx| {
13108 project.open_local_buffer(path!("/file.rs"), cx)
13109 })
13110 .await
13111 .unwrap();
13112
13113 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13114 let (editor, cx) = cx.add_window_view(|window, cx| {
13115 build_editor_with_project(project.clone(), buffer, window, cx)
13116 });
13117 editor.update_in(cx, |editor, window, cx| {
13118 editor.set_text("one\ntwo\nthree\n", window, cx)
13119 });
13120
13121 let _no_format_server = no_format_servers.next().await.unwrap();
13122 let format_server = format_servers.next().await.unwrap();
13123
13124 format_server.set_request_handler::<lsp::request::Formatting, _, _>(
13125 move |params, _| async move {
13126 assert_eq!(
13127 params.text_document.uri,
13128 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13129 );
13130 Ok(Some(vec![lsp::TextEdit::new(
13131 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
13132 ", ".to_string(),
13133 )]))
13134 },
13135 );
13136
13137 let save = editor
13138 .update_in(cx, |editor, window, cx| {
13139 editor.save(
13140 SaveOptions {
13141 format: true,
13142 autosave: false,
13143 },
13144 project.clone(),
13145 window,
13146 cx,
13147 )
13148 })
13149 .unwrap();
13150 save.await;
13151
13152 assert_eq!(
13153 editor.update(cx, |editor, cx| editor.text(cx)),
13154 "one, two\nthree\n"
13155 );
13156}
13157
13158#[gpui::test]
13159async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
13160 init_test(cx, |settings| {
13161 settings.defaults.ensure_final_newline_on_save = Some(false);
13162 });
13163
13164 let fs = FakeFs::new(cx.executor());
13165 fs.insert_file(path!("/file.txt"), "foo".into()).await;
13166
13167 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
13168
13169 let buffer = project
13170 .update(cx, |project, cx| {
13171 project.open_local_buffer(path!("/file.txt"), cx)
13172 })
13173 .await
13174 .unwrap();
13175
13176 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13177 let (editor, cx) = cx.add_window_view(|window, cx| {
13178 build_editor_with_project(project.clone(), buffer, window, cx)
13179 });
13180 editor.update_in(cx, |editor, window, cx| {
13181 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
13182 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13183 });
13184 });
13185 assert!(!cx.read(|cx| editor.is_dirty(cx)));
13186
13187 editor.update_in(cx, |editor, window, cx| {
13188 editor.handle_input("\n", window, cx)
13189 });
13190 cx.run_until_parked();
13191 save(&editor, &project, cx).await;
13192 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
13193
13194 editor.update_in(cx, |editor, window, cx| {
13195 editor.undo(&Default::default(), window, cx);
13196 });
13197 save(&editor, &project, cx).await;
13198 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
13199
13200 editor.update_in(cx, |editor, window, cx| {
13201 editor.redo(&Default::default(), window, cx);
13202 });
13203 cx.run_until_parked();
13204 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
13205
13206 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
13207 let save = editor
13208 .update_in(cx, |editor, window, cx| {
13209 editor.save(
13210 SaveOptions {
13211 format: true,
13212 autosave: false,
13213 },
13214 project.clone(),
13215 window,
13216 cx,
13217 )
13218 })
13219 .unwrap();
13220 save.await;
13221 assert!(!cx.read(|cx| editor.is_dirty(cx)));
13222 }
13223}
13224
13225#[gpui::test]
13226async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
13227 init_test(cx, |_| {});
13228
13229 let cols = 4;
13230 let rows = 10;
13231 let sample_text_1 = sample_text(rows, cols, 'a');
13232 assert_eq!(
13233 sample_text_1,
13234 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
13235 );
13236 let sample_text_2 = sample_text(rows, cols, 'l');
13237 assert_eq!(
13238 sample_text_2,
13239 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
13240 );
13241 let sample_text_3 = sample_text(rows, cols, 'v').replace('\u{7f}', ".");
13242 assert_eq!(
13243 sample_text_3,
13244 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n...."
13245 );
13246
13247 let fs = FakeFs::new(cx.executor());
13248 fs.insert_tree(
13249 path!("/a"),
13250 json!({
13251 "main.rs": sample_text_1,
13252 "other.rs": sample_text_2,
13253 "lib.rs": sample_text_3,
13254 }),
13255 )
13256 .await;
13257
13258 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13259 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
13260 let cx = &mut VisualTestContext::from_window(*window, cx);
13261
13262 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13263 language_registry.add(rust_lang());
13264 let mut fake_servers = language_registry.register_fake_lsp(
13265 "Rust",
13266 FakeLspAdapter {
13267 capabilities: lsp::ServerCapabilities {
13268 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13269 ..Default::default()
13270 },
13271 ..Default::default()
13272 },
13273 );
13274
13275 let worktree = project.update(cx, |project, cx| {
13276 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
13277 assert_eq!(worktrees.len(), 1);
13278 worktrees.pop().unwrap()
13279 });
13280 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
13281
13282 let buffer_1 = project
13283 .update(cx, |project, cx| {
13284 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
13285 })
13286 .await
13287 .unwrap();
13288 let buffer_2 = project
13289 .update(cx, |project, cx| {
13290 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
13291 })
13292 .await
13293 .unwrap();
13294 let buffer_3 = project
13295 .update(cx, |project, cx| {
13296 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
13297 })
13298 .await
13299 .unwrap();
13300
13301 let multi_buffer = cx.new(|cx| {
13302 let mut multi_buffer = MultiBuffer::new(ReadWrite);
13303 multi_buffer.set_excerpts_for_path(
13304 PathKey::sorted(0),
13305 buffer_1.clone(),
13306 [
13307 Point::new(0, 0)..Point::new(2, 4),
13308 Point::new(5, 0)..Point::new(6, 4),
13309 Point::new(9, 0)..Point::new(9, 4),
13310 ],
13311 0,
13312 cx,
13313 );
13314 multi_buffer.set_excerpts_for_path(
13315 PathKey::sorted(1),
13316 buffer_2.clone(),
13317 [
13318 Point::new(0, 0)..Point::new(2, 4),
13319 Point::new(5, 0)..Point::new(6, 4),
13320 Point::new(9, 0)..Point::new(9, 4),
13321 ],
13322 0,
13323 cx,
13324 );
13325 multi_buffer.set_excerpts_for_path(
13326 PathKey::sorted(2),
13327 buffer_3.clone(),
13328 [
13329 Point::new(0, 0)..Point::new(2, 4),
13330 Point::new(5, 0)..Point::new(6, 4),
13331 Point::new(9, 0)..Point::new(9, 4),
13332 ],
13333 0,
13334 cx,
13335 );
13336 assert_eq!(multi_buffer.excerpt_ids().len(), 9);
13337 multi_buffer
13338 });
13339 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
13340 Editor::new(
13341 EditorMode::full(),
13342 multi_buffer,
13343 Some(project.clone()),
13344 window,
13345 cx,
13346 )
13347 });
13348
13349 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13350 let a = editor.text(cx).find("aaaa").unwrap();
13351 editor.change_selections(
13352 SelectionEffects::scroll(Autoscroll::Next),
13353 window,
13354 cx,
13355 |s| s.select_ranges(Some(MultiBufferOffset(a + 1)..MultiBufferOffset(a + 2))),
13356 );
13357 editor.insert("|one|two|three|", window, cx);
13358 });
13359 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
13360 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13361 let n = editor.text(cx).find("nnnn").unwrap();
13362 editor.change_selections(
13363 SelectionEffects::scroll(Autoscroll::Next),
13364 window,
13365 cx,
13366 |s| s.select_ranges(Some(MultiBufferOffset(n + 4)..MultiBufferOffset(n + 14))),
13367 );
13368 editor.insert("|four|five|six|", window, cx);
13369 });
13370 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
13371
13372 // First two buffers should be edited, but not the third one.
13373 pretty_assertions::assert_eq!(
13374 editor_content_with_blocks(&multi_buffer_editor, cx),
13375 indoc! {"
13376 § main.rs
13377 § -----
13378 a|one|two|three|aa
13379 bbbb
13380 cccc
13381 § -----
13382 ffff
13383 gggg
13384 § -----
13385 jjjj
13386 § other.rs
13387 § -----
13388 llll
13389 mmmm
13390 nnnn|four|five|six|
13391 § -----
13392
13393 § -----
13394 uuuu
13395 § lib.rs
13396 § -----
13397 vvvv
13398 wwww
13399 xxxx
13400 § -----
13401 {{{{
13402 ||||
13403 § -----
13404 ...."}
13405 );
13406 buffer_1.update(cx, |buffer, _| {
13407 assert!(buffer.is_dirty());
13408 assert_eq!(
13409 buffer.text(),
13410 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
13411 )
13412 });
13413 buffer_2.update(cx, |buffer, _| {
13414 assert!(buffer.is_dirty());
13415 assert_eq!(
13416 buffer.text(),
13417 "llll\nmmmm\nnnnn|four|five|six|\noooo\npppp\n\nssss\ntttt\nuuuu",
13418 )
13419 });
13420 buffer_3.update(cx, |buffer, _| {
13421 assert!(!buffer.is_dirty());
13422 assert_eq!(buffer.text(), sample_text_3,)
13423 });
13424 cx.executor().run_until_parked();
13425
13426 let save = multi_buffer_editor
13427 .update_in(cx, |editor, window, cx| {
13428 editor.save(
13429 SaveOptions {
13430 format: true,
13431 autosave: false,
13432 },
13433 project.clone(),
13434 window,
13435 cx,
13436 )
13437 })
13438 .unwrap();
13439
13440 let fake_server = fake_servers.next().await.unwrap();
13441 fake_server
13442 .server
13443 .on_request::<lsp::request::Formatting, _, _>(move |_params, _| async move {
13444 Ok(Some(vec![lsp::TextEdit::new(
13445 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
13446 "[formatted]".to_string(),
13447 )]))
13448 })
13449 .detach();
13450 save.await;
13451
13452 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
13453 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
13454 assert_eq!(
13455 editor_content_with_blocks(&multi_buffer_editor, cx),
13456 indoc! {"
13457 § main.rs
13458 § -----
13459 a|o[formatted]bbbb
13460 cccc
13461 § -----
13462 ffff
13463 gggg
13464 § -----
13465 jjjj
13466
13467 § other.rs
13468 § -----
13469 lll[formatted]mmmm
13470 nnnn|four|five|six|
13471 § -----
13472
13473 § -----
13474 uuuu
13475
13476 § lib.rs
13477 § -----
13478 vvvv
13479 wwww
13480 xxxx
13481 § -----
13482 {{{{
13483 ||||
13484 § -----
13485 ...."}
13486 );
13487 buffer_1.update(cx, |buffer, _| {
13488 assert!(!buffer.is_dirty());
13489 assert_eq!(
13490 buffer.text(),
13491 "a|o[formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n",
13492 )
13493 });
13494 // Diff < left / right > :
13495 // lll[formatted]mmmm
13496 // <nnnn|four|five|six|
13497 // <oooo
13498 // >nnnn|four|five|six|oooo
13499 // pppp
13500 // <
13501 // ssss
13502 // tttt
13503 // uuuu
13504
13505 buffer_2.update(cx, |buffer, _| {
13506 assert!(!buffer.is_dirty());
13507 assert_eq!(
13508 buffer.text(),
13509 "lll[formatted]mmmm\nnnnn|four|five|six|\noooo\npppp\n\nssss\ntttt\nuuuu\n",
13510 )
13511 });
13512 buffer_3.update(cx, |buffer, _| {
13513 assert!(!buffer.is_dirty());
13514 assert_eq!(buffer.text(), sample_text_3,)
13515 });
13516}
13517
13518#[gpui::test]
13519async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
13520 init_test(cx, |_| {});
13521
13522 let fs = FakeFs::new(cx.executor());
13523 fs.insert_tree(
13524 path!("/dir"),
13525 json!({
13526 "file1.rs": "fn main() { println!(\"hello\"); }",
13527 "file2.rs": "fn test() { println!(\"test\"); }",
13528 "file3.rs": "fn other() { println!(\"other\"); }\n",
13529 }),
13530 )
13531 .await;
13532
13533 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
13534 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
13535 let cx = &mut VisualTestContext::from_window(*window, cx);
13536
13537 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13538 language_registry.add(rust_lang());
13539
13540 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
13541 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
13542
13543 // Open three buffers
13544 let buffer_1 = project
13545 .update(cx, |project, cx| {
13546 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
13547 })
13548 .await
13549 .unwrap();
13550 let buffer_2 = project
13551 .update(cx, |project, cx| {
13552 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
13553 })
13554 .await
13555 .unwrap();
13556 let buffer_3 = project
13557 .update(cx, |project, cx| {
13558 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
13559 })
13560 .await
13561 .unwrap();
13562
13563 // Create a multi-buffer with all three buffers
13564 let multi_buffer = cx.new(|cx| {
13565 let mut multi_buffer = MultiBuffer::new(ReadWrite);
13566 multi_buffer.set_excerpts_for_path(
13567 PathKey::sorted(0),
13568 buffer_1.clone(),
13569 [Point::new(0, 0)..Point::new(1, 0)],
13570 0,
13571 cx,
13572 );
13573 multi_buffer.set_excerpts_for_path(
13574 PathKey::sorted(1),
13575 buffer_2.clone(),
13576 [Point::new(0, 0)..Point::new(1, 0)],
13577 0,
13578 cx,
13579 );
13580 multi_buffer.set_excerpts_for_path(
13581 PathKey::sorted(2),
13582 buffer_3.clone(),
13583 [Point::new(0, 0)..Point::new(1, 0)],
13584 0,
13585 cx,
13586 );
13587 multi_buffer
13588 });
13589
13590 let editor = cx.new_window_entity(|window, cx| {
13591 Editor::new(
13592 EditorMode::full(),
13593 multi_buffer,
13594 Some(project.clone()),
13595 window,
13596 cx,
13597 )
13598 });
13599
13600 // Edit only the first buffer
13601 editor.update_in(cx, |editor, window, cx| {
13602 editor.change_selections(
13603 SelectionEffects::scroll(Autoscroll::Next),
13604 window,
13605 cx,
13606 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
13607 );
13608 editor.insert("// edited", window, cx);
13609 });
13610
13611 // Verify that only buffer 1 is dirty
13612 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
13613 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
13614 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
13615
13616 // Get write counts after file creation (files were created with initial content)
13617 // We expect each file to have been written once during creation
13618 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
13619 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
13620 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
13621
13622 // Perform autosave
13623 let save_task = editor.update_in(cx, |editor, window, cx| {
13624 editor.save(
13625 SaveOptions {
13626 format: true,
13627 autosave: true,
13628 },
13629 project.clone(),
13630 window,
13631 cx,
13632 )
13633 });
13634 save_task.await.unwrap();
13635
13636 // Only the dirty buffer should have been saved
13637 assert_eq!(
13638 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
13639 1,
13640 "Buffer 1 was dirty, so it should have been written once during autosave"
13641 );
13642 assert_eq!(
13643 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
13644 0,
13645 "Buffer 2 was clean, so it should not have been written during autosave"
13646 );
13647 assert_eq!(
13648 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
13649 0,
13650 "Buffer 3 was clean, so it should not have been written during autosave"
13651 );
13652
13653 // Verify buffer states after autosave
13654 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
13655 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
13656 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
13657
13658 // Now perform a manual save (format = true)
13659 let save_task = editor.update_in(cx, |editor, window, cx| {
13660 editor.save(
13661 SaveOptions {
13662 format: true,
13663 autosave: false,
13664 },
13665 project.clone(),
13666 window,
13667 cx,
13668 )
13669 });
13670 save_task.await.unwrap();
13671
13672 // During manual save, clean buffers don't get written to disk
13673 // They just get did_save called for language server notifications
13674 assert_eq!(
13675 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
13676 1,
13677 "Buffer 1 should only have been written once total (during autosave, not manual save)"
13678 );
13679 assert_eq!(
13680 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
13681 0,
13682 "Buffer 2 should not have been written at all"
13683 );
13684 assert_eq!(
13685 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
13686 0,
13687 "Buffer 3 should not have been written at all"
13688 );
13689}
13690
13691async fn setup_range_format_test(
13692 cx: &mut TestAppContext,
13693) -> (
13694 Entity<Project>,
13695 Entity<Editor>,
13696 &mut gpui::VisualTestContext,
13697 lsp::FakeLanguageServer,
13698) {
13699 init_test(cx, |_| {});
13700
13701 let fs = FakeFs::new(cx.executor());
13702 fs.insert_file(path!("/file.rs"), Default::default()).await;
13703
13704 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13705
13706 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13707 language_registry.add(rust_lang());
13708 let mut fake_servers = language_registry.register_fake_lsp(
13709 "Rust",
13710 FakeLspAdapter {
13711 capabilities: lsp::ServerCapabilities {
13712 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
13713 ..lsp::ServerCapabilities::default()
13714 },
13715 ..FakeLspAdapter::default()
13716 },
13717 );
13718
13719 let buffer = project
13720 .update(cx, |project, cx| {
13721 project.open_local_buffer(path!("/file.rs"), cx)
13722 })
13723 .await
13724 .unwrap();
13725
13726 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13727 let (editor, cx) = cx.add_window_view(|window, cx| {
13728 build_editor_with_project(project.clone(), buffer, window, cx)
13729 });
13730
13731 let fake_server = fake_servers.next().await.unwrap();
13732
13733 (project, editor, cx, fake_server)
13734}
13735
13736#[gpui::test]
13737async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
13738 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
13739
13740 editor.update_in(cx, |editor, window, cx| {
13741 editor.set_text("one\ntwo\nthree\n", window, cx)
13742 });
13743 assert!(cx.read(|cx| editor.is_dirty(cx)));
13744
13745 let save = editor
13746 .update_in(cx, |editor, window, cx| {
13747 editor.save(
13748 SaveOptions {
13749 format: true,
13750 autosave: false,
13751 },
13752 project.clone(),
13753 window,
13754 cx,
13755 )
13756 })
13757 .unwrap();
13758 fake_server
13759 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
13760 assert_eq!(
13761 params.text_document.uri,
13762 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13763 );
13764 assert_eq!(params.options.tab_size, 4);
13765 Ok(Some(vec![lsp::TextEdit::new(
13766 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
13767 ", ".to_string(),
13768 )]))
13769 })
13770 .next()
13771 .await;
13772 save.await;
13773 assert_eq!(
13774 editor.update(cx, |editor, cx| editor.text(cx)),
13775 "one, two\nthree\n"
13776 );
13777 assert!(!cx.read(|cx| editor.is_dirty(cx)));
13778}
13779
13780#[gpui::test]
13781async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
13782 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
13783
13784 editor.update_in(cx, |editor, window, cx| {
13785 editor.set_text("one\ntwo\nthree\n", window, cx)
13786 });
13787 assert!(cx.read(|cx| editor.is_dirty(cx)));
13788
13789 // Test that save still works when formatting hangs
13790 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
13791 move |params, _| async move {
13792 assert_eq!(
13793 params.text_document.uri,
13794 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13795 );
13796 futures::future::pending::<()>().await;
13797 unreachable!()
13798 },
13799 );
13800 let save = editor
13801 .update_in(cx, |editor, window, cx| {
13802 editor.save(
13803 SaveOptions {
13804 format: true,
13805 autosave: false,
13806 },
13807 project.clone(),
13808 window,
13809 cx,
13810 )
13811 })
13812 .unwrap();
13813 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
13814 save.await;
13815 assert_eq!(
13816 editor.update(cx, |editor, cx| editor.text(cx)),
13817 "one\ntwo\nthree\n"
13818 );
13819 assert!(!cx.read(|cx| editor.is_dirty(cx)));
13820}
13821
13822#[gpui::test]
13823async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
13824 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
13825
13826 // Buffer starts clean, no formatting should be requested
13827 let save = editor
13828 .update_in(cx, |editor, window, cx| {
13829 editor.save(
13830 SaveOptions {
13831 format: false,
13832 autosave: false,
13833 },
13834 project.clone(),
13835 window,
13836 cx,
13837 )
13838 })
13839 .unwrap();
13840 let _pending_format_request = fake_server
13841 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
13842 panic!("Should not be invoked");
13843 })
13844 .next();
13845 save.await;
13846 cx.run_until_parked();
13847}
13848
13849#[gpui::test]
13850async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
13851 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
13852
13853 // Set Rust language override and assert overridden tabsize is sent to language server
13854 update_test_language_settings(cx, &|settings| {
13855 settings.languages.0.insert(
13856 "Rust".into(),
13857 LanguageSettingsContent {
13858 tab_size: NonZeroU32::new(8),
13859 ..Default::default()
13860 },
13861 );
13862 });
13863
13864 editor.update_in(cx, |editor, window, cx| {
13865 editor.set_text("something_new\n", window, cx)
13866 });
13867 assert!(cx.read(|cx| editor.is_dirty(cx)));
13868 let save = editor
13869 .update_in(cx, |editor, window, cx| {
13870 editor.save(
13871 SaveOptions {
13872 format: true,
13873 autosave: false,
13874 },
13875 project.clone(),
13876 window,
13877 cx,
13878 )
13879 })
13880 .unwrap();
13881 fake_server
13882 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
13883 assert_eq!(
13884 params.text_document.uri,
13885 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13886 );
13887 assert_eq!(params.options.tab_size, 8);
13888 Ok(Some(Vec::new()))
13889 })
13890 .next()
13891 .await;
13892 save.await;
13893}
13894
13895#[gpui::test]
13896async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
13897 init_test(cx, |settings| {
13898 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
13899 settings::LanguageServerFormatterSpecifier::Current,
13900 )))
13901 });
13902
13903 let fs = FakeFs::new(cx.executor());
13904 fs.insert_file(path!("/file.rs"), Default::default()).await;
13905
13906 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13907
13908 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13909 language_registry.add(Arc::new(Language::new(
13910 LanguageConfig {
13911 name: "Rust".into(),
13912 matcher: LanguageMatcher {
13913 path_suffixes: vec!["rs".to_string()],
13914 ..Default::default()
13915 },
13916 ..LanguageConfig::default()
13917 },
13918 Some(tree_sitter_rust::LANGUAGE.into()),
13919 )));
13920 update_test_language_settings(cx, &|settings| {
13921 // Enable Prettier formatting for the same buffer, and ensure
13922 // LSP is called instead of Prettier.
13923 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
13924 });
13925 let mut fake_servers = language_registry.register_fake_lsp(
13926 "Rust",
13927 FakeLspAdapter {
13928 capabilities: lsp::ServerCapabilities {
13929 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13930 ..Default::default()
13931 },
13932 ..Default::default()
13933 },
13934 );
13935
13936 let buffer = project
13937 .update(cx, |project, cx| {
13938 project.open_local_buffer(path!("/file.rs"), cx)
13939 })
13940 .await
13941 .unwrap();
13942
13943 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13944 let (editor, cx) = cx.add_window_view(|window, cx| {
13945 build_editor_with_project(project.clone(), buffer, window, cx)
13946 });
13947 editor.update_in(cx, |editor, window, cx| {
13948 editor.set_text("one\ntwo\nthree\n", window, cx)
13949 });
13950
13951 let fake_server = fake_servers.next().await.unwrap();
13952
13953 let format = editor
13954 .update_in(cx, |editor, window, cx| {
13955 editor.perform_format(
13956 project.clone(),
13957 FormatTrigger::Manual,
13958 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
13959 window,
13960 cx,
13961 )
13962 })
13963 .unwrap();
13964 fake_server
13965 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
13966 assert_eq!(
13967 params.text_document.uri,
13968 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13969 );
13970 assert_eq!(params.options.tab_size, 4);
13971 Ok(Some(vec![lsp::TextEdit::new(
13972 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
13973 ", ".to_string(),
13974 )]))
13975 })
13976 .next()
13977 .await;
13978 format.await;
13979 assert_eq!(
13980 editor.update(cx, |editor, cx| editor.text(cx)),
13981 "one, two\nthree\n"
13982 );
13983
13984 editor.update_in(cx, |editor, window, cx| {
13985 editor.set_text("one\ntwo\nthree\n", window, cx)
13986 });
13987 // Ensure we don't lock if formatting hangs.
13988 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
13989 move |params, _| async move {
13990 assert_eq!(
13991 params.text_document.uri,
13992 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13993 );
13994 futures::future::pending::<()>().await;
13995 unreachable!()
13996 },
13997 );
13998 let format = editor
13999 .update_in(cx, |editor, window, cx| {
14000 editor.perform_format(
14001 project,
14002 FormatTrigger::Manual,
14003 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
14004 window,
14005 cx,
14006 )
14007 })
14008 .unwrap();
14009 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
14010 format.await;
14011 assert_eq!(
14012 editor.update(cx, |editor, cx| editor.text(cx)),
14013 "one\ntwo\nthree\n"
14014 );
14015}
14016
14017#[gpui::test]
14018async fn test_multiple_formatters(cx: &mut TestAppContext) {
14019 init_test(cx, |settings| {
14020 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
14021 settings.defaults.formatter = Some(FormatterList::Vec(vec![
14022 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
14023 Formatter::CodeAction("code-action-1".into()),
14024 Formatter::CodeAction("code-action-2".into()),
14025 ]))
14026 });
14027
14028 let fs = FakeFs::new(cx.executor());
14029 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
14030 .await;
14031
14032 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14033 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14034 language_registry.add(rust_lang());
14035
14036 let mut fake_servers = language_registry.register_fake_lsp(
14037 "Rust",
14038 FakeLspAdapter {
14039 capabilities: lsp::ServerCapabilities {
14040 document_formatting_provider: Some(lsp::OneOf::Left(true)),
14041 execute_command_provider: Some(lsp::ExecuteCommandOptions {
14042 commands: vec!["the-command-for-code-action-1".into()],
14043 ..Default::default()
14044 }),
14045 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14046 ..Default::default()
14047 },
14048 ..Default::default()
14049 },
14050 );
14051
14052 let buffer = project
14053 .update(cx, |project, cx| {
14054 project.open_local_buffer(path!("/file.rs"), cx)
14055 })
14056 .await
14057 .unwrap();
14058
14059 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14060 let (editor, cx) = cx.add_window_view(|window, cx| {
14061 build_editor_with_project(project.clone(), buffer, window, cx)
14062 });
14063
14064 let fake_server = fake_servers.next().await.unwrap();
14065 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
14066 move |_params, _| async move {
14067 Ok(Some(vec![lsp::TextEdit::new(
14068 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
14069 "applied-formatting\n".to_string(),
14070 )]))
14071 },
14072 );
14073 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14074 move |params, _| async move {
14075 let requested_code_actions = params.context.only.expect("Expected code action request");
14076 assert_eq!(requested_code_actions.len(), 1);
14077
14078 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
14079 let code_action = match requested_code_actions[0].as_str() {
14080 "code-action-1" => lsp::CodeAction {
14081 kind: Some("code-action-1".into()),
14082 edit: Some(lsp::WorkspaceEdit::new(
14083 [(
14084 uri,
14085 vec![lsp::TextEdit::new(
14086 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
14087 "applied-code-action-1-edit\n".to_string(),
14088 )],
14089 )]
14090 .into_iter()
14091 .collect(),
14092 )),
14093 command: Some(lsp::Command {
14094 command: "the-command-for-code-action-1".into(),
14095 ..Default::default()
14096 }),
14097 ..Default::default()
14098 },
14099 "code-action-2" => lsp::CodeAction {
14100 kind: Some("code-action-2".into()),
14101 edit: Some(lsp::WorkspaceEdit::new(
14102 [(
14103 uri,
14104 vec![lsp::TextEdit::new(
14105 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
14106 "applied-code-action-2-edit\n".to_string(),
14107 )],
14108 )]
14109 .into_iter()
14110 .collect(),
14111 )),
14112 ..Default::default()
14113 },
14114 req => panic!("Unexpected code action request: {:?}", req),
14115 };
14116 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14117 code_action,
14118 )]))
14119 },
14120 );
14121
14122 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
14123 move |params, _| async move { Ok(params) }
14124 });
14125
14126 let command_lock = Arc::new(futures::lock::Mutex::new(()));
14127 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
14128 let fake = fake_server.clone();
14129 let lock = command_lock.clone();
14130 move |params, _| {
14131 assert_eq!(params.command, "the-command-for-code-action-1");
14132 let fake = fake.clone();
14133 let lock = lock.clone();
14134 async move {
14135 lock.lock().await;
14136 fake.server
14137 .request::<lsp::request::ApplyWorkspaceEdit>(
14138 lsp::ApplyWorkspaceEditParams {
14139 label: None,
14140 edit: lsp::WorkspaceEdit {
14141 changes: Some(
14142 [(
14143 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
14144 vec![lsp::TextEdit {
14145 range: lsp::Range::new(
14146 lsp::Position::new(0, 0),
14147 lsp::Position::new(0, 0),
14148 ),
14149 new_text: "applied-code-action-1-command\n".into(),
14150 }],
14151 )]
14152 .into_iter()
14153 .collect(),
14154 ),
14155 ..Default::default()
14156 },
14157 },
14158 DEFAULT_LSP_REQUEST_TIMEOUT,
14159 )
14160 .await
14161 .into_response()
14162 .unwrap();
14163 Ok(Some(json!(null)))
14164 }
14165 }
14166 });
14167
14168 editor
14169 .update_in(cx, |editor, window, cx| {
14170 editor.perform_format(
14171 project.clone(),
14172 FormatTrigger::Manual,
14173 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
14174 window,
14175 cx,
14176 )
14177 })
14178 .unwrap()
14179 .await;
14180 editor.update(cx, |editor, cx| {
14181 assert_eq!(
14182 editor.text(cx),
14183 r#"
14184 applied-code-action-2-edit
14185 applied-code-action-1-command
14186 applied-code-action-1-edit
14187 applied-formatting
14188 one
14189 two
14190 three
14191 "#
14192 .unindent()
14193 );
14194 });
14195
14196 editor.update_in(cx, |editor, window, cx| {
14197 editor.undo(&Default::default(), window, cx);
14198 assert_eq!(editor.text(cx), "one \ntwo \nthree");
14199 });
14200
14201 // Perform a manual edit while waiting for an LSP command
14202 // that's being run as part of a formatting code action.
14203 let lock_guard = command_lock.lock().await;
14204 let format = editor
14205 .update_in(cx, |editor, window, cx| {
14206 editor.perform_format(
14207 project.clone(),
14208 FormatTrigger::Manual,
14209 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
14210 window,
14211 cx,
14212 )
14213 })
14214 .unwrap();
14215 cx.run_until_parked();
14216 editor.update(cx, |editor, cx| {
14217 assert_eq!(
14218 editor.text(cx),
14219 r#"
14220 applied-code-action-1-edit
14221 applied-formatting
14222 one
14223 two
14224 three
14225 "#
14226 .unindent()
14227 );
14228
14229 editor.buffer.update(cx, |buffer, cx| {
14230 let ix = buffer.len(cx);
14231 buffer.edit([(ix..ix, "edited\n")], None, cx);
14232 });
14233 });
14234
14235 // Allow the LSP command to proceed. Because the buffer was edited,
14236 // the second code action will not be run.
14237 drop(lock_guard);
14238 format.await;
14239 editor.update_in(cx, |editor, window, cx| {
14240 assert_eq!(
14241 editor.text(cx),
14242 r#"
14243 applied-code-action-1-command
14244 applied-code-action-1-edit
14245 applied-formatting
14246 one
14247 two
14248 three
14249 edited
14250 "#
14251 .unindent()
14252 );
14253
14254 // The manual edit is undone first, because it is the last thing the user did
14255 // (even though the command completed afterwards).
14256 editor.undo(&Default::default(), window, cx);
14257 assert_eq!(
14258 editor.text(cx),
14259 r#"
14260 applied-code-action-1-command
14261 applied-code-action-1-edit
14262 applied-formatting
14263 one
14264 two
14265 three
14266 "#
14267 .unindent()
14268 );
14269
14270 // All the formatting (including the command, which completed after the manual edit)
14271 // is undone together.
14272 editor.undo(&Default::default(), window, cx);
14273 assert_eq!(editor.text(cx), "one \ntwo \nthree");
14274 });
14275}
14276
14277#[gpui::test]
14278async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
14279 init_test(cx, |settings| {
14280 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
14281 settings::LanguageServerFormatterSpecifier::Current,
14282 )]))
14283 });
14284
14285 let fs = FakeFs::new(cx.executor());
14286 fs.insert_file(path!("/file.ts"), Default::default()).await;
14287
14288 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14289
14290 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14291 language_registry.add(Arc::new(Language::new(
14292 LanguageConfig {
14293 name: "TypeScript".into(),
14294 matcher: LanguageMatcher {
14295 path_suffixes: vec!["ts".to_string()],
14296 ..Default::default()
14297 },
14298 ..LanguageConfig::default()
14299 },
14300 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14301 )));
14302 update_test_language_settings(cx, &|settings| {
14303 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
14304 });
14305 let mut fake_servers = language_registry.register_fake_lsp(
14306 "TypeScript",
14307 FakeLspAdapter {
14308 capabilities: lsp::ServerCapabilities {
14309 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14310 ..Default::default()
14311 },
14312 ..Default::default()
14313 },
14314 );
14315
14316 let buffer = project
14317 .update(cx, |project, cx| {
14318 project.open_local_buffer(path!("/file.ts"), cx)
14319 })
14320 .await
14321 .unwrap();
14322
14323 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14324 let (editor, cx) = cx.add_window_view(|window, cx| {
14325 build_editor_with_project(project.clone(), buffer, window, cx)
14326 });
14327 editor.update_in(cx, |editor, window, cx| {
14328 editor.set_text(
14329 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
14330 window,
14331 cx,
14332 )
14333 });
14334
14335 let fake_server = fake_servers.next().await.unwrap();
14336
14337 let format = editor
14338 .update_in(cx, |editor, window, cx| {
14339 editor.perform_code_action_kind(
14340 project.clone(),
14341 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
14342 window,
14343 cx,
14344 )
14345 })
14346 .unwrap();
14347 fake_server
14348 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
14349 assert_eq!(
14350 params.text_document.uri,
14351 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
14352 );
14353 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14354 lsp::CodeAction {
14355 title: "Organize Imports".to_string(),
14356 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
14357 edit: Some(lsp::WorkspaceEdit {
14358 changes: Some(
14359 [(
14360 params.text_document.uri.clone(),
14361 vec![lsp::TextEdit::new(
14362 lsp::Range::new(
14363 lsp::Position::new(1, 0),
14364 lsp::Position::new(2, 0),
14365 ),
14366 "".to_string(),
14367 )],
14368 )]
14369 .into_iter()
14370 .collect(),
14371 ),
14372 ..Default::default()
14373 }),
14374 ..Default::default()
14375 },
14376 )]))
14377 })
14378 .next()
14379 .await;
14380 format.await;
14381 assert_eq!(
14382 editor.update(cx, |editor, cx| editor.text(cx)),
14383 "import { a } from 'module';\n\nconst x = a;\n"
14384 );
14385
14386 editor.update_in(cx, |editor, window, cx| {
14387 editor.set_text(
14388 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
14389 window,
14390 cx,
14391 )
14392 });
14393 // Ensure we don't lock if code action hangs.
14394 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14395 move |params, _| async move {
14396 assert_eq!(
14397 params.text_document.uri,
14398 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
14399 );
14400 futures::future::pending::<()>().await;
14401 unreachable!()
14402 },
14403 );
14404 let format = editor
14405 .update_in(cx, |editor, window, cx| {
14406 editor.perform_code_action_kind(
14407 project,
14408 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
14409 window,
14410 cx,
14411 )
14412 })
14413 .unwrap();
14414 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
14415 format.await;
14416 assert_eq!(
14417 editor.update(cx, |editor, cx| editor.text(cx)),
14418 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
14419 );
14420}
14421
14422#[gpui::test]
14423async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
14424 init_test(cx, |_| {});
14425
14426 let mut cx = EditorLspTestContext::new_rust(
14427 lsp::ServerCapabilities {
14428 document_formatting_provider: Some(lsp::OneOf::Left(true)),
14429 ..Default::default()
14430 },
14431 cx,
14432 )
14433 .await;
14434
14435 cx.set_state(indoc! {"
14436 one.twoˇ
14437 "});
14438
14439 // The format request takes a long time. When it completes, it inserts
14440 // a newline and an indent before the `.`
14441 cx.lsp
14442 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
14443 let executor = cx.background_executor().clone();
14444 async move {
14445 executor.timer(Duration::from_millis(100)).await;
14446 Ok(Some(vec![lsp::TextEdit {
14447 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
14448 new_text: "\n ".into(),
14449 }]))
14450 }
14451 });
14452
14453 // Submit a format request.
14454 let format_1 = cx
14455 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
14456 .unwrap();
14457 cx.executor().run_until_parked();
14458
14459 // Submit a second format request.
14460 let format_2 = cx
14461 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
14462 .unwrap();
14463 cx.executor().run_until_parked();
14464
14465 // Wait for both format requests to complete
14466 cx.executor().advance_clock(Duration::from_millis(200));
14467 format_1.await.unwrap();
14468 format_2.await.unwrap();
14469
14470 // The formatting edits only happens once.
14471 cx.assert_editor_state(indoc! {"
14472 one
14473 .twoˇ
14474 "});
14475}
14476
14477#[gpui::test]
14478async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
14479 init_test(cx, |settings| {
14480 settings.defaults.formatter = Some(FormatterList::default())
14481 });
14482
14483 let mut cx = EditorLspTestContext::new_rust(
14484 lsp::ServerCapabilities {
14485 document_formatting_provider: Some(lsp::OneOf::Left(true)),
14486 ..Default::default()
14487 },
14488 cx,
14489 )
14490 .await;
14491
14492 // Record which buffer changes have been sent to the language server
14493 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
14494 cx.lsp
14495 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
14496 let buffer_changes = buffer_changes.clone();
14497 move |params, _| {
14498 buffer_changes.lock().extend(
14499 params
14500 .content_changes
14501 .into_iter()
14502 .map(|e| (e.range.unwrap(), e.text)),
14503 );
14504 }
14505 });
14506 // Handle formatting requests to the language server.
14507 cx.lsp
14508 .set_request_handler::<lsp::request::Formatting, _, _>({
14509 move |_, _| {
14510 // Insert blank lines between each line of the buffer.
14511 async move {
14512 // TODO: this assertion is not reliably true. Currently nothing guarantees that we deliver
14513 // DidChangedTextDocument to the LSP before sending the formatting request.
14514 // assert_eq!(
14515 // &buffer_changes.lock()[1..],
14516 // &[
14517 // (
14518 // lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
14519 // "".into()
14520 // ),
14521 // (
14522 // lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
14523 // "".into()
14524 // ),
14525 // (
14526 // lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
14527 // "\n".into()
14528 // ),
14529 // ]
14530 // );
14531
14532 Ok(Some(vec![
14533 lsp::TextEdit {
14534 range: lsp::Range::new(
14535 lsp::Position::new(1, 0),
14536 lsp::Position::new(1, 0),
14537 ),
14538 new_text: "\n".into(),
14539 },
14540 lsp::TextEdit {
14541 range: lsp::Range::new(
14542 lsp::Position::new(2, 0),
14543 lsp::Position::new(2, 0),
14544 ),
14545 new_text: "\n".into(),
14546 },
14547 ]))
14548 }
14549 }
14550 });
14551
14552 // Set up a buffer white some trailing whitespace and no trailing newline.
14553 cx.set_state(
14554 &[
14555 "one ", //
14556 "twoˇ", //
14557 "three ", //
14558 "four", //
14559 ]
14560 .join("\n"),
14561 );
14562
14563 // Submit a format request.
14564 let format = cx
14565 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
14566 .unwrap();
14567
14568 cx.run_until_parked();
14569 // After formatting the buffer, the trailing whitespace is stripped,
14570 // a newline is appended, and the edits provided by the language server
14571 // have been applied.
14572 format.await.unwrap();
14573
14574 cx.assert_editor_state(
14575 &[
14576 "one", //
14577 "", //
14578 "twoˇ", //
14579 "", //
14580 "three", //
14581 "four", //
14582 "", //
14583 ]
14584 .join("\n"),
14585 );
14586
14587 // Undoing the formatting undoes the trailing whitespace removal, the
14588 // trailing newline, and the LSP edits.
14589 cx.update_buffer(|buffer, cx| buffer.undo(cx));
14590 cx.assert_editor_state(
14591 &[
14592 "one ", //
14593 "twoˇ", //
14594 "three ", //
14595 "four", //
14596 ]
14597 .join("\n"),
14598 );
14599}
14600
14601#[gpui::test]
14602async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
14603 cx: &mut TestAppContext,
14604) {
14605 init_test(cx, |_| {});
14606
14607 cx.update(|cx| {
14608 cx.update_global::<SettingsStore, _>(|settings, cx| {
14609 settings.update_user_settings(cx, |settings| {
14610 settings.editor.auto_signature_help = Some(true);
14611 settings.editor.hover_popover_delay = Some(DelayMs(300));
14612 });
14613 });
14614 });
14615
14616 let mut cx = EditorLspTestContext::new_rust(
14617 lsp::ServerCapabilities {
14618 signature_help_provider: Some(lsp::SignatureHelpOptions {
14619 ..Default::default()
14620 }),
14621 ..Default::default()
14622 },
14623 cx,
14624 )
14625 .await;
14626
14627 let language = Language::new(
14628 LanguageConfig {
14629 name: "Rust".into(),
14630 brackets: BracketPairConfig {
14631 pairs: vec![
14632 BracketPair {
14633 start: "{".to_string(),
14634 end: "}".to_string(),
14635 close: true,
14636 surround: true,
14637 newline: true,
14638 },
14639 BracketPair {
14640 start: "(".to_string(),
14641 end: ")".to_string(),
14642 close: true,
14643 surround: true,
14644 newline: true,
14645 },
14646 BracketPair {
14647 start: "/*".to_string(),
14648 end: " */".to_string(),
14649 close: true,
14650 surround: true,
14651 newline: true,
14652 },
14653 BracketPair {
14654 start: "[".to_string(),
14655 end: "]".to_string(),
14656 close: false,
14657 surround: false,
14658 newline: true,
14659 },
14660 BracketPair {
14661 start: "\"".to_string(),
14662 end: "\"".to_string(),
14663 close: true,
14664 surround: true,
14665 newline: false,
14666 },
14667 BracketPair {
14668 start: "<".to_string(),
14669 end: ">".to_string(),
14670 close: false,
14671 surround: true,
14672 newline: true,
14673 },
14674 ],
14675 ..Default::default()
14676 },
14677 autoclose_before: "})]".to_string(),
14678 ..Default::default()
14679 },
14680 Some(tree_sitter_rust::LANGUAGE.into()),
14681 );
14682 let language = Arc::new(language);
14683
14684 cx.language_registry().add(language.clone());
14685 cx.update_buffer(|buffer, cx| {
14686 buffer.set_language(Some(language), cx);
14687 });
14688
14689 cx.set_state(
14690 &r#"
14691 fn main() {
14692 sampleˇ
14693 }
14694 "#
14695 .unindent(),
14696 );
14697
14698 cx.update_editor(|editor, window, cx| {
14699 editor.handle_input("(", window, cx);
14700 });
14701 cx.assert_editor_state(
14702 &"
14703 fn main() {
14704 sample(ˇ)
14705 }
14706 "
14707 .unindent(),
14708 );
14709
14710 let mocked_response = lsp::SignatureHelp {
14711 signatures: vec![lsp::SignatureInformation {
14712 label: "fn sample(param1: u8, param2: u8)".to_string(),
14713 documentation: None,
14714 parameters: Some(vec![
14715 lsp::ParameterInformation {
14716 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14717 documentation: None,
14718 },
14719 lsp::ParameterInformation {
14720 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14721 documentation: None,
14722 },
14723 ]),
14724 active_parameter: None,
14725 }],
14726 active_signature: Some(0),
14727 active_parameter: Some(0),
14728 };
14729 handle_signature_help_request(&mut cx, mocked_response).await;
14730
14731 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14732 .await;
14733
14734 cx.editor(|editor, _, _| {
14735 let signature_help_state = editor.signature_help_state.popover().cloned();
14736 let signature = signature_help_state.unwrap();
14737 assert_eq!(
14738 signature.signatures[signature.current_signature].label,
14739 "fn sample(param1: u8, param2: u8)"
14740 );
14741 });
14742}
14743
14744#[gpui::test]
14745async fn test_signature_help_delay_only_for_auto(cx: &mut TestAppContext) {
14746 init_test(cx, |_| {});
14747
14748 let delay_ms = 500;
14749 cx.update(|cx| {
14750 cx.update_global::<SettingsStore, _>(|settings, cx| {
14751 settings.update_user_settings(cx, |settings| {
14752 settings.editor.auto_signature_help = Some(true);
14753 settings.editor.show_signature_help_after_edits = Some(false);
14754 settings.editor.hover_popover_delay = Some(DelayMs(delay_ms));
14755 });
14756 });
14757 });
14758
14759 let mut cx = EditorLspTestContext::new_rust(
14760 lsp::ServerCapabilities {
14761 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14762 ..lsp::ServerCapabilities::default()
14763 },
14764 cx,
14765 )
14766 .await;
14767
14768 let mocked_response = lsp::SignatureHelp {
14769 signatures: vec![lsp::SignatureInformation {
14770 label: "fn sample(param1: u8)".to_string(),
14771 documentation: None,
14772 parameters: Some(vec![lsp::ParameterInformation {
14773 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14774 documentation: None,
14775 }]),
14776 active_parameter: None,
14777 }],
14778 active_signature: Some(0),
14779 active_parameter: Some(0),
14780 };
14781
14782 cx.set_state(indoc! {"
14783 fn main() {
14784 sample(ˇ);
14785 }
14786
14787 fn sample(param1: u8) {}
14788 "});
14789
14790 // Manual trigger should show immediately without delay
14791 cx.update_editor(|editor, window, cx| {
14792 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14793 });
14794 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14795 cx.run_until_parked();
14796 cx.editor(|editor, _, _| {
14797 assert!(
14798 editor.signature_help_state.is_shown(),
14799 "Manual trigger should show signature help without delay"
14800 );
14801 });
14802
14803 cx.update_editor(|editor, _, cx| {
14804 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
14805 });
14806 cx.run_until_parked();
14807 cx.editor(|editor, _, _| {
14808 assert!(!editor.signature_help_state.is_shown());
14809 });
14810
14811 // Auto trigger (cursor movement into brackets) should respect delay
14812 cx.set_state(indoc! {"
14813 fn main() {
14814 sampleˇ();
14815 }
14816
14817 fn sample(param1: u8) {}
14818 "});
14819 cx.update_editor(|editor, window, cx| {
14820 editor.move_right(&MoveRight, window, cx);
14821 });
14822 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14823 cx.run_until_parked();
14824 cx.editor(|editor, _, _| {
14825 assert!(
14826 !editor.signature_help_state.is_shown(),
14827 "Auto trigger should wait for delay before showing signature help"
14828 );
14829 });
14830
14831 cx.executor()
14832 .advance_clock(Duration::from_millis(delay_ms + 50));
14833 cx.run_until_parked();
14834 cx.editor(|editor, _, _| {
14835 assert!(
14836 editor.signature_help_state.is_shown(),
14837 "Auto trigger should show signature help after delay elapsed"
14838 );
14839 });
14840}
14841
14842#[gpui::test]
14843async fn test_signature_help_after_edits_no_delay(cx: &mut TestAppContext) {
14844 init_test(cx, |_| {});
14845
14846 let delay_ms = 500;
14847 cx.update(|cx| {
14848 cx.update_global::<SettingsStore, _>(|settings, cx| {
14849 settings.update_user_settings(cx, |settings| {
14850 settings.editor.auto_signature_help = Some(false);
14851 settings.editor.show_signature_help_after_edits = Some(true);
14852 settings.editor.hover_popover_delay = Some(DelayMs(delay_ms));
14853 });
14854 });
14855 });
14856
14857 let mut cx = EditorLspTestContext::new_rust(
14858 lsp::ServerCapabilities {
14859 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14860 ..lsp::ServerCapabilities::default()
14861 },
14862 cx,
14863 )
14864 .await;
14865
14866 let language = Arc::new(Language::new(
14867 LanguageConfig {
14868 name: "Rust".into(),
14869 brackets: BracketPairConfig {
14870 pairs: vec![BracketPair {
14871 start: "(".to_string(),
14872 end: ")".to_string(),
14873 close: true,
14874 surround: true,
14875 newline: true,
14876 }],
14877 ..BracketPairConfig::default()
14878 },
14879 autoclose_before: "})".to_string(),
14880 ..LanguageConfig::default()
14881 },
14882 Some(tree_sitter_rust::LANGUAGE.into()),
14883 ));
14884 cx.language_registry().add(language.clone());
14885 cx.update_buffer(|buffer, cx| {
14886 buffer.set_language(Some(language), cx);
14887 });
14888
14889 let mocked_response = lsp::SignatureHelp {
14890 signatures: vec![lsp::SignatureInformation {
14891 label: "fn sample(param1: u8)".to_string(),
14892 documentation: None,
14893 parameters: Some(vec![lsp::ParameterInformation {
14894 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14895 documentation: None,
14896 }]),
14897 active_parameter: None,
14898 }],
14899 active_signature: Some(0),
14900 active_parameter: Some(0),
14901 };
14902
14903 cx.set_state(indoc! {"
14904 fn main() {
14905 sampleˇ
14906 }
14907 "});
14908
14909 // Typing bracket should show signature help immediately without delay
14910 cx.update_editor(|editor, window, cx| {
14911 editor.handle_input("(", window, cx);
14912 });
14913 handle_signature_help_request(&mut cx, mocked_response).await;
14914 cx.run_until_parked();
14915 cx.editor(|editor, _, _| {
14916 assert!(
14917 editor.signature_help_state.is_shown(),
14918 "show_signature_help_after_edits should show signature help without delay"
14919 );
14920 });
14921}
14922
14923#[gpui::test]
14924async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
14925 init_test(cx, |_| {});
14926
14927 cx.update(|cx| {
14928 cx.update_global::<SettingsStore, _>(|settings, cx| {
14929 settings.update_user_settings(cx, |settings| {
14930 settings.editor.auto_signature_help = Some(false);
14931 settings.editor.show_signature_help_after_edits = Some(false);
14932 });
14933 });
14934 });
14935
14936 let mut cx = EditorLspTestContext::new_rust(
14937 lsp::ServerCapabilities {
14938 signature_help_provider: Some(lsp::SignatureHelpOptions {
14939 ..Default::default()
14940 }),
14941 ..Default::default()
14942 },
14943 cx,
14944 )
14945 .await;
14946
14947 let language = Language::new(
14948 LanguageConfig {
14949 name: "Rust".into(),
14950 brackets: BracketPairConfig {
14951 pairs: vec![
14952 BracketPair {
14953 start: "{".to_string(),
14954 end: "}".to_string(),
14955 close: true,
14956 surround: true,
14957 newline: true,
14958 },
14959 BracketPair {
14960 start: "(".to_string(),
14961 end: ")".to_string(),
14962 close: true,
14963 surround: true,
14964 newline: true,
14965 },
14966 BracketPair {
14967 start: "/*".to_string(),
14968 end: " */".to_string(),
14969 close: true,
14970 surround: true,
14971 newline: true,
14972 },
14973 BracketPair {
14974 start: "[".to_string(),
14975 end: "]".to_string(),
14976 close: false,
14977 surround: false,
14978 newline: true,
14979 },
14980 BracketPair {
14981 start: "\"".to_string(),
14982 end: "\"".to_string(),
14983 close: true,
14984 surround: true,
14985 newline: false,
14986 },
14987 BracketPair {
14988 start: "<".to_string(),
14989 end: ">".to_string(),
14990 close: false,
14991 surround: true,
14992 newline: true,
14993 },
14994 ],
14995 ..Default::default()
14996 },
14997 autoclose_before: "})]".to_string(),
14998 ..Default::default()
14999 },
15000 Some(tree_sitter_rust::LANGUAGE.into()),
15001 );
15002 let language = Arc::new(language);
15003
15004 cx.language_registry().add(language.clone());
15005 cx.update_buffer(|buffer, cx| {
15006 buffer.set_language(Some(language), cx);
15007 });
15008
15009 // Ensure that signature_help is not called when no signature help is enabled.
15010 cx.set_state(
15011 &r#"
15012 fn main() {
15013 sampleˇ
15014 }
15015 "#
15016 .unindent(),
15017 );
15018 cx.update_editor(|editor, window, cx| {
15019 editor.handle_input("(", window, cx);
15020 });
15021 cx.assert_editor_state(
15022 &"
15023 fn main() {
15024 sample(ˇ)
15025 }
15026 "
15027 .unindent(),
15028 );
15029 cx.editor(|editor, _, _| {
15030 assert!(editor.signature_help_state.task().is_none());
15031 });
15032
15033 let mocked_response = lsp::SignatureHelp {
15034 signatures: vec![lsp::SignatureInformation {
15035 label: "fn sample(param1: u8, param2: u8)".to_string(),
15036 documentation: None,
15037 parameters: Some(vec![
15038 lsp::ParameterInformation {
15039 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15040 documentation: None,
15041 },
15042 lsp::ParameterInformation {
15043 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15044 documentation: None,
15045 },
15046 ]),
15047 active_parameter: None,
15048 }],
15049 active_signature: Some(0),
15050 active_parameter: Some(0),
15051 };
15052
15053 // Ensure that signature_help is called when enabled afte edits
15054 cx.update(|_, cx| {
15055 cx.update_global::<SettingsStore, _>(|settings, cx| {
15056 settings.update_user_settings(cx, |settings| {
15057 settings.editor.auto_signature_help = Some(false);
15058 settings.editor.show_signature_help_after_edits = Some(true);
15059 });
15060 });
15061 });
15062 cx.set_state(
15063 &r#"
15064 fn main() {
15065 sampleˇ
15066 }
15067 "#
15068 .unindent(),
15069 );
15070 cx.update_editor(|editor, window, cx| {
15071 editor.handle_input("(", window, cx);
15072 });
15073 cx.assert_editor_state(
15074 &"
15075 fn main() {
15076 sample(ˇ)
15077 }
15078 "
15079 .unindent(),
15080 );
15081 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
15082 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15083 .await;
15084 cx.update_editor(|editor, _, _| {
15085 let signature_help_state = editor.signature_help_state.popover().cloned();
15086 assert!(signature_help_state.is_some());
15087 let signature = signature_help_state.unwrap();
15088 assert_eq!(
15089 signature.signatures[signature.current_signature].label,
15090 "fn sample(param1: u8, param2: u8)"
15091 );
15092 editor.signature_help_state = SignatureHelpState::default();
15093 });
15094
15095 // Ensure that signature_help is called when auto signature help override is enabled
15096 cx.update(|_, cx| {
15097 cx.update_global::<SettingsStore, _>(|settings, cx| {
15098 settings.update_user_settings(cx, |settings| {
15099 settings.editor.auto_signature_help = Some(true);
15100 settings.editor.show_signature_help_after_edits = Some(false);
15101 });
15102 });
15103 });
15104 cx.set_state(
15105 &r#"
15106 fn main() {
15107 sampleˇ
15108 }
15109 "#
15110 .unindent(),
15111 );
15112 cx.update_editor(|editor, window, cx| {
15113 editor.handle_input("(", window, cx);
15114 });
15115 cx.assert_editor_state(
15116 &"
15117 fn main() {
15118 sample(ˇ)
15119 }
15120 "
15121 .unindent(),
15122 );
15123 handle_signature_help_request(&mut cx, mocked_response).await;
15124 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15125 .await;
15126 cx.editor(|editor, _, _| {
15127 let signature_help_state = editor.signature_help_state.popover().cloned();
15128 assert!(signature_help_state.is_some());
15129 let signature = signature_help_state.unwrap();
15130 assert_eq!(
15131 signature.signatures[signature.current_signature].label,
15132 "fn sample(param1: u8, param2: u8)"
15133 );
15134 });
15135}
15136
15137#[gpui::test]
15138async fn test_signature_help(cx: &mut TestAppContext) {
15139 init_test(cx, |_| {});
15140 cx.update(|cx| {
15141 cx.update_global::<SettingsStore, _>(|settings, cx| {
15142 settings.update_user_settings(cx, |settings| {
15143 settings.editor.auto_signature_help = Some(true);
15144 });
15145 });
15146 });
15147
15148 let mut cx = EditorLspTestContext::new_rust(
15149 lsp::ServerCapabilities {
15150 signature_help_provider: Some(lsp::SignatureHelpOptions {
15151 ..Default::default()
15152 }),
15153 ..Default::default()
15154 },
15155 cx,
15156 )
15157 .await;
15158
15159 // A test that directly calls `show_signature_help`
15160 cx.update_editor(|editor, window, cx| {
15161 editor.show_signature_help(&ShowSignatureHelp, window, cx);
15162 });
15163
15164 let mocked_response = lsp::SignatureHelp {
15165 signatures: vec![lsp::SignatureInformation {
15166 label: "fn sample(param1: u8, param2: u8)".to_string(),
15167 documentation: None,
15168 parameters: Some(vec![
15169 lsp::ParameterInformation {
15170 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15171 documentation: None,
15172 },
15173 lsp::ParameterInformation {
15174 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15175 documentation: None,
15176 },
15177 ]),
15178 active_parameter: None,
15179 }],
15180 active_signature: Some(0),
15181 active_parameter: Some(0),
15182 };
15183 handle_signature_help_request(&mut cx, mocked_response).await;
15184
15185 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15186 .await;
15187
15188 cx.editor(|editor, _, _| {
15189 let signature_help_state = editor.signature_help_state.popover().cloned();
15190 assert!(signature_help_state.is_some());
15191 let signature = signature_help_state.unwrap();
15192 assert_eq!(
15193 signature.signatures[signature.current_signature].label,
15194 "fn sample(param1: u8, param2: u8)"
15195 );
15196 });
15197
15198 // When exiting outside from inside the brackets, `signature_help` is closed.
15199 cx.set_state(indoc! {"
15200 fn main() {
15201 sample(ˇ);
15202 }
15203
15204 fn sample(param1: u8, param2: u8) {}
15205 "});
15206
15207 cx.update_editor(|editor, window, cx| {
15208 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15209 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
15210 });
15211 });
15212
15213 let mocked_response = lsp::SignatureHelp {
15214 signatures: Vec::new(),
15215 active_signature: None,
15216 active_parameter: None,
15217 };
15218 handle_signature_help_request(&mut cx, mocked_response).await;
15219
15220 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
15221 .await;
15222
15223 cx.editor(|editor, _, _| {
15224 assert!(!editor.signature_help_state.is_shown());
15225 });
15226
15227 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
15228 cx.set_state(indoc! {"
15229 fn main() {
15230 sample(ˇ);
15231 }
15232
15233 fn sample(param1: u8, param2: u8) {}
15234 "});
15235
15236 let mocked_response = lsp::SignatureHelp {
15237 signatures: vec![lsp::SignatureInformation {
15238 label: "fn sample(param1: u8, param2: u8)".to_string(),
15239 documentation: None,
15240 parameters: Some(vec![
15241 lsp::ParameterInformation {
15242 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15243 documentation: None,
15244 },
15245 lsp::ParameterInformation {
15246 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15247 documentation: None,
15248 },
15249 ]),
15250 active_parameter: None,
15251 }],
15252 active_signature: Some(0),
15253 active_parameter: Some(0),
15254 };
15255 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
15256 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15257 .await;
15258 cx.editor(|editor, _, _| {
15259 assert!(editor.signature_help_state.is_shown());
15260 });
15261
15262 // Restore the popover with more parameter input
15263 cx.set_state(indoc! {"
15264 fn main() {
15265 sample(param1, param2ˇ);
15266 }
15267
15268 fn sample(param1: u8, param2: u8) {}
15269 "});
15270
15271 let mocked_response = lsp::SignatureHelp {
15272 signatures: vec![lsp::SignatureInformation {
15273 label: "fn sample(param1: u8, param2: u8)".to_string(),
15274 documentation: None,
15275 parameters: Some(vec![
15276 lsp::ParameterInformation {
15277 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15278 documentation: None,
15279 },
15280 lsp::ParameterInformation {
15281 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15282 documentation: None,
15283 },
15284 ]),
15285 active_parameter: None,
15286 }],
15287 active_signature: Some(0),
15288 active_parameter: Some(1),
15289 };
15290 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
15291 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15292 .await;
15293
15294 // When selecting a range, the popover is gone.
15295 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
15296 cx.update_editor(|editor, window, cx| {
15297 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15298 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
15299 })
15300 });
15301 cx.assert_editor_state(indoc! {"
15302 fn main() {
15303 sample(param1, «ˇparam2»);
15304 }
15305
15306 fn sample(param1: u8, param2: u8) {}
15307 "});
15308 cx.editor(|editor, _, _| {
15309 assert!(!editor.signature_help_state.is_shown());
15310 });
15311
15312 // When unselecting again, the popover is back if within the brackets.
15313 cx.update_editor(|editor, window, cx| {
15314 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15315 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
15316 })
15317 });
15318 cx.assert_editor_state(indoc! {"
15319 fn main() {
15320 sample(param1, ˇparam2);
15321 }
15322
15323 fn sample(param1: u8, param2: u8) {}
15324 "});
15325 handle_signature_help_request(&mut cx, mocked_response).await;
15326 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15327 .await;
15328 cx.editor(|editor, _, _| {
15329 assert!(editor.signature_help_state.is_shown());
15330 });
15331
15332 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
15333 cx.update_editor(|editor, window, cx| {
15334 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15335 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
15336 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
15337 })
15338 });
15339 cx.assert_editor_state(indoc! {"
15340 fn main() {
15341 sample(param1, ˇparam2);
15342 }
15343
15344 fn sample(param1: u8, param2: u8) {}
15345 "});
15346
15347 let mocked_response = lsp::SignatureHelp {
15348 signatures: vec![lsp::SignatureInformation {
15349 label: "fn sample(param1: u8, param2: u8)".to_string(),
15350 documentation: None,
15351 parameters: Some(vec![
15352 lsp::ParameterInformation {
15353 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15354 documentation: None,
15355 },
15356 lsp::ParameterInformation {
15357 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15358 documentation: None,
15359 },
15360 ]),
15361 active_parameter: None,
15362 }],
15363 active_signature: Some(0),
15364 active_parameter: Some(1),
15365 };
15366 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
15367 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15368 .await;
15369 cx.update_editor(|editor, _, cx| {
15370 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
15371 });
15372 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
15373 .await;
15374 cx.update_editor(|editor, window, cx| {
15375 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15376 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
15377 })
15378 });
15379 cx.assert_editor_state(indoc! {"
15380 fn main() {
15381 sample(param1, «ˇparam2»);
15382 }
15383
15384 fn sample(param1: u8, param2: u8) {}
15385 "});
15386 cx.update_editor(|editor, window, cx| {
15387 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15388 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
15389 })
15390 });
15391 cx.assert_editor_state(indoc! {"
15392 fn main() {
15393 sample(param1, ˇparam2);
15394 }
15395
15396 fn sample(param1: u8, param2: u8) {}
15397 "});
15398 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
15399 .await;
15400}
15401
15402#[gpui::test]
15403async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
15404 init_test(cx, |_| {});
15405
15406 let mut cx = EditorLspTestContext::new_rust(
15407 lsp::ServerCapabilities {
15408 signature_help_provider: Some(lsp::SignatureHelpOptions {
15409 ..Default::default()
15410 }),
15411 ..Default::default()
15412 },
15413 cx,
15414 )
15415 .await;
15416
15417 cx.set_state(indoc! {"
15418 fn main() {
15419 overloadedˇ
15420 }
15421 "});
15422
15423 cx.update_editor(|editor, window, cx| {
15424 editor.handle_input("(", window, cx);
15425 editor.show_signature_help(&ShowSignatureHelp, window, cx);
15426 });
15427
15428 // Mock response with 3 signatures
15429 let mocked_response = lsp::SignatureHelp {
15430 signatures: vec![
15431 lsp::SignatureInformation {
15432 label: "fn overloaded(x: i32)".to_string(),
15433 documentation: None,
15434 parameters: Some(vec![lsp::ParameterInformation {
15435 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
15436 documentation: None,
15437 }]),
15438 active_parameter: None,
15439 },
15440 lsp::SignatureInformation {
15441 label: "fn overloaded(x: i32, y: i32)".to_string(),
15442 documentation: None,
15443 parameters: Some(vec![
15444 lsp::ParameterInformation {
15445 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
15446 documentation: None,
15447 },
15448 lsp::ParameterInformation {
15449 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
15450 documentation: None,
15451 },
15452 ]),
15453 active_parameter: None,
15454 },
15455 lsp::SignatureInformation {
15456 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
15457 documentation: None,
15458 parameters: Some(vec![
15459 lsp::ParameterInformation {
15460 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
15461 documentation: None,
15462 },
15463 lsp::ParameterInformation {
15464 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
15465 documentation: None,
15466 },
15467 lsp::ParameterInformation {
15468 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
15469 documentation: None,
15470 },
15471 ]),
15472 active_parameter: None,
15473 },
15474 ],
15475 active_signature: Some(1),
15476 active_parameter: Some(0),
15477 };
15478 handle_signature_help_request(&mut cx, mocked_response).await;
15479
15480 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15481 .await;
15482
15483 // Verify we have multiple signatures and the right one is selected
15484 cx.editor(|editor, _, _| {
15485 let popover = editor.signature_help_state.popover().cloned().unwrap();
15486 assert_eq!(popover.signatures.len(), 3);
15487 // active_signature was 1, so that should be the current
15488 assert_eq!(popover.current_signature, 1);
15489 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
15490 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
15491 assert_eq!(
15492 popover.signatures[2].label,
15493 "fn overloaded(x: i32, y: i32, z: i32)"
15494 );
15495 });
15496
15497 // Test navigation functionality
15498 cx.update_editor(|editor, window, cx| {
15499 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
15500 });
15501
15502 cx.editor(|editor, _, _| {
15503 let popover = editor.signature_help_state.popover().cloned().unwrap();
15504 assert_eq!(popover.current_signature, 2);
15505 });
15506
15507 // Test wrap around
15508 cx.update_editor(|editor, window, cx| {
15509 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
15510 });
15511
15512 cx.editor(|editor, _, _| {
15513 let popover = editor.signature_help_state.popover().cloned().unwrap();
15514 assert_eq!(popover.current_signature, 0);
15515 });
15516
15517 // Test previous navigation
15518 cx.update_editor(|editor, window, cx| {
15519 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
15520 });
15521
15522 cx.editor(|editor, _, _| {
15523 let popover = editor.signature_help_state.popover().cloned().unwrap();
15524 assert_eq!(popover.current_signature, 2);
15525 });
15526}
15527
15528#[gpui::test]
15529async fn test_completion_mode(cx: &mut TestAppContext) {
15530 init_test(cx, |_| {});
15531 let mut cx = EditorLspTestContext::new_rust(
15532 lsp::ServerCapabilities {
15533 completion_provider: Some(lsp::CompletionOptions {
15534 resolve_provider: Some(true),
15535 ..Default::default()
15536 }),
15537 ..Default::default()
15538 },
15539 cx,
15540 )
15541 .await;
15542
15543 struct Run {
15544 run_description: &'static str,
15545 initial_state: String,
15546 buffer_marked_text: String,
15547 completion_label: &'static str,
15548 completion_text: &'static str,
15549 expected_with_insert_mode: String,
15550 expected_with_replace_mode: String,
15551 expected_with_replace_subsequence_mode: String,
15552 expected_with_replace_suffix_mode: String,
15553 }
15554
15555 let runs = [
15556 Run {
15557 run_description: "Start of word matches completion text",
15558 initial_state: "before ediˇ after".into(),
15559 buffer_marked_text: "before <edi|> after".into(),
15560 completion_label: "editor",
15561 completion_text: "editor",
15562 expected_with_insert_mode: "before editorˇ after".into(),
15563 expected_with_replace_mode: "before editorˇ after".into(),
15564 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
15565 expected_with_replace_suffix_mode: "before editorˇ after".into(),
15566 },
15567 Run {
15568 run_description: "Accept same text at the middle of the word",
15569 initial_state: "before ediˇtor after".into(),
15570 buffer_marked_text: "before <edi|tor> after".into(),
15571 completion_label: "editor",
15572 completion_text: "editor",
15573 expected_with_insert_mode: "before editorˇtor after".into(),
15574 expected_with_replace_mode: "before editorˇ after".into(),
15575 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
15576 expected_with_replace_suffix_mode: "before editorˇ after".into(),
15577 },
15578 Run {
15579 run_description: "End of word matches completion text -- cursor at end",
15580 initial_state: "before torˇ after".into(),
15581 buffer_marked_text: "before <tor|> after".into(),
15582 completion_label: "editor",
15583 completion_text: "editor",
15584 expected_with_insert_mode: "before editorˇ after".into(),
15585 expected_with_replace_mode: "before editorˇ after".into(),
15586 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
15587 expected_with_replace_suffix_mode: "before editorˇ after".into(),
15588 },
15589 Run {
15590 run_description: "End of word matches completion text -- cursor at start",
15591 initial_state: "before ˇtor after".into(),
15592 buffer_marked_text: "before <|tor> after".into(),
15593 completion_label: "editor",
15594 completion_text: "editor",
15595 expected_with_insert_mode: "before editorˇtor after".into(),
15596 expected_with_replace_mode: "before editorˇ after".into(),
15597 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
15598 expected_with_replace_suffix_mode: "before editorˇ after".into(),
15599 },
15600 Run {
15601 run_description: "Prepend text containing whitespace",
15602 initial_state: "pˇfield: bool".into(),
15603 buffer_marked_text: "<p|field>: bool".into(),
15604 completion_label: "pub ",
15605 completion_text: "pub ",
15606 expected_with_insert_mode: "pub ˇfield: bool".into(),
15607 expected_with_replace_mode: "pub ˇ: bool".into(),
15608 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
15609 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
15610 },
15611 Run {
15612 run_description: "Add element to start of list",
15613 initial_state: "[element_ˇelement_2]".into(),
15614 buffer_marked_text: "[<element_|element_2>]".into(),
15615 completion_label: "element_1",
15616 completion_text: "element_1",
15617 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
15618 expected_with_replace_mode: "[element_1ˇ]".into(),
15619 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
15620 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
15621 },
15622 Run {
15623 run_description: "Add element to start of list -- first and second elements are equal",
15624 initial_state: "[elˇelement]".into(),
15625 buffer_marked_text: "[<el|element>]".into(),
15626 completion_label: "element",
15627 completion_text: "element",
15628 expected_with_insert_mode: "[elementˇelement]".into(),
15629 expected_with_replace_mode: "[elementˇ]".into(),
15630 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
15631 expected_with_replace_suffix_mode: "[elementˇ]".into(),
15632 },
15633 Run {
15634 run_description: "Ends with matching suffix",
15635 initial_state: "SubˇError".into(),
15636 buffer_marked_text: "<Sub|Error>".into(),
15637 completion_label: "SubscriptionError",
15638 completion_text: "SubscriptionError",
15639 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
15640 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
15641 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
15642 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
15643 },
15644 Run {
15645 run_description: "Suffix is a subsequence -- contiguous",
15646 initial_state: "SubˇErr".into(),
15647 buffer_marked_text: "<Sub|Err>".into(),
15648 completion_label: "SubscriptionError",
15649 completion_text: "SubscriptionError",
15650 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
15651 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
15652 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
15653 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
15654 },
15655 Run {
15656 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
15657 initial_state: "Suˇscrirr".into(),
15658 buffer_marked_text: "<Su|scrirr>".into(),
15659 completion_label: "SubscriptionError",
15660 completion_text: "SubscriptionError",
15661 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
15662 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
15663 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
15664 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
15665 },
15666 Run {
15667 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
15668 initial_state: "foo(indˇix)".into(),
15669 buffer_marked_text: "foo(<ind|ix>)".into(),
15670 completion_label: "node_index",
15671 completion_text: "node_index",
15672 expected_with_insert_mode: "foo(node_indexˇix)".into(),
15673 expected_with_replace_mode: "foo(node_indexˇ)".into(),
15674 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
15675 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
15676 },
15677 Run {
15678 run_description: "Replace range ends before cursor - should extend to cursor",
15679 initial_state: "before editˇo after".into(),
15680 buffer_marked_text: "before <{ed}>it|o after".into(),
15681 completion_label: "editor",
15682 completion_text: "editor",
15683 expected_with_insert_mode: "before editorˇo after".into(),
15684 expected_with_replace_mode: "before editorˇo after".into(),
15685 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
15686 expected_with_replace_suffix_mode: "before editorˇo after".into(),
15687 },
15688 Run {
15689 run_description: "Uses label for suffix matching",
15690 initial_state: "before ediˇtor after".into(),
15691 buffer_marked_text: "before <edi|tor> after".into(),
15692 completion_label: "editor",
15693 completion_text: "editor()",
15694 expected_with_insert_mode: "before editor()ˇtor after".into(),
15695 expected_with_replace_mode: "before editor()ˇ after".into(),
15696 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
15697 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
15698 },
15699 Run {
15700 run_description: "Case insensitive subsequence and suffix matching",
15701 initial_state: "before EDiˇtoR after".into(),
15702 buffer_marked_text: "before <EDi|toR> after".into(),
15703 completion_label: "editor",
15704 completion_text: "editor",
15705 expected_with_insert_mode: "before editorˇtoR after".into(),
15706 expected_with_replace_mode: "before editorˇ after".into(),
15707 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
15708 expected_with_replace_suffix_mode: "before editorˇ after".into(),
15709 },
15710 ];
15711
15712 for run in runs {
15713 let run_variations = [
15714 (LspInsertMode::Insert, run.expected_with_insert_mode),
15715 (LspInsertMode::Replace, run.expected_with_replace_mode),
15716 (
15717 LspInsertMode::ReplaceSubsequence,
15718 run.expected_with_replace_subsequence_mode,
15719 ),
15720 (
15721 LspInsertMode::ReplaceSuffix,
15722 run.expected_with_replace_suffix_mode,
15723 ),
15724 ];
15725
15726 for (lsp_insert_mode, expected_text) in run_variations {
15727 eprintln!(
15728 "run = {:?}, mode = {lsp_insert_mode:.?}",
15729 run.run_description,
15730 );
15731
15732 update_test_language_settings(&mut cx, &|settings| {
15733 settings.defaults.completions = Some(CompletionSettingsContent {
15734 lsp_insert_mode: Some(lsp_insert_mode),
15735 words: Some(WordsCompletionMode::Disabled),
15736 words_min_length: Some(0),
15737 ..Default::default()
15738 });
15739 });
15740
15741 cx.set_state(&run.initial_state);
15742
15743 // Set up resolve handler before showing completions, since resolve may be
15744 // triggered when menu becomes visible (for documentation), not just on confirm.
15745 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(
15746 move |_, _, _| async move {
15747 Ok(lsp::CompletionItem {
15748 additional_text_edits: None,
15749 ..Default::default()
15750 })
15751 },
15752 );
15753
15754 cx.update_editor(|editor, window, cx| {
15755 editor.show_completions(&ShowCompletions, window, cx);
15756 });
15757
15758 let counter = Arc::new(AtomicUsize::new(0));
15759 handle_completion_request_with_insert_and_replace(
15760 &mut cx,
15761 &run.buffer_marked_text,
15762 vec![(run.completion_label, run.completion_text)],
15763 counter.clone(),
15764 )
15765 .await;
15766 cx.condition(|editor, _| editor.context_menu_visible())
15767 .await;
15768 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15769
15770 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15771 editor
15772 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15773 .unwrap()
15774 });
15775 cx.assert_editor_state(&expected_text);
15776 apply_additional_edits.await.unwrap();
15777 }
15778 }
15779}
15780
15781#[gpui::test]
15782async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
15783 init_test(cx, |_| {});
15784 let mut cx = EditorLspTestContext::new_rust(
15785 lsp::ServerCapabilities {
15786 completion_provider: Some(lsp::CompletionOptions {
15787 resolve_provider: Some(true),
15788 ..Default::default()
15789 }),
15790 ..Default::default()
15791 },
15792 cx,
15793 )
15794 .await;
15795
15796 let initial_state = "SubˇError";
15797 let buffer_marked_text = "<Sub|Error>";
15798 let completion_text = "SubscriptionError";
15799 let expected_with_insert_mode = "SubscriptionErrorˇError";
15800 let expected_with_replace_mode = "SubscriptionErrorˇ";
15801
15802 update_test_language_settings(&mut cx, &|settings| {
15803 settings.defaults.completions = Some(CompletionSettingsContent {
15804 words: Some(WordsCompletionMode::Disabled),
15805 words_min_length: Some(0),
15806 // set the opposite here to ensure that the action is overriding the default behavior
15807 lsp_insert_mode: Some(LspInsertMode::Insert),
15808 ..Default::default()
15809 });
15810 });
15811
15812 cx.set_state(initial_state);
15813 cx.update_editor(|editor, window, cx| {
15814 editor.show_completions(&ShowCompletions, window, cx);
15815 });
15816
15817 let counter = Arc::new(AtomicUsize::new(0));
15818 handle_completion_request_with_insert_and_replace(
15819 &mut cx,
15820 buffer_marked_text,
15821 vec![(completion_text, completion_text)],
15822 counter.clone(),
15823 )
15824 .await;
15825 cx.condition(|editor, _| editor.context_menu_visible())
15826 .await;
15827 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15828
15829 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15830 editor
15831 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
15832 .unwrap()
15833 });
15834 cx.assert_editor_state(expected_with_replace_mode);
15835 handle_resolve_completion_request(&mut cx, None).await;
15836 apply_additional_edits.await.unwrap();
15837
15838 update_test_language_settings(&mut cx, &|settings| {
15839 settings.defaults.completions = Some(CompletionSettingsContent {
15840 words: Some(WordsCompletionMode::Disabled),
15841 words_min_length: Some(0),
15842 // set the opposite here to ensure that the action is overriding the default behavior
15843 lsp_insert_mode: Some(LspInsertMode::Replace),
15844 ..Default::default()
15845 });
15846 });
15847
15848 cx.set_state(initial_state);
15849 cx.update_editor(|editor, window, cx| {
15850 editor.show_completions(&ShowCompletions, window, cx);
15851 });
15852 handle_completion_request_with_insert_and_replace(
15853 &mut cx,
15854 buffer_marked_text,
15855 vec![(completion_text, completion_text)],
15856 counter.clone(),
15857 )
15858 .await;
15859 cx.condition(|editor, _| editor.context_menu_visible())
15860 .await;
15861 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15862
15863 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15864 editor
15865 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
15866 .unwrap()
15867 });
15868 cx.assert_editor_state(expected_with_insert_mode);
15869 handle_resolve_completion_request(&mut cx, None).await;
15870 apply_additional_edits.await.unwrap();
15871}
15872
15873#[gpui::test]
15874async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
15875 init_test(cx, |_| {});
15876 let mut cx = EditorLspTestContext::new_rust(
15877 lsp::ServerCapabilities {
15878 completion_provider: Some(lsp::CompletionOptions {
15879 resolve_provider: Some(true),
15880 ..Default::default()
15881 }),
15882 ..Default::default()
15883 },
15884 cx,
15885 )
15886 .await;
15887
15888 // scenario: surrounding text matches completion text
15889 let completion_text = "to_offset";
15890 let initial_state = indoc! {"
15891 1. buf.to_offˇsuffix
15892 2. buf.to_offˇsuf
15893 3. buf.to_offˇfix
15894 4. buf.to_offˇ
15895 5. into_offˇensive
15896 6. ˇsuffix
15897 7. let ˇ //
15898 8. aaˇzz
15899 9. buf.to_off«zzzzzˇ»suffix
15900 10. buf.«ˇzzzzz»suffix
15901 11. to_off«ˇzzzzz»
15902
15903 buf.to_offˇsuffix // newest cursor
15904 "};
15905 let completion_marked_buffer = indoc! {"
15906 1. buf.to_offsuffix
15907 2. buf.to_offsuf
15908 3. buf.to_offfix
15909 4. buf.to_off
15910 5. into_offensive
15911 6. suffix
15912 7. let //
15913 8. aazz
15914 9. buf.to_offzzzzzsuffix
15915 10. buf.zzzzzsuffix
15916 11. to_offzzzzz
15917
15918 buf.<to_off|suffix> // newest cursor
15919 "};
15920 let expected = indoc! {"
15921 1. buf.to_offsetˇ
15922 2. buf.to_offsetˇsuf
15923 3. buf.to_offsetˇfix
15924 4. buf.to_offsetˇ
15925 5. into_offsetˇensive
15926 6. to_offsetˇsuffix
15927 7. let to_offsetˇ //
15928 8. aato_offsetˇzz
15929 9. buf.to_offsetˇ
15930 10. buf.to_offsetˇsuffix
15931 11. to_offsetˇ
15932
15933 buf.to_offsetˇ // newest cursor
15934 "};
15935 cx.set_state(initial_state);
15936 cx.update_editor(|editor, window, cx| {
15937 editor.show_completions(&ShowCompletions, window, cx);
15938 });
15939 handle_completion_request_with_insert_and_replace(
15940 &mut cx,
15941 completion_marked_buffer,
15942 vec![(completion_text, completion_text)],
15943 Arc::new(AtomicUsize::new(0)),
15944 )
15945 .await;
15946 cx.condition(|editor, _| editor.context_menu_visible())
15947 .await;
15948 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15949 editor
15950 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
15951 .unwrap()
15952 });
15953 cx.assert_editor_state(expected);
15954 handle_resolve_completion_request(&mut cx, None).await;
15955 apply_additional_edits.await.unwrap();
15956
15957 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
15958 let completion_text = "foo_and_bar";
15959 let initial_state = indoc! {"
15960 1. ooanbˇ
15961 2. zooanbˇ
15962 3. ooanbˇz
15963 4. zooanbˇz
15964 5. ooanˇ
15965 6. oanbˇ
15966
15967 ooanbˇ
15968 "};
15969 let completion_marked_buffer = indoc! {"
15970 1. ooanb
15971 2. zooanb
15972 3. ooanbz
15973 4. zooanbz
15974 5. ooan
15975 6. oanb
15976
15977 <ooanb|>
15978 "};
15979 let expected = indoc! {"
15980 1. foo_and_barˇ
15981 2. zfoo_and_barˇ
15982 3. foo_and_barˇz
15983 4. zfoo_and_barˇz
15984 5. ooanfoo_and_barˇ
15985 6. oanbfoo_and_barˇ
15986
15987 foo_and_barˇ
15988 "};
15989 cx.set_state(initial_state);
15990 cx.update_editor(|editor, window, cx| {
15991 editor.show_completions(&ShowCompletions, window, cx);
15992 });
15993 handle_completion_request_with_insert_and_replace(
15994 &mut cx,
15995 completion_marked_buffer,
15996 vec![(completion_text, completion_text)],
15997 Arc::new(AtomicUsize::new(0)),
15998 )
15999 .await;
16000 cx.condition(|editor, _| editor.context_menu_visible())
16001 .await;
16002 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16003 editor
16004 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
16005 .unwrap()
16006 });
16007 cx.assert_editor_state(expected);
16008 handle_resolve_completion_request(&mut cx, None).await;
16009 apply_additional_edits.await.unwrap();
16010
16011 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
16012 // (expects the same as if it was inserted at the end)
16013 let completion_text = "foo_and_bar";
16014 let initial_state = indoc! {"
16015 1. ooˇanb
16016 2. zooˇanb
16017 3. ooˇanbz
16018 4. zooˇanbz
16019
16020 ooˇanb
16021 "};
16022 let completion_marked_buffer = indoc! {"
16023 1. ooanb
16024 2. zooanb
16025 3. ooanbz
16026 4. zooanbz
16027
16028 <oo|anb>
16029 "};
16030 let expected = indoc! {"
16031 1. foo_and_barˇ
16032 2. zfoo_and_barˇ
16033 3. foo_and_barˇz
16034 4. zfoo_and_barˇz
16035
16036 foo_and_barˇ
16037 "};
16038 cx.set_state(initial_state);
16039 cx.update_editor(|editor, window, cx| {
16040 editor.show_completions(&ShowCompletions, window, cx);
16041 });
16042 handle_completion_request_with_insert_and_replace(
16043 &mut cx,
16044 completion_marked_buffer,
16045 vec![(completion_text, completion_text)],
16046 Arc::new(AtomicUsize::new(0)),
16047 )
16048 .await;
16049 cx.condition(|editor, _| editor.context_menu_visible())
16050 .await;
16051 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16052 editor
16053 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
16054 .unwrap()
16055 });
16056 cx.assert_editor_state(expected);
16057 handle_resolve_completion_request(&mut cx, None).await;
16058 apply_additional_edits.await.unwrap();
16059}
16060
16061// This used to crash
16062#[gpui::test]
16063async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
16064 init_test(cx, |_| {});
16065
16066 let buffer_text = indoc! {"
16067 fn main() {
16068 10.satu;
16069
16070 //
16071 // separate1
16072 // separate2
16073 // separate3
16074 //
16075
16076 10.satu20;
16077 }
16078 "};
16079 let multibuffer_text_with_selections = indoc! {"
16080 fn main() {
16081 10.satuˇ;
16082
16083 //
16084
16085 10.satuˇ20;
16086 }
16087 "};
16088 let expected_multibuffer = indoc! {"
16089 fn main() {
16090 10.saturating_sub()ˇ;
16091
16092 //
16093
16094 10.saturating_sub()ˇ;
16095 }
16096 "};
16097
16098 let fs = FakeFs::new(cx.executor());
16099 fs.insert_tree(
16100 path!("/a"),
16101 json!({
16102 "main.rs": buffer_text,
16103 }),
16104 )
16105 .await;
16106
16107 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16108 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16109 language_registry.add(rust_lang());
16110 let mut fake_servers = language_registry.register_fake_lsp(
16111 "Rust",
16112 FakeLspAdapter {
16113 capabilities: lsp::ServerCapabilities {
16114 completion_provider: Some(lsp::CompletionOptions {
16115 resolve_provider: None,
16116 ..lsp::CompletionOptions::default()
16117 }),
16118 ..lsp::ServerCapabilities::default()
16119 },
16120 ..FakeLspAdapter::default()
16121 },
16122 );
16123 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
16124 let workspace = window
16125 .read_with(cx, |mw, _| mw.workspace().clone())
16126 .unwrap();
16127 let cx = &mut VisualTestContext::from_window(*window, cx);
16128 let buffer = project
16129 .update(cx, |project, cx| {
16130 project.open_local_buffer(path!("/a/main.rs"), cx)
16131 })
16132 .await
16133 .unwrap();
16134
16135 let multi_buffer = cx.new(|cx| {
16136 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
16137 multi_buffer.set_excerpts_for_path(
16138 PathKey::sorted(0),
16139 buffer.clone(),
16140 [
16141 Point::zero()..Point::new(2, 0),
16142 Point::new(7, 0)..buffer.read(cx).max_point(),
16143 ],
16144 0,
16145 cx,
16146 );
16147 multi_buffer
16148 });
16149
16150 let editor = workspace.update_in(cx, |_, window, cx| {
16151 cx.new(|cx| {
16152 Editor::new(
16153 EditorMode::Full {
16154 scale_ui_elements_with_buffer_font_size: false,
16155 show_active_line_background: false,
16156 sizing_behavior: SizingBehavior::Default,
16157 },
16158 multi_buffer.clone(),
16159 Some(project.clone()),
16160 window,
16161 cx,
16162 )
16163 })
16164 });
16165
16166 let pane = workspace.update_in(cx, |workspace, _, _| workspace.active_pane().clone());
16167 pane.update_in(cx, |pane, window, cx| {
16168 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
16169 });
16170
16171 let fake_server = fake_servers.next().await.unwrap();
16172 cx.run_until_parked();
16173
16174 editor.update_in(cx, |editor, window, cx| {
16175 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16176 s.select_ranges([
16177 Point::new(1, 11)..Point::new(1, 11),
16178 Point::new(5, 11)..Point::new(5, 11),
16179 ])
16180 });
16181
16182 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
16183 });
16184
16185 editor.update_in(cx, |editor, window, cx| {
16186 editor.show_completions(&ShowCompletions, window, cx);
16187 });
16188
16189 fake_server
16190 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16191 let completion_item = lsp::CompletionItem {
16192 label: "saturating_sub()".into(),
16193 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
16194 lsp::InsertReplaceEdit {
16195 new_text: "saturating_sub()".to_owned(),
16196 insert: lsp::Range::new(
16197 lsp::Position::new(9, 7),
16198 lsp::Position::new(9, 11),
16199 ),
16200 replace: lsp::Range::new(
16201 lsp::Position::new(9, 7),
16202 lsp::Position::new(9, 13),
16203 ),
16204 },
16205 )),
16206 ..lsp::CompletionItem::default()
16207 };
16208
16209 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
16210 })
16211 .next()
16212 .await
16213 .unwrap();
16214
16215 cx.condition(&editor, |editor, _| editor.context_menu_visible())
16216 .await;
16217
16218 editor
16219 .update_in(cx, |editor, window, cx| {
16220 editor
16221 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
16222 .unwrap()
16223 })
16224 .await
16225 .unwrap();
16226
16227 editor.update(cx, |editor, cx| {
16228 assert_text_with_selections(editor, expected_multibuffer, cx);
16229 })
16230}
16231
16232#[gpui::test]
16233async fn test_completion(cx: &mut TestAppContext) {
16234 init_test(cx, |_| {});
16235
16236 let mut cx = EditorLspTestContext::new_rust(
16237 lsp::ServerCapabilities {
16238 completion_provider: Some(lsp::CompletionOptions {
16239 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16240 resolve_provider: Some(true),
16241 ..Default::default()
16242 }),
16243 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
16244 ..Default::default()
16245 },
16246 cx,
16247 )
16248 .await;
16249 let counter = Arc::new(AtomicUsize::new(0));
16250
16251 cx.set_state(indoc! {"
16252 oneˇ
16253 two
16254 three
16255 "});
16256 cx.simulate_keystroke(".");
16257 handle_completion_request(
16258 indoc! {"
16259 one.|<>
16260 two
16261 three
16262 "},
16263 vec!["first_completion", "second_completion"],
16264 true,
16265 counter.clone(),
16266 &mut cx,
16267 )
16268 .await;
16269 cx.condition(|editor, _| editor.context_menu_visible())
16270 .await;
16271 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16272
16273 let _handler = handle_signature_help_request(
16274 &mut cx,
16275 lsp::SignatureHelp {
16276 signatures: vec![lsp::SignatureInformation {
16277 label: "test signature".to_string(),
16278 documentation: None,
16279 parameters: Some(vec![lsp::ParameterInformation {
16280 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
16281 documentation: None,
16282 }]),
16283 active_parameter: None,
16284 }],
16285 active_signature: None,
16286 active_parameter: None,
16287 },
16288 );
16289 cx.update_editor(|editor, window, cx| {
16290 assert!(
16291 !editor.signature_help_state.is_shown(),
16292 "No signature help was called for"
16293 );
16294 editor.show_signature_help(&ShowSignatureHelp, window, cx);
16295 });
16296 cx.run_until_parked();
16297 cx.update_editor(|editor, _, _| {
16298 assert!(
16299 !editor.signature_help_state.is_shown(),
16300 "No signature help should be shown when completions menu is open"
16301 );
16302 });
16303
16304 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16305 editor.context_menu_next(&Default::default(), window, cx);
16306 editor
16307 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16308 .unwrap()
16309 });
16310 cx.assert_editor_state(indoc! {"
16311 one.second_completionˇ
16312 two
16313 three
16314 "});
16315
16316 handle_resolve_completion_request(
16317 &mut cx,
16318 Some(vec![
16319 (
16320 //This overlaps with the primary completion edit which is
16321 //misbehavior from the LSP spec, test that we filter it out
16322 indoc! {"
16323 one.second_ˇcompletion
16324 two
16325 threeˇ
16326 "},
16327 "overlapping additional edit",
16328 ),
16329 (
16330 indoc! {"
16331 one.second_completion
16332 two
16333 threeˇ
16334 "},
16335 "\nadditional edit",
16336 ),
16337 ]),
16338 )
16339 .await;
16340 apply_additional_edits.await.unwrap();
16341 cx.assert_editor_state(indoc! {"
16342 one.second_completionˇ
16343 two
16344 three
16345 additional edit
16346 "});
16347
16348 cx.set_state(indoc! {"
16349 one.second_completion
16350 twoˇ
16351 threeˇ
16352 additional edit
16353 "});
16354 cx.simulate_keystroke(" ");
16355 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
16356 cx.simulate_keystroke("s");
16357 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
16358
16359 cx.assert_editor_state(indoc! {"
16360 one.second_completion
16361 two sˇ
16362 three sˇ
16363 additional edit
16364 "});
16365 handle_completion_request(
16366 indoc! {"
16367 one.second_completion
16368 two s
16369 three <s|>
16370 additional edit
16371 "},
16372 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
16373 true,
16374 counter.clone(),
16375 &mut cx,
16376 )
16377 .await;
16378 cx.condition(|editor, _| editor.context_menu_visible())
16379 .await;
16380 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
16381
16382 cx.simulate_keystroke("i");
16383
16384 handle_completion_request(
16385 indoc! {"
16386 one.second_completion
16387 two si
16388 three <si|>
16389 additional edit
16390 "},
16391 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
16392 true,
16393 counter.clone(),
16394 &mut cx,
16395 )
16396 .await;
16397 cx.condition(|editor, _| editor.context_menu_visible())
16398 .await;
16399 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
16400
16401 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16402 editor
16403 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16404 .unwrap()
16405 });
16406 cx.assert_editor_state(indoc! {"
16407 one.second_completion
16408 two sixth_completionˇ
16409 three sixth_completionˇ
16410 additional edit
16411 "});
16412
16413 apply_additional_edits.await.unwrap();
16414
16415 update_test_language_settings(&mut cx, &|settings| {
16416 settings.defaults.show_completions_on_input = Some(false);
16417 });
16418 cx.set_state("editorˇ");
16419 cx.simulate_keystroke(".");
16420 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
16421 cx.simulate_keystrokes("c l o");
16422 cx.assert_editor_state("editor.cloˇ");
16423 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
16424 cx.update_editor(|editor, window, cx| {
16425 editor.show_completions(&ShowCompletions, window, cx);
16426 });
16427 handle_completion_request(
16428 "editor.<clo|>",
16429 vec!["close", "clobber"],
16430 true,
16431 counter.clone(),
16432 &mut cx,
16433 )
16434 .await;
16435 cx.condition(|editor, _| editor.context_menu_visible())
16436 .await;
16437 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
16438
16439 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16440 editor
16441 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16442 .unwrap()
16443 });
16444 cx.assert_editor_state("editor.clobberˇ");
16445 handle_resolve_completion_request(&mut cx, None).await;
16446 apply_additional_edits.await.unwrap();
16447}
16448
16449#[gpui::test]
16450async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
16451 init_test(cx, |_| {});
16452
16453 let fs = FakeFs::new(cx.executor());
16454 fs.insert_tree(
16455 path!("/a"),
16456 json!({
16457 "main.rs": "",
16458 }),
16459 )
16460 .await;
16461
16462 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16463 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16464 language_registry.add(rust_lang());
16465 let command_calls = Arc::new(AtomicUsize::new(0));
16466 let registered_command = "_the/command";
16467
16468 let closure_command_calls = command_calls.clone();
16469 let mut fake_servers = language_registry.register_fake_lsp(
16470 "Rust",
16471 FakeLspAdapter {
16472 capabilities: lsp::ServerCapabilities {
16473 completion_provider: Some(lsp::CompletionOptions {
16474 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16475 ..lsp::CompletionOptions::default()
16476 }),
16477 execute_command_provider: Some(lsp::ExecuteCommandOptions {
16478 commands: vec![registered_command.to_owned()],
16479 ..lsp::ExecuteCommandOptions::default()
16480 }),
16481 ..lsp::ServerCapabilities::default()
16482 },
16483 initializer: Some(Box::new(move |fake_server| {
16484 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
16485 move |params, _| async move {
16486 Ok(Some(lsp::CompletionResponse::Array(vec![
16487 lsp::CompletionItem {
16488 label: "registered_command".to_owned(),
16489 text_edit: gen_text_edit(¶ms, ""),
16490 command: Some(lsp::Command {
16491 title: registered_command.to_owned(),
16492 command: "_the/command".to_owned(),
16493 arguments: Some(vec![serde_json::Value::Bool(true)]),
16494 }),
16495 ..lsp::CompletionItem::default()
16496 },
16497 lsp::CompletionItem {
16498 label: "unregistered_command".to_owned(),
16499 text_edit: gen_text_edit(¶ms, ""),
16500 command: Some(lsp::Command {
16501 title: "????????????".to_owned(),
16502 command: "????????????".to_owned(),
16503 arguments: Some(vec![serde_json::Value::Null]),
16504 }),
16505 ..lsp::CompletionItem::default()
16506 },
16507 ])))
16508 },
16509 );
16510 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
16511 let command_calls = closure_command_calls.clone();
16512 move |params, _| {
16513 assert_eq!(params.command, registered_command);
16514 let command_calls = command_calls.clone();
16515 async move {
16516 command_calls.fetch_add(1, atomic::Ordering::Release);
16517 Ok(Some(json!(null)))
16518 }
16519 }
16520 });
16521 })),
16522 ..FakeLspAdapter::default()
16523 },
16524 );
16525 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
16526 let workspace = window
16527 .read_with(cx, |mw, _| mw.workspace().clone())
16528 .unwrap();
16529 let cx = &mut VisualTestContext::from_window(*window, cx);
16530 let editor = workspace
16531 .update_in(cx, |workspace, window, cx| {
16532 workspace.open_abs_path(
16533 PathBuf::from(path!("/a/main.rs")),
16534 OpenOptions::default(),
16535 window,
16536 cx,
16537 )
16538 })
16539 .await
16540 .unwrap()
16541 .downcast::<Editor>()
16542 .unwrap();
16543 let _fake_server = fake_servers.next().await.unwrap();
16544 cx.run_until_parked();
16545
16546 editor.update_in(cx, |editor, window, cx| {
16547 cx.focus_self(window);
16548 editor.move_to_end(&MoveToEnd, window, cx);
16549 editor.handle_input(".", window, cx);
16550 });
16551 cx.run_until_parked();
16552 editor.update(cx, |editor, _| {
16553 assert!(editor.context_menu_visible());
16554 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16555 {
16556 let completion_labels = menu
16557 .completions
16558 .borrow()
16559 .iter()
16560 .map(|c| c.label.text.clone())
16561 .collect::<Vec<_>>();
16562 assert_eq!(
16563 completion_labels,
16564 &["registered_command", "unregistered_command",],
16565 );
16566 } else {
16567 panic!("expected completion menu to be open");
16568 }
16569 });
16570
16571 editor
16572 .update_in(cx, |editor, window, cx| {
16573 editor
16574 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16575 .unwrap()
16576 })
16577 .await
16578 .unwrap();
16579 cx.run_until_parked();
16580 assert_eq!(
16581 command_calls.load(atomic::Ordering::Acquire),
16582 1,
16583 "For completion with a registered command, Zed should send a command execution request",
16584 );
16585
16586 editor.update_in(cx, |editor, window, cx| {
16587 cx.focus_self(window);
16588 editor.handle_input(".", window, cx);
16589 });
16590 cx.run_until_parked();
16591 editor.update(cx, |editor, _| {
16592 assert!(editor.context_menu_visible());
16593 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16594 {
16595 let completion_labels = menu
16596 .completions
16597 .borrow()
16598 .iter()
16599 .map(|c| c.label.text.clone())
16600 .collect::<Vec<_>>();
16601 assert_eq!(
16602 completion_labels,
16603 &["registered_command", "unregistered_command",],
16604 );
16605 } else {
16606 panic!("expected completion menu to be open");
16607 }
16608 });
16609 editor
16610 .update_in(cx, |editor, window, cx| {
16611 editor.context_menu_next(&Default::default(), window, cx);
16612 editor
16613 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16614 .unwrap()
16615 })
16616 .await
16617 .unwrap();
16618 cx.run_until_parked();
16619 assert_eq!(
16620 command_calls.load(atomic::Ordering::Acquire),
16621 1,
16622 "For completion with an unregistered command, Zed should not send a command execution request",
16623 );
16624}
16625
16626#[gpui::test]
16627async fn test_completion_reuse(cx: &mut TestAppContext) {
16628 init_test(cx, |_| {});
16629
16630 let mut cx = EditorLspTestContext::new_rust(
16631 lsp::ServerCapabilities {
16632 completion_provider: Some(lsp::CompletionOptions {
16633 trigger_characters: Some(vec![".".to_string()]),
16634 ..Default::default()
16635 }),
16636 ..Default::default()
16637 },
16638 cx,
16639 )
16640 .await;
16641
16642 let counter = Arc::new(AtomicUsize::new(0));
16643 cx.set_state("objˇ");
16644 cx.simulate_keystroke(".");
16645
16646 // Initial completion request returns complete results
16647 let is_incomplete = false;
16648 handle_completion_request(
16649 "obj.|<>",
16650 vec!["a", "ab", "abc"],
16651 is_incomplete,
16652 counter.clone(),
16653 &mut cx,
16654 )
16655 .await;
16656 cx.run_until_parked();
16657 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16658 cx.assert_editor_state("obj.ˇ");
16659 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
16660
16661 // Type "a" - filters existing completions
16662 cx.simulate_keystroke("a");
16663 cx.run_until_parked();
16664 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16665 cx.assert_editor_state("obj.aˇ");
16666 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
16667
16668 // Type "b" - filters existing completions
16669 cx.simulate_keystroke("b");
16670 cx.run_until_parked();
16671 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16672 cx.assert_editor_state("obj.abˇ");
16673 check_displayed_completions(vec!["ab", "abc"], &mut cx);
16674
16675 // Type "c" - filters existing completions
16676 cx.simulate_keystroke("c");
16677 cx.run_until_parked();
16678 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16679 cx.assert_editor_state("obj.abcˇ");
16680 check_displayed_completions(vec!["abc"], &mut cx);
16681
16682 // Backspace to delete "c" - filters existing completions
16683 cx.update_editor(|editor, window, cx| {
16684 editor.backspace(&Backspace, window, cx);
16685 });
16686 cx.run_until_parked();
16687 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16688 cx.assert_editor_state("obj.abˇ");
16689 check_displayed_completions(vec!["ab", "abc"], &mut cx);
16690
16691 // Moving cursor to the left dismisses menu.
16692 cx.update_editor(|editor, window, cx| {
16693 editor.move_left(&MoveLeft, window, cx);
16694 });
16695 cx.run_until_parked();
16696 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16697 cx.assert_editor_state("obj.aˇb");
16698 cx.update_editor(|editor, _, _| {
16699 assert_eq!(editor.context_menu_visible(), false);
16700 });
16701
16702 // Type "b" - new request
16703 cx.simulate_keystroke("b");
16704 let is_incomplete = false;
16705 handle_completion_request(
16706 "obj.<ab|>a",
16707 vec!["ab", "abc"],
16708 is_incomplete,
16709 counter.clone(),
16710 &mut cx,
16711 )
16712 .await;
16713 cx.run_until_parked();
16714 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
16715 cx.assert_editor_state("obj.abˇb");
16716 check_displayed_completions(vec!["ab", "abc"], &mut cx);
16717
16718 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
16719 cx.update_editor(|editor, window, cx| {
16720 editor.backspace(&Backspace, window, cx);
16721 });
16722 let is_incomplete = false;
16723 handle_completion_request(
16724 "obj.<a|>b",
16725 vec!["a", "ab", "abc"],
16726 is_incomplete,
16727 counter.clone(),
16728 &mut cx,
16729 )
16730 .await;
16731 cx.run_until_parked();
16732 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
16733 cx.assert_editor_state("obj.aˇb");
16734 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
16735
16736 // Backspace to delete "a" - dismisses menu.
16737 cx.update_editor(|editor, window, cx| {
16738 editor.backspace(&Backspace, window, cx);
16739 });
16740 cx.run_until_parked();
16741 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
16742 cx.assert_editor_state("obj.ˇb");
16743 cx.update_editor(|editor, _, _| {
16744 assert_eq!(editor.context_menu_visible(), false);
16745 });
16746}
16747
16748#[gpui::test]
16749async fn test_word_completion(cx: &mut TestAppContext) {
16750 let lsp_fetch_timeout_ms = 10;
16751 init_test(cx, |language_settings| {
16752 language_settings.defaults.completions = Some(CompletionSettingsContent {
16753 words_min_length: Some(0),
16754 lsp_fetch_timeout_ms: Some(10),
16755 lsp_insert_mode: Some(LspInsertMode::Insert),
16756 ..Default::default()
16757 });
16758 });
16759
16760 let mut cx = EditorLspTestContext::new_rust(
16761 lsp::ServerCapabilities {
16762 completion_provider: Some(lsp::CompletionOptions {
16763 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16764 ..lsp::CompletionOptions::default()
16765 }),
16766 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
16767 ..lsp::ServerCapabilities::default()
16768 },
16769 cx,
16770 )
16771 .await;
16772
16773 let throttle_completions = Arc::new(AtomicBool::new(false));
16774
16775 let lsp_throttle_completions = throttle_completions.clone();
16776 let _completion_requests_handler =
16777 cx.lsp
16778 .server
16779 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
16780 let lsp_throttle_completions = lsp_throttle_completions.clone();
16781 let cx = cx.clone();
16782 async move {
16783 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
16784 cx.background_executor()
16785 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
16786 .await;
16787 }
16788 Ok(Some(lsp::CompletionResponse::Array(vec![
16789 lsp::CompletionItem {
16790 label: "first".into(),
16791 ..lsp::CompletionItem::default()
16792 },
16793 lsp::CompletionItem {
16794 label: "last".into(),
16795 ..lsp::CompletionItem::default()
16796 },
16797 ])))
16798 }
16799 });
16800
16801 cx.set_state(indoc! {"
16802 oneˇ
16803 two
16804 three
16805 "});
16806 cx.simulate_keystroke(".");
16807 cx.executor().run_until_parked();
16808 cx.condition(|editor, _| editor.context_menu_visible())
16809 .await;
16810 cx.update_editor(|editor, window, cx| {
16811 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16812 {
16813 assert_eq!(
16814 completion_menu_entries(menu),
16815 &["first", "last"],
16816 "When LSP server is fast to reply, no fallback word completions are used"
16817 );
16818 } else {
16819 panic!("expected completion menu to be open");
16820 }
16821 editor.cancel(&Cancel, window, cx);
16822 });
16823 cx.executor().run_until_parked();
16824 cx.condition(|editor, _| !editor.context_menu_visible())
16825 .await;
16826
16827 throttle_completions.store(true, atomic::Ordering::Release);
16828 cx.simulate_keystroke(".");
16829 cx.executor()
16830 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
16831 cx.executor().run_until_parked();
16832 cx.condition(|editor, _| editor.context_menu_visible())
16833 .await;
16834 cx.update_editor(|editor, _, _| {
16835 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16836 {
16837 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
16838 "When LSP server is slow, document words can be shown instead, if configured accordingly");
16839 } else {
16840 panic!("expected completion menu to be open");
16841 }
16842 });
16843}
16844
16845#[gpui::test]
16846async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
16847 init_test(cx, |language_settings| {
16848 language_settings.defaults.completions = Some(CompletionSettingsContent {
16849 words: Some(WordsCompletionMode::Enabled),
16850 words_min_length: Some(0),
16851 lsp_insert_mode: Some(LspInsertMode::Insert),
16852 ..Default::default()
16853 });
16854 });
16855
16856 let mut cx = EditorLspTestContext::new_rust(
16857 lsp::ServerCapabilities {
16858 completion_provider: Some(lsp::CompletionOptions {
16859 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16860 ..lsp::CompletionOptions::default()
16861 }),
16862 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
16863 ..lsp::ServerCapabilities::default()
16864 },
16865 cx,
16866 )
16867 .await;
16868
16869 let _completion_requests_handler =
16870 cx.lsp
16871 .server
16872 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
16873 Ok(Some(lsp::CompletionResponse::Array(vec![
16874 lsp::CompletionItem {
16875 label: "first".into(),
16876 ..lsp::CompletionItem::default()
16877 },
16878 lsp::CompletionItem {
16879 label: "last".into(),
16880 ..lsp::CompletionItem::default()
16881 },
16882 ])))
16883 });
16884
16885 cx.set_state(indoc! {"ˇ
16886 first
16887 last
16888 second
16889 "});
16890 cx.simulate_keystroke(".");
16891 cx.executor().run_until_parked();
16892 cx.condition(|editor, _| editor.context_menu_visible())
16893 .await;
16894 cx.update_editor(|editor, _, _| {
16895 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16896 {
16897 assert_eq!(
16898 completion_menu_entries(menu),
16899 &["first", "last", "second"],
16900 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
16901 );
16902 } else {
16903 panic!("expected completion menu to be open");
16904 }
16905 });
16906}
16907
16908#[gpui::test]
16909async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
16910 init_test(cx, |language_settings| {
16911 language_settings.defaults.completions = Some(CompletionSettingsContent {
16912 words: Some(WordsCompletionMode::Disabled),
16913 words_min_length: Some(0),
16914 lsp_insert_mode: Some(LspInsertMode::Insert),
16915 ..Default::default()
16916 });
16917 });
16918
16919 let mut cx = EditorLspTestContext::new_rust(
16920 lsp::ServerCapabilities {
16921 completion_provider: Some(lsp::CompletionOptions {
16922 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16923 ..lsp::CompletionOptions::default()
16924 }),
16925 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
16926 ..lsp::ServerCapabilities::default()
16927 },
16928 cx,
16929 )
16930 .await;
16931
16932 let _completion_requests_handler =
16933 cx.lsp
16934 .server
16935 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
16936 panic!("LSP completions should not be queried when dealing with word completions")
16937 });
16938
16939 cx.set_state(indoc! {"ˇ
16940 first
16941 last
16942 second
16943 "});
16944 cx.update_editor(|editor, window, cx| {
16945 editor.show_word_completions(&ShowWordCompletions, window, cx);
16946 });
16947 cx.executor().run_until_parked();
16948 cx.condition(|editor, _| editor.context_menu_visible())
16949 .await;
16950 cx.update_editor(|editor, _, _| {
16951 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16952 {
16953 assert_eq!(
16954 completion_menu_entries(menu),
16955 &["first", "last", "second"],
16956 "`ShowWordCompletions` action should show word completions"
16957 );
16958 } else {
16959 panic!("expected completion menu to be open");
16960 }
16961 });
16962
16963 cx.simulate_keystroke("l");
16964 cx.executor().run_until_parked();
16965 cx.condition(|editor, _| editor.context_menu_visible())
16966 .await;
16967 cx.update_editor(|editor, _, _| {
16968 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16969 {
16970 assert_eq!(
16971 completion_menu_entries(menu),
16972 &["last"],
16973 "After showing word completions, further editing should filter them and not query the LSP"
16974 );
16975 } else {
16976 panic!("expected completion menu to be open");
16977 }
16978 });
16979}
16980
16981#[gpui::test]
16982async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
16983 init_test(cx, |language_settings| {
16984 language_settings.defaults.completions = Some(CompletionSettingsContent {
16985 words_min_length: Some(0),
16986 lsp: Some(false),
16987 lsp_insert_mode: Some(LspInsertMode::Insert),
16988 ..Default::default()
16989 });
16990 });
16991
16992 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16993
16994 cx.set_state(indoc! {"ˇ
16995 0_usize
16996 let
16997 33
16998 4.5f32
16999 "});
17000 cx.update_editor(|editor, window, cx| {
17001 editor.show_completions(&ShowCompletions, window, cx);
17002 });
17003 cx.executor().run_until_parked();
17004 cx.condition(|editor, _| editor.context_menu_visible())
17005 .await;
17006 cx.update_editor(|editor, window, cx| {
17007 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17008 {
17009 assert_eq!(
17010 completion_menu_entries(menu),
17011 &["let"],
17012 "With no digits in the completion query, no digits should be in the word completions"
17013 );
17014 } else {
17015 panic!("expected completion menu to be open");
17016 }
17017 editor.cancel(&Cancel, window, cx);
17018 });
17019
17020 cx.set_state(indoc! {"3ˇ
17021 0_usize
17022 let
17023 3
17024 33.35f32
17025 "});
17026 cx.update_editor(|editor, window, cx| {
17027 editor.show_completions(&ShowCompletions, window, cx);
17028 });
17029 cx.executor().run_until_parked();
17030 cx.condition(|editor, _| editor.context_menu_visible())
17031 .await;
17032 cx.update_editor(|editor, _, _| {
17033 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17034 {
17035 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
17036 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
17037 } else {
17038 panic!("expected completion menu to be open");
17039 }
17040 });
17041}
17042
17043#[gpui::test]
17044async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
17045 init_test(cx, |language_settings| {
17046 language_settings.defaults.completions = Some(CompletionSettingsContent {
17047 words: Some(WordsCompletionMode::Enabled),
17048 words_min_length: Some(3),
17049 lsp_insert_mode: Some(LspInsertMode::Insert),
17050 ..Default::default()
17051 });
17052 });
17053
17054 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17055 cx.set_state(indoc! {"ˇ
17056 wow
17057 wowen
17058 wowser
17059 "});
17060 cx.simulate_keystroke("w");
17061 cx.executor().run_until_parked();
17062 cx.update_editor(|editor, _, _| {
17063 if editor.context_menu.borrow_mut().is_some() {
17064 panic!(
17065 "expected completion menu to be hidden, as words completion threshold is not met"
17066 );
17067 }
17068 });
17069
17070 cx.update_editor(|editor, window, cx| {
17071 editor.show_word_completions(&ShowWordCompletions, window, cx);
17072 });
17073 cx.executor().run_until_parked();
17074 cx.update_editor(|editor, window, cx| {
17075 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17076 {
17077 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");
17078 } else {
17079 panic!("expected completion menu to be open after the word completions are called with an action");
17080 }
17081
17082 editor.cancel(&Cancel, window, cx);
17083 });
17084 cx.update_editor(|editor, _, _| {
17085 if editor.context_menu.borrow_mut().is_some() {
17086 panic!("expected completion menu to be hidden after canceling");
17087 }
17088 });
17089
17090 cx.simulate_keystroke("o");
17091 cx.executor().run_until_parked();
17092 cx.update_editor(|editor, _, _| {
17093 if editor.context_menu.borrow_mut().is_some() {
17094 panic!(
17095 "expected completion menu to be hidden, as words completion threshold is not met still"
17096 );
17097 }
17098 });
17099
17100 cx.simulate_keystroke("w");
17101 cx.executor().run_until_parked();
17102 cx.update_editor(|editor, _, _| {
17103 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17104 {
17105 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
17106 } else {
17107 panic!("expected completion menu to be open after the word completions threshold is met");
17108 }
17109 });
17110}
17111
17112#[gpui::test]
17113async fn test_word_completions_disabled(cx: &mut TestAppContext) {
17114 init_test(cx, |language_settings| {
17115 language_settings.defaults.completions = Some(CompletionSettingsContent {
17116 words: Some(WordsCompletionMode::Enabled),
17117 words_min_length: Some(0),
17118 lsp_insert_mode: Some(LspInsertMode::Insert),
17119 ..Default::default()
17120 });
17121 });
17122
17123 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17124 cx.update_editor(|editor, _, _| {
17125 editor.disable_word_completions();
17126 });
17127 cx.set_state(indoc! {"ˇ
17128 wow
17129 wowen
17130 wowser
17131 "});
17132 cx.simulate_keystroke("w");
17133 cx.executor().run_until_parked();
17134 cx.update_editor(|editor, _, _| {
17135 if editor.context_menu.borrow_mut().is_some() {
17136 panic!(
17137 "expected completion menu to be hidden, as words completion are disabled for this editor"
17138 );
17139 }
17140 });
17141
17142 cx.update_editor(|editor, window, cx| {
17143 editor.show_word_completions(&ShowWordCompletions, window, cx);
17144 });
17145 cx.executor().run_until_parked();
17146 cx.update_editor(|editor, _, _| {
17147 if editor.context_menu.borrow_mut().is_some() {
17148 panic!(
17149 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
17150 );
17151 }
17152 });
17153}
17154
17155#[gpui::test]
17156async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
17157 init_test(cx, |language_settings| {
17158 language_settings.defaults.completions = Some(CompletionSettingsContent {
17159 words: Some(WordsCompletionMode::Disabled),
17160 words_min_length: Some(0),
17161 lsp_insert_mode: Some(LspInsertMode::Insert),
17162 ..Default::default()
17163 });
17164 });
17165
17166 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17167 cx.update_editor(|editor, _, _| {
17168 editor.set_completion_provider(None);
17169 });
17170 cx.set_state(indoc! {"ˇ
17171 wow
17172 wowen
17173 wowser
17174 "});
17175 cx.simulate_keystroke("w");
17176 cx.executor().run_until_parked();
17177 cx.update_editor(|editor, _, _| {
17178 if editor.context_menu.borrow_mut().is_some() {
17179 panic!("expected completion menu to be hidden, as disabled in settings");
17180 }
17181 });
17182}
17183
17184fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
17185 let position = || lsp::Position {
17186 line: params.text_document_position.position.line,
17187 character: params.text_document_position.position.character,
17188 };
17189 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17190 range: lsp::Range {
17191 start: position(),
17192 end: position(),
17193 },
17194 new_text: text.to_string(),
17195 }))
17196}
17197
17198#[gpui::test]
17199async fn test_multiline_completion(cx: &mut TestAppContext) {
17200 init_test(cx, |_| {});
17201
17202 let fs = FakeFs::new(cx.executor());
17203 fs.insert_tree(
17204 path!("/a"),
17205 json!({
17206 "main.ts": "a",
17207 }),
17208 )
17209 .await;
17210
17211 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17212 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17213 let typescript_language = Arc::new(Language::new(
17214 LanguageConfig {
17215 name: "TypeScript".into(),
17216 matcher: LanguageMatcher {
17217 path_suffixes: vec!["ts".to_string()],
17218 ..LanguageMatcher::default()
17219 },
17220 line_comments: vec!["// ".into()],
17221 ..LanguageConfig::default()
17222 },
17223 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17224 ));
17225 language_registry.add(typescript_language.clone());
17226 let mut fake_servers = language_registry.register_fake_lsp(
17227 "TypeScript",
17228 FakeLspAdapter {
17229 capabilities: lsp::ServerCapabilities {
17230 completion_provider: Some(lsp::CompletionOptions {
17231 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
17232 ..lsp::CompletionOptions::default()
17233 }),
17234 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
17235 ..lsp::ServerCapabilities::default()
17236 },
17237 // Emulate vtsls label generation
17238 label_for_completion: Some(Box::new(|item, _| {
17239 let text = if let Some(description) = item
17240 .label_details
17241 .as_ref()
17242 .and_then(|label_details| label_details.description.as_ref())
17243 {
17244 format!("{} {}", item.label, description)
17245 } else if let Some(detail) = &item.detail {
17246 format!("{} {}", item.label, detail)
17247 } else {
17248 item.label.clone()
17249 };
17250 Some(language::CodeLabel::plain(text, None))
17251 })),
17252 ..FakeLspAdapter::default()
17253 },
17254 );
17255 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
17256 let workspace = window
17257 .read_with(cx, |mw, _| mw.workspace().clone())
17258 .unwrap();
17259 let cx = &mut VisualTestContext::from_window(*window, cx);
17260 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
17261 workspace.project().update(cx, |project, cx| {
17262 project.worktrees(cx).next().unwrap().read(cx).id()
17263 })
17264 });
17265
17266 let _buffer = project
17267 .update(cx, |project, cx| {
17268 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
17269 })
17270 .await
17271 .unwrap();
17272 let editor = workspace
17273 .update_in(cx, |workspace, window, cx| {
17274 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
17275 })
17276 .await
17277 .unwrap()
17278 .downcast::<Editor>()
17279 .unwrap();
17280 let fake_server = fake_servers.next().await.unwrap();
17281 cx.run_until_parked();
17282
17283 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
17284 let multiline_label_2 = "a\nb\nc\n";
17285 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
17286 let multiline_description = "d\ne\nf\n";
17287 let multiline_detail_2 = "g\nh\ni\n";
17288
17289 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
17290 move |params, _| async move {
17291 Ok(Some(lsp::CompletionResponse::Array(vec![
17292 lsp::CompletionItem {
17293 label: multiline_label.to_string(),
17294 text_edit: gen_text_edit(¶ms, "new_text_1"),
17295 ..lsp::CompletionItem::default()
17296 },
17297 lsp::CompletionItem {
17298 label: "single line label 1".to_string(),
17299 detail: Some(multiline_detail.to_string()),
17300 text_edit: gen_text_edit(¶ms, "new_text_2"),
17301 ..lsp::CompletionItem::default()
17302 },
17303 lsp::CompletionItem {
17304 label: "single line label 2".to_string(),
17305 label_details: Some(lsp::CompletionItemLabelDetails {
17306 description: Some(multiline_description.to_string()),
17307 detail: None,
17308 }),
17309 text_edit: gen_text_edit(¶ms, "new_text_2"),
17310 ..lsp::CompletionItem::default()
17311 },
17312 lsp::CompletionItem {
17313 label: multiline_label_2.to_string(),
17314 detail: Some(multiline_detail_2.to_string()),
17315 text_edit: gen_text_edit(¶ms, "new_text_3"),
17316 ..lsp::CompletionItem::default()
17317 },
17318 lsp::CompletionItem {
17319 label: "Label with many spaces and \t but without newlines".to_string(),
17320 detail: Some(
17321 "Details with many spaces and \t but without newlines".to_string(),
17322 ),
17323 text_edit: gen_text_edit(¶ms, "new_text_4"),
17324 ..lsp::CompletionItem::default()
17325 },
17326 ])))
17327 },
17328 );
17329
17330 editor.update_in(cx, |editor, window, cx| {
17331 cx.focus_self(window);
17332 editor.move_to_end(&MoveToEnd, window, cx);
17333 editor.handle_input(".", window, cx);
17334 });
17335 cx.run_until_parked();
17336 completion_handle.next().await.unwrap();
17337
17338 editor.update(cx, |editor, _| {
17339 assert!(editor.context_menu_visible());
17340 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17341 {
17342 let completion_labels = menu
17343 .completions
17344 .borrow()
17345 .iter()
17346 .map(|c| c.label.text.clone())
17347 .collect::<Vec<_>>();
17348 assert_eq!(
17349 completion_labels,
17350 &[
17351 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
17352 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
17353 "single line label 2 d e f ",
17354 "a b c g h i ",
17355 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
17356 ],
17357 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
17358 );
17359
17360 for completion in menu
17361 .completions
17362 .borrow()
17363 .iter() {
17364 assert_eq!(
17365 completion.label.filter_range,
17366 0..completion.label.text.len(),
17367 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
17368 );
17369 }
17370 } else {
17371 panic!("expected completion menu to be open");
17372 }
17373 });
17374}
17375
17376#[gpui::test]
17377async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
17378 init_test(cx, |_| {});
17379 let mut cx = EditorLspTestContext::new_rust(
17380 lsp::ServerCapabilities {
17381 completion_provider: Some(lsp::CompletionOptions {
17382 trigger_characters: Some(vec![".".to_string()]),
17383 ..Default::default()
17384 }),
17385 ..Default::default()
17386 },
17387 cx,
17388 )
17389 .await;
17390 cx.lsp
17391 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17392 Ok(Some(lsp::CompletionResponse::Array(vec![
17393 lsp::CompletionItem {
17394 label: "first".into(),
17395 ..Default::default()
17396 },
17397 lsp::CompletionItem {
17398 label: "last".into(),
17399 ..Default::default()
17400 },
17401 ])))
17402 });
17403 cx.set_state("variableˇ");
17404 cx.simulate_keystroke(".");
17405 cx.executor().run_until_parked();
17406
17407 cx.update_editor(|editor, _, _| {
17408 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17409 {
17410 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
17411 } else {
17412 panic!("expected completion menu to be open");
17413 }
17414 });
17415
17416 cx.update_editor(|editor, window, cx| {
17417 editor.move_page_down(&MovePageDown::default(), window, cx);
17418 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17419 {
17420 assert!(
17421 menu.selected_item == 1,
17422 "expected PageDown to select the last item from the context menu"
17423 );
17424 } else {
17425 panic!("expected completion menu to stay open after PageDown");
17426 }
17427 });
17428
17429 cx.update_editor(|editor, window, cx| {
17430 editor.move_page_up(&MovePageUp::default(), window, cx);
17431 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17432 {
17433 assert!(
17434 menu.selected_item == 0,
17435 "expected PageUp to select the first item from the context menu"
17436 );
17437 } else {
17438 panic!("expected completion menu to stay open after PageUp");
17439 }
17440 });
17441}
17442
17443#[gpui::test]
17444async fn test_as_is_completions(cx: &mut TestAppContext) {
17445 init_test(cx, |_| {});
17446 let mut cx = EditorLspTestContext::new_rust(
17447 lsp::ServerCapabilities {
17448 completion_provider: Some(lsp::CompletionOptions {
17449 ..Default::default()
17450 }),
17451 ..Default::default()
17452 },
17453 cx,
17454 )
17455 .await;
17456 cx.lsp
17457 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17458 Ok(Some(lsp::CompletionResponse::Array(vec![
17459 lsp::CompletionItem {
17460 label: "unsafe".into(),
17461 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17462 range: lsp::Range {
17463 start: lsp::Position {
17464 line: 1,
17465 character: 2,
17466 },
17467 end: lsp::Position {
17468 line: 1,
17469 character: 3,
17470 },
17471 },
17472 new_text: "unsafe".to_string(),
17473 })),
17474 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
17475 ..Default::default()
17476 },
17477 ])))
17478 });
17479 cx.set_state("fn a() {}\n nˇ");
17480 cx.executor().run_until_parked();
17481 cx.update_editor(|editor, window, cx| {
17482 editor.trigger_completion_on_input("n", true, window, cx)
17483 });
17484 cx.executor().run_until_parked();
17485
17486 cx.update_editor(|editor, window, cx| {
17487 editor.confirm_completion(&Default::default(), window, cx)
17488 });
17489 cx.executor().run_until_parked();
17490 cx.assert_editor_state("fn a() {}\n unsafeˇ");
17491}
17492
17493#[gpui::test]
17494async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
17495 init_test(cx, |_| {});
17496 let language =
17497 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
17498 let mut cx = EditorLspTestContext::new(
17499 language,
17500 lsp::ServerCapabilities {
17501 completion_provider: Some(lsp::CompletionOptions {
17502 ..lsp::CompletionOptions::default()
17503 }),
17504 ..lsp::ServerCapabilities::default()
17505 },
17506 cx,
17507 )
17508 .await;
17509
17510 cx.set_state(
17511 "#ifndef BAR_H
17512#define BAR_H
17513
17514#include <stdbool.h>
17515
17516int fn_branch(bool do_branch1, bool do_branch2);
17517
17518#endif // BAR_H
17519ˇ",
17520 );
17521 cx.executor().run_until_parked();
17522 cx.update_editor(|editor, window, cx| {
17523 editor.handle_input("#", window, cx);
17524 });
17525 cx.executor().run_until_parked();
17526 cx.update_editor(|editor, window, cx| {
17527 editor.handle_input("i", window, cx);
17528 });
17529 cx.executor().run_until_parked();
17530 cx.update_editor(|editor, window, cx| {
17531 editor.handle_input("n", window, cx);
17532 });
17533 cx.executor().run_until_parked();
17534 cx.assert_editor_state(
17535 "#ifndef BAR_H
17536#define BAR_H
17537
17538#include <stdbool.h>
17539
17540int fn_branch(bool do_branch1, bool do_branch2);
17541
17542#endif // BAR_H
17543#inˇ",
17544 );
17545
17546 cx.lsp
17547 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17548 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17549 is_incomplete: false,
17550 item_defaults: None,
17551 items: vec![lsp::CompletionItem {
17552 kind: Some(lsp::CompletionItemKind::SNIPPET),
17553 label_details: Some(lsp::CompletionItemLabelDetails {
17554 detail: Some("header".to_string()),
17555 description: None,
17556 }),
17557 label: " include".to_string(),
17558 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17559 range: lsp::Range {
17560 start: lsp::Position {
17561 line: 8,
17562 character: 1,
17563 },
17564 end: lsp::Position {
17565 line: 8,
17566 character: 1,
17567 },
17568 },
17569 new_text: "include \"$0\"".to_string(),
17570 })),
17571 sort_text: Some("40b67681include".to_string()),
17572 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17573 filter_text: Some("include".to_string()),
17574 insert_text: Some("include \"$0\"".to_string()),
17575 ..lsp::CompletionItem::default()
17576 }],
17577 })))
17578 });
17579 cx.update_editor(|editor, window, cx| {
17580 editor.show_completions(&ShowCompletions, window, cx);
17581 });
17582 cx.executor().run_until_parked();
17583 cx.update_editor(|editor, window, cx| {
17584 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
17585 });
17586 cx.executor().run_until_parked();
17587 cx.assert_editor_state(
17588 "#ifndef BAR_H
17589#define BAR_H
17590
17591#include <stdbool.h>
17592
17593int fn_branch(bool do_branch1, bool do_branch2);
17594
17595#endif // BAR_H
17596#include \"ˇ\"",
17597 );
17598
17599 cx.lsp
17600 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17601 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17602 is_incomplete: true,
17603 item_defaults: None,
17604 items: vec![lsp::CompletionItem {
17605 kind: Some(lsp::CompletionItemKind::FILE),
17606 label: "AGL/".to_string(),
17607 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17608 range: lsp::Range {
17609 start: lsp::Position {
17610 line: 8,
17611 character: 10,
17612 },
17613 end: lsp::Position {
17614 line: 8,
17615 character: 11,
17616 },
17617 },
17618 new_text: "AGL/".to_string(),
17619 })),
17620 sort_text: Some("40b67681AGL/".to_string()),
17621 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17622 filter_text: Some("AGL/".to_string()),
17623 insert_text: Some("AGL/".to_string()),
17624 ..lsp::CompletionItem::default()
17625 }],
17626 })))
17627 });
17628 cx.update_editor(|editor, window, cx| {
17629 editor.show_completions(&ShowCompletions, window, cx);
17630 });
17631 cx.executor().run_until_parked();
17632 cx.update_editor(|editor, window, cx| {
17633 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
17634 });
17635 cx.executor().run_until_parked();
17636 cx.assert_editor_state(
17637 r##"#ifndef BAR_H
17638#define BAR_H
17639
17640#include <stdbool.h>
17641
17642int fn_branch(bool do_branch1, bool do_branch2);
17643
17644#endif // BAR_H
17645#include "AGL/ˇ"##,
17646 );
17647
17648 cx.update_editor(|editor, window, cx| {
17649 editor.handle_input("\"", window, cx);
17650 });
17651 cx.executor().run_until_parked();
17652 cx.assert_editor_state(
17653 r##"#ifndef BAR_H
17654#define BAR_H
17655
17656#include <stdbool.h>
17657
17658int fn_branch(bool do_branch1, bool do_branch2);
17659
17660#endif // BAR_H
17661#include "AGL/"ˇ"##,
17662 );
17663}
17664
17665#[gpui::test]
17666async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
17667 init_test(cx, |_| {});
17668
17669 let mut cx = EditorLspTestContext::new_rust(
17670 lsp::ServerCapabilities {
17671 completion_provider: Some(lsp::CompletionOptions {
17672 trigger_characters: Some(vec![".".to_string()]),
17673 resolve_provider: Some(false),
17674 ..lsp::CompletionOptions::default()
17675 }),
17676 ..lsp::ServerCapabilities::default()
17677 },
17678 cx,
17679 )
17680 .await;
17681
17682 cx.set_state("fn main() { let a = 2ˇ; }");
17683 cx.simulate_keystroke(".");
17684 let completion_item = lsp::CompletionItem {
17685 label: "Some".into(),
17686 kind: Some(lsp::CompletionItemKind::SNIPPET),
17687 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17688 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17689 kind: lsp::MarkupKind::Markdown,
17690 value: "```rust\nSome(2)\n```".to_string(),
17691 })),
17692 deprecated: Some(false),
17693 sort_text: Some("Some".to_string()),
17694 filter_text: Some("Some".to_string()),
17695 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17696 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17697 range: lsp::Range {
17698 start: lsp::Position {
17699 line: 0,
17700 character: 22,
17701 },
17702 end: lsp::Position {
17703 line: 0,
17704 character: 22,
17705 },
17706 },
17707 new_text: "Some(2)".to_string(),
17708 })),
17709 additional_text_edits: Some(vec![lsp::TextEdit {
17710 range: lsp::Range {
17711 start: lsp::Position {
17712 line: 0,
17713 character: 20,
17714 },
17715 end: lsp::Position {
17716 line: 0,
17717 character: 22,
17718 },
17719 },
17720 new_text: "".to_string(),
17721 }]),
17722 ..Default::default()
17723 };
17724
17725 let closure_completion_item = completion_item.clone();
17726 let counter = Arc::new(AtomicUsize::new(0));
17727 let counter_clone = counter.clone();
17728 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17729 let task_completion_item = closure_completion_item.clone();
17730 counter_clone.fetch_add(1, atomic::Ordering::Release);
17731 async move {
17732 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17733 is_incomplete: true,
17734 item_defaults: None,
17735 items: vec![task_completion_item],
17736 })))
17737 }
17738 });
17739
17740 cx.executor().run_until_parked();
17741 cx.condition(|editor, _| editor.context_menu_visible())
17742 .await;
17743 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
17744 assert!(request.next().await.is_some());
17745 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
17746
17747 cx.simulate_keystrokes("S o m");
17748 cx.condition(|editor, _| editor.context_menu_visible())
17749 .await;
17750 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
17751 assert!(request.next().await.is_some());
17752 assert!(request.next().await.is_some());
17753 assert!(request.next().await.is_some());
17754 request.close();
17755 assert!(request.next().await.is_none());
17756 assert_eq!(
17757 counter.load(atomic::Ordering::Acquire),
17758 4,
17759 "With the completions menu open, only one LSP request should happen per input"
17760 );
17761}
17762
17763#[gpui::test]
17764async fn test_toggle_comment(cx: &mut TestAppContext) {
17765 init_test(cx, |_| {});
17766 let mut cx = EditorTestContext::new(cx).await;
17767 let language = Arc::new(Language::new(
17768 LanguageConfig {
17769 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
17770 ..Default::default()
17771 },
17772 Some(tree_sitter_rust::LANGUAGE.into()),
17773 ));
17774 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
17775
17776 // If multiple selections intersect a line, the line is only toggled once.
17777 cx.set_state(indoc! {"
17778 fn a() {
17779 «//b();
17780 ˇ»// «c();
17781 //ˇ» d();
17782 }
17783 "});
17784
17785 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17786
17787 cx.assert_editor_state(indoc! {"
17788 fn a() {
17789 «b();
17790 ˇ»«c();
17791 ˇ» d();
17792 }
17793 "});
17794
17795 // The comment prefix is inserted at the same column for every line in a
17796 // selection.
17797 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17798
17799 cx.assert_editor_state(indoc! {"
17800 fn a() {
17801 // «b();
17802 ˇ»// «c();
17803 ˇ» // d();
17804 }
17805 "});
17806
17807 // If a selection ends at the beginning of a line, that line is not toggled.
17808 cx.set_selections_state(indoc! {"
17809 fn a() {
17810 // b();
17811 «// c();
17812 ˇ» // d();
17813 }
17814 "});
17815
17816 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17817
17818 cx.assert_editor_state(indoc! {"
17819 fn a() {
17820 // b();
17821 «c();
17822 ˇ» // d();
17823 }
17824 "});
17825
17826 // If a selection span a single line and is empty, the line is toggled.
17827 cx.set_state(indoc! {"
17828 fn a() {
17829 a();
17830 b();
17831 ˇ
17832 }
17833 "});
17834
17835 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17836
17837 cx.assert_editor_state(indoc! {"
17838 fn a() {
17839 a();
17840 b();
17841 //•ˇ
17842 }
17843 "});
17844
17845 // If a selection span multiple lines, empty lines are not toggled.
17846 cx.set_state(indoc! {"
17847 fn a() {
17848 «a();
17849
17850 c();ˇ»
17851 }
17852 "});
17853
17854 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17855
17856 cx.assert_editor_state(indoc! {"
17857 fn a() {
17858 // «a();
17859
17860 // c();ˇ»
17861 }
17862 "});
17863
17864 // If a selection includes multiple comment prefixes, all lines are uncommented.
17865 cx.set_state(indoc! {"
17866 fn a() {
17867 «// a();
17868 /// b();
17869 //! c();ˇ»
17870 }
17871 "});
17872
17873 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17874
17875 cx.assert_editor_state(indoc! {"
17876 fn a() {
17877 «a();
17878 b();
17879 c();ˇ»
17880 }
17881 "});
17882}
17883
17884#[gpui::test]
17885async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
17886 init_test(cx, |_| {});
17887 let mut cx = EditorTestContext::new(cx).await;
17888 let language = Arc::new(Language::new(
17889 LanguageConfig {
17890 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
17891 ..Default::default()
17892 },
17893 Some(tree_sitter_rust::LANGUAGE.into()),
17894 ));
17895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
17896
17897 let toggle_comments = &ToggleComments {
17898 advance_downwards: false,
17899 ignore_indent: true,
17900 };
17901
17902 // If multiple selections intersect a line, the line is only toggled once.
17903 cx.set_state(indoc! {"
17904 fn a() {
17905 // «b();
17906 // c();
17907 // ˇ» d();
17908 }
17909 "});
17910
17911 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17912
17913 cx.assert_editor_state(indoc! {"
17914 fn a() {
17915 «b();
17916 c();
17917 ˇ» d();
17918 }
17919 "});
17920
17921 // The comment prefix is inserted at the beginning of each line
17922 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17923
17924 cx.assert_editor_state(indoc! {"
17925 fn a() {
17926 // «b();
17927 // c();
17928 // ˇ» d();
17929 }
17930 "});
17931
17932 // If a selection ends at the beginning of a line, that line is not toggled.
17933 cx.set_selections_state(indoc! {"
17934 fn a() {
17935 // b();
17936 // «c();
17937 ˇ»// d();
17938 }
17939 "});
17940
17941 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17942
17943 cx.assert_editor_state(indoc! {"
17944 fn a() {
17945 // b();
17946 «c();
17947 ˇ»// d();
17948 }
17949 "});
17950
17951 // If a selection span a single line and is empty, the line is toggled.
17952 cx.set_state(indoc! {"
17953 fn a() {
17954 a();
17955 b();
17956 ˇ
17957 }
17958 "});
17959
17960 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17961
17962 cx.assert_editor_state(indoc! {"
17963 fn a() {
17964 a();
17965 b();
17966 //ˇ
17967 }
17968 "});
17969
17970 // If a selection span multiple lines, empty lines are not toggled.
17971 cx.set_state(indoc! {"
17972 fn a() {
17973 «a();
17974
17975 c();ˇ»
17976 }
17977 "});
17978
17979 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17980
17981 cx.assert_editor_state(indoc! {"
17982 fn a() {
17983 // «a();
17984
17985 // c();ˇ»
17986 }
17987 "});
17988
17989 // If a selection includes multiple comment prefixes, all lines are uncommented.
17990 cx.set_state(indoc! {"
17991 fn a() {
17992 // «a();
17993 /// b();
17994 //! c();ˇ»
17995 }
17996 "});
17997
17998 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17999
18000 cx.assert_editor_state(indoc! {"
18001 fn a() {
18002 «a();
18003 b();
18004 c();ˇ»
18005 }
18006 "});
18007}
18008
18009#[gpui::test]
18010async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
18011 init_test(cx, |_| {});
18012
18013 let language = Arc::new(Language::new(
18014 LanguageConfig {
18015 line_comments: vec!["// ".into()],
18016 ..Default::default()
18017 },
18018 Some(tree_sitter_rust::LANGUAGE.into()),
18019 ));
18020
18021 let mut cx = EditorTestContext::new(cx).await;
18022
18023 cx.language_registry().add(language.clone());
18024 cx.update_buffer(|buffer, cx| {
18025 buffer.set_language(Some(language), cx);
18026 });
18027
18028 let toggle_comments = &ToggleComments {
18029 advance_downwards: true,
18030 ignore_indent: false,
18031 };
18032
18033 // Single cursor on one line -> advance
18034 // Cursor moves horizontally 3 characters as well on non-blank line
18035 cx.set_state(indoc!(
18036 "fn a() {
18037 ˇdog();
18038 cat();
18039 }"
18040 ));
18041 cx.update_editor(|editor, window, cx| {
18042 editor.toggle_comments(toggle_comments, window, cx);
18043 });
18044 cx.assert_editor_state(indoc!(
18045 "fn a() {
18046 // dog();
18047 catˇ();
18048 }"
18049 ));
18050
18051 // Single selection on one line -> don't advance
18052 cx.set_state(indoc!(
18053 "fn a() {
18054 «dog()ˇ»;
18055 cat();
18056 }"
18057 ));
18058 cx.update_editor(|editor, window, cx| {
18059 editor.toggle_comments(toggle_comments, window, cx);
18060 });
18061 cx.assert_editor_state(indoc!(
18062 "fn a() {
18063 // «dog()ˇ»;
18064 cat();
18065 }"
18066 ));
18067
18068 // Multiple cursors on one line -> advance
18069 cx.set_state(indoc!(
18070 "fn a() {
18071 ˇdˇog();
18072 cat();
18073 }"
18074 ));
18075 cx.update_editor(|editor, window, cx| {
18076 editor.toggle_comments(toggle_comments, window, cx);
18077 });
18078 cx.assert_editor_state(indoc!(
18079 "fn a() {
18080 // dog();
18081 catˇ(ˇ);
18082 }"
18083 ));
18084
18085 // Multiple cursors on one line, with selection -> don't advance
18086 cx.set_state(indoc!(
18087 "fn a() {
18088 ˇdˇog«()ˇ»;
18089 cat();
18090 }"
18091 ));
18092 cx.update_editor(|editor, window, cx| {
18093 editor.toggle_comments(toggle_comments, window, cx);
18094 });
18095 cx.assert_editor_state(indoc!(
18096 "fn a() {
18097 // ˇdˇog«()ˇ»;
18098 cat();
18099 }"
18100 ));
18101
18102 // Single cursor on one line -> advance
18103 // Cursor moves to column 0 on blank line
18104 cx.set_state(indoc!(
18105 "fn a() {
18106 ˇdog();
18107
18108 cat();
18109 }"
18110 ));
18111 cx.update_editor(|editor, window, cx| {
18112 editor.toggle_comments(toggle_comments, window, cx);
18113 });
18114 cx.assert_editor_state(indoc!(
18115 "fn a() {
18116 // dog();
18117 ˇ
18118 cat();
18119 }"
18120 ));
18121
18122 // Single cursor on one line -> advance
18123 // Cursor starts and ends at column 0
18124 cx.set_state(indoc!(
18125 "fn a() {
18126 ˇ dog();
18127 cat();
18128 }"
18129 ));
18130 cx.update_editor(|editor, window, cx| {
18131 editor.toggle_comments(toggle_comments, window, cx);
18132 });
18133 cx.assert_editor_state(indoc!(
18134 "fn a() {
18135 // dog();
18136 ˇ cat();
18137 }"
18138 ));
18139}
18140
18141#[gpui::test]
18142async fn test_toggle_block_comment(cx: &mut TestAppContext) {
18143 init_test(cx, |_| {});
18144
18145 let mut cx = EditorTestContext::new(cx).await;
18146
18147 let html_language = Arc::new(
18148 Language::new(
18149 LanguageConfig {
18150 name: "HTML".into(),
18151 block_comment: Some(BlockCommentConfig {
18152 start: "<!-- ".into(),
18153 prefix: "".into(),
18154 end: " -->".into(),
18155 tab_size: 0,
18156 }),
18157 ..Default::default()
18158 },
18159 Some(tree_sitter_html::LANGUAGE.into()),
18160 )
18161 .with_injection_query(
18162 r#"
18163 (script_element
18164 (raw_text) @injection.content
18165 (#set! injection.language "javascript"))
18166 "#,
18167 )
18168 .unwrap(),
18169 );
18170
18171 let javascript_language = Arc::new(Language::new(
18172 LanguageConfig {
18173 name: "JavaScript".into(),
18174 line_comments: vec!["// ".into()],
18175 ..Default::default()
18176 },
18177 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18178 ));
18179
18180 cx.language_registry().add(html_language.clone());
18181 cx.language_registry().add(javascript_language);
18182 cx.update_buffer(|buffer, cx| {
18183 buffer.set_language(Some(html_language), cx);
18184 });
18185
18186 // Toggle comments for empty selections
18187 cx.set_state(
18188 &r#"
18189 <p>A</p>ˇ
18190 <p>B</p>ˇ
18191 <p>C</p>ˇ
18192 "#
18193 .unindent(),
18194 );
18195 cx.update_editor(|editor, window, cx| {
18196 editor.toggle_comments(&ToggleComments::default(), window, cx)
18197 });
18198 cx.assert_editor_state(
18199 &r#"
18200 <!-- <p>A</p>ˇ -->
18201 <!-- <p>B</p>ˇ -->
18202 <!-- <p>C</p>ˇ -->
18203 "#
18204 .unindent(),
18205 );
18206 cx.update_editor(|editor, window, cx| {
18207 editor.toggle_comments(&ToggleComments::default(), window, cx)
18208 });
18209 cx.assert_editor_state(
18210 &r#"
18211 <p>A</p>ˇ
18212 <p>B</p>ˇ
18213 <p>C</p>ˇ
18214 "#
18215 .unindent(),
18216 );
18217
18218 // Toggle comments for mixture of empty and non-empty selections, where
18219 // multiple selections occupy a given line.
18220 cx.set_state(
18221 &r#"
18222 <p>A«</p>
18223 <p>ˇ»B</p>ˇ
18224 <p>C«</p>
18225 <p>ˇ»D</p>ˇ
18226 "#
18227 .unindent(),
18228 );
18229
18230 cx.update_editor(|editor, window, cx| {
18231 editor.toggle_comments(&ToggleComments::default(), window, cx)
18232 });
18233 cx.assert_editor_state(
18234 &r#"
18235 <!-- <p>A«</p>
18236 <p>ˇ»B</p>ˇ -->
18237 <!-- <p>C«</p>
18238 <p>ˇ»D</p>ˇ -->
18239 "#
18240 .unindent(),
18241 );
18242 cx.update_editor(|editor, window, cx| {
18243 editor.toggle_comments(&ToggleComments::default(), window, cx)
18244 });
18245 cx.assert_editor_state(
18246 &r#"
18247 <p>A«</p>
18248 <p>ˇ»B</p>ˇ
18249 <p>C«</p>
18250 <p>ˇ»D</p>ˇ
18251 "#
18252 .unindent(),
18253 );
18254
18255 // Toggle comments when different languages are active for different
18256 // selections.
18257 cx.set_state(
18258 &r#"
18259 ˇ<script>
18260 ˇvar x = new Y();
18261 ˇ</script>
18262 "#
18263 .unindent(),
18264 );
18265 cx.executor().run_until_parked();
18266 cx.update_editor(|editor, window, cx| {
18267 editor.toggle_comments(&ToggleComments::default(), window, cx)
18268 });
18269 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
18270 // Uncommenting and commenting from this position brings in even more wrong artifacts.
18271 cx.assert_editor_state(
18272 &r#"
18273 <!-- ˇ<script> -->
18274 // ˇvar x = new Y();
18275 <!-- ˇ</script> -->
18276 "#
18277 .unindent(),
18278 );
18279}
18280
18281#[gpui::test]
18282fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
18283 init_test(cx, |_| {});
18284
18285 let buffer = cx.new(|cx| Buffer::local(sample_text(6, 4, 'a'), cx));
18286 let multibuffer = cx.new(|cx| {
18287 let mut multibuffer = MultiBuffer::new(ReadWrite);
18288 multibuffer.set_excerpts_for_path(
18289 PathKey::sorted(0),
18290 buffer.clone(),
18291 [
18292 Point::new(0, 0)..Point::new(0, 4),
18293 Point::new(5, 0)..Point::new(5, 4),
18294 ],
18295 0,
18296 cx,
18297 );
18298 assert_eq!(multibuffer.read(cx).text(), "aaaa\nffff");
18299 multibuffer
18300 });
18301
18302 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
18303 editor.update_in(cx, |editor, window, cx| {
18304 assert_eq!(editor.text(cx), "aaaa\nffff");
18305 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18306 s.select_ranges([
18307 Point::new(0, 0)..Point::new(0, 0),
18308 Point::new(1, 0)..Point::new(1, 0),
18309 ])
18310 });
18311
18312 editor.handle_input("X", window, cx);
18313 assert_eq!(editor.text(cx), "Xaaaa\nXffff");
18314 assert_eq!(
18315 editor.selections.ranges(&editor.display_snapshot(cx)),
18316 [
18317 Point::new(0, 1)..Point::new(0, 1),
18318 Point::new(1, 1)..Point::new(1, 1),
18319 ]
18320 );
18321
18322 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
18323 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18324 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
18325 });
18326 editor.backspace(&Default::default(), window, cx);
18327 assert_eq!(editor.text(cx), "Xa\nfff");
18328 assert_eq!(
18329 editor.selections.ranges(&editor.display_snapshot(cx)),
18330 [Point::new(1, 0)..Point::new(1, 0)]
18331 );
18332
18333 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18334 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
18335 });
18336 editor.backspace(&Default::default(), window, cx);
18337 assert_eq!(editor.text(cx), "X\nff");
18338 assert_eq!(
18339 editor.selections.ranges(&editor.display_snapshot(cx)),
18340 [Point::new(0, 1)..Point::new(0, 1)]
18341 );
18342 });
18343}
18344
18345#[gpui::test]
18346fn test_refresh_selections(cx: &mut TestAppContext) {
18347 init_test(cx, |_| {});
18348
18349 let buffer = cx.new(|cx| Buffer::local(sample_text(5, 4, 'a'), cx));
18350 let multibuffer = cx.new(|cx| {
18351 let mut multibuffer = MultiBuffer::new(ReadWrite);
18352 multibuffer.set_excerpts_for_path(
18353 PathKey::sorted(0),
18354 buffer.clone(),
18355 [
18356 Point::new(0, 0)..Point::new(1, 4),
18357 Point::new(3, 0)..Point::new(4, 4),
18358 ],
18359 0,
18360 cx,
18361 );
18362 multibuffer
18363 });
18364
18365 let editor = cx.add_window(|window, cx| {
18366 let mut editor = build_editor(multibuffer.clone(), window, cx);
18367 let snapshot = editor.snapshot(window, cx);
18368 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18369 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
18370 });
18371 editor.begin_selection(
18372 Point::new(2, 1).to_display_point(&snapshot),
18373 true,
18374 1,
18375 window,
18376 cx,
18377 );
18378 assert_eq!(
18379 editor.selections.ranges(&editor.display_snapshot(cx)),
18380 [
18381 Point::new(1, 3)..Point::new(1, 3),
18382 Point::new(2, 1)..Point::new(2, 1),
18383 ]
18384 );
18385 editor
18386 });
18387
18388 // Refreshing selections is a no-op when excerpts haven't changed.
18389 _ = editor.update(cx, |editor, window, cx| {
18390 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
18391 assert_eq!(
18392 editor.selections.ranges(&editor.display_snapshot(cx)),
18393 [
18394 Point::new(1, 3)..Point::new(1, 3),
18395 Point::new(2, 1)..Point::new(2, 1),
18396 ]
18397 );
18398 });
18399
18400 multibuffer.update(cx, |multibuffer, cx| {
18401 multibuffer.set_excerpts_for_path(
18402 PathKey::sorted(0),
18403 buffer.clone(),
18404 [Point::new(3, 0)..Point::new(4, 4)],
18405 0,
18406 cx,
18407 );
18408 });
18409 _ = editor.update(cx, |editor, window, cx| {
18410 // Removing an excerpt causes the first selection to become degenerate.
18411 assert_eq!(
18412 editor.selections.ranges(&editor.display_snapshot(cx)),
18413 [
18414 Point::new(0, 0)..Point::new(0, 0),
18415 Point::new(0, 1)..Point::new(0, 1)
18416 ]
18417 );
18418
18419 // Refreshing selections will relocate the first selection to the original buffer
18420 // location.
18421 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
18422 assert_eq!(
18423 editor.selections.ranges(&editor.display_snapshot(cx)),
18424 [
18425 Point::new(0, 0)..Point::new(0, 0),
18426 Point::new(0, 1)..Point::new(0, 1),
18427 ]
18428 );
18429 assert!(editor.selections.pending_anchor().is_some());
18430 });
18431}
18432
18433#[gpui::test]
18434fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
18435 init_test(cx, |_| {});
18436
18437 let buffer = cx.new(|cx| Buffer::local(sample_text(5, 4, 'a'), cx));
18438 let multibuffer = cx.new(|cx| {
18439 let mut multibuffer = MultiBuffer::new(ReadWrite);
18440 multibuffer.set_excerpts_for_path(
18441 PathKey::sorted(0),
18442 buffer.clone(),
18443 [
18444 Point::new(0, 0)..Point::new(1, 4),
18445 Point::new(3, 0)..Point::new(4, 4),
18446 ],
18447 0,
18448 cx,
18449 );
18450 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\ndddd\neeee");
18451 multibuffer
18452 });
18453
18454 let editor = cx.add_window(|window, cx| {
18455 let mut editor = build_editor(multibuffer.clone(), window, cx);
18456 let snapshot = editor.snapshot(window, cx);
18457 editor.begin_selection(
18458 Point::new(1, 3).to_display_point(&snapshot),
18459 false,
18460 1,
18461 window,
18462 cx,
18463 );
18464 assert_eq!(
18465 editor.selections.ranges(&editor.display_snapshot(cx)),
18466 [Point::new(1, 3)..Point::new(1, 3)]
18467 );
18468 editor
18469 });
18470
18471 multibuffer.update(cx, |multibuffer, cx| {
18472 multibuffer.set_excerpts_for_path(
18473 PathKey::sorted(0),
18474 buffer.clone(),
18475 [Point::new(3, 0)..Point::new(4, 4)],
18476 0,
18477 cx,
18478 );
18479 });
18480 _ = editor.update(cx, |editor, window, cx| {
18481 assert_eq!(
18482 editor.selections.ranges(&editor.display_snapshot(cx)),
18483 [Point::new(0, 0)..Point::new(0, 0)]
18484 );
18485
18486 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
18487 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
18488 assert_eq!(
18489 editor.selections.ranges(&editor.display_snapshot(cx)),
18490 [Point::new(0, 0)..Point::new(0, 0)]
18491 );
18492 assert!(editor.selections.pending_anchor().is_some());
18493 });
18494}
18495
18496#[gpui::test]
18497async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
18498 init_test(cx, |_| {});
18499
18500 let language = Arc::new(
18501 Language::new(
18502 LanguageConfig {
18503 brackets: BracketPairConfig {
18504 pairs: vec![
18505 BracketPair {
18506 start: "{".to_string(),
18507 end: "}".to_string(),
18508 close: true,
18509 surround: true,
18510 newline: true,
18511 },
18512 BracketPair {
18513 start: "/* ".to_string(),
18514 end: " */".to_string(),
18515 close: true,
18516 surround: true,
18517 newline: true,
18518 },
18519 ],
18520 ..Default::default()
18521 },
18522 ..Default::default()
18523 },
18524 Some(tree_sitter_rust::LANGUAGE.into()),
18525 )
18526 .with_indents_query("")
18527 .unwrap(),
18528 );
18529
18530 let text = concat!(
18531 "{ }\n", //
18532 " x\n", //
18533 " /* */\n", //
18534 "x\n", //
18535 "{{} }\n", //
18536 );
18537
18538 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18539 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18540 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18541 editor
18542 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
18543 .await;
18544
18545 editor.update_in(cx, |editor, window, cx| {
18546 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18547 s.select_display_ranges([
18548 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
18549 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
18550 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
18551 ])
18552 });
18553 editor.newline(&Newline, window, cx);
18554
18555 assert_eq!(
18556 editor.buffer().read(cx).read(cx).text(),
18557 concat!(
18558 "{ \n", // Suppress rustfmt
18559 "\n", //
18560 "}\n", //
18561 " x\n", //
18562 " /* \n", //
18563 " \n", //
18564 " */\n", //
18565 "x\n", //
18566 "{{} \n", //
18567 "}\n", //
18568 )
18569 );
18570 });
18571}
18572
18573#[gpui::test]
18574fn test_highlighted_ranges(cx: &mut TestAppContext) {
18575 init_test(cx, |_| {});
18576
18577 let editor = cx.add_window(|window, cx| {
18578 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
18579 build_editor(buffer, window, cx)
18580 });
18581
18582 _ = editor.update(cx, |editor, window, cx| {
18583 let buffer = editor.buffer.read(cx).snapshot(cx);
18584
18585 let anchor_range =
18586 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
18587
18588 editor.highlight_background(
18589 HighlightKey::ColorizeBracket(0),
18590 &[
18591 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
18592 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
18593 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
18594 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
18595 ],
18596 |_, _| Hsla::red(),
18597 cx,
18598 );
18599 editor.highlight_background(
18600 HighlightKey::ColorizeBracket(1),
18601 &[
18602 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
18603 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
18604 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
18605 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
18606 ],
18607 |_, _| Hsla::green(),
18608 cx,
18609 );
18610
18611 let snapshot = editor.snapshot(window, cx);
18612 let highlighted_ranges = editor.sorted_background_highlights_in_range(
18613 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
18614 &snapshot,
18615 cx.theme(),
18616 );
18617 assert_eq!(
18618 highlighted_ranges,
18619 &[
18620 (
18621 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
18622 Hsla::green(),
18623 ),
18624 (
18625 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
18626 Hsla::red(),
18627 ),
18628 (
18629 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
18630 Hsla::green(),
18631 ),
18632 (
18633 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
18634 Hsla::red(),
18635 ),
18636 ]
18637 );
18638 assert_eq!(
18639 editor.sorted_background_highlights_in_range(
18640 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
18641 &snapshot,
18642 cx.theme(),
18643 ),
18644 &[(
18645 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
18646 Hsla::red(),
18647 )]
18648 );
18649 });
18650}
18651
18652#[gpui::test]
18653async fn test_following(cx: &mut TestAppContext) {
18654 init_test(cx, |_| {});
18655
18656 let fs = FakeFs::new(cx.executor());
18657 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
18658
18659 let buffer = project.update(cx, |project, cx| {
18660 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
18661 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
18662 });
18663 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
18664 let follower = cx.update(|cx| {
18665 cx.open_window(
18666 WindowOptions {
18667 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
18668 gpui::Point::new(px(0.), px(0.)),
18669 gpui::Point::new(px(10.), px(80.)),
18670 ))),
18671 ..Default::default()
18672 },
18673 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
18674 )
18675 .unwrap()
18676 });
18677
18678 let is_still_following = Rc::new(RefCell::new(true));
18679 let follower_edit_event_count = Rc::new(RefCell::new(0));
18680 let pending_update = Rc::new(RefCell::new(None));
18681 let leader_entity = leader.root(cx).unwrap();
18682 let follower_entity = follower.root(cx).unwrap();
18683 _ = follower.update(cx, {
18684 let update = pending_update.clone();
18685 let is_still_following = is_still_following.clone();
18686 let follower_edit_event_count = follower_edit_event_count.clone();
18687 |_, window, cx| {
18688 cx.subscribe_in(
18689 &leader_entity,
18690 window,
18691 move |_, leader, event, window, cx| {
18692 leader.update(cx, |leader, cx| {
18693 leader.add_event_to_update_proto(
18694 event,
18695 &mut update.borrow_mut(),
18696 window,
18697 cx,
18698 );
18699 });
18700 },
18701 )
18702 .detach();
18703
18704 cx.subscribe_in(
18705 &follower_entity,
18706 window,
18707 move |_, _, event: &EditorEvent, _window, _cx| {
18708 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
18709 *is_still_following.borrow_mut() = false;
18710 }
18711
18712 if let EditorEvent::BufferEdited = event {
18713 *follower_edit_event_count.borrow_mut() += 1;
18714 }
18715 },
18716 )
18717 .detach();
18718 }
18719 });
18720
18721 // Update the selections only
18722 _ = leader.update(cx, |leader, window, cx| {
18723 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18724 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
18725 });
18726 });
18727 follower
18728 .update(cx, |follower, window, cx| {
18729 follower.apply_update_proto(
18730 &project,
18731 pending_update.borrow_mut().take().unwrap(),
18732 window,
18733 cx,
18734 )
18735 })
18736 .unwrap()
18737 .await
18738 .unwrap();
18739 _ = follower.update(cx, |follower, _, cx| {
18740 assert_eq!(
18741 follower.selections.ranges(&follower.display_snapshot(cx)),
18742 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
18743 );
18744 });
18745 assert!(*is_still_following.borrow());
18746 assert_eq!(*follower_edit_event_count.borrow(), 0);
18747
18748 // Update the scroll position only
18749 _ = leader.update(cx, |leader, window, cx| {
18750 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
18751 });
18752 follower
18753 .update(cx, |follower, window, cx| {
18754 follower.apply_update_proto(
18755 &project,
18756 pending_update.borrow_mut().take().unwrap(),
18757 window,
18758 cx,
18759 )
18760 })
18761 .unwrap()
18762 .await
18763 .unwrap();
18764 assert_eq!(
18765 follower
18766 .update(cx, |follower, _, cx| follower.scroll_position(cx))
18767 .unwrap(),
18768 gpui::Point::new(1.5, 3.5)
18769 );
18770 assert!(*is_still_following.borrow());
18771 assert_eq!(*follower_edit_event_count.borrow(), 0);
18772
18773 // Update the selections and scroll position. The follower's scroll position is updated
18774 // via autoscroll, not via the leader's exact scroll position.
18775 _ = leader.update(cx, |leader, window, cx| {
18776 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18777 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
18778 });
18779 leader.request_autoscroll(Autoscroll::newest(), cx);
18780 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
18781 });
18782 follower
18783 .update(cx, |follower, window, cx| {
18784 follower.apply_update_proto(
18785 &project,
18786 pending_update.borrow_mut().take().unwrap(),
18787 window,
18788 cx,
18789 )
18790 })
18791 .unwrap()
18792 .await
18793 .unwrap();
18794 _ = follower.update(cx, |follower, _, cx| {
18795 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
18796 assert_eq!(
18797 follower.selections.ranges(&follower.display_snapshot(cx)),
18798 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
18799 );
18800 });
18801 assert!(*is_still_following.borrow());
18802
18803 // Creating a pending selection that precedes another selection
18804 _ = leader.update(cx, |leader, window, cx| {
18805 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18806 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
18807 });
18808 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
18809 });
18810 follower
18811 .update(cx, |follower, window, cx| {
18812 follower.apply_update_proto(
18813 &project,
18814 pending_update.borrow_mut().take().unwrap(),
18815 window,
18816 cx,
18817 )
18818 })
18819 .unwrap()
18820 .await
18821 .unwrap();
18822 _ = follower.update(cx, |follower, _, cx| {
18823 assert_eq!(
18824 follower.selections.ranges(&follower.display_snapshot(cx)),
18825 vec![
18826 MultiBufferOffset(0)..MultiBufferOffset(0),
18827 MultiBufferOffset(1)..MultiBufferOffset(1)
18828 ]
18829 );
18830 });
18831 assert!(*is_still_following.borrow());
18832
18833 // Extend the pending selection so that it surrounds another selection
18834 _ = leader.update(cx, |leader, window, cx| {
18835 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
18836 });
18837 follower
18838 .update(cx, |follower, window, cx| {
18839 follower.apply_update_proto(
18840 &project,
18841 pending_update.borrow_mut().take().unwrap(),
18842 window,
18843 cx,
18844 )
18845 })
18846 .unwrap()
18847 .await
18848 .unwrap();
18849 _ = follower.update(cx, |follower, _, cx| {
18850 assert_eq!(
18851 follower.selections.ranges(&follower.display_snapshot(cx)),
18852 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
18853 );
18854 });
18855
18856 // Scrolling locally breaks the follow
18857 _ = follower.update(cx, |follower, window, cx| {
18858 let top_anchor = follower
18859 .buffer()
18860 .read(cx)
18861 .read(cx)
18862 .anchor_after(MultiBufferOffset(0));
18863 follower.set_scroll_anchor(
18864 ScrollAnchor {
18865 anchor: top_anchor,
18866 offset: gpui::Point::new(0.0, 0.5),
18867 },
18868 window,
18869 cx,
18870 );
18871 });
18872 assert!(!(*is_still_following.borrow()));
18873}
18874
18875#[gpui::test]
18876async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
18877 init_test(cx, |_| {});
18878
18879 let fs = FakeFs::new(cx.executor());
18880 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
18881 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
18882 let workspace = window
18883 .read_with(cx, |mw, _| mw.workspace().clone())
18884 .unwrap();
18885 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
18886
18887 let cx = &mut VisualTestContext::from_window(*window, cx);
18888
18889 let leader = pane.update_in(cx, |_, window, cx| {
18890 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
18891 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
18892 });
18893
18894 // Start following the editor when it has no excerpts.
18895 let mut state_message =
18896 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
18897 let workspace_entity = workspace.clone();
18898 let follower_1 = cx
18899 .update_window(*window, |_, window, cx| {
18900 Editor::from_state_proto(
18901 workspace_entity,
18902 ViewId {
18903 creator: CollaboratorId::PeerId(PeerId::default()),
18904 id: 0,
18905 },
18906 &mut state_message,
18907 window,
18908 cx,
18909 )
18910 })
18911 .unwrap()
18912 .unwrap()
18913 .await
18914 .unwrap();
18915
18916 let update_message = Rc::new(RefCell::new(None));
18917 follower_1.update_in(cx, {
18918 let update = update_message.clone();
18919 |_, window, cx| {
18920 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
18921 leader.update(cx, |leader, cx| {
18922 leader.add_event_to_update_proto(event, &mut update.borrow_mut(), window, cx);
18923 });
18924 })
18925 .detach();
18926 }
18927 });
18928
18929 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
18930 (
18931 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
18932 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
18933 )
18934 });
18935
18936 // Insert some excerpts.
18937 leader.update(cx, |leader, cx| {
18938 leader.buffer.update(cx, |multibuffer, cx| {
18939 multibuffer.set_excerpts_for_path(
18940 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
18941 buffer_1.clone(),
18942 vec![
18943 Point::row_range(0..3),
18944 Point::row_range(1..6),
18945 Point::row_range(12..15),
18946 ],
18947 0,
18948 cx,
18949 );
18950 multibuffer.set_excerpts_for_path(
18951 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
18952 buffer_2.clone(),
18953 vec![Point::row_range(0..6), Point::row_range(8..12)],
18954 0,
18955 cx,
18956 );
18957 });
18958 });
18959
18960 // Apply the update of adding the excerpts.
18961 follower_1
18962 .update_in(cx, |follower, window, cx| {
18963 follower.apply_update_proto(
18964 &project,
18965 update_message.borrow().clone().unwrap(),
18966 window,
18967 cx,
18968 )
18969 })
18970 .await
18971 .unwrap();
18972 assert_eq!(
18973 follower_1.update(cx, |editor, cx| editor.text(cx)),
18974 leader.update(cx, |editor, cx| editor.text(cx))
18975 );
18976 update_message.borrow_mut().take();
18977
18978 // Start following separately after it already has excerpts.
18979 let mut state_message =
18980 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
18981 let workspace_entity = workspace.clone();
18982 let follower_2 = cx
18983 .update_window(*window, |_, window, cx| {
18984 Editor::from_state_proto(
18985 workspace_entity,
18986 ViewId {
18987 creator: CollaboratorId::PeerId(PeerId::default()),
18988 id: 0,
18989 },
18990 &mut state_message,
18991 window,
18992 cx,
18993 )
18994 })
18995 .unwrap()
18996 .unwrap()
18997 .await
18998 .unwrap();
18999 assert_eq!(
19000 follower_2.update(cx, |editor, cx| editor.text(cx)),
19001 leader.update(cx, |editor, cx| editor.text(cx))
19002 );
19003
19004 // Remove some excerpts.
19005 leader.update(cx, |leader, cx| {
19006 leader.buffer.update(cx, |multibuffer, cx| {
19007 multibuffer.remove_excerpts_for_path(
19008 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
19009 cx,
19010 );
19011 });
19012 });
19013
19014 // Apply the update of removing the excerpts.
19015 follower_1
19016 .update_in(cx, |follower, window, cx| {
19017 follower.apply_update_proto(
19018 &project,
19019 update_message.borrow().clone().unwrap(),
19020 window,
19021 cx,
19022 )
19023 })
19024 .await
19025 .unwrap();
19026 follower_2
19027 .update_in(cx, |follower, window, cx| {
19028 follower.apply_update_proto(
19029 &project,
19030 update_message.borrow().clone().unwrap(),
19031 window,
19032 cx,
19033 )
19034 })
19035 .await
19036 .unwrap();
19037 update_message.borrow_mut().take();
19038 assert_eq!(
19039 follower_1.update(cx, |editor, cx| editor.text(cx)),
19040 leader.update(cx, |editor, cx| editor.text(cx))
19041 );
19042}
19043
19044#[gpui::test]
19045async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19046 init_test(cx, |_| {});
19047
19048 let mut cx = EditorTestContext::new(cx).await;
19049 let lsp_store =
19050 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
19051
19052 cx.set_state(indoc! {"
19053 ˇfn func(abc def: i32) -> u32 {
19054 }
19055 "});
19056
19057 cx.update(|_, cx| {
19058 lsp_store.update(cx, |lsp_store, cx| {
19059 lsp_store
19060 .update_diagnostics(
19061 LanguageServerId(0),
19062 lsp::PublishDiagnosticsParams {
19063 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
19064 version: None,
19065 diagnostics: vec![
19066 lsp::Diagnostic {
19067 range: lsp::Range::new(
19068 lsp::Position::new(0, 11),
19069 lsp::Position::new(0, 12),
19070 ),
19071 severity: Some(lsp::DiagnosticSeverity::ERROR),
19072 ..Default::default()
19073 },
19074 lsp::Diagnostic {
19075 range: lsp::Range::new(
19076 lsp::Position::new(0, 12),
19077 lsp::Position::new(0, 15),
19078 ),
19079 severity: Some(lsp::DiagnosticSeverity::ERROR),
19080 ..Default::default()
19081 },
19082 lsp::Diagnostic {
19083 range: lsp::Range::new(
19084 lsp::Position::new(0, 25),
19085 lsp::Position::new(0, 28),
19086 ),
19087 severity: Some(lsp::DiagnosticSeverity::ERROR),
19088 ..Default::default()
19089 },
19090 ],
19091 },
19092 None,
19093 DiagnosticSourceKind::Pushed,
19094 &[],
19095 cx,
19096 )
19097 .unwrap()
19098 });
19099 });
19100
19101 executor.run_until_parked();
19102
19103 cx.update_editor(|editor, window, cx| {
19104 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19105 });
19106
19107 cx.assert_editor_state(indoc! {"
19108 fn func(abc def: i32) -> ˇu32 {
19109 }
19110 "});
19111
19112 cx.update_editor(|editor, window, cx| {
19113 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19114 });
19115
19116 cx.assert_editor_state(indoc! {"
19117 fn func(abc ˇdef: i32) -> u32 {
19118 }
19119 "});
19120
19121 cx.update_editor(|editor, window, cx| {
19122 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19123 });
19124
19125 cx.assert_editor_state(indoc! {"
19126 fn func(abcˇ def: i32) -> u32 {
19127 }
19128 "});
19129
19130 cx.update_editor(|editor, window, cx| {
19131 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19132 });
19133
19134 cx.assert_editor_state(indoc! {"
19135 fn func(abc def: i32) -> ˇu32 {
19136 }
19137 "});
19138}
19139
19140#[gpui::test]
19141async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19142 init_test(cx, |_| {});
19143
19144 let mut cx = EditorTestContext::new(cx).await;
19145
19146 let diff_base = r#"
19147 use some::mod;
19148
19149 const A: u32 = 42;
19150
19151 fn main() {
19152 println!("hello");
19153
19154 println!("world");
19155 }
19156 "#
19157 .unindent();
19158
19159 // Edits are modified, removed, modified, added
19160 cx.set_state(
19161 &r#"
19162 use some::modified;
19163
19164 ˇ
19165 fn main() {
19166 println!("hello there");
19167
19168 println!("around the");
19169 println!("world");
19170 }
19171 "#
19172 .unindent(),
19173 );
19174
19175 cx.set_head_text(&diff_base);
19176 executor.run_until_parked();
19177
19178 cx.update_editor(|editor, window, cx| {
19179 //Wrap around the bottom of the buffer
19180 for _ in 0..3 {
19181 editor.go_to_next_hunk(&GoToHunk, window, cx);
19182 }
19183 });
19184
19185 cx.assert_editor_state(
19186 &r#"
19187 ˇuse some::modified;
19188
19189
19190 fn main() {
19191 println!("hello there");
19192
19193 println!("around the");
19194 println!("world");
19195 }
19196 "#
19197 .unindent(),
19198 );
19199
19200 cx.update_editor(|editor, window, cx| {
19201 //Wrap around the top of the buffer
19202 for _ in 0..2 {
19203 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19204 }
19205 });
19206
19207 cx.assert_editor_state(
19208 &r#"
19209 use some::modified;
19210
19211
19212 fn main() {
19213 ˇ println!("hello there");
19214
19215 println!("around the");
19216 println!("world");
19217 }
19218 "#
19219 .unindent(),
19220 );
19221
19222 cx.update_editor(|editor, window, cx| {
19223 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19224 });
19225
19226 cx.assert_editor_state(
19227 &r#"
19228 use some::modified;
19229
19230 ˇ
19231 fn main() {
19232 println!("hello there");
19233
19234 println!("around the");
19235 println!("world");
19236 }
19237 "#
19238 .unindent(),
19239 );
19240
19241 cx.update_editor(|editor, window, cx| {
19242 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19243 });
19244
19245 cx.assert_editor_state(
19246 &r#"
19247 ˇuse some::modified;
19248
19249
19250 fn main() {
19251 println!("hello there");
19252
19253 println!("around the");
19254 println!("world");
19255 }
19256 "#
19257 .unindent(),
19258 );
19259
19260 cx.update_editor(|editor, window, cx| {
19261 for _ in 0..2 {
19262 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19263 }
19264 });
19265
19266 cx.assert_editor_state(
19267 &r#"
19268 use some::modified;
19269
19270
19271 fn main() {
19272 ˇ println!("hello there");
19273
19274 println!("around the");
19275 println!("world");
19276 }
19277 "#
19278 .unindent(),
19279 );
19280
19281 cx.update_editor(|editor, window, cx| {
19282 editor.fold(&Fold, window, cx);
19283 });
19284
19285 cx.update_editor(|editor, window, cx| {
19286 editor.go_to_next_hunk(&GoToHunk, window, cx);
19287 });
19288
19289 cx.assert_editor_state(
19290 &r#"
19291 ˇuse some::modified;
19292
19293
19294 fn main() {
19295 println!("hello there");
19296
19297 println!("around the");
19298 println!("world");
19299 }
19300 "#
19301 .unindent(),
19302 );
19303}
19304
19305#[test]
19306fn test_split_words() {
19307 fn split(text: &str) -> Vec<&str> {
19308 split_words(text).collect()
19309 }
19310
19311 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
19312 assert_eq!(split("hello_world"), &["hello_", "world"]);
19313 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
19314 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
19315 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
19316 assert_eq!(split("helloworld"), &["helloworld"]);
19317
19318 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
19319}
19320
19321#[test]
19322fn test_split_words_for_snippet_prefix() {
19323 fn split(text: &str) -> Vec<&str> {
19324 snippet_candidate_suffixes(text, &|c| c.is_alphanumeric() || c == '_').collect()
19325 }
19326
19327 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
19328 assert_eq!(split("hello_world"), &["hello_world"]);
19329 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
19330 assert_eq!(split("Hello_World"), &["Hello_World"]);
19331 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
19332 assert_eq!(split("helloworld"), &["helloworld"]);
19333 assert_eq!(
19334 split("this@is!@#$^many . symbols"),
19335 &[
19336 "symbols",
19337 " symbols",
19338 ". symbols",
19339 " . symbols",
19340 " . symbols",
19341 " . symbols",
19342 "many . symbols",
19343 "^many . symbols",
19344 "$^many . symbols",
19345 "#$^many . symbols",
19346 "@#$^many . symbols",
19347 "!@#$^many . symbols",
19348 "is!@#$^many . symbols",
19349 "@is!@#$^many . symbols",
19350 "this@is!@#$^many . symbols",
19351 ],
19352 );
19353 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
19354}
19355
19356#[gpui::test]
19357async fn test_move_to_syntax_node_relative_jumps(tcx: &mut TestAppContext) {
19358 init_test(tcx, |_| {});
19359
19360 let mut cx = EditorLspTestContext::new(
19361 Arc::into_inner(markdown_lang()).unwrap(),
19362 Default::default(),
19363 tcx,
19364 )
19365 .await;
19366
19367 async fn assert(offset: i8, before: &str, after: &str, cx: &mut EditorLspTestContext) {
19368 let _state_context = cx.set_state(before);
19369 cx.run_until_parked();
19370 cx.update_editor(|editor, window, cx| editor.go_to_symbol_by_offset(window, cx, offset))
19371 .await
19372 .unwrap();
19373 cx.run_until_parked();
19374 cx.assert_editor_state(after);
19375 }
19376
19377 const ABOVE: i8 = -1;
19378 const BELOW: i8 = 1;
19379
19380 assert(
19381 ABOVE,
19382 indoc! {"
19383 # Foo
19384
19385 ˇFoo foo foo
19386
19387 # Bar
19388
19389 Bar bar bar
19390 "},
19391 indoc! {"
19392 ˇ# Foo
19393
19394 Foo foo foo
19395
19396 # Bar
19397
19398 Bar bar bar
19399 "},
19400 &mut cx,
19401 )
19402 .await;
19403
19404 assert(
19405 ABOVE,
19406 indoc! {"
19407 ˇ# Foo
19408
19409 Foo foo foo
19410
19411 # Bar
19412
19413 Bar bar bar
19414 "},
19415 indoc! {"
19416 ˇ# Foo
19417
19418 Foo foo foo
19419
19420 # Bar
19421
19422 Bar bar bar
19423 "},
19424 &mut cx,
19425 )
19426 .await;
19427
19428 assert(
19429 BELOW,
19430 indoc! {"
19431 ˇ# Foo
19432
19433 Foo foo foo
19434
19435 # Bar
19436
19437 Bar bar bar
19438 "},
19439 indoc! {"
19440 # Foo
19441
19442 Foo foo foo
19443
19444 ˇ# Bar
19445
19446 Bar bar bar
19447 "},
19448 &mut cx,
19449 )
19450 .await;
19451
19452 assert(
19453 BELOW,
19454 indoc! {"
19455 # Foo
19456
19457 ˇFoo foo foo
19458
19459 # Bar
19460
19461 Bar bar bar
19462 "},
19463 indoc! {"
19464 # Foo
19465
19466 Foo foo foo
19467
19468 ˇ# Bar
19469
19470 Bar bar bar
19471 "},
19472 &mut cx,
19473 )
19474 .await;
19475
19476 assert(
19477 BELOW,
19478 indoc! {"
19479 # Foo
19480
19481 Foo foo foo
19482
19483 ˇ# Bar
19484
19485 Bar bar bar
19486 "},
19487 indoc! {"
19488 # Foo
19489
19490 Foo foo foo
19491
19492 ˇ# Bar
19493
19494 Bar bar bar
19495 "},
19496 &mut cx,
19497 )
19498 .await;
19499
19500 assert(
19501 BELOW,
19502 indoc! {"
19503 # Foo
19504
19505 Foo foo foo
19506
19507 # Bar
19508 ˇ
19509 Bar bar bar
19510 "},
19511 indoc! {"
19512 # Foo
19513
19514 Foo foo foo
19515
19516 # Bar
19517 ˇ
19518 Bar bar bar
19519 "},
19520 &mut cx,
19521 )
19522 .await;
19523}
19524
19525#[gpui::test]
19526async fn test_move_to_syntax_node_relative_dead_zone(tcx: &mut TestAppContext) {
19527 init_test(tcx, |_| {});
19528
19529 let mut cx = EditorLspTestContext::new(
19530 Arc::into_inner(rust_lang()).unwrap(),
19531 Default::default(),
19532 tcx,
19533 )
19534 .await;
19535
19536 async fn assert(offset: i8, before: &str, after: &str, cx: &mut EditorLspTestContext) {
19537 let _state_context = cx.set_state(before);
19538 cx.run_until_parked();
19539 cx.update_editor(|editor, window, cx| editor.go_to_symbol_by_offset(window, cx, offset))
19540 .await
19541 .unwrap();
19542 cx.run_until_parked();
19543 cx.assert_editor_state(after);
19544 }
19545
19546 const ABOVE: i8 = -1;
19547 const BELOW: i8 = 1;
19548
19549 assert(
19550 ABOVE,
19551 indoc! {"
19552 fn foo() {
19553 // foo fn
19554 }
19555
19556 ˇ// this zone is not inside any top level outline node
19557
19558 fn bar() {
19559 // bar fn
19560 let _ = 2;
19561 }
19562 "},
19563 indoc! {"
19564 ˇfn foo() {
19565 // foo fn
19566 }
19567
19568 // this zone is not inside any top level outline node
19569
19570 fn bar() {
19571 // bar fn
19572 let _ = 2;
19573 }
19574 "},
19575 &mut cx,
19576 )
19577 .await;
19578
19579 assert(
19580 BELOW,
19581 indoc! {"
19582 fn foo() {
19583 // foo fn
19584 }
19585
19586 ˇ// this zone is not inside any top level outline node
19587
19588 fn bar() {
19589 // bar fn
19590 let _ = 2;
19591 }
19592 "},
19593 indoc! {"
19594 fn foo() {
19595 // foo fn
19596 }
19597
19598 // this zone is not inside any top level outline node
19599
19600 ˇfn bar() {
19601 // bar fn
19602 let _ = 2;
19603 }
19604 "},
19605 &mut cx,
19606 )
19607 .await;
19608}
19609
19610#[gpui::test]
19611async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
19612 init_test(cx, |_| {});
19613
19614 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
19615
19616 #[track_caller]
19617 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
19618 let _state_context = cx.set_state(before);
19619 cx.run_until_parked();
19620 cx.update_editor(|editor, window, cx| {
19621 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
19622 });
19623 cx.run_until_parked();
19624 cx.assert_editor_state(after);
19625 }
19626
19627 // Outside bracket jumps to outside of matching bracket
19628 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
19629 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
19630
19631 // Inside bracket jumps to inside of matching bracket
19632 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
19633 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
19634
19635 // When outside a bracket and inside, favor jumping to the inside bracket
19636 assert(
19637 "console.log('foo', [1, 2, 3]ˇ);",
19638 "console.log('foo', ˇ[1, 2, 3]);",
19639 &mut cx,
19640 );
19641 assert(
19642 "console.log(ˇ'foo', [1, 2, 3]);",
19643 "console.log('foo'ˇ, [1, 2, 3]);",
19644 &mut cx,
19645 );
19646
19647 // Bias forward if two options are equally likely
19648 assert(
19649 "let result = curried_fun()ˇ();",
19650 "let result = curried_fun()()ˇ;",
19651 &mut cx,
19652 );
19653
19654 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
19655 assert(
19656 indoc! {"
19657 function test() {
19658 console.log('test')ˇ
19659 }"},
19660 indoc! {"
19661 function test() {
19662 console.logˇ('test')
19663 }"},
19664 &mut cx,
19665 );
19666}
19667
19668#[gpui::test]
19669async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
19670 init_test(cx, |_| {});
19671 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
19672 language_registry.add(markdown_lang());
19673 language_registry.add(rust_lang());
19674 let buffer = cx.new(|cx| {
19675 let mut buffer = language::Buffer::local(
19676 indoc! {"
19677 ```rs
19678 impl Worktree {
19679 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
19680 }
19681 }
19682 ```
19683 "},
19684 cx,
19685 );
19686 buffer.set_language_registry(language_registry.clone());
19687 buffer.set_language(Some(markdown_lang()), cx);
19688 buffer
19689 });
19690 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19691 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
19692 cx.executor().run_until_parked();
19693 _ = editor.update(cx, |editor, window, cx| {
19694 // Case 1: Test outer enclosing brackets
19695 select_ranges(
19696 editor,
19697 &indoc! {"
19698 ```rs
19699 impl Worktree {
19700 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
19701 }
19702 }ˇ
19703 ```
19704 "},
19705 window,
19706 cx,
19707 );
19708 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
19709 assert_text_with_selections(
19710 editor,
19711 &indoc! {"
19712 ```rs
19713 impl Worktree ˇ{
19714 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
19715 }
19716 }
19717 ```
19718 "},
19719 cx,
19720 );
19721 // Case 2: Test inner enclosing brackets
19722 select_ranges(
19723 editor,
19724 &indoc! {"
19725 ```rs
19726 impl Worktree {
19727 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
19728 }ˇ
19729 }
19730 ```
19731 "},
19732 window,
19733 cx,
19734 );
19735 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
19736 assert_text_with_selections(
19737 editor,
19738 &indoc! {"
19739 ```rs
19740 impl Worktree {
19741 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
19742 }
19743 }
19744 ```
19745 "},
19746 cx,
19747 );
19748 });
19749}
19750
19751#[gpui::test]
19752async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
19753 init_test(cx, |_| {});
19754
19755 let fs = FakeFs::new(cx.executor());
19756 fs.insert_tree(
19757 path!("/a"),
19758 json!({
19759 "main.rs": "fn main() { let a = 5; }",
19760 "other.rs": "// Test file",
19761 }),
19762 )
19763 .await;
19764 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19765
19766 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19767 language_registry.add(Arc::new(Language::new(
19768 LanguageConfig {
19769 name: "Rust".into(),
19770 matcher: LanguageMatcher {
19771 path_suffixes: vec!["rs".to_string()],
19772 ..Default::default()
19773 },
19774 brackets: BracketPairConfig {
19775 pairs: vec![BracketPair {
19776 start: "{".to_string(),
19777 end: "}".to_string(),
19778 close: true,
19779 surround: true,
19780 newline: true,
19781 }],
19782 disabled_scopes_by_bracket_ix: Vec::new(),
19783 },
19784 ..Default::default()
19785 },
19786 Some(tree_sitter_rust::LANGUAGE.into()),
19787 )));
19788 let mut fake_servers = language_registry.register_fake_lsp(
19789 "Rust",
19790 FakeLspAdapter {
19791 capabilities: lsp::ServerCapabilities {
19792 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
19793 first_trigger_character: "{".to_string(),
19794 more_trigger_character: None,
19795 }),
19796 ..Default::default()
19797 },
19798 ..Default::default()
19799 },
19800 );
19801
19802 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
19803 let workspace = window
19804 .read_with(cx, |mw, _| mw.workspace().clone())
19805 .unwrap();
19806
19807 let cx = &mut VisualTestContext::from_window(*window, cx);
19808
19809 let worktree_id = workspace.update_in(cx, |workspace, _, cx| {
19810 workspace.project().update(cx, |project, cx| {
19811 project.worktrees(cx).next().unwrap().read(cx).id()
19812 })
19813 });
19814
19815 let buffer = project
19816 .update(cx, |project, cx| {
19817 project.open_local_buffer(path!("/a/main.rs"), cx)
19818 })
19819 .await
19820 .unwrap();
19821 let editor_handle = workspace
19822 .update_in(cx, |workspace, window, cx| {
19823 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
19824 })
19825 .await
19826 .unwrap()
19827 .downcast::<Editor>()
19828 .unwrap();
19829
19830 let fake_server = fake_servers.next().await.unwrap();
19831
19832 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
19833 |params, _| async move {
19834 assert_eq!(
19835 params.text_document_position.text_document.uri,
19836 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
19837 );
19838 assert_eq!(
19839 params.text_document_position.position,
19840 lsp::Position::new(0, 21),
19841 );
19842
19843 Ok(Some(vec![lsp::TextEdit {
19844 new_text: "]".to_string(),
19845 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19846 }]))
19847 },
19848 );
19849
19850 editor_handle.update_in(cx, |editor, window, cx| {
19851 window.focus(&editor.focus_handle(cx), cx);
19852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19853 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
19854 });
19855 editor.handle_input("{", window, cx);
19856 });
19857
19858 cx.executor().run_until_parked();
19859
19860 buffer.update(cx, |buffer, _| {
19861 assert_eq!(
19862 buffer.text(),
19863 "fn main() { let a = {5}; }",
19864 "No extra braces from on type formatting should appear in the buffer"
19865 )
19866 });
19867}
19868
19869#[gpui::test(iterations = 20, seeds(31))]
19870async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
19871 init_test(cx, |_| {});
19872
19873 let mut cx = EditorLspTestContext::new_rust(
19874 lsp::ServerCapabilities {
19875 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
19876 first_trigger_character: ".".to_string(),
19877 more_trigger_character: None,
19878 }),
19879 ..Default::default()
19880 },
19881 cx,
19882 )
19883 .await;
19884
19885 cx.update_buffer(|buffer, _| {
19886 // This causes autoindent to be async.
19887 buffer.set_sync_parse_timeout(None)
19888 });
19889
19890 cx.set_state("fn c() {\n d()ˇ\n}\n");
19891 cx.simulate_keystroke("\n");
19892 cx.run_until_parked();
19893
19894 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
19895 let mut request =
19896 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
19897 let buffer_cloned = buffer_cloned.clone();
19898 async move {
19899 buffer_cloned.update(&mut cx, |buffer, _| {
19900 assert_eq!(
19901 buffer.text(),
19902 "fn c() {\n d()\n .\n}\n",
19903 "OnTypeFormatting should triggered after autoindent applied"
19904 )
19905 });
19906
19907 Ok(Some(vec![]))
19908 }
19909 });
19910
19911 cx.simulate_keystroke(".");
19912 cx.run_until_parked();
19913
19914 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
19915 assert!(request.next().await.is_some());
19916 request.close();
19917 assert!(request.next().await.is_none());
19918}
19919
19920#[gpui::test]
19921async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
19922 init_test(cx, |_| {});
19923
19924 let fs = FakeFs::new(cx.executor());
19925 fs.insert_tree(
19926 path!("/a"),
19927 json!({
19928 "main.rs": "fn main() { let a = 5; }",
19929 "other.rs": "// Test file",
19930 }),
19931 )
19932 .await;
19933
19934 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19935
19936 let server_restarts = Arc::new(AtomicUsize::new(0));
19937 let closure_restarts = Arc::clone(&server_restarts);
19938 let language_server_name = "test language server";
19939 let language_name: LanguageName = "Rust".into();
19940
19941 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19942 language_registry.add(Arc::new(Language::new(
19943 LanguageConfig {
19944 name: language_name.clone(),
19945 matcher: LanguageMatcher {
19946 path_suffixes: vec!["rs".to_string()],
19947 ..Default::default()
19948 },
19949 ..Default::default()
19950 },
19951 Some(tree_sitter_rust::LANGUAGE.into()),
19952 )));
19953 let mut fake_servers = language_registry.register_fake_lsp(
19954 "Rust",
19955 FakeLspAdapter {
19956 name: language_server_name,
19957 initialization_options: Some(json!({
19958 "testOptionValue": true
19959 })),
19960 initializer: Some(Box::new(move |fake_server| {
19961 let task_restarts = Arc::clone(&closure_restarts);
19962 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
19963 task_restarts.fetch_add(1, atomic::Ordering::Release);
19964 futures::future::ready(Ok(()))
19965 });
19966 })),
19967 ..Default::default()
19968 },
19969 );
19970
19971 let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
19972 let _buffer = project
19973 .update(cx, |project, cx| {
19974 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
19975 })
19976 .await
19977 .unwrap();
19978 let _fake_server = fake_servers.next().await.unwrap();
19979 update_test_language_settings(cx, &|language_settings| {
19980 language_settings.languages.0.insert(
19981 language_name.clone().0.to_string(),
19982 LanguageSettingsContent {
19983 tab_size: NonZeroU32::new(8),
19984 ..Default::default()
19985 },
19986 );
19987 });
19988 cx.executor().run_until_parked();
19989 assert_eq!(
19990 server_restarts.load(atomic::Ordering::Acquire),
19991 0,
19992 "Should not restart LSP server on an unrelated change"
19993 );
19994
19995 update_test_project_settings(cx, &|project_settings| {
19996 project_settings.lsp.0.insert(
19997 "Some other server name".into(),
19998 LspSettings {
19999 binary: None,
20000 settings: None,
20001 initialization_options: Some(json!({
20002 "some other init value": false
20003 })),
20004 enable_lsp_tasks: false,
20005 fetch: None,
20006 },
20007 );
20008 });
20009 cx.executor().run_until_parked();
20010 assert_eq!(
20011 server_restarts.load(atomic::Ordering::Acquire),
20012 0,
20013 "Should not restart LSP server on an unrelated LSP settings change"
20014 );
20015
20016 update_test_project_settings(cx, &|project_settings| {
20017 project_settings.lsp.0.insert(
20018 language_server_name.into(),
20019 LspSettings {
20020 binary: None,
20021 settings: None,
20022 initialization_options: Some(json!({
20023 "anotherInitValue": false
20024 })),
20025 enable_lsp_tasks: false,
20026 fetch: None,
20027 },
20028 );
20029 });
20030 cx.executor().run_until_parked();
20031 assert_eq!(
20032 server_restarts.load(atomic::Ordering::Acquire),
20033 1,
20034 "Should restart LSP server on a related LSP settings change"
20035 );
20036
20037 update_test_project_settings(cx, &|project_settings| {
20038 project_settings.lsp.0.insert(
20039 language_server_name.into(),
20040 LspSettings {
20041 binary: None,
20042 settings: None,
20043 initialization_options: Some(json!({
20044 "anotherInitValue": false
20045 })),
20046 enable_lsp_tasks: false,
20047 fetch: None,
20048 },
20049 );
20050 });
20051 cx.executor().run_until_parked();
20052 assert_eq!(
20053 server_restarts.load(atomic::Ordering::Acquire),
20054 1,
20055 "Should not restart LSP server on a related LSP settings change that is the same"
20056 );
20057
20058 update_test_project_settings(cx, &|project_settings| {
20059 project_settings.lsp.0.insert(
20060 language_server_name.into(),
20061 LspSettings {
20062 binary: None,
20063 settings: None,
20064 initialization_options: None,
20065 enable_lsp_tasks: false,
20066 fetch: None,
20067 },
20068 );
20069 });
20070 cx.executor().run_until_parked();
20071 assert_eq!(
20072 server_restarts.load(atomic::Ordering::Acquire),
20073 2,
20074 "Should restart LSP server on another related LSP settings change"
20075 );
20076}
20077
20078#[gpui::test]
20079async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
20080 init_test(cx, |_| {});
20081
20082 let mut cx = EditorLspTestContext::new_rust(
20083 lsp::ServerCapabilities {
20084 completion_provider: Some(lsp::CompletionOptions {
20085 trigger_characters: Some(vec![".".to_string()]),
20086 resolve_provider: Some(true),
20087 ..Default::default()
20088 }),
20089 ..Default::default()
20090 },
20091 cx,
20092 )
20093 .await;
20094
20095 cx.set_state("fn main() { let a = 2ˇ; }");
20096 cx.simulate_keystroke(".");
20097 let completion_item = lsp::CompletionItem {
20098 label: "some".into(),
20099 kind: Some(lsp::CompletionItemKind::SNIPPET),
20100 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
20101 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
20102 kind: lsp::MarkupKind::Markdown,
20103 value: "```rust\nSome(2)\n```".to_string(),
20104 })),
20105 deprecated: Some(false),
20106 sort_text: Some("fffffff2".to_string()),
20107 filter_text: Some("some".to_string()),
20108 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
20109 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20110 range: lsp::Range {
20111 start: lsp::Position {
20112 line: 0,
20113 character: 22,
20114 },
20115 end: lsp::Position {
20116 line: 0,
20117 character: 22,
20118 },
20119 },
20120 new_text: "Some(2)".to_string(),
20121 })),
20122 additional_text_edits: Some(vec![lsp::TextEdit {
20123 range: lsp::Range {
20124 start: lsp::Position {
20125 line: 0,
20126 character: 20,
20127 },
20128 end: lsp::Position {
20129 line: 0,
20130 character: 22,
20131 },
20132 },
20133 new_text: "".to_string(),
20134 }]),
20135 ..Default::default()
20136 };
20137
20138 let closure_completion_item = completion_item.clone();
20139 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
20140 let task_completion_item = closure_completion_item.clone();
20141 async move {
20142 Ok(Some(lsp::CompletionResponse::Array(vec![
20143 task_completion_item,
20144 ])))
20145 }
20146 });
20147
20148 request.next().await;
20149
20150 cx.condition(|editor, _| editor.context_menu_visible())
20151 .await;
20152 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
20153 editor
20154 .confirm_completion(&ConfirmCompletion::default(), window, cx)
20155 .unwrap()
20156 });
20157 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
20158
20159 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20160 let task_completion_item = completion_item.clone();
20161 async move { Ok(task_completion_item) }
20162 })
20163 .next()
20164 .await
20165 .unwrap();
20166 apply_additional_edits.await.unwrap();
20167 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
20168}
20169
20170#[gpui::test]
20171async fn test_completions_with_additional_edits_and_multiple_cursors(cx: &mut TestAppContext) {
20172 init_test(cx, |_| {});
20173
20174 let mut cx = EditorLspTestContext::new_typescript(
20175 lsp::ServerCapabilities {
20176 completion_provider: Some(lsp::CompletionOptions {
20177 resolve_provider: Some(true),
20178 ..Default::default()
20179 }),
20180 ..Default::default()
20181 },
20182 cx,
20183 )
20184 .await;
20185
20186 cx.set_state(
20187 "import { «Fooˇ» } from './types';\n\nclass Bar {\n method(): «Fooˇ» { return new Foo(); }\n}",
20188 );
20189
20190 cx.simulate_keystroke("F");
20191 cx.simulate_keystroke("o");
20192
20193 let completion_item = lsp::CompletionItem {
20194 label: "FooBar".into(),
20195 kind: Some(lsp::CompletionItemKind::CLASS),
20196 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20197 range: lsp::Range {
20198 start: lsp::Position {
20199 line: 3,
20200 character: 14,
20201 },
20202 end: lsp::Position {
20203 line: 3,
20204 character: 16,
20205 },
20206 },
20207 new_text: "FooBar".to_string(),
20208 })),
20209 additional_text_edits: Some(vec![lsp::TextEdit {
20210 range: lsp::Range {
20211 start: lsp::Position {
20212 line: 0,
20213 character: 9,
20214 },
20215 end: lsp::Position {
20216 line: 0,
20217 character: 11,
20218 },
20219 },
20220 new_text: "FooBar".to_string(),
20221 }]),
20222 ..Default::default()
20223 };
20224
20225 let closure_completion_item = completion_item.clone();
20226 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
20227 let task_completion_item = closure_completion_item.clone();
20228 async move {
20229 Ok(Some(lsp::CompletionResponse::Array(vec![
20230 task_completion_item,
20231 ])))
20232 }
20233 });
20234
20235 request.next().await;
20236
20237 cx.condition(|editor, _| editor.context_menu_visible())
20238 .await;
20239 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
20240 editor
20241 .confirm_completion(&ConfirmCompletion::default(), window, cx)
20242 .unwrap()
20243 });
20244
20245 cx.assert_editor_state(
20246 "import { FooBarˇ } from './types';\n\nclass Bar {\n method(): FooBarˇ { return new Foo(); }\n}",
20247 );
20248
20249 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20250 let task_completion_item = completion_item.clone();
20251 async move { Ok(task_completion_item) }
20252 })
20253 .next()
20254 .await
20255 .unwrap();
20256
20257 apply_additional_edits.await.unwrap();
20258
20259 cx.assert_editor_state(
20260 "import { FooBarˇ } from './types';\n\nclass Bar {\n method(): FooBarˇ { return new Foo(); }\n}",
20261 );
20262}
20263
20264#[gpui::test]
20265async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
20266 init_test(cx, |_| {});
20267
20268 let mut cx = EditorLspTestContext::new_rust(
20269 lsp::ServerCapabilities {
20270 completion_provider: Some(lsp::CompletionOptions {
20271 trigger_characters: Some(vec![".".to_string()]),
20272 resolve_provider: Some(true),
20273 ..Default::default()
20274 }),
20275 ..Default::default()
20276 },
20277 cx,
20278 )
20279 .await;
20280
20281 cx.set_state("fn main() { let a = 2ˇ; }");
20282 cx.simulate_keystroke(".");
20283
20284 let item1 = lsp::CompletionItem {
20285 label: "method id()".to_string(),
20286 filter_text: Some("id".to_string()),
20287 detail: None,
20288 documentation: None,
20289 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20290 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
20291 new_text: ".id".to_string(),
20292 })),
20293 ..lsp::CompletionItem::default()
20294 };
20295
20296 let item2 = lsp::CompletionItem {
20297 label: "other".to_string(),
20298 filter_text: Some("other".to_string()),
20299 detail: None,
20300 documentation: None,
20301 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20302 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
20303 new_text: ".other".to_string(),
20304 })),
20305 ..lsp::CompletionItem::default()
20306 };
20307
20308 let item1 = item1.clone();
20309 cx.set_request_handler::<lsp::request::Completion, _, _>({
20310 let item1 = item1.clone();
20311 move |_, _, _| {
20312 let item1 = item1.clone();
20313 let item2 = item2.clone();
20314 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
20315 }
20316 })
20317 .next()
20318 .await;
20319
20320 cx.condition(|editor, _| editor.context_menu_visible())
20321 .await;
20322 cx.update_editor(|editor, _, _| {
20323 let context_menu = editor.context_menu.borrow_mut();
20324 let context_menu = context_menu
20325 .as_ref()
20326 .expect("Should have the context menu deployed");
20327 match context_menu {
20328 CodeContextMenu::Completions(completions_menu) => {
20329 let completions = completions_menu.completions.borrow_mut();
20330 assert_eq!(
20331 completions
20332 .iter()
20333 .map(|completion| &completion.label.text)
20334 .collect::<Vec<_>>(),
20335 vec!["method id()", "other"]
20336 )
20337 }
20338 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
20339 }
20340 });
20341
20342 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
20343 let item1 = item1.clone();
20344 move |_, item_to_resolve, _| {
20345 let item1 = item1.clone();
20346 async move {
20347 if item1 == item_to_resolve {
20348 Ok(lsp::CompletionItem {
20349 label: "method id()".to_string(),
20350 filter_text: Some("id".to_string()),
20351 detail: Some("Now resolved!".to_string()),
20352 documentation: Some(lsp::Documentation::String("Docs".to_string())),
20353 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20354 range: lsp::Range::new(
20355 lsp::Position::new(0, 22),
20356 lsp::Position::new(0, 22),
20357 ),
20358 new_text: ".id".to_string(),
20359 })),
20360 ..lsp::CompletionItem::default()
20361 })
20362 } else {
20363 Ok(item_to_resolve)
20364 }
20365 }
20366 }
20367 })
20368 .next()
20369 .await
20370 .unwrap();
20371 cx.run_until_parked();
20372
20373 cx.update_editor(|editor, window, cx| {
20374 editor.context_menu_next(&Default::default(), window, cx);
20375 });
20376 cx.run_until_parked();
20377
20378 cx.update_editor(|editor, _, _| {
20379 let context_menu = editor.context_menu.borrow_mut();
20380 let context_menu = context_menu
20381 .as_ref()
20382 .expect("Should have the context menu deployed");
20383 match context_menu {
20384 CodeContextMenu::Completions(completions_menu) => {
20385 let completions = completions_menu.completions.borrow_mut();
20386 assert_eq!(
20387 completions
20388 .iter()
20389 .map(|completion| &completion.label.text)
20390 .collect::<Vec<_>>(),
20391 vec!["method id() Now resolved!", "other"],
20392 "Should update first completion label, but not second as the filter text did not match."
20393 );
20394 }
20395 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
20396 }
20397 });
20398}
20399
20400#[gpui::test]
20401async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
20402 init_test(cx, |_| {});
20403 let mut cx = EditorLspTestContext::new_rust(
20404 lsp::ServerCapabilities {
20405 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
20406 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
20407 completion_provider: Some(lsp::CompletionOptions {
20408 resolve_provider: Some(true),
20409 ..Default::default()
20410 }),
20411 ..Default::default()
20412 },
20413 cx,
20414 )
20415 .await;
20416 cx.set_state(indoc! {"
20417 struct TestStruct {
20418 field: i32
20419 }
20420
20421 fn mainˇ() {
20422 let unused_var = 42;
20423 let test_struct = TestStruct { field: 42 };
20424 }
20425 "});
20426 let symbol_range = cx.lsp_range(indoc! {"
20427 struct TestStruct {
20428 field: i32
20429 }
20430
20431 «fn main»() {
20432 let unused_var = 42;
20433 let test_struct = TestStruct { field: 42 };
20434 }
20435 "});
20436 let mut hover_requests =
20437 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
20438 Ok(Some(lsp::Hover {
20439 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
20440 kind: lsp::MarkupKind::Markdown,
20441 value: "Function documentation".to_string(),
20442 }),
20443 range: Some(symbol_range),
20444 }))
20445 });
20446
20447 // Case 1: Test that code action menu hide hover popover
20448 cx.dispatch_action(Hover);
20449 hover_requests.next().await;
20450 cx.condition(|editor, _| editor.hover_state.visible()).await;
20451 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
20452 move |_, _, _| async move {
20453 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
20454 lsp::CodeAction {
20455 title: "Remove unused variable".to_string(),
20456 kind: Some(CodeActionKind::QUICKFIX),
20457 edit: Some(lsp::WorkspaceEdit {
20458 changes: Some(
20459 [(
20460 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
20461 vec![lsp::TextEdit {
20462 range: lsp::Range::new(
20463 lsp::Position::new(5, 4),
20464 lsp::Position::new(5, 27),
20465 ),
20466 new_text: "".to_string(),
20467 }],
20468 )]
20469 .into_iter()
20470 .collect(),
20471 ),
20472 ..Default::default()
20473 }),
20474 ..Default::default()
20475 },
20476 )]))
20477 },
20478 );
20479 cx.update_editor(|editor, window, cx| {
20480 editor.toggle_code_actions(
20481 &ToggleCodeActions {
20482 deployed_from: None,
20483 quick_launch: false,
20484 },
20485 window,
20486 cx,
20487 );
20488 });
20489 code_action_requests.next().await;
20490 cx.run_until_parked();
20491 cx.condition(|editor, _| editor.context_menu_visible())
20492 .await;
20493 cx.update_editor(|editor, _, _| {
20494 assert!(
20495 !editor.hover_state.visible(),
20496 "Hover popover should be hidden when code action menu is shown"
20497 );
20498 // Hide code actions
20499 editor.context_menu.take();
20500 });
20501
20502 // Case 2: Test that code completions hide hover popover
20503 cx.dispatch_action(Hover);
20504 hover_requests.next().await;
20505 cx.condition(|editor, _| editor.hover_state.visible()).await;
20506 let counter = Arc::new(AtomicUsize::new(0));
20507 let mut completion_requests =
20508 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
20509 let counter = counter.clone();
20510 async move {
20511 counter.fetch_add(1, atomic::Ordering::Release);
20512 Ok(Some(lsp::CompletionResponse::Array(vec![
20513 lsp::CompletionItem {
20514 label: "main".into(),
20515 kind: Some(lsp::CompletionItemKind::FUNCTION),
20516 detail: Some("() -> ()".to_string()),
20517 ..Default::default()
20518 },
20519 lsp::CompletionItem {
20520 label: "TestStruct".into(),
20521 kind: Some(lsp::CompletionItemKind::STRUCT),
20522 detail: Some("struct TestStruct".to_string()),
20523 ..Default::default()
20524 },
20525 ])))
20526 }
20527 });
20528 cx.update_editor(|editor, window, cx| {
20529 editor.show_completions(&ShowCompletions, window, cx);
20530 });
20531 completion_requests.next().await;
20532 cx.condition(|editor, _| editor.context_menu_visible())
20533 .await;
20534 cx.update_editor(|editor, _, _| {
20535 assert!(
20536 !editor.hover_state.visible(),
20537 "Hover popover should be hidden when completion menu is shown"
20538 );
20539 });
20540}
20541
20542#[gpui::test]
20543async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
20544 init_test(cx, |_| {});
20545
20546 let mut cx = EditorLspTestContext::new_rust(
20547 lsp::ServerCapabilities {
20548 completion_provider: Some(lsp::CompletionOptions {
20549 trigger_characters: Some(vec![".".to_string()]),
20550 resolve_provider: Some(true),
20551 ..Default::default()
20552 }),
20553 ..Default::default()
20554 },
20555 cx,
20556 )
20557 .await;
20558
20559 cx.set_state("fn main() { let a = 2ˇ; }");
20560 cx.simulate_keystroke(".");
20561
20562 let unresolved_item_1 = lsp::CompletionItem {
20563 label: "id".to_string(),
20564 filter_text: Some("id".to_string()),
20565 detail: None,
20566 documentation: None,
20567 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20568 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
20569 new_text: ".id".to_string(),
20570 })),
20571 ..lsp::CompletionItem::default()
20572 };
20573 let resolved_item_1 = lsp::CompletionItem {
20574 additional_text_edits: Some(vec![lsp::TextEdit {
20575 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
20576 new_text: "!!".to_string(),
20577 }]),
20578 ..unresolved_item_1.clone()
20579 };
20580 let unresolved_item_2 = lsp::CompletionItem {
20581 label: "other".to_string(),
20582 filter_text: Some("other".to_string()),
20583 detail: None,
20584 documentation: None,
20585 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20586 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
20587 new_text: ".other".to_string(),
20588 })),
20589 ..lsp::CompletionItem::default()
20590 };
20591 let resolved_item_2 = lsp::CompletionItem {
20592 additional_text_edits: Some(vec![lsp::TextEdit {
20593 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
20594 new_text: "??".to_string(),
20595 }]),
20596 ..unresolved_item_2.clone()
20597 };
20598
20599 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
20600 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
20601 cx.lsp
20602 .server
20603 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
20604 let unresolved_item_1 = unresolved_item_1.clone();
20605 let resolved_item_1 = resolved_item_1.clone();
20606 let unresolved_item_2 = unresolved_item_2.clone();
20607 let resolved_item_2 = resolved_item_2.clone();
20608 let resolve_requests_1 = resolve_requests_1.clone();
20609 let resolve_requests_2 = resolve_requests_2.clone();
20610 move |unresolved_request, _| {
20611 let unresolved_item_1 = unresolved_item_1.clone();
20612 let resolved_item_1 = resolved_item_1.clone();
20613 let unresolved_item_2 = unresolved_item_2.clone();
20614 let resolved_item_2 = resolved_item_2.clone();
20615 let resolve_requests_1 = resolve_requests_1.clone();
20616 let resolve_requests_2 = resolve_requests_2.clone();
20617 async move {
20618 if unresolved_request == unresolved_item_1 {
20619 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
20620 Ok(resolved_item_1.clone())
20621 } else if unresolved_request == unresolved_item_2 {
20622 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
20623 Ok(resolved_item_2.clone())
20624 } else {
20625 panic!("Unexpected completion item {unresolved_request:?}")
20626 }
20627 }
20628 }
20629 })
20630 .detach();
20631
20632 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
20633 let unresolved_item_1 = unresolved_item_1.clone();
20634 let unresolved_item_2 = unresolved_item_2.clone();
20635 async move {
20636 Ok(Some(lsp::CompletionResponse::Array(vec![
20637 unresolved_item_1,
20638 unresolved_item_2,
20639 ])))
20640 }
20641 })
20642 .next()
20643 .await;
20644
20645 cx.condition(|editor, _| editor.context_menu_visible())
20646 .await;
20647 cx.update_editor(|editor, _, _| {
20648 let context_menu = editor.context_menu.borrow_mut();
20649 let context_menu = context_menu
20650 .as_ref()
20651 .expect("Should have the context menu deployed");
20652 match context_menu {
20653 CodeContextMenu::Completions(completions_menu) => {
20654 let completions = completions_menu.completions.borrow_mut();
20655 assert_eq!(
20656 completions
20657 .iter()
20658 .map(|completion| &completion.label.text)
20659 .collect::<Vec<_>>(),
20660 vec!["id", "other"]
20661 )
20662 }
20663 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
20664 }
20665 });
20666 cx.run_until_parked();
20667
20668 cx.update_editor(|editor, window, cx| {
20669 editor.context_menu_next(&ContextMenuNext, window, cx);
20670 });
20671 cx.run_until_parked();
20672 cx.update_editor(|editor, window, cx| {
20673 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
20674 });
20675 cx.run_until_parked();
20676 cx.update_editor(|editor, window, cx| {
20677 editor.context_menu_next(&ContextMenuNext, window, cx);
20678 });
20679 cx.run_until_parked();
20680 cx.update_editor(|editor, window, cx| {
20681 editor
20682 .compose_completion(&ComposeCompletion::default(), window, cx)
20683 .expect("No task returned")
20684 })
20685 .await
20686 .expect("Completion failed");
20687 cx.run_until_parked();
20688
20689 cx.update_editor(|editor, _, cx| {
20690 assert_eq!(
20691 resolve_requests_1.load(atomic::Ordering::Acquire),
20692 1,
20693 "Should always resolve once despite multiple selections"
20694 );
20695 assert_eq!(
20696 resolve_requests_2.load(atomic::Ordering::Acquire),
20697 1,
20698 "Should always resolve once after multiple selections and applying the completion"
20699 );
20700 assert_eq!(
20701 editor.text(cx),
20702 "fn main() { let a = ??.other; }",
20703 "Should use resolved data when applying the completion"
20704 );
20705 });
20706}
20707
20708#[gpui::test]
20709async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
20710 init_test(cx, |_| {});
20711
20712 let item_0 = lsp::CompletionItem {
20713 label: "abs".into(),
20714 insert_text: Some("abs".into()),
20715 data: Some(json!({ "very": "special"})),
20716 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
20717 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20718 lsp::InsertReplaceEdit {
20719 new_text: "abs".to_string(),
20720 insert: lsp::Range::default(),
20721 replace: lsp::Range::default(),
20722 },
20723 )),
20724 ..lsp::CompletionItem::default()
20725 };
20726 let items = iter::once(item_0.clone())
20727 .chain((11..51).map(|i| lsp::CompletionItem {
20728 label: format!("item_{}", i),
20729 insert_text: Some(format!("item_{}", i)),
20730 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
20731 ..lsp::CompletionItem::default()
20732 }))
20733 .collect::<Vec<_>>();
20734
20735 let default_commit_characters = vec!["?".to_string()];
20736 let default_data = json!({ "default": "data"});
20737 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
20738 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
20739 let default_edit_range = lsp::Range {
20740 start: lsp::Position {
20741 line: 0,
20742 character: 5,
20743 },
20744 end: lsp::Position {
20745 line: 0,
20746 character: 5,
20747 },
20748 };
20749
20750 let mut cx = EditorLspTestContext::new_rust(
20751 lsp::ServerCapabilities {
20752 completion_provider: Some(lsp::CompletionOptions {
20753 trigger_characters: Some(vec![".".to_string()]),
20754 resolve_provider: Some(true),
20755 ..Default::default()
20756 }),
20757 ..Default::default()
20758 },
20759 cx,
20760 )
20761 .await;
20762
20763 cx.set_state("fn main() { let a = 2ˇ; }");
20764 cx.simulate_keystroke(".");
20765
20766 let completion_data = default_data.clone();
20767 let completion_characters = default_commit_characters.clone();
20768 let completion_items = items.clone();
20769 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
20770 let default_data = completion_data.clone();
20771 let default_commit_characters = completion_characters.clone();
20772 let items = completion_items.clone();
20773 async move {
20774 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
20775 items,
20776 item_defaults: Some(lsp::CompletionListItemDefaults {
20777 data: Some(default_data.clone()),
20778 commit_characters: Some(default_commit_characters.clone()),
20779 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
20780 default_edit_range,
20781 )),
20782 insert_text_format: Some(default_insert_text_format),
20783 insert_text_mode: Some(default_insert_text_mode),
20784 }),
20785 ..lsp::CompletionList::default()
20786 })))
20787 }
20788 })
20789 .next()
20790 .await;
20791
20792 let resolved_items = Arc::new(Mutex::new(Vec::new()));
20793 cx.lsp
20794 .server
20795 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
20796 let closure_resolved_items = resolved_items.clone();
20797 move |item_to_resolve, _| {
20798 let closure_resolved_items = closure_resolved_items.clone();
20799 async move {
20800 closure_resolved_items.lock().push(item_to_resolve.clone());
20801 Ok(item_to_resolve)
20802 }
20803 }
20804 })
20805 .detach();
20806
20807 cx.condition(|editor, _| editor.context_menu_visible())
20808 .await;
20809 cx.run_until_parked();
20810 cx.update_editor(|editor, _, _| {
20811 let menu = editor.context_menu.borrow_mut();
20812 match menu.as_ref().expect("should have the completions menu") {
20813 CodeContextMenu::Completions(completions_menu) => {
20814 assert_eq!(
20815 completions_menu
20816 .entries
20817 .borrow()
20818 .iter()
20819 .map(|mat| mat.string.clone())
20820 .collect::<Vec<String>>(),
20821 items
20822 .iter()
20823 .map(|completion| completion.label.clone())
20824 .collect::<Vec<String>>()
20825 );
20826 }
20827 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
20828 }
20829 });
20830 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
20831 // with 4 from the end.
20832 assert_eq!(
20833 *resolved_items.lock(),
20834 [&items[0..16], &items[items.len() - 4..items.len()]]
20835 .concat()
20836 .iter()
20837 .cloned()
20838 .map(|mut item| {
20839 if item.data.is_none() {
20840 item.data = Some(default_data.clone());
20841 }
20842 item
20843 })
20844 .collect::<Vec<lsp::CompletionItem>>(),
20845 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
20846 );
20847 resolved_items.lock().clear();
20848
20849 cx.update_editor(|editor, window, cx| {
20850 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
20851 });
20852 cx.run_until_parked();
20853 // Completions that have already been resolved are skipped.
20854 assert_eq!(
20855 *resolved_items.lock(),
20856 items[items.len() - 17..items.len() - 4]
20857 .iter()
20858 .cloned()
20859 .map(|mut item| {
20860 if item.data.is_none() {
20861 item.data = Some(default_data.clone());
20862 }
20863 item
20864 })
20865 .collect::<Vec<lsp::CompletionItem>>()
20866 );
20867 resolved_items.lock().clear();
20868}
20869
20870#[gpui::test]
20871async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
20872 init_test(cx, |_| {});
20873
20874 let mut cx = EditorLspTestContext::new(
20875 Language::new(
20876 LanguageConfig {
20877 matcher: LanguageMatcher {
20878 path_suffixes: vec!["jsx".into()],
20879 ..Default::default()
20880 },
20881 overrides: [(
20882 "element".into(),
20883 LanguageConfigOverride {
20884 completion_query_characters: Override::Set(['-'].into_iter().collect()),
20885 ..Default::default()
20886 },
20887 )]
20888 .into_iter()
20889 .collect(),
20890 ..Default::default()
20891 },
20892 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
20893 )
20894 .with_override_query("(jsx_self_closing_element) @element")
20895 .unwrap(),
20896 lsp::ServerCapabilities {
20897 completion_provider: Some(lsp::CompletionOptions {
20898 trigger_characters: Some(vec![":".to_string()]),
20899 ..Default::default()
20900 }),
20901 ..Default::default()
20902 },
20903 cx,
20904 )
20905 .await;
20906
20907 cx.lsp
20908 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20909 Ok(Some(lsp::CompletionResponse::Array(vec![
20910 lsp::CompletionItem {
20911 label: "bg-blue".into(),
20912 ..Default::default()
20913 },
20914 lsp::CompletionItem {
20915 label: "bg-red".into(),
20916 ..Default::default()
20917 },
20918 lsp::CompletionItem {
20919 label: "bg-yellow".into(),
20920 ..Default::default()
20921 },
20922 ])))
20923 });
20924
20925 cx.set_state(r#"<p class="bgˇ" />"#);
20926
20927 // Trigger completion when typing a dash, because the dash is an extra
20928 // word character in the 'element' scope, which contains the cursor.
20929 cx.simulate_keystroke("-");
20930 cx.executor().run_until_parked();
20931 cx.update_editor(|editor, _, _| {
20932 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
20933 {
20934 assert_eq!(
20935 completion_menu_entries(menu),
20936 &["bg-blue", "bg-red", "bg-yellow"]
20937 );
20938 } else {
20939 panic!("expected completion menu to be open");
20940 }
20941 });
20942
20943 cx.simulate_keystroke("l");
20944 cx.executor().run_until_parked();
20945 cx.update_editor(|editor, _, _| {
20946 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
20947 {
20948 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
20949 } else {
20950 panic!("expected completion menu to be open");
20951 }
20952 });
20953
20954 // When filtering completions, consider the character after the '-' to
20955 // be the start of a subword.
20956 cx.set_state(r#"<p class="yelˇ" />"#);
20957 cx.simulate_keystroke("l");
20958 cx.executor().run_until_parked();
20959 cx.update_editor(|editor, _, _| {
20960 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
20961 {
20962 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
20963 } else {
20964 panic!("expected completion menu to be open");
20965 }
20966 });
20967}
20968
20969fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
20970 let entries = menu.entries.borrow();
20971 entries.iter().map(|mat| mat.string.clone()).collect()
20972}
20973
20974#[gpui::test]
20975async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
20976 init_test(cx, |settings| {
20977 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
20978 });
20979
20980 let fs = FakeFs::new(cx.executor());
20981 fs.insert_file(path!("/file.ts"), Default::default()).await;
20982
20983 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
20984 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20985
20986 language_registry.add(Arc::new(Language::new(
20987 LanguageConfig {
20988 name: "TypeScript".into(),
20989 matcher: LanguageMatcher {
20990 path_suffixes: vec!["ts".to_string()],
20991 ..Default::default()
20992 },
20993 ..Default::default()
20994 },
20995 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20996 )));
20997 update_test_language_settings(cx, &|settings| {
20998 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
20999 });
21000
21001 let test_plugin = "test_plugin";
21002 let _ = language_registry.register_fake_lsp(
21003 "TypeScript",
21004 FakeLspAdapter {
21005 prettier_plugins: vec![test_plugin],
21006 ..Default::default()
21007 },
21008 );
21009
21010 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
21011 let buffer = project
21012 .update(cx, |project, cx| {
21013 project.open_local_buffer(path!("/file.ts"), cx)
21014 })
21015 .await
21016 .unwrap();
21017
21018 let buffer_text = "one\ntwo\nthree\n";
21019 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
21020 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
21021 editor.update_in(cx, |editor, window, cx| {
21022 editor.set_text(buffer_text, window, cx)
21023 });
21024
21025 editor
21026 .update_in(cx, |editor, window, cx| {
21027 editor.perform_format(
21028 project.clone(),
21029 FormatTrigger::Manual,
21030 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
21031 window,
21032 cx,
21033 )
21034 })
21035 .unwrap()
21036 .await;
21037 assert_eq!(
21038 editor.update(cx, |editor, cx| editor.text(cx)),
21039 buffer_text.to_string() + prettier_format_suffix,
21040 "Test prettier formatting was not applied to the original buffer text",
21041 );
21042
21043 update_test_language_settings(cx, &|settings| {
21044 settings.defaults.formatter = Some(FormatterList::default())
21045 });
21046 let format = editor.update_in(cx, |editor, window, cx| {
21047 editor.perform_format(
21048 project.clone(),
21049 FormatTrigger::Manual,
21050 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
21051 window,
21052 cx,
21053 )
21054 });
21055 format.await.unwrap();
21056 assert_eq!(
21057 editor.update(cx, |editor, cx| editor.text(cx)),
21058 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
21059 "Autoformatting (via test prettier) was not applied to the original buffer text",
21060 );
21061}
21062
21063#[gpui::test]
21064async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
21065 init_test(cx, |settings| {
21066 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
21067 });
21068
21069 let fs = FakeFs::new(cx.executor());
21070 fs.insert_file(path!("/file.settings"), Default::default())
21071 .await;
21072
21073 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
21074 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21075
21076 let ts_lang = Arc::new(Language::new(
21077 LanguageConfig {
21078 name: "TypeScript".into(),
21079 matcher: LanguageMatcher {
21080 path_suffixes: vec!["ts".to_string()],
21081 ..LanguageMatcher::default()
21082 },
21083 prettier_parser_name: Some("typescript".to_string()),
21084 ..LanguageConfig::default()
21085 },
21086 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21087 ));
21088
21089 language_registry.add(ts_lang.clone());
21090
21091 update_test_language_settings(cx, &|settings| {
21092 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
21093 });
21094
21095 let test_plugin = "test_plugin";
21096 let _ = language_registry.register_fake_lsp(
21097 "TypeScript",
21098 FakeLspAdapter {
21099 prettier_plugins: vec![test_plugin],
21100 ..Default::default()
21101 },
21102 );
21103
21104 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
21105 let buffer = project
21106 .update(cx, |project, cx| {
21107 project.open_local_buffer(path!("/file.settings"), cx)
21108 })
21109 .await
21110 .unwrap();
21111
21112 project.update(cx, |project, cx| {
21113 project.set_language_for_buffer(&buffer, ts_lang, cx)
21114 });
21115
21116 let buffer_text = "one\ntwo\nthree\n";
21117 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
21118 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
21119 editor.update_in(cx, |editor, window, cx| {
21120 editor.set_text(buffer_text, window, cx)
21121 });
21122
21123 editor
21124 .update_in(cx, |editor, window, cx| {
21125 editor.perform_format(
21126 project.clone(),
21127 FormatTrigger::Manual,
21128 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
21129 window,
21130 cx,
21131 )
21132 })
21133 .unwrap()
21134 .await;
21135 assert_eq!(
21136 editor.update(cx, |editor, cx| editor.text(cx)),
21137 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
21138 "Test prettier formatting was not applied to the original buffer text",
21139 );
21140
21141 update_test_language_settings(cx, &|settings| {
21142 settings.defaults.formatter = Some(FormatterList::default())
21143 });
21144 let format = editor.update_in(cx, |editor, window, cx| {
21145 editor.perform_format(
21146 project.clone(),
21147 FormatTrigger::Manual,
21148 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
21149 window,
21150 cx,
21151 )
21152 });
21153 format.await.unwrap();
21154
21155 assert_eq!(
21156 editor.update(cx, |editor, cx| editor.text(cx)),
21157 buffer_text.to_string()
21158 + prettier_format_suffix
21159 + "\ntypescript\n"
21160 + prettier_format_suffix
21161 + "\ntypescript",
21162 "Autoformatting (via test prettier) was not applied to the original buffer text",
21163 );
21164}
21165
21166#[gpui::test]
21167async fn test_addition_reverts(cx: &mut TestAppContext) {
21168 init_test(cx, |_| {});
21169 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
21170 let base_text = indoc! {r#"
21171 struct Row;
21172 struct Row1;
21173 struct Row2;
21174
21175 struct Row4;
21176 struct Row5;
21177 struct Row6;
21178
21179 struct Row8;
21180 struct Row9;
21181 struct Row10;"#};
21182
21183 // When addition hunks are not adjacent to carets, no hunk revert is performed
21184 assert_hunk_revert(
21185 indoc! {r#"struct Row;
21186 struct Row1;
21187 struct Row1.1;
21188 struct Row1.2;
21189 struct Row2;ˇ
21190
21191 struct Row4;
21192 struct Row5;
21193 struct Row6;
21194
21195 struct Row8;
21196 ˇstruct Row9;
21197 struct Row9.1;
21198 struct Row9.2;
21199 struct Row9.3;
21200 struct Row10;"#},
21201 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
21202 indoc! {r#"struct Row;
21203 struct Row1;
21204 struct Row1.1;
21205 struct Row1.2;
21206 struct Row2;ˇ
21207
21208 struct Row4;
21209 struct Row5;
21210 struct Row6;
21211
21212 struct Row8;
21213 ˇstruct Row9;
21214 struct Row9.1;
21215 struct Row9.2;
21216 struct Row9.3;
21217 struct Row10;"#},
21218 base_text,
21219 &mut cx,
21220 );
21221 // Same for selections
21222 assert_hunk_revert(
21223 indoc! {r#"struct Row;
21224 struct Row1;
21225 struct Row2;
21226 struct Row2.1;
21227 struct Row2.2;
21228 «ˇ
21229 struct Row4;
21230 struct» Row5;
21231 «struct Row6;
21232 ˇ»
21233 struct Row9.1;
21234 struct Row9.2;
21235 struct Row9.3;
21236 struct Row8;
21237 struct Row9;
21238 struct Row10;"#},
21239 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
21240 indoc! {r#"struct Row;
21241 struct Row1;
21242 struct Row2;
21243 struct Row2.1;
21244 struct Row2.2;
21245 «ˇ
21246 struct Row4;
21247 struct» Row5;
21248 «struct Row6;
21249 ˇ»
21250 struct Row9.1;
21251 struct Row9.2;
21252 struct Row9.3;
21253 struct Row8;
21254 struct Row9;
21255 struct Row10;"#},
21256 base_text,
21257 &mut cx,
21258 );
21259
21260 // When carets and selections intersect the addition hunks, those are reverted.
21261 // Adjacent carets got merged.
21262 assert_hunk_revert(
21263 indoc! {r#"struct Row;
21264 ˇ// something on the top
21265 struct Row1;
21266 struct Row2;
21267 struct Roˇw3.1;
21268 struct Row2.2;
21269 struct Row2.3;ˇ
21270
21271 struct Row4;
21272 struct ˇRow5.1;
21273 struct Row5.2;
21274 struct «Rowˇ»5.3;
21275 struct Row5;
21276 struct Row6;
21277 ˇ
21278 struct Row9.1;
21279 struct «Rowˇ»9.2;
21280 struct «ˇRow»9.3;
21281 struct Row8;
21282 struct Row9;
21283 «ˇ// something on bottom»
21284 struct Row10;"#},
21285 vec![
21286 DiffHunkStatusKind::Added,
21287 DiffHunkStatusKind::Added,
21288 DiffHunkStatusKind::Added,
21289 DiffHunkStatusKind::Added,
21290 DiffHunkStatusKind::Added,
21291 ],
21292 indoc! {r#"struct Row;
21293 ˇstruct Row1;
21294 struct Row2;
21295 ˇ
21296 struct Row4;
21297 ˇstruct Row5;
21298 struct Row6;
21299 ˇ
21300 ˇstruct Row8;
21301 struct Row9;
21302 ˇstruct Row10;"#},
21303 base_text,
21304 &mut cx,
21305 );
21306}
21307
21308#[gpui::test]
21309async fn test_modification_reverts(cx: &mut TestAppContext) {
21310 init_test(cx, |_| {});
21311 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
21312 let base_text = indoc! {r#"
21313 struct Row;
21314 struct Row1;
21315 struct Row2;
21316
21317 struct Row4;
21318 struct Row5;
21319 struct Row6;
21320
21321 struct Row8;
21322 struct Row9;
21323 struct Row10;"#};
21324
21325 // Modification hunks behave the same as the addition ones.
21326 assert_hunk_revert(
21327 indoc! {r#"struct Row;
21328 struct Row1;
21329 struct Row33;
21330 ˇ
21331 struct Row4;
21332 struct Row5;
21333 struct Row6;
21334 ˇ
21335 struct Row99;
21336 struct Row9;
21337 struct Row10;"#},
21338 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
21339 indoc! {r#"struct Row;
21340 struct Row1;
21341 struct Row33;
21342 ˇ
21343 struct Row4;
21344 struct Row5;
21345 struct Row6;
21346 ˇ
21347 struct Row99;
21348 struct Row9;
21349 struct Row10;"#},
21350 base_text,
21351 &mut cx,
21352 );
21353 assert_hunk_revert(
21354 indoc! {r#"struct Row;
21355 struct Row1;
21356 struct Row33;
21357 «ˇ
21358 struct Row4;
21359 struct» Row5;
21360 «struct Row6;
21361 ˇ»
21362 struct Row99;
21363 struct Row9;
21364 struct Row10;"#},
21365 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
21366 indoc! {r#"struct Row;
21367 struct Row1;
21368 struct Row33;
21369 «ˇ
21370 struct Row4;
21371 struct» Row5;
21372 «struct Row6;
21373 ˇ»
21374 struct Row99;
21375 struct Row9;
21376 struct Row10;"#},
21377 base_text,
21378 &mut cx,
21379 );
21380
21381 assert_hunk_revert(
21382 indoc! {r#"ˇstruct Row1.1;
21383 struct Row1;
21384 «ˇstr»uct Row22;
21385
21386 struct ˇRow44;
21387 struct Row5;
21388 struct «Rˇ»ow66;ˇ
21389
21390 «struˇ»ct Row88;
21391 struct Row9;
21392 struct Row1011;ˇ"#},
21393 vec![
21394 DiffHunkStatusKind::Modified,
21395 DiffHunkStatusKind::Modified,
21396 DiffHunkStatusKind::Modified,
21397 DiffHunkStatusKind::Modified,
21398 DiffHunkStatusKind::Modified,
21399 DiffHunkStatusKind::Modified,
21400 ],
21401 indoc! {r#"struct Row;
21402 ˇstruct Row1;
21403 struct Row2;
21404 ˇ
21405 struct Row4;
21406 ˇstruct Row5;
21407 struct Row6;
21408 ˇ
21409 struct Row8;
21410 ˇstruct Row9;
21411 struct Row10;ˇ"#},
21412 base_text,
21413 &mut cx,
21414 );
21415}
21416
21417#[gpui::test]
21418async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
21419 init_test(cx, |_| {});
21420 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
21421 let base_text = indoc! {r#"
21422 one
21423
21424 two
21425 three
21426 "#};
21427
21428 cx.set_head_text(base_text);
21429 cx.set_state("\nˇ\n");
21430 cx.executor().run_until_parked();
21431 cx.update_editor(|editor, _window, cx| {
21432 editor.expand_selected_diff_hunks(cx);
21433 });
21434 cx.executor().run_until_parked();
21435 cx.update_editor(|editor, window, cx| {
21436 editor.backspace(&Default::default(), window, cx);
21437 });
21438 cx.run_until_parked();
21439 cx.assert_state_with_diff(
21440 indoc! {r#"
21441
21442 - two
21443 - threeˇ
21444 +
21445 "#}
21446 .to_string(),
21447 );
21448}
21449
21450#[gpui::test]
21451async fn test_deletion_reverts(cx: &mut TestAppContext) {
21452 init_test(cx, |_| {});
21453 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
21454 let base_text = indoc! {r#"struct Row;
21455struct Row1;
21456struct Row2;
21457
21458struct Row4;
21459struct Row5;
21460struct Row6;
21461
21462struct Row8;
21463struct Row9;
21464struct Row10;"#};
21465
21466 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
21467 assert_hunk_revert(
21468 indoc! {r#"struct Row;
21469 struct Row2;
21470
21471 ˇstruct Row4;
21472 struct Row5;
21473 struct Row6;
21474 ˇ
21475 struct Row8;
21476 struct Row10;"#},
21477 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
21478 indoc! {r#"struct Row;
21479 struct Row2;
21480
21481 ˇstruct Row4;
21482 struct Row5;
21483 struct Row6;
21484 ˇ
21485 struct Row8;
21486 struct Row10;"#},
21487 base_text,
21488 &mut cx,
21489 );
21490 assert_hunk_revert(
21491 indoc! {r#"struct Row;
21492 struct Row2;
21493
21494 «ˇstruct Row4;
21495 struct» Row5;
21496 «struct Row6;
21497 ˇ»
21498 struct Row8;
21499 struct Row10;"#},
21500 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
21501 indoc! {r#"struct Row;
21502 struct Row2;
21503
21504 «ˇstruct Row4;
21505 struct» Row5;
21506 «struct Row6;
21507 ˇ»
21508 struct Row8;
21509 struct Row10;"#},
21510 base_text,
21511 &mut cx,
21512 );
21513
21514 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
21515 assert_hunk_revert(
21516 indoc! {r#"struct Row;
21517 ˇstruct Row2;
21518
21519 struct Row4;
21520 struct Row5;
21521 struct Row6;
21522
21523 struct Row8;ˇ
21524 struct Row10;"#},
21525 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
21526 indoc! {r#"struct Row;
21527 struct Row1;
21528 ˇstruct Row2;
21529
21530 struct Row4;
21531 struct Row5;
21532 struct Row6;
21533
21534 struct Row8;ˇ
21535 struct Row9;
21536 struct Row10;"#},
21537 base_text,
21538 &mut cx,
21539 );
21540 assert_hunk_revert(
21541 indoc! {r#"struct Row;
21542 struct Row2«ˇ;
21543 struct Row4;
21544 struct» Row5;
21545 «struct Row6;
21546
21547 struct Row8;ˇ»
21548 struct Row10;"#},
21549 vec![
21550 DiffHunkStatusKind::Deleted,
21551 DiffHunkStatusKind::Deleted,
21552 DiffHunkStatusKind::Deleted,
21553 ],
21554 indoc! {r#"struct Row;
21555 struct Row1;
21556 struct Row2«ˇ;
21557
21558 struct Row4;
21559 struct» Row5;
21560 «struct Row6;
21561
21562 struct Row8;ˇ»
21563 struct Row9;
21564 struct Row10;"#},
21565 base_text,
21566 &mut cx,
21567 );
21568}
21569
21570#[gpui::test]
21571async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
21572 init_test(cx, |_| {});
21573
21574 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
21575 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
21576 let base_text_3 =
21577 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
21578
21579 let text_1 = edit_first_char_of_every_line(base_text_1);
21580 let text_2 = edit_first_char_of_every_line(base_text_2);
21581 let text_3 = edit_first_char_of_every_line(base_text_3);
21582
21583 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
21584 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
21585 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
21586
21587 let multibuffer = cx.new(|cx| {
21588 let mut multibuffer = MultiBuffer::new(ReadWrite);
21589 multibuffer.set_excerpts_for_path(
21590 PathKey::sorted(0),
21591 buffer_1.clone(),
21592 [
21593 Point::new(0, 0)..Point::new(2, 0),
21594 Point::new(5, 0)..Point::new(6, 0),
21595 Point::new(9, 0)..Point::new(9, 4),
21596 ],
21597 0,
21598 cx,
21599 );
21600 multibuffer.set_excerpts_for_path(
21601 PathKey::sorted(1),
21602 buffer_2.clone(),
21603 [
21604 Point::new(0, 0)..Point::new(2, 0),
21605 Point::new(5, 0)..Point::new(6, 0),
21606 Point::new(9, 0)..Point::new(9, 4),
21607 ],
21608 0,
21609 cx,
21610 );
21611 multibuffer.set_excerpts_for_path(
21612 PathKey::sorted(2),
21613 buffer_3.clone(),
21614 [
21615 Point::new(0, 0)..Point::new(2, 0),
21616 Point::new(5, 0)..Point::new(6, 0),
21617 Point::new(9, 0)..Point::new(9, 4),
21618 ],
21619 0,
21620 cx,
21621 );
21622 multibuffer
21623 });
21624
21625 let fs = FakeFs::new(cx.executor());
21626 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21627 let (editor, cx) = cx
21628 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
21629 editor.update_in(cx, |editor, _window, cx| {
21630 for (buffer, diff_base) in [
21631 (buffer_1.clone(), base_text_1),
21632 (buffer_2.clone(), base_text_2),
21633 (buffer_3.clone(), base_text_3),
21634 ] {
21635 let diff = cx.new(|cx| {
21636 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
21637 });
21638 editor
21639 .buffer
21640 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
21641 }
21642 });
21643 cx.executor().run_until_parked();
21644
21645 editor.update_in(cx, |editor, window, cx| {
21646 assert_eq!(editor.display_text(cx), "\n\nXaaa\nXbbb\nXccc\n\nXfff\nXggg\n\nXjjj\n\n\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\n\n\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}");
21647 editor.select_all(&SelectAll, window, cx);
21648 editor.git_restore(&Default::default(), window, cx);
21649 });
21650 cx.executor().run_until_parked();
21651
21652 // When all ranges are selected, all buffer hunks are reverted.
21653 editor.update(cx, |editor, cx| {
21654 assert_eq!(editor.display_text(cx), "\n\naaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\n\n\n\n\nllll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}\n\n\n\n");
21655 });
21656 buffer_1.update(cx, |buffer, _| {
21657 assert_eq!(buffer.text(), base_text_1);
21658 });
21659 buffer_2.update(cx, |buffer, _| {
21660 assert_eq!(buffer.text(), base_text_2);
21661 });
21662 buffer_3.update(cx, |buffer, _| {
21663 assert_eq!(buffer.text(), base_text_3);
21664 });
21665
21666 editor.update_in(cx, |editor, window, cx| {
21667 editor.undo(&Default::default(), window, cx);
21668 });
21669
21670 editor.update_in(cx, |editor, window, cx| {
21671 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21672 s.select_ranges(Some(Point::new(0, 0)..Point::new(5, 0)));
21673 });
21674 editor.git_restore(&Default::default(), window, cx);
21675 });
21676
21677 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
21678 // but not affect buffer_2 and its related excerpts.
21679 editor.update(cx, |editor, cx| {
21680 assert_eq!(
21681 editor.display_text(cx),
21682 "\n\naaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\n\n\n\n\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\n\n\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}"
21683 );
21684 });
21685 buffer_1.update(cx, |buffer, _| {
21686 assert_eq!(buffer.text(), base_text_1);
21687 });
21688 buffer_2.update(cx, |buffer, _| {
21689 assert_eq!(
21690 buffer.text(),
21691 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
21692 );
21693 });
21694 buffer_3.update(cx, |buffer, _| {
21695 assert_eq!(
21696 buffer.text(),
21697 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
21698 );
21699 });
21700
21701 fn edit_first_char_of_every_line(text: &str) -> String {
21702 text.split('\n')
21703 .map(|line| format!("X{}", &line[1..]))
21704 .collect::<Vec<_>>()
21705 .join("\n")
21706 }
21707}
21708
21709#[gpui::test]
21710async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
21711 init_test(cx, |_| {});
21712
21713 let cols = 4;
21714 let rows = 10;
21715 let sample_text_1 = sample_text(rows, cols, 'a');
21716 assert_eq!(
21717 sample_text_1,
21718 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
21719 );
21720 let sample_text_2 = sample_text(rows, cols, 'l');
21721 assert_eq!(
21722 sample_text_2,
21723 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
21724 );
21725 let sample_text_3 = sample_text(rows, cols, 'v');
21726 assert_eq!(
21727 sample_text_3,
21728 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
21729 );
21730
21731 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
21732 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
21733 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
21734
21735 let multi_buffer = cx.new(|cx| {
21736 let mut multibuffer = MultiBuffer::new(ReadWrite);
21737 multibuffer.set_excerpts_for_path(
21738 PathKey::sorted(0),
21739 buffer_1.clone(),
21740 [
21741 Point::new(0, 0)..Point::new(2, 0),
21742 Point::new(5, 0)..Point::new(6, 0),
21743 Point::new(9, 0)..Point::new(9, 4),
21744 ],
21745 0,
21746 cx,
21747 );
21748 multibuffer.set_excerpts_for_path(
21749 PathKey::sorted(1),
21750 buffer_2.clone(),
21751 [
21752 Point::new(0, 0)..Point::new(2, 0),
21753 Point::new(5, 0)..Point::new(6, 0),
21754 Point::new(9, 0)..Point::new(9, 4),
21755 ],
21756 0,
21757 cx,
21758 );
21759 multibuffer.set_excerpts_for_path(
21760 PathKey::sorted(2),
21761 buffer_3.clone(),
21762 [
21763 Point::new(0, 0)..Point::new(2, 0),
21764 Point::new(5, 0)..Point::new(6, 0),
21765 Point::new(9, 0)..Point::new(9, 4),
21766 ],
21767 0,
21768 cx,
21769 );
21770 multibuffer
21771 });
21772
21773 let fs = FakeFs::new(cx.executor());
21774 fs.insert_tree(
21775 "/a",
21776 json!({
21777 "main.rs": sample_text_1,
21778 "other.rs": sample_text_2,
21779 "lib.rs": sample_text_3,
21780 }),
21781 )
21782 .await;
21783 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21784 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
21785 let workspace = window
21786 .read_with(cx, |mw, _| mw.workspace().clone())
21787 .unwrap();
21788 let cx = &mut VisualTestContext::from_window(*window, cx);
21789 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21790 Editor::new(
21791 EditorMode::full(),
21792 multi_buffer,
21793 Some(project.clone()),
21794 window,
21795 cx,
21796 )
21797 });
21798 let multibuffer_item_id = workspace.update_in(cx, |workspace, window, cx| {
21799 assert!(
21800 workspace.active_item(cx).is_none(),
21801 "active item should be None before the first item is added"
21802 );
21803 workspace.add_item_to_active_pane(
21804 Box::new(multi_buffer_editor.clone()),
21805 None,
21806 true,
21807 window,
21808 cx,
21809 );
21810 let active_item = workspace
21811 .active_item(cx)
21812 .expect("should have an active item after adding the multi buffer");
21813 assert_eq!(
21814 active_item.buffer_kind(cx),
21815 ItemBufferKind::Multibuffer,
21816 "A multi buffer was expected to active after adding"
21817 );
21818 active_item.item_id()
21819 });
21820
21821 cx.executor().run_until_parked();
21822
21823 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21824 editor.change_selections(
21825 SelectionEffects::scroll(Autoscroll::Next),
21826 window,
21827 cx,
21828 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
21829 );
21830 editor.open_excerpts(&OpenExcerpts, window, cx);
21831 });
21832 cx.executor().run_until_parked();
21833 let first_item_id = workspace.update_in(cx, |workspace, window, cx| {
21834 let active_item = workspace
21835 .active_item(cx)
21836 .expect("should have an active item after navigating into the 1st buffer");
21837 let first_item_id = active_item.item_id();
21838 assert_ne!(
21839 first_item_id, multibuffer_item_id,
21840 "Should navigate into the 1st buffer and activate it"
21841 );
21842 assert_eq!(
21843 active_item.buffer_kind(cx),
21844 ItemBufferKind::Singleton,
21845 "New active item should be a singleton buffer"
21846 );
21847 assert_eq!(
21848 active_item
21849 .act_as::<Editor>(cx)
21850 .expect("should have navigated into an editor for the 1st buffer")
21851 .read(cx)
21852 .text(cx),
21853 sample_text_1
21854 );
21855
21856 workspace
21857 .go_back(workspace.active_pane().downgrade(), window, cx)
21858 .detach_and_log_err(cx);
21859
21860 first_item_id
21861 });
21862
21863 cx.executor().run_until_parked();
21864 workspace.update_in(cx, |workspace, _, cx| {
21865 let active_item = workspace
21866 .active_item(cx)
21867 .expect("should have an active item after navigating back");
21868 assert_eq!(
21869 active_item.item_id(),
21870 multibuffer_item_id,
21871 "Should navigate back to the multi buffer"
21872 );
21873 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
21874 });
21875
21876 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21877 editor.change_selections(
21878 SelectionEffects::scroll(Autoscroll::Next),
21879 window,
21880 cx,
21881 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
21882 );
21883 editor.open_excerpts(&OpenExcerpts, window, cx);
21884 });
21885 cx.executor().run_until_parked();
21886 let second_item_id = workspace.update_in(cx, |workspace, window, cx| {
21887 let active_item = workspace
21888 .active_item(cx)
21889 .expect("should have an active item after navigating into the 2nd buffer");
21890 let second_item_id = active_item.item_id();
21891 assert_ne!(
21892 second_item_id, multibuffer_item_id,
21893 "Should navigate away from the multibuffer"
21894 );
21895 assert_ne!(
21896 second_item_id, first_item_id,
21897 "Should navigate into the 2nd buffer and activate it"
21898 );
21899 assert_eq!(
21900 active_item.buffer_kind(cx),
21901 ItemBufferKind::Singleton,
21902 "New active item should be a singleton buffer"
21903 );
21904 assert_eq!(
21905 active_item
21906 .act_as::<Editor>(cx)
21907 .expect("should have navigated into an editor")
21908 .read(cx)
21909 .text(cx),
21910 sample_text_2
21911 );
21912
21913 workspace
21914 .go_back(workspace.active_pane().downgrade(), window, cx)
21915 .detach_and_log_err(cx);
21916
21917 second_item_id
21918 });
21919
21920 cx.executor().run_until_parked();
21921 workspace.update_in(cx, |workspace, _, cx| {
21922 let active_item = workspace
21923 .active_item(cx)
21924 .expect("should have an active item after navigating back from the 2nd buffer");
21925 assert_eq!(
21926 active_item.item_id(),
21927 multibuffer_item_id,
21928 "Should navigate back from the 2nd buffer to the multi buffer"
21929 );
21930 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
21931 });
21932
21933 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21934 editor.change_selections(
21935 SelectionEffects::scroll(Autoscroll::Next),
21936 window,
21937 cx,
21938 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
21939 );
21940 editor.open_excerpts(&OpenExcerpts, window, cx);
21941 });
21942 cx.executor().run_until_parked();
21943 workspace.update_in(cx, |workspace, window, cx| {
21944 let active_item = workspace
21945 .active_item(cx)
21946 .expect("should have an active item after navigating into the 3rd buffer");
21947 let third_item_id = active_item.item_id();
21948 assert_ne!(
21949 third_item_id, multibuffer_item_id,
21950 "Should navigate into the 3rd buffer and activate it"
21951 );
21952 assert_ne!(third_item_id, first_item_id);
21953 assert_ne!(third_item_id, second_item_id);
21954 assert_eq!(
21955 active_item.buffer_kind(cx),
21956 ItemBufferKind::Singleton,
21957 "New active item should be a singleton buffer"
21958 );
21959 assert_eq!(
21960 active_item
21961 .act_as::<Editor>(cx)
21962 .expect("should have navigated into an editor")
21963 .read(cx)
21964 .text(cx),
21965 sample_text_3
21966 );
21967
21968 workspace
21969 .go_back(workspace.active_pane().downgrade(), window, cx)
21970 .detach_and_log_err(cx);
21971 });
21972
21973 cx.executor().run_until_parked();
21974 workspace.update_in(cx, |workspace, _, cx| {
21975 let active_item = workspace
21976 .active_item(cx)
21977 .expect("should have an active item after navigating back from the 3rd buffer");
21978 assert_eq!(
21979 active_item.item_id(),
21980 multibuffer_item_id,
21981 "Should navigate back from the 3rd buffer to the multi buffer"
21982 );
21983 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
21984 });
21985}
21986
21987#[gpui::test]
21988async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21989 init_test(cx, |_| {});
21990
21991 let mut cx = EditorTestContext::new(cx).await;
21992
21993 let diff_base = r#"
21994 use some::mod;
21995
21996 const A: u32 = 42;
21997
21998 fn main() {
21999 println!("hello");
22000
22001 println!("world");
22002 }
22003 "#
22004 .unindent();
22005
22006 cx.set_state(
22007 &r#"
22008 use some::modified;
22009
22010 ˇ
22011 fn main() {
22012 println!("hello there");
22013
22014 println!("around the");
22015 println!("world");
22016 }
22017 "#
22018 .unindent(),
22019 );
22020
22021 cx.set_head_text(&diff_base);
22022 executor.run_until_parked();
22023
22024 cx.update_editor(|editor, window, cx| {
22025 editor.go_to_next_hunk(&GoToHunk, window, cx);
22026 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
22027 });
22028 executor.run_until_parked();
22029 cx.assert_state_with_diff(
22030 r#"
22031 use some::modified;
22032
22033
22034 fn main() {
22035 - println!("hello");
22036 + ˇ println!("hello there");
22037
22038 println!("around the");
22039 println!("world");
22040 }
22041 "#
22042 .unindent(),
22043 );
22044
22045 cx.update_editor(|editor, window, cx| {
22046 for _ in 0..2 {
22047 editor.go_to_next_hunk(&GoToHunk, window, cx);
22048 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
22049 }
22050 });
22051 executor.run_until_parked();
22052 cx.assert_state_with_diff(
22053 r#"
22054 - use some::mod;
22055 + ˇuse some::modified;
22056
22057
22058 fn main() {
22059 - println!("hello");
22060 + println!("hello there");
22061
22062 + println!("around the");
22063 println!("world");
22064 }
22065 "#
22066 .unindent(),
22067 );
22068
22069 cx.update_editor(|editor, window, cx| {
22070 editor.go_to_next_hunk(&GoToHunk, window, cx);
22071 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
22072 });
22073 executor.run_until_parked();
22074 cx.assert_state_with_diff(
22075 r#"
22076 - use some::mod;
22077 + use some::modified;
22078
22079 - const A: u32 = 42;
22080 ˇ
22081 fn main() {
22082 - println!("hello");
22083 + println!("hello there");
22084
22085 + println!("around the");
22086 println!("world");
22087 }
22088 "#
22089 .unindent(),
22090 );
22091
22092 cx.update_editor(|editor, window, cx| {
22093 editor.cancel(&Cancel, window, cx);
22094 });
22095
22096 cx.assert_state_with_diff(
22097 r#"
22098 use some::modified;
22099
22100 ˇ
22101 fn main() {
22102 println!("hello there");
22103
22104 println!("around the");
22105 println!("world");
22106 }
22107 "#
22108 .unindent(),
22109 );
22110}
22111
22112#[gpui::test]
22113async fn test_diff_base_change_with_expanded_diff_hunks(
22114 executor: BackgroundExecutor,
22115 cx: &mut TestAppContext,
22116) {
22117 init_test(cx, |_| {});
22118
22119 let mut cx = EditorTestContext::new(cx).await;
22120
22121 let diff_base = r#"
22122 use some::mod1;
22123 use some::mod2;
22124
22125 const A: u32 = 42;
22126 const B: u32 = 42;
22127 const C: u32 = 42;
22128
22129 fn main() {
22130 println!("hello");
22131
22132 println!("world");
22133 }
22134 "#
22135 .unindent();
22136
22137 cx.set_state(
22138 &r#"
22139 use some::mod2;
22140
22141 const A: u32 = 42;
22142 const C: u32 = 42;
22143
22144 fn main(ˇ) {
22145 //println!("hello");
22146
22147 println!("world");
22148 //
22149 //
22150 }
22151 "#
22152 .unindent(),
22153 );
22154
22155 cx.set_head_text(&diff_base);
22156 executor.run_until_parked();
22157
22158 cx.update_editor(|editor, window, cx| {
22159 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22160 });
22161 executor.run_until_parked();
22162 cx.assert_state_with_diff(
22163 r#"
22164 - use some::mod1;
22165 use some::mod2;
22166
22167 const A: u32 = 42;
22168 - const B: u32 = 42;
22169 const C: u32 = 42;
22170
22171 fn main(ˇ) {
22172 - println!("hello");
22173 + //println!("hello");
22174
22175 println!("world");
22176 + //
22177 + //
22178 }
22179 "#
22180 .unindent(),
22181 );
22182
22183 cx.set_head_text("new diff base!");
22184 executor.run_until_parked();
22185 cx.assert_state_with_diff(
22186 r#"
22187 - new diff base!
22188 + use some::mod2;
22189 +
22190 + const A: u32 = 42;
22191 + const C: u32 = 42;
22192 +
22193 + fn main(ˇ) {
22194 + //println!("hello");
22195 +
22196 + println!("world");
22197 + //
22198 + //
22199 + }
22200 "#
22201 .unindent(),
22202 );
22203}
22204
22205#[gpui::test]
22206async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
22207 init_test(cx, |_| {});
22208
22209 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
22210 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
22211 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
22212 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
22213 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
22214 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
22215
22216 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
22217 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
22218 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
22219
22220 let multi_buffer = cx.new(|cx| {
22221 let mut multibuffer = MultiBuffer::new(ReadWrite);
22222 multibuffer.set_excerpts_for_path(
22223 PathKey::sorted(0),
22224 buffer_1.clone(),
22225 [
22226 Point::new(0, 0)..Point::new(2, 3),
22227 Point::new(5, 0)..Point::new(6, 3),
22228 Point::new(9, 0)..Point::new(10, 3),
22229 ],
22230 0,
22231 cx,
22232 );
22233 multibuffer.set_excerpts_for_path(
22234 PathKey::sorted(1),
22235 buffer_2.clone(),
22236 [
22237 Point::new(0, 0)..Point::new(2, 3),
22238 Point::new(5, 0)..Point::new(6, 3),
22239 Point::new(9, 0)..Point::new(10, 3),
22240 ],
22241 0,
22242 cx,
22243 );
22244 multibuffer.set_excerpts_for_path(
22245 PathKey::sorted(2),
22246 buffer_3.clone(),
22247 [
22248 Point::new(0, 0)..Point::new(2, 3),
22249 Point::new(5, 0)..Point::new(6, 3),
22250 Point::new(9, 0)..Point::new(10, 3),
22251 ],
22252 0,
22253 cx,
22254 );
22255 assert_eq!(multibuffer.excerpt_ids().len(), 9);
22256 multibuffer
22257 });
22258
22259 let editor =
22260 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
22261 editor
22262 .update(cx, |editor, _window, cx| {
22263 for (buffer, diff_base) in [
22264 (buffer_1.clone(), file_1_old),
22265 (buffer_2.clone(), file_2_old),
22266 (buffer_3.clone(), file_3_old),
22267 ] {
22268 let diff = cx.new(|cx| {
22269 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
22270 });
22271 editor
22272 .buffer
22273 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
22274 }
22275 })
22276 .unwrap();
22277
22278 let mut cx = EditorTestContext::for_editor(editor, cx).await;
22279 cx.run_until_parked();
22280
22281 cx.assert_editor_state(
22282 &"
22283 ˇaaa
22284 ccc
22285 ddd
22286 ggg
22287 hhh
22288
22289 lll
22290 mmm
22291 NNN
22292 qqq
22293 rrr
22294 uuu
22295 111
22296 222
22297 333
22298 666
22299 777
22300 000
22301 !!!"
22302 .unindent(),
22303 );
22304
22305 cx.update_editor(|editor, window, cx| {
22306 editor.select_all(&SelectAll, window, cx);
22307 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
22308 });
22309 cx.executor().run_until_parked();
22310
22311 cx.assert_state_with_diff(
22312 "
22313 «aaa
22314 - bbb
22315 ccc
22316 ddd
22317 ggg
22318 hhh
22319
22320 lll
22321 mmm
22322 - nnn
22323 + NNN
22324 qqq
22325 rrr
22326 uuu
22327 111
22328 222
22329 333
22330 + 666
22331 777
22332 000
22333 !!!ˇ»"
22334 .unindent(),
22335 );
22336}
22337
22338#[gpui::test]
22339async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
22340 init_test(cx, |_| {});
22341
22342 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
22343 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
22344
22345 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
22346 let multi_buffer = cx.new(|cx| {
22347 let mut multibuffer = MultiBuffer::new(ReadWrite);
22348 multibuffer.set_excerpts_for_path(
22349 PathKey::sorted(0),
22350 buffer.clone(),
22351 [
22352 Point::new(0, 0)..Point::new(1, 3),
22353 Point::new(4, 0)..Point::new(6, 3),
22354 Point::new(9, 0)..Point::new(9, 3),
22355 ],
22356 0,
22357 cx,
22358 );
22359 assert_eq!(multibuffer.excerpt_ids().len(), 3);
22360 multibuffer
22361 });
22362
22363 let editor =
22364 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
22365 editor
22366 .update(cx, |editor, _window, cx| {
22367 let diff = cx.new(|cx| {
22368 BufferDiff::new_with_base_text(base, &buffer.read(cx).text_snapshot(), cx)
22369 });
22370 editor
22371 .buffer
22372 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
22373 })
22374 .unwrap();
22375
22376 let mut cx = EditorTestContext::for_editor(editor, cx).await;
22377 cx.run_until_parked();
22378
22379 cx.update_editor(|editor, window, cx| {
22380 editor.expand_all_diff_hunks(&Default::default(), window, cx)
22381 });
22382 cx.executor().run_until_parked();
22383
22384 // When the start of a hunk coincides with the start of its excerpt,
22385 // the hunk is expanded. When the start of a hunk is earlier than
22386 // the start of its excerpt, the hunk is not expanded.
22387 cx.assert_state_with_diff(
22388 "
22389 ˇaaa
22390 - bbb
22391 + BBB
22392 - ddd
22393 - eee
22394 + DDD
22395 + EEE
22396 fff
22397 iii"
22398 .unindent(),
22399 );
22400}
22401
22402#[gpui::test]
22403async fn test_edits_around_expanded_insertion_hunks(
22404 executor: BackgroundExecutor,
22405 cx: &mut TestAppContext,
22406) {
22407 init_test(cx, |_| {});
22408
22409 let mut cx = EditorTestContext::new(cx).await;
22410
22411 let diff_base = r#"
22412 use some::mod1;
22413 use some::mod2;
22414
22415 const A: u32 = 42;
22416
22417 fn main() {
22418 println!("hello");
22419
22420 println!("world");
22421 }
22422 "#
22423 .unindent();
22424 executor.run_until_parked();
22425 cx.set_state(
22426 &r#"
22427 use some::mod1;
22428 use some::mod2;
22429
22430 const A: u32 = 42;
22431 const B: u32 = 42;
22432 const C: u32 = 42;
22433 ˇ
22434
22435 fn main() {
22436 println!("hello");
22437
22438 println!("world");
22439 }
22440 "#
22441 .unindent(),
22442 );
22443
22444 cx.set_head_text(&diff_base);
22445 executor.run_until_parked();
22446
22447 cx.update_editor(|editor, window, cx| {
22448 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22449 });
22450 executor.run_until_parked();
22451
22452 cx.assert_state_with_diff(
22453 r#"
22454 use some::mod1;
22455 use some::mod2;
22456
22457 const A: u32 = 42;
22458 + const B: u32 = 42;
22459 + const C: u32 = 42;
22460 + ˇ
22461
22462 fn main() {
22463 println!("hello");
22464
22465 println!("world");
22466 }
22467 "#
22468 .unindent(),
22469 );
22470
22471 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
22472 executor.run_until_parked();
22473
22474 cx.assert_state_with_diff(
22475 r#"
22476 use some::mod1;
22477 use some::mod2;
22478
22479 const A: u32 = 42;
22480 + const B: u32 = 42;
22481 + const C: u32 = 42;
22482 + const D: u32 = 42;
22483 + ˇ
22484
22485 fn main() {
22486 println!("hello");
22487
22488 println!("world");
22489 }
22490 "#
22491 .unindent(),
22492 );
22493
22494 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
22495 executor.run_until_parked();
22496
22497 cx.assert_state_with_diff(
22498 r#"
22499 use some::mod1;
22500 use some::mod2;
22501
22502 const A: u32 = 42;
22503 + const B: u32 = 42;
22504 + const C: u32 = 42;
22505 + const D: u32 = 42;
22506 + const E: u32 = 42;
22507 + ˇ
22508
22509 fn main() {
22510 println!("hello");
22511
22512 println!("world");
22513 }
22514 "#
22515 .unindent(),
22516 );
22517
22518 cx.update_editor(|editor, window, cx| {
22519 editor.delete_line(&DeleteLine, window, cx);
22520 });
22521 executor.run_until_parked();
22522
22523 cx.assert_state_with_diff(
22524 r#"
22525 use some::mod1;
22526 use some::mod2;
22527
22528 const A: u32 = 42;
22529 + const B: u32 = 42;
22530 + const C: u32 = 42;
22531 + const D: u32 = 42;
22532 + const E: u32 = 42;
22533 ˇ
22534 fn main() {
22535 println!("hello");
22536
22537 println!("world");
22538 }
22539 "#
22540 .unindent(),
22541 );
22542
22543 cx.update_editor(|editor, window, cx| {
22544 editor.move_up(&MoveUp, window, cx);
22545 editor.delete_line(&DeleteLine, window, cx);
22546 editor.move_up(&MoveUp, window, cx);
22547 editor.delete_line(&DeleteLine, window, cx);
22548 editor.move_up(&MoveUp, window, cx);
22549 editor.delete_line(&DeleteLine, window, cx);
22550 });
22551 executor.run_until_parked();
22552 cx.assert_state_with_diff(
22553 r#"
22554 use some::mod1;
22555 use some::mod2;
22556
22557 const A: u32 = 42;
22558 + const B: u32 = 42;
22559 ˇ
22560 fn main() {
22561 println!("hello");
22562
22563 println!("world");
22564 }
22565 "#
22566 .unindent(),
22567 );
22568
22569 cx.update_editor(|editor, window, cx| {
22570 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
22571 editor.delete_line(&DeleteLine, window, cx);
22572 });
22573 executor.run_until_parked();
22574 cx.assert_state_with_diff(
22575 r#"
22576 ˇ
22577 fn main() {
22578 println!("hello");
22579
22580 println!("world");
22581 }
22582 "#
22583 .unindent(),
22584 );
22585}
22586
22587#[gpui::test]
22588async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
22589 init_test(cx, |_| {});
22590
22591 let mut cx = EditorTestContext::new(cx).await;
22592 cx.set_head_text(indoc! { "
22593 one
22594 two
22595 three
22596 four
22597 five
22598 "
22599 });
22600 cx.set_state(indoc! { "
22601 one
22602 ˇthree
22603 five
22604 "});
22605 cx.run_until_parked();
22606 cx.update_editor(|editor, window, cx| {
22607 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
22608 });
22609 cx.assert_state_with_diff(
22610 indoc! { "
22611 one
22612 - two
22613 ˇthree
22614 - four
22615 five
22616 "}
22617 .to_string(),
22618 );
22619 cx.update_editor(|editor, window, cx| {
22620 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
22621 });
22622
22623 cx.assert_state_with_diff(
22624 indoc! { "
22625 one
22626 ˇthree
22627 five
22628 "}
22629 .to_string(),
22630 );
22631
22632 cx.update_editor(|editor, window, cx| {
22633 editor.move_up(&MoveUp, window, cx);
22634 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
22635 });
22636 cx.assert_state_with_diff(
22637 indoc! { "
22638 ˇone
22639 - two
22640 three
22641 five
22642 "}
22643 .to_string(),
22644 );
22645
22646 cx.update_editor(|editor, window, cx| {
22647 editor.move_down(&MoveDown, window, cx);
22648 editor.move_down(&MoveDown, window, cx);
22649 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
22650 });
22651 cx.assert_state_with_diff(
22652 indoc! { "
22653 one
22654 - two
22655 ˇthree
22656 - four
22657 five
22658 "}
22659 .to_string(),
22660 );
22661
22662 cx.set_state(indoc! { "
22663 one
22664 ˇTWO
22665 three
22666 four
22667 five
22668 "});
22669 cx.run_until_parked();
22670 cx.update_editor(|editor, window, cx| {
22671 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
22672 });
22673
22674 cx.assert_state_with_diff(
22675 indoc! { "
22676 one
22677 - two
22678 + ˇTWO
22679 three
22680 four
22681 five
22682 "}
22683 .to_string(),
22684 );
22685 cx.update_editor(|editor, window, cx| {
22686 editor.move_up(&Default::default(), window, cx);
22687 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
22688 });
22689 cx.assert_state_with_diff(
22690 indoc! { "
22691 one
22692 ˇTWO
22693 three
22694 four
22695 five
22696 "}
22697 .to_string(),
22698 );
22699}
22700
22701#[gpui::test]
22702async fn test_toggling_adjacent_diff_hunks_2(
22703 executor: BackgroundExecutor,
22704 cx: &mut TestAppContext,
22705) {
22706 init_test(cx, |_| {});
22707
22708 let mut cx = EditorTestContext::new(cx).await;
22709
22710 let diff_base = r#"
22711 lineA
22712 lineB
22713 lineC
22714 lineD
22715 "#
22716 .unindent();
22717
22718 cx.set_state(
22719 &r#"
22720 ˇlineA1
22721 lineB
22722 lineD
22723 "#
22724 .unindent(),
22725 );
22726 cx.set_head_text(&diff_base);
22727 executor.run_until_parked();
22728
22729 cx.update_editor(|editor, window, cx| {
22730 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
22731 });
22732 executor.run_until_parked();
22733 cx.assert_state_with_diff(
22734 r#"
22735 - lineA
22736 + ˇlineA1
22737 lineB
22738 lineD
22739 "#
22740 .unindent(),
22741 );
22742
22743 cx.update_editor(|editor, window, cx| {
22744 editor.move_down(&MoveDown, window, cx);
22745 editor.move_right(&MoveRight, window, cx);
22746 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
22747 });
22748 executor.run_until_parked();
22749 cx.assert_state_with_diff(
22750 r#"
22751 - lineA
22752 + lineA1
22753 lˇineB
22754 - lineC
22755 lineD
22756 "#
22757 .unindent(),
22758 );
22759}
22760
22761#[gpui::test]
22762async fn test_edits_around_expanded_deletion_hunks(
22763 executor: BackgroundExecutor,
22764 cx: &mut TestAppContext,
22765) {
22766 init_test(cx, |_| {});
22767
22768 let mut cx = EditorTestContext::new(cx).await;
22769
22770 let diff_base = r#"
22771 use some::mod1;
22772 use some::mod2;
22773
22774 const A: u32 = 42;
22775 const B: u32 = 42;
22776 const C: u32 = 42;
22777
22778
22779 fn main() {
22780 println!("hello");
22781
22782 println!("world");
22783 }
22784 "#
22785 .unindent();
22786 executor.run_until_parked();
22787 cx.set_state(
22788 &r#"
22789 use some::mod1;
22790 use some::mod2;
22791
22792 ˇconst B: u32 = 42;
22793 const C: u32 = 42;
22794
22795
22796 fn main() {
22797 println!("hello");
22798
22799 println!("world");
22800 }
22801 "#
22802 .unindent(),
22803 );
22804
22805 cx.set_head_text(&diff_base);
22806 executor.run_until_parked();
22807
22808 cx.update_editor(|editor, window, cx| {
22809 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22810 });
22811 executor.run_until_parked();
22812
22813 cx.assert_state_with_diff(
22814 r#"
22815 use some::mod1;
22816 use some::mod2;
22817
22818 - const A: u32 = 42;
22819 ˇconst B: u32 = 42;
22820 const C: u32 = 42;
22821
22822
22823 fn main() {
22824 println!("hello");
22825
22826 println!("world");
22827 }
22828 "#
22829 .unindent(),
22830 );
22831
22832 cx.update_editor(|editor, window, cx| {
22833 editor.delete_line(&DeleteLine, window, cx);
22834 });
22835 executor.run_until_parked();
22836 cx.assert_state_with_diff(
22837 r#"
22838 use some::mod1;
22839 use some::mod2;
22840
22841 - const A: u32 = 42;
22842 - const B: u32 = 42;
22843 ˇconst C: u32 = 42;
22844
22845
22846 fn main() {
22847 println!("hello");
22848
22849 println!("world");
22850 }
22851 "#
22852 .unindent(),
22853 );
22854
22855 cx.update_editor(|editor, window, cx| {
22856 editor.delete_line(&DeleteLine, window, cx);
22857 });
22858 executor.run_until_parked();
22859 cx.assert_state_with_diff(
22860 r#"
22861 use some::mod1;
22862 use some::mod2;
22863
22864 - const A: u32 = 42;
22865 - const B: u32 = 42;
22866 - const C: u32 = 42;
22867 ˇ
22868
22869 fn main() {
22870 println!("hello");
22871
22872 println!("world");
22873 }
22874 "#
22875 .unindent(),
22876 );
22877
22878 cx.update_editor(|editor, window, cx| {
22879 editor.handle_input("replacement", window, cx);
22880 });
22881 executor.run_until_parked();
22882 cx.assert_state_with_diff(
22883 r#"
22884 use some::mod1;
22885 use some::mod2;
22886
22887 - const A: u32 = 42;
22888 - const B: u32 = 42;
22889 - const C: u32 = 42;
22890 -
22891 + replacementˇ
22892
22893 fn main() {
22894 println!("hello");
22895
22896 println!("world");
22897 }
22898 "#
22899 .unindent(),
22900 );
22901}
22902
22903#[gpui::test]
22904async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22905 init_test(cx, |_| {});
22906
22907 let mut cx = EditorTestContext::new(cx).await;
22908
22909 let base_text = r#"
22910 one
22911 two
22912 three
22913 four
22914 five
22915 "#
22916 .unindent();
22917 executor.run_until_parked();
22918 cx.set_state(
22919 &r#"
22920 one
22921 two
22922 fˇour
22923 five
22924 "#
22925 .unindent(),
22926 );
22927
22928 cx.set_head_text(&base_text);
22929 executor.run_until_parked();
22930
22931 cx.update_editor(|editor, window, cx| {
22932 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22933 });
22934 executor.run_until_parked();
22935
22936 cx.assert_state_with_diff(
22937 r#"
22938 one
22939 two
22940 - three
22941 fˇour
22942 five
22943 "#
22944 .unindent(),
22945 );
22946
22947 cx.update_editor(|editor, window, cx| {
22948 editor.backspace(&Backspace, window, cx);
22949 editor.backspace(&Backspace, window, cx);
22950 });
22951 executor.run_until_parked();
22952 cx.assert_state_with_diff(
22953 r#"
22954 one
22955 two
22956 - threeˇ
22957 - four
22958 + our
22959 five
22960 "#
22961 .unindent(),
22962 );
22963}
22964
22965#[gpui::test]
22966async fn test_edit_after_expanded_modification_hunk(
22967 executor: BackgroundExecutor,
22968 cx: &mut TestAppContext,
22969) {
22970 init_test(cx, |_| {});
22971
22972 let mut cx = EditorTestContext::new(cx).await;
22973
22974 let diff_base = r#"
22975 use some::mod1;
22976 use some::mod2;
22977
22978 const A: u32 = 42;
22979 const B: u32 = 42;
22980 const C: u32 = 42;
22981 const D: u32 = 42;
22982
22983
22984 fn main() {
22985 println!("hello");
22986
22987 println!("world");
22988 }"#
22989 .unindent();
22990
22991 cx.set_state(
22992 &r#"
22993 use some::mod1;
22994 use some::mod2;
22995
22996 const A: u32 = 42;
22997 const B: u32 = 42;
22998 const C: u32 = 43ˇ
22999 const D: u32 = 42;
23000
23001
23002 fn main() {
23003 println!("hello");
23004
23005 println!("world");
23006 }"#
23007 .unindent(),
23008 );
23009
23010 cx.set_head_text(&diff_base);
23011 executor.run_until_parked();
23012 cx.update_editor(|editor, window, cx| {
23013 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23014 });
23015 executor.run_until_parked();
23016
23017 cx.assert_state_with_diff(
23018 r#"
23019 use some::mod1;
23020 use some::mod2;
23021
23022 const A: u32 = 42;
23023 const B: u32 = 42;
23024 - const C: u32 = 42;
23025 + const C: u32 = 43ˇ
23026 const D: u32 = 42;
23027
23028
23029 fn main() {
23030 println!("hello");
23031
23032 println!("world");
23033 }"#
23034 .unindent(),
23035 );
23036
23037 cx.update_editor(|editor, window, cx| {
23038 editor.handle_input("\nnew_line\n", window, cx);
23039 });
23040 executor.run_until_parked();
23041
23042 cx.assert_state_with_diff(
23043 r#"
23044 use some::mod1;
23045 use some::mod2;
23046
23047 const A: u32 = 42;
23048 const B: u32 = 42;
23049 - const C: u32 = 42;
23050 + const C: u32 = 43
23051 + new_line
23052 + ˇ
23053 const D: u32 = 42;
23054
23055
23056 fn main() {
23057 println!("hello");
23058
23059 println!("world");
23060 }"#
23061 .unindent(),
23062 );
23063}
23064
23065#[gpui::test]
23066async fn test_stage_and_unstage_added_file_hunk(
23067 executor: BackgroundExecutor,
23068 cx: &mut TestAppContext,
23069) {
23070 init_test(cx, |_| {});
23071
23072 let mut cx = EditorTestContext::new(cx).await;
23073 cx.update_editor(|editor, _, cx| {
23074 editor.set_expand_all_diff_hunks(cx);
23075 });
23076
23077 let working_copy = r#"
23078 ˇfn main() {
23079 println!("hello, world!");
23080 }
23081 "#
23082 .unindent();
23083
23084 cx.set_state(&working_copy);
23085 executor.run_until_parked();
23086
23087 cx.assert_state_with_diff(
23088 r#"
23089 + ˇfn main() {
23090 + println!("hello, world!");
23091 + }
23092 "#
23093 .unindent(),
23094 );
23095 cx.assert_index_text(None);
23096
23097 cx.update_editor(|editor, window, cx| {
23098 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
23099 });
23100 executor.run_until_parked();
23101 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
23102 cx.assert_state_with_diff(
23103 r#"
23104 + ˇfn main() {
23105 + println!("hello, world!");
23106 + }
23107 "#
23108 .unindent(),
23109 );
23110
23111 cx.update_editor(|editor, window, cx| {
23112 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
23113 });
23114 executor.run_until_parked();
23115 cx.assert_index_text(None);
23116}
23117
23118async fn setup_indent_guides_editor(
23119 text: &str,
23120 cx: &mut TestAppContext,
23121) -> (BufferId, EditorTestContext) {
23122 init_test(cx, |_| {});
23123
23124 let mut cx = EditorTestContext::new(cx).await;
23125
23126 let buffer_id = cx.update_editor(|editor, window, cx| {
23127 editor.set_text(text, window, cx);
23128 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
23129
23130 buffer_ids[0]
23131 });
23132
23133 (buffer_id, cx)
23134}
23135
23136fn assert_indent_guides(
23137 range: Range<u32>,
23138 expected: Vec<IndentGuide>,
23139 active_indices: Option<Vec<usize>>,
23140 cx: &mut EditorTestContext,
23141) {
23142 let indent_guides = cx.update_editor(|editor, window, cx| {
23143 let snapshot = editor.snapshot(window, cx).display_snapshot;
23144 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
23145 editor,
23146 MultiBufferRow(range.start)..MultiBufferRow(range.end),
23147 true,
23148 &snapshot,
23149 cx,
23150 );
23151
23152 indent_guides.sort_by(|a, b| {
23153 a.depth.cmp(&b.depth).then(
23154 a.start_row
23155 .cmp(&b.start_row)
23156 .then(a.end_row.cmp(&b.end_row)),
23157 )
23158 });
23159 indent_guides
23160 });
23161
23162 if let Some(expected) = active_indices {
23163 let active_indices = cx.update_editor(|editor, window, cx| {
23164 let snapshot = editor.snapshot(window, cx).display_snapshot;
23165 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
23166 });
23167
23168 assert_eq!(
23169 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
23170 expected,
23171 "Active indent guide indices do not match"
23172 );
23173 }
23174
23175 assert_eq!(indent_guides, expected, "Indent guides do not match");
23176}
23177
23178fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
23179 IndentGuide {
23180 buffer_id,
23181 start_row: MultiBufferRow(start_row),
23182 end_row: MultiBufferRow(end_row),
23183 depth,
23184 tab_size: 4,
23185 settings: IndentGuideSettings {
23186 enabled: true,
23187 line_width: 1,
23188 active_line_width: 1,
23189 coloring: IndentGuideColoring::default(),
23190 background_coloring: IndentGuideBackgroundColoring::default(),
23191 },
23192 }
23193}
23194
23195#[gpui::test]
23196async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
23197 let (buffer_id, mut cx) = setup_indent_guides_editor(
23198 &"
23199 fn main() {
23200 let a = 1;
23201 }"
23202 .unindent(),
23203 cx,
23204 )
23205 .await;
23206
23207 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
23208}
23209
23210#[gpui::test]
23211async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
23212 let (buffer_id, mut cx) = setup_indent_guides_editor(
23213 &"
23214 fn main() {
23215 let a = 1;
23216 let b = 2;
23217 }"
23218 .unindent(),
23219 cx,
23220 )
23221 .await;
23222
23223 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
23224}
23225
23226#[gpui::test]
23227async fn test_indent_guide_nested(cx: &mut TestAppContext) {
23228 let (buffer_id, mut cx) = setup_indent_guides_editor(
23229 &"
23230 fn main() {
23231 let a = 1;
23232 if a == 3 {
23233 let b = 2;
23234 } else {
23235 let c = 3;
23236 }
23237 }"
23238 .unindent(),
23239 cx,
23240 )
23241 .await;
23242
23243 assert_indent_guides(
23244 0..8,
23245 vec![
23246 indent_guide(buffer_id, 1, 6, 0),
23247 indent_guide(buffer_id, 3, 3, 1),
23248 indent_guide(buffer_id, 5, 5, 1),
23249 ],
23250 None,
23251 &mut cx,
23252 );
23253}
23254
23255#[gpui::test]
23256async fn test_indent_guide_tab(cx: &mut TestAppContext) {
23257 let (buffer_id, mut cx) = setup_indent_guides_editor(
23258 &"
23259 fn main() {
23260 let a = 1;
23261 let b = 2;
23262 let c = 3;
23263 }"
23264 .unindent(),
23265 cx,
23266 )
23267 .await;
23268
23269 assert_indent_guides(
23270 0..5,
23271 vec![
23272 indent_guide(buffer_id, 1, 3, 0),
23273 indent_guide(buffer_id, 2, 2, 1),
23274 ],
23275 None,
23276 &mut cx,
23277 );
23278}
23279
23280#[gpui::test]
23281async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
23282 let (buffer_id, mut cx) = setup_indent_guides_editor(
23283 &"
23284 fn main() {
23285 let a = 1;
23286
23287 let c = 3;
23288 }"
23289 .unindent(),
23290 cx,
23291 )
23292 .await;
23293
23294 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
23295}
23296
23297#[gpui::test]
23298async fn test_indent_guide_complex(cx: &mut TestAppContext) {
23299 let (buffer_id, mut cx) = setup_indent_guides_editor(
23300 &"
23301 fn main() {
23302 let a = 1;
23303
23304 let c = 3;
23305
23306 if a == 3 {
23307 let b = 2;
23308 } else {
23309 let c = 3;
23310 }
23311 }"
23312 .unindent(),
23313 cx,
23314 )
23315 .await;
23316
23317 assert_indent_guides(
23318 0..11,
23319 vec![
23320 indent_guide(buffer_id, 1, 9, 0),
23321 indent_guide(buffer_id, 6, 6, 1),
23322 indent_guide(buffer_id, 8, 8, 1),
23323 ],
23324 None,
23325 &mut cx,
23326 );
23327}
23328
23329#[gpui::test]
23330async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
23331 let (buffer_id, mut cx) = setup_indent_guides_editor(
23332 &"
23333 fn main() {
23334 let a = 1;
23335
23336 let c = 3;
23337
23338 if a == 3 {
23339 let b = 2;
23340 } else {
23341 let c = 3;
23342 }
23343 }"
23344 .unindent(),
23345 cx,
23346 )
23347 .await;
23348
23349 assert_indent_guides(
23350 1..11,
23351 vec![
23352 indent_guide(buffer_id, 1, 9, 0),
23353 indent_guide(buffer_id, 6, 6, 1),
23354 indent_guide(buffer_id, 8, 8, 1),
23355 ],
23356 None,
23357 &mut cx,
23358 );
23359}
23360
23361#[gpui::test]
23362async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
23363 let (buffer_id, mut cx) = setup_indent_guides_editor(
23364 &"
23365 fn main() {
23366 let a = 1;
23367
23368 let c = 3;
23369
23370 if a == 3 {
23371 let b = 2;
23372 } else {
23373 let c = 3;
23374 }
23375 }"
23376 .unindent(),
23377 cx,
23378 )
23379 .await;
23380
23381 assert_indent_guides(
23382 1..10,
23383 vec![
23384 indent_guide(buffer_id, 1, 9, 0),
23385 indent_guide(buffer_id, 6, 6, 1),
23386 indent_guide(buffer_id, 8, 8, 1),
23387 ],
23388 None,
23389 &mut cx,
23390 );
23391}
23392
23393#[gpui::test]
23394async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
23395 let (buffer_id, mut cx) = setup_indent_guides_editor(
23396 &"
23397 fn main() {
23398 if a {
23399 b(
23400 c,
23401 d,
23402 )
23403 } else {
23404 e(
23405 f
23406 )
23407 }
23408 }"
23409 .unindent(),
23410 cx,
23411 )
23412 .await;
23413
23414 assert_indent_guides(
23415 0..11,
23416 vec![
23417 indent_guide(buffer_id, 1, 10, 0),
23418 indent_guide(buffer_id, 2, 5, 1),
23419 indent_guide(buffer_id, 7, 9, 1),
23420 indent_guide(buffer_id, 3, 4, 2),
23421 indent_guide(buffer_id, 8, 8, 2),
23422 ],
23423 None,
23424 &mut cx,
23425 );
23426
23427 cx.update_editor(|editor, window, cx| {
23428 editor.fold_at(MultiBufferRow(2), window, cx);
23429 assert_eq!(
23430 editor.display_text(cx),
23431 "
23432 fn main() {
23433 if a {
23434 b(⋯
23435 )
23436 } else {
23437 e(
23438 f
23439 )
23440 }
23441 }"
23442 .unindent()
23443 );
23444 });
23445
23446 assert_indent_guides(
23447 0..11,
23448 vec![
23449 indent_guide(buffer_id, 1, 10, 0),
23450 indent_guide(buffer_id, 2, 5, 1),
23451 indent_guide(buffer_id, 7, 9, 1),
23452 indent_guide(buffer_id, 8, 8, 2),
23453 ],
23454 None,
23455 &mut cx,
23456 );
23457}
23458
23459#[gpui::test]
23460async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
23461 let (buffer_id, mut cx) = setup_indent_guides_editor(
23462 &"
23463 block1
23464 block2
23465 block3
23466 block4
23467 block2
23468 block1
23469 block1"
23470 .unindent(),
23471 cx,
23472 )
23473 .await;
23474
23475 assert_indent_guides(
23476 1..10,
23477 vec![
23478 indent_guide(buffer_id, 1, 4, 0),
23479 indent_guide(buffer_id, 2, 3, 1),
23480 indent_guide(buffer_id, 3, 3, 2),
23481 ],
23482 None,
23483 &mut cx,
23484 );
23485}
23486
23487#[gpui::test]
23488async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
23489 let (buffer_id, mut cx) = setup_indent_guides_editor(
23490 &"
23491 block1
23492 block2
23493 block3
23494
23495 block1
23496 block1"
23497 .unindent(),
23498 cx,
23499 )
23500 .await;
23501
23502 assert_indent_guides(
23503 0..6,
23504 vec![
23505 indent_guide(buffer_id, 1, 2, 0),
23506 indent_guide(buffer_id, 2, 2, 1),
23507 ],
23508 None,
23509 &mut cx,
23510 );
23511}
23512
23513#[gpui::test]
23514async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
23515 let (buffer_id, mut cx) = setup_indent_guides_editor(
23516 &"
23517 function component() {
23518 \treturn (
23519 \t\t\t
23520 \t\t<div>
23521 \t\t\t<abc></abc>
23522 \t\t</div>
23523 \t)
23524 }"
23525 .unindent(),
23526 cx,
23527 )
23528 .await;
23529
23530 assert_indent_guides(
23531 0..8,
23532 vec![
23533 indent_guide(buffer_id, 1, 6, 0),
23534 indent_guide(buffer_id, 2, 5, 1),
23535 indent_guide(buffer_id, 4, 4, 2),
23536 ],
23537 None,
23538 &mut cx,
23539 );
23540}
23541
23542#[gpui::test]
23543async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
23544 let (buffer_id, mut cx) = setup_indent_guides_editor(
23545 &"
23546 function component() {
23547 \treturn (
23548 \t
23549 \t\t<div>
23550 \t\t\t<abc></abc>
23551 \t\t</div>
23552 \t)
23553 }"
23554 .unindent(),
23555 cx,
23556 )
23557 .await;
23558
23559 assert_indent_guides(
23560 0..8,
23561 vec![
23562 indent_guide(buffer_id, 1, 6, 0),
23563 indent_guide(buffer_id, 2, 5, 1),
23564 indent_guide(buffer_id, 4, 4, 2),
23565 ],
23566 None,
23567 &mut cx,
23568 );
23569}
23570
23571#[gpui::test]
23572async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
23573 let (buffer_id, mut cx) = setup_indent_guides_editor(
23574 &"
23575 block1
23576
23577
23578
23579 block2
23580 "
23581 .unindent(),
23582 cx,
23583 )
23584 .await;
23585
23586 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
23587}
23588
23589#[gpui::test]
23590async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
23591 let (buffer_id, mut cx) = setup_indent_guides_editor(
23592 &"
23593 def a:
23594 \tb = 3
23595 \tif True:
23596 \t\tc = 4
23597 \t\td = 5
23598 \tprint(b)
23599 "
23600 .unindent(),
23601 cx,
23602 )
23603 .await;
23604
23605 assert_indent_guides(
23606 0..6,
23607 vec![
23608 indent_guide(buffer_id, 1, 5, 0),
23609 indent_guide(buffer_id, 3, 4, 1),
23610 ],
23611 None,
23612 &mut cx,
23613 );
23614}
23615
23616#[gpui::test]
23617async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
23618 let (buffer_id, mut cx) = setup_indent_guides_editor(
23619 &"
23620 fn main() {
23621 let a = 1;
23622 }"
23623 .unindent(),
23624 cx,
23625 )
23626 .await;
23627
23628 cx.update_editor(|editor, window, cx| {
23629 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23630 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
23631 });
23632 });
23633
23634 assert_indent_guides(
23635 0..3,
23636 vec![indent_guide(buffer_id, 1, 1, 0)],
23637 Some(vec![0]),
23638 &mut cx,
23639 );
23640}
23641
23642#[gpui::test]
23643async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
23644 let (buffer_id, mut cx) = setup_indent_guides_editor(
23645 &"
23646 fn main() {
23647 if 1 == 2 {
23648 let a = 1;
23649 }
23650 }"
23651 .unindent(),
23652 cx,
23653 )
23654 .await;
23655
23656 cx.update_editor(|editor, window, cx| {
23657 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23658 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
23659 });
23660 });
23661 cx.run_until_parked();
23662
23663 assert_indent_guides(
23664 0..4,
23665 vec![
23666 indent_guide(buffer_id, 1, 3, 0),
23667 indent_guide(buffer_id, 2, 2, 1),
23668 ],
23669 Some(vec![1]),
23670 &mut cx,
23671 );
23672
23673 cx.update_editor(|editor, window, cx| {
23674 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23675 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
23676 });
23677 });
23678 cx.run_until_parked();
23679
23680 assert_indent_guides(
23681 0..4,
23682 vec![
23683 indent_guide(buffer_id, 1, 3, 0),
23684 indent_guide(buffer_id, 2, 2, 1),
23685 ],
23686 Some(vec![1]),
23687 &mut cx,
23688 );
23689
23690 cx.update_editor(|editor, window, cx| {
23691 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23692 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
23693 });
23694 });
23695 cx.run_until_parked();
23696
23697 assert_indent_guides(
23698 0..4,
23699 vec![
23700 indent_guide(buffer_id, 1, 3, 0),
23701 indent_guide(buffer_id, 2, 2, 1),
23702 ],
23703 Some(vec![0]),
23704 &mut cx,
23705 );
23706}
23707
23708#[gpui::test]
23709async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
23710 let (buffer_id, mut cx) = setup_indent_guides_editor(
23711 &"
23712 fn main() {
23713 let a = 1;
23714
23715 let b = 2;
23716 }"
23717 .unindent(),
23718 cx,
23719 )
23720 .await;
23721
23722 cx.update_editor(|editor, window, cx| {
23723 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23724 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
23725 });
23726 });
23727
23728 assert_indent_guides(
23729 0..5,
23730 vec![indent_guide(buffer_id, 1, 3, 0)],
23731 Some(vec![0]),
23732 &mut cx,
23733 );
23734}
23735
23736#[gpui::test]
23737async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
23738 let (buffer_id, mut cx) = setup_indent_guides_editor(
23739 &"
23740 def m:
23741 a = 1
23742 pass"
23743 .unindent(),
23744 cx,
23745 )
23746 .await;
23747
23748 cx.update_editor(|editor, window, cx| {
23749 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23750 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
23751 });
23752 });
23753
23754 assert_indent_guides(
23755 0..3,
23756 vec![indent_guide(buffer_id, 1, 2, 0)],
23757 Some(vec![0]),
23758 &mut cx,
23759 );
23760}
23761
23762#[gpui::test]
23763async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
23764 init_test(cx, |_| {});
23765 let mut cx = EditorTestContext::new(cx).await;
23766 let text = indoc! {
23767 "
23768 impl A {
23769 fn b() {
23770 0;
23771 3;
23772 5;
23773 6;
23774 7;
23775 }
23776 }
23777 "
23778 };
23779 let base_text = indoc! {
23780 "
23781 impl A {
23782 fn b() {
23783 0;
23784 1;
23785 2;
23786 3;
23787 4;
23788 }
23789 fn c() {
23790 5;
23791 6;
23792 7;
23793 }
23794 }
23795 "
23796 };
23797
23798 cx.update_editor(|editor, window, cx| {
23799 editor.set_text(text, window, cx);
23800
23801 editor.buffer().update(cx, |multibuffer, cx| {
23802 let buffer = multibuffer.as_singleton().unwrap();
23803 let diff = cx.new(|cx| {
23804 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
23805 });
23806
23807 multibuffer.set_all_diff_hunks_expanded(cx);
23808 multibuffer.add_diff(diff, cx);
23809
23810 buffer.read(cx).remote_id()
23811 })
23812 });
23813 cx.run_until_parked();
23814
23815 cx.assert_state_with_diff(
23816 indoc! { "
23817 impl A {
23818 fn b() {
23819 0;
23820 - 1;
23821 - 2;
23822 3;
23823 - 4;
23824 - }
23825 - fn c() {
23826 5;
23827 6;
23828 7;
23829 }
23830 }
23831 ˇ"
23832 }
23833 .to_string(),
23834 );
23835
23836 let mut actual_guides = cx.update_editor(|editor, window, cx| {
23837 editor
23838 .snapshot(window, cx)
23839 .buffer_snapshot()
23840 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
23841 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
23842 .collect::<Vec<_>>()
23843 });
23844 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
23845 assert_eq!(
23846 actual_guides,
23847 vec![
23848 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
23849 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
23850 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
23851 ]
23852 );
23853}
23854
23855#[gpui::test]
23856async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
23857 init_test(cx, |_| {});
23858 let mut cx = EditorTestContext::new(cx).await;
23859
23860 let diff_base = r#"
23861 a
23862 b
23863 c
23864 "#
23865 .unindent();
23866
23867 cx.set_state(
23868 &r#"
23869 ˇA
23870 b
23871 C
23872 "#
23873 .unindent(),
23874 );
23875 cx.set_head_text(&diff_base);
23876 cx.update_editor(|editor, window, cx| {
23877 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23878 });
23879 executor.run_until_parked();
23880
23881 let both_hunks_expanded = r#"
23882 - a
23883 + ˇA
23884 b
23885 - c
23886 + C
23887 "#
23888 .unindent();
23889
23890 cx.assert_state_with_diff(both_hunks_expanded.clone());
23891
23892 let hunk_ranges = cx.update_editor(|editor, window, cx| {
23893 let snapshot = editor.snapshot(window, cx);
23894 let hunks = editor
23895 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
23896 .collect::<Vec<_>>();
23897 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
23898 hunks
23899 .into_iter()
23900 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
23901 .collect::<Vec<_>>()
23902 });
23903 assert_eq!(hunk_ranges.len(), 2);
23904
23905 cx.update_editor(|editor, _, cx| {
23906 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
23907 });
23908 executor.run_until_parked();
23909
23910 let second_hunk_expanded = r#"
23911 ˇA
23912 b
23913 - c
23914 + C
23915 "#
23916 .unindent();
23917
23918 cx.assert_state_with_diff(second_hunk_expanded);
23919
23920 cx.update_editor(|editor, _, cx| {
23921 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
23922 });
23923 executor.run_until_parked();
23924
23925 cx.assert_state_with_diff(both_hunks_expanded.clone());
23926
23927 cx.update_editor(|editor, _, cx| {
23928 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
23929 });
23930 executor.run_until_parked();
23931
23932 let first_hunk_expanded = r#"
23933 - a
23934 + ˇA
23935 b
23936 C
23937 "#
23938 .unindent();
23939
23940 cx.assert_state_with_diff(first_hunk_expanded);
23941
23942 cx.update_editor(|editor, _, cx| {
23943 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
23944 });
23945 executor.run_until_parked();
23946
23947 cx.assert_state_with_diff(both_hunks_expanded);
23948
23949 cx.set_state(
23950 &r#"
23951 ˇA
23952 b
23953 "#
23954 .unindent(),
23955 );
23956 cx.run_until_parked();
23957
23958 // TODO this cursor position seems bad
23959 cx.assert_state_with_diff(
23960 r#"
23961 - ˇa
23962 + A
23963 b
23964 "#
23965 .unindent(),
23966 );
23967
23968 cx.update_editor(|editor, window, cx| {
23969 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23970 });
23971
23972 cx.assert_state_with_diff(
23973 r#"
23974 - ˇa
23975 + A
23976 b
23977 - c
23978 "#
23979 .unindent(),
23980 );
23981
23982 let hunk_ranges = cx.update_editor(|editor, window, cx| {
23983 let snapshot = editor.snapshot(window, cx);
23984 let hunks = editor
23985 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
23986 .collect::<Vec<_>>();
23987 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
23988 hunks
23989 .into_iter()
23990 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
23991 .collect::<Vec<_>>()
23992 });
23993 assert_eq!(hunk_ranges.len(), 2);
23994
23995 cx.update_editor(|editor, _, cx| {
23996 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
23997 });
23998 executor.run_until_parked();
23999
24000 cx.assert_state_with_diff(
24001 r#"
24002 - ˇa
24003 + A
24004 b
24005 "#
24006 .unindent(),
24007 );
24008}
24009
24010#[gpui::test]
24011async fn test_toggle_deletion_hunk_at_start_of_file(
24012 executor: BackgroundExecutor,
24013 cx: &mut TestAppContext,
24014) {
24015 init_test(cx, |_| {});
24016 let mut cx = EditorTestContext::new(cx).await;
24017
24018 let diff_base = r#"
24019 a
24020 b
24021 c
24022 "#
24023 .unindent();
24024
24025 cx.set_state(
24026 &r#"
24027 ˇb
24028 c
24029 "#
24030 .unindent(),
24031 );
24032 cx.set_head_text(&diff_base);
24033 cx.update_editor(|editor, window, cx| {
24034 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
24035 });
24036 executor.run_until_parked();
24037
24038 let hunk_expanded = r#"
24039 - a
24040 ˇb
24041 c
24042 "#
24043 .unindent();
24044
24045 cx.assert_state_with_diff(hunk_expanded.clone());
24046
24047 let hunk_ranges = cx.update_editor(|editor, window, cx| {
24048 let snapshot = editor.snapshot(window, cx);
24049 let hunks = editor
24050 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
24051 .collect::<Vec<_>>();
24052 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
24053 hunks
24054 .into_iter()
24055 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
24056 .collect::<Vec<_>>()
24057 });
24058 assert_eq!(hunk_ranges.len(), 1);
24059
24060 cx.update_editor(|editor, _, cx| {
24061 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
24062 });
24063 executor.run_until_parked();
24064
24065 let hunk_collapsed = r#"
24066 ˇb
24067 c
24068 "#
24069 .unindent();
24070
24071 cx.assert_state_with_diff(hunk_collapsed);
24072
24073 cx.update_editor(|editor, _, cx| {
24074 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
24075 });
24076 executor.run_until_parked();
24077
24078 cx.assert_state_with_diff(hunk_expanded);
24079}
24080
24081#[gpui::test]
24082async fn test_select_smaller_syntax_node_after_diff_hunk_collapse(
24083 executor: BackgroundExecutor,
24084 cx: &mut TestAppContext,
24085) {
24086 init_test(cx, |_| {});
24087
24088 let mut cx = EditorTestContext::new(cx).await;
24089 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
24090
24091 cx.set_state(
24092 &r#"
24093 fn main() {
24094 let x = ˇ1;
24095 }
24096 "#
24097 .unindent(),
24098 );
24099
24100 let diff_base = r#"
24101 fn removed_one() {
24102 println!("this function was deleted");
24103 }
24104
24105 fn removed_two() {
24106 println!("this function was also deleted");
24107 }
24108
24109 fn main() {
24110 let x = 1;
24111 }
24112 "#
24113 .unindent();
24114 cx.set_head_text(&diff_base);
24115 executor.run_until_parked();
24116
24117 cx.update_editor(|editor, window, cx| {
24118 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
24119 });
24120 executor.run_until_parked();
24121
24122 cx.update_editor(|editor, window, cx| {
24123 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
24124 });
24125
24126 cx.update_editor(|editor, window, cx| {
24127 editor.collapse_all_diff_hunks(&CollapseAllDiffHunks, window, cx);
24128 });
24129 executor.run_until_parked();
24130
24131 cx.update_editor(|editor, window, cx| {
24132 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
24133 });
24134}
24135
24136#[gpui::test]
24137async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
24138 executor: BackgroundExecutor,
24139 cx: &mut TestAppContext,
24140) {
24141 init_test(cx, |_| {});
24142 let mut cx = EditorTestContext::new(cx).await;
24143
24144 cx.set_state("ˇnew\nsecond\nthird\n");
24145 cx.set_head_text("old\nsecond\nthird\n");
24146 cx.update_editor(|editor, window, cx| {
24147 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
24148 });
24149 executor.run_until_parked();
24150 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
24151
24152 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
24153 cx.update_editor(|editor, window, cx| {
24154 let snapshot = editor.snapshot(window, cx);
24155 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
24156 let hunks = editor
24157 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
24158 .collect::<Vec<_>>();
24159 assert_eq!(hunks.len(), 1);
24160 let hunk_range = Anchor::range_in_buffer(excerpt_id, hunks[0].buffer_range.clone());
24161 editor.toggle_single_diff_hunk(hunk_range, cx)
24162 });
24163 executor.run_until_parked();
24164 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
24165
24166 // Keep the editor scrolled to the top so the full hunk remains visible.
24167 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
24168}
24169
24170#[gpui::test]
24171async fn test_display_diff_hunks(cx: &mut TestAppContext) {
24172 init_test(cx, |_| {});
24173
24174 let fs = FakeFs::new(cx.executor());
24175 fs.insert_tree(
24176 path!("/test"),
24177 json!({
24178 ".git": {},
24179 "file-1": "ONE\n",
24180 "file-2": "TWO\n",
24181 "file-3": "THREE\n",
24182 }),
24183 )
24184 .await;
24185
24186 fs.set_head_for_repo(
24187 path!("/test/.git").as_ref(),
24188 &[
24189 ("file-1", "one\n".into()),
24190 ("file-2", "two\n".into()),
24191 ("file-3", "three\n".into()),
24192 ],
24193 "deadbeef",
24194 );
24195
24196 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
24197 let mut buffers = vec![];
24198 for i in 1..=3 {
24199 let buffer = project
24200 .update(cx, |project, cx| {
24201 let path = format!(path!("/test/file-{}"), i);
24202 project.open_local_buffer(path, cx)
24203 })
24204 .await
24205 .unwrap();
24206 buffers.push(buffer);
24207 }
24208
24209 let multibuffer = cx.new(|cx| {
24210 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
24211 multibuffer.set_all_diff_hunks_expanded(cx);
24212 for buffer in &buffers {
24213 let snapshot = buffer.read(cx).snapshot();
24214 multibuffer.set_excerpts_for_path(
24215 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
24216 buffer.clone(),
24217 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
24218 2,
24219 cx,
24220 );
24221 }
24222 multibuffer
24223 });
24224
24225 let editor = cx.add_window(|window, cx| {
24226 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
24227 });
24228 cx.run_until_parked();
24229
24230 let snapshot = editor
24231 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
24232 .unwrap();
24233 let hunks = snapshot
24234 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
24235 .map(|hunk| match hunk {
24236 DisplayDiffHunk::Unfolded {
24237 display_row_range, ..
24238 } => display_row_range,
24239 DisplayDiffHunk::Folded { .. } => unreachable!(),
24240 })
24241 .collect::<Vec<_>>();
24242 assert_eq!(
24243 hunks,
24244 [
24245 DisplayRow(2)..DisplayRow(4),
24246 DisplayRow(7)..DisplayRow(9),
24247 DisplayRow(12)..DisplayRow(14),
24248 ]
24249 );
24250}
24251
24252#[gpui::test]
24253async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
24254 init_test(cx, |_| {});
24255
24256 let mut cx = EditorTestContext::new(cx).await;
24257 cx.set_head_text(indoc! { "
24258 one
24259 two
24260 three
24261 four
24262 five
24263 "
24264 });
24265 cx.set_index_text(indoc! { "
24266 one
24267 two
24268 three
24269 four
24270 five
24271 "
24272 });
24273 cx.set_state(indoc! {"
24274 one
24275 TWO
24276 ˇTHREE
24277 FOUR
24278 five
24279 "});
24280 cx.run_until_parked();
24281 cx.update_editor(|editor, window, cx| {
24282 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
24283 });
24284 cx.run_until_parked();
24285 cx.assert_index_text(Some(indoc! {"
24286 one
24287 TWO
24288 THREE
24289 FOUR
24290 five
24291 "}));
24292 cx.set_state(indoc! { "
24293 one
24294 TWO
24295 ˇTHREE-HUNDRED
24296 FOUR
24297 five
24298 "});
24299 cx.run_until_parked();
24300 cx.update_editor(|editor, window, cx| {
24301 let snapshot = editor.snapshot(window, cx);
24302 let hunks = editor
24303 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
24304 .collect::<Vec<_>>();
24305 assert_eq!(hunks.len(), 1);
24306 assert_eq!(
24307 hunks[0].status(),
24308 DiffHunkStatus {
24309 kind: DiffHunkStatusKind::Modified,
24310 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
24311 }
24312 );
24313
24314 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
24315 });
24316 cx.run_until_parked();
24317 cx.assert_index_text(Some(indoc! {"
24318 one
24319 TWO
24320 THREE-HUNDRED
24321 FOUR
24322 five
24323 "}));
24324}
24325
24326#[gpui::test]
24327fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
24328 init_test(cx, |_| {});
24329
24330 let editor = cx.add_window(|window, cx| {
24331 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
24332 build_editor(buffer, window, cx)
24333 });
24334
24335 let render_args = Arc::new(Mutex::new(None));
24336 let snapshot = editor
24337 .update(cx, |editor, window, cx| {
24338 let snapshot = editor.buffer().read(cx).snapshot(cx);
24339 let range =
24340 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
24341
24342 struct RenderArgs {
24343 row: MultiBufferRow,
24344 folded: bool,
24345 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
24346 }
24347
24348 let crease = Crease::inline(
24349 range,
24350 FoldPlaceholder::test(),
24351 {
24352 let toggle_callback = render_args.clone();
24353 move |row, folded, callback, _window, _cx| {
24354 *toggle_callback.lock() = Some(RenderArgs {
24355 row,
24356 folded,
24357 callback,
24358 });
24359 div()
24360 }
24361 },
24362 |_row, _folded, _window, _cx| div(),
24363 );
24364
24365 editor.insert_creases(Some(crease), cx);
24366 let snapshot = editor.snapshot(window, cx);
24367 let _div =
24368 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
24369 snapshot
24370 })
24371 .unwrap();
24372
24373 let render_args = render_args.lock().take().unwrap();
24374 assert_eq!(render_args.row, MultiBufferRow(1));
24375 assert!(!render_args.folded);
24376 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
24377
24378 cx.update_window(*editor, |_, window, cx| {
24379 (render_args.callback)(true, window, cx)
24380 })
24381 .unwrap();
24382 let snapshot = editor
24383 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
24384 .unwrap();
24385 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
24386
24387 cx.update_window(*editor, |_, window, cx| {
24388 (render_args.callback)(false, window, cx)
24389 })
24390 .unwrap();
24391 let snapshot = editor
24392 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
24393 .unwrap();
24394 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
24395}
24396
24397#[gpui::test]
24398async fn test_input_text(cx: &mut TestAppContext) {
24399 init_test(cx, |_| {});
24400 let mut cx = EditorTestContext::new(cx).await;
24401
24402 cx.set_state(
24403 &r#"ˇone
24404 two
24405
24406 three
24407 fourˇ
24408 five
24409
24410 siˇx"#
24411 .unindent(),
24412 );
24413
24414 cx.dispatch_action(HandleInput(String::new()));
24415 cx.assert_editor_state(
24416 &r#"ˇone
24417 two
24418
24419 three
24420 fourˇ
24421 five
24422
24423 siˇx"#
24424 .unindent(),
24425 );
24426
24427 cx.dispatch_action(HandleInput("AAAA".to_string()));
24428 cx.assert_editor_state(
24429 &r#"AAAAˇone
24430 two
24431
24432 three
24433 fourAAAAˇ
24434 five
24435
24436 siAAAAˇx"#
24437 .unindent(),
24438 );
24439}
24440
24441#[gpui::test]
24442async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
24443 init_test(cx, |_| {});
24444
24445 let mut cx = EditorTestContext::new(cx).await;
24446 cx.set_state(
24447 r#"let foo = 1;
24448let foo = 2;
24449let foo = 3;
24450let fooˇ = 4;
24451let foo = 5;
24452let foo = 6;
24453let foo = 7;
24454let foo = 8;
24455let foo = 9;
24456let foo = 10;
24457let foo = 11;
24458let foo = 12;
24459let foo = 13;
24460let foo = 14;
24461let foo = 15;"#,
24462 );
24463
24464 cx.update_editor(|e, window, cx| {
24465 assert_eq!(
24466 e.next_scroll_position,
24467 NextScrollCursorCenterTopBottom::Center,
24468 "Default next scroll direction is center",
24469 );
24470
24471 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
24472 assert_eq!(
24473 e.next_scroll_position,
24474 NextScrollCursorCenterTopBottom::Top,
24475 "After center, next scroll direction should be top",
24476 );
24477
24478 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
24479 assert_eq!(
24480 e.next_scroll_position,
24481 NextScrollCursorCenterTopBottom::Bottom,
24482 "After top, next scroll direction should be bottom",
24483 );
24484
24485 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
24486 assert_eq!(
24487 e.next_scroll_position,
24488 NextScrollCursorCenterTopBottom::Center,
24489 "After bottom, scrolling should start over",
24490 );
24491
24492 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
24493 assert_eq!(
24494 e.next_scroll_position,
24495 NextScrollCursorCenterTopBottom::Top,
24496 "Scrolling continues if retriggered fast enough"
24497 );
24498 });
24499
24500 cx.executor()
24501 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
24502 cx.executor().run_until_parked();
24503 cx.update_editor(|e, _, _| {
24504 assert_eq!(
24505 e.next_scroll_position,
24506 NextScrollCursorCenterTopBottom::Center,
24507 "If scrolling is not triggered fast enough, it should reset"
24508 );
24509 });
24510}
24511
24512#[gpui::test]
24513async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
24514 init_test(cx, |_| {});
24515 let mut cx = EditorLspTestContext::new_rust(
24516 lsp::ServerCapabilities {
24517 definition_provider: Some(lsp::OneOf::Left(true)),
24518 references_provider: Some(lsp::OneOf::Left(true)),
24519 ..lsp::ServerCapabilities::default()
24520 },
24521 cx,
24522 )
24523 .await;
24524
24525 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
24526 let go_to_definition = cx
24527 .lsp
24528 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
24529 move |params, _| async move {
24530 if empty_go_to_definition {
24531 Ok(None)
24532 } else {
24533 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
24534 uri: params.text_document_position_params.text_document.uri,
24535 range: lsp::Range::new(
24536 lsp::Position::new(4, 3),
24537 lsp::Position::new(4, 6),
24538 ),
24539 })))
24540 }
24541 },
24542 );
24543 let references = cx
24544 .lsp
24545 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
24546 Ok(Some(vec![lsp::Location {
24547 uri: params.text_document_position.text_document.uri,
24548 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
24549 }]))
24550 });
24551 (go_to_definition, references)
24552 };
24553
24554 cx.set_state(
24555 &r#"fn one() {
24556 let mut a = ˇtwo();
24557 }
24558
24559 fn two() {}"#
24560 .unindent(),
24561 );
24562 set_up_lsp_handlers(false, &mut cx);
24563 let navigated = cx
24564 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
24565 .await
24566 .expect("Failed to navigate to definition");
24567 assert_eq!(
24568 navigated,
24569 Navigated::Yes,
24570 "Should have navigated to definition from the GetDefinition response"
24571 );
24572 cx.assert_editor_state(
24573 &r#"fn one() {
24574 let mut a = two();
24575 }
24576
24577 fn «twoˇ»() {}"#
24578 .unindent(),
24579 );
24580
24581 let editors = cx.update_workspace(|workspace, _, cx| {
24582 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
24583 });
24584 cx.update_editor(|_, _, test_editor_cx| {
24585 assert_eq!(
24586 editors.len(),
24587 1,
24588 "Initially, only one, test, editor should be open in the workspace"
24589 );
24590 assert_eq!(
24591 test_editor_cx.entity(),
24592 editors.last().expect("Asserted len is 1").clone()
24593 );
24594 });
24595
24596 set_up_lsp_handlers(true, &mut cx);
24597 let navigated = cx
24598 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
24599 .await
24600 .expect("Failed to navigate to lookup references");
24601 assert_eq!(
24602 navigated,
24603 Navigated::Yes,
24604 "Should have navigated to references as a fallback after empty GoToDefinition response"
24605 );
24606 // We should not change the selections in the existing file,
24607 // if opening another milti buffer with the references
24608 cx.assert_editor_state(
24609 &r#"fn one() {
24610 let mut a = two();
24611 }
24612
24613 fn «twoˇ»() {}"#
24614 .unindent(),
24615 );
24616 let editors = cx.update_workspace(|workspace, _, cx| {
24617 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
24618 });
24619 cx.update_editor(|_, _, test_editor_cx| {
24620 assert_eq!(
24621 editors.len(),
24622 2,
24623 "After falling back to references search, we open a new editor with the results"
24624 );
24625 let references_fallback_text = editors
24626 .into_iter()
24627 .find(|new_editor| *new_editor != test_editor_cx.entity())
24628 .expect("Should have one non-test editor now")
24629 .read(test_editor_cx)
24630 .text(test_editor_cx);
24631 assert_eq!(
24632 references_fallback_text, "fn one() {\n let mut a = two();\n}",
24633 "Should use the range from the references response and not the GoToDefinition one"
24634 );
24635 });
24636}
24637
24638#[gpui::test]
24639async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
24640 init_test(cx, |_| {});
24641 cx.update(|cx| {
24642 let mut editor_settings = EditorSettings::get_global(cx).clone();
24643 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
24644 EditorSettings::override_global(editor_settings, cx);
24645 });
24646 let mut cx = EditorLspTestContext::new_rust(
24647 lsp::ServerCapabilities {
24648 definition_provider: Some(lsp::OneOf::Left(true)),
24649 references_provider: Some(lsp::OneOf::Left(true)),
24650 ..lsp::ServerCapabilities::default()
24651 },
24652 cx,
24653 )
24654 .await;
24655 let original_state = r#"fn one() {
24656 let mut a = ˇtwo();
24657 }
24658
24659 fn two() {}"#
24660 .unindent();
24661 cx.set_state(&original_state);
24662
24663 let mut go_to_definition = cx
24664 .lsp
24665 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
24666 move |_, _| async move { Ok(None) },
24667 );
24668 let _references = cx
24669 .lsp
24670 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
24671 panic!("Should not call for references with no go to definition fallback")
24672 });
24673
24674 let navigated = cx
24675 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
24676 .await
24677 .expect("Failed to navigate to lookup references");
24678 go_to_definition
24679 .next()
24680 .await
24681 .expect("Should have called the go_to_definition handler");
24682
24683 assert_eq!(
24684 navigated,
24685 Navigated::No,
24686 "Should have navigated to references as a fallback after empty GoToDefinition response"
24687 );
24688 cx.assert_editor_state(&original_state);
24689 let editors = cx.update_workspace(|workspace, _, cx| {
24690 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
24691 });
24692 cx.update_editor(|_, _, _| {
24693 assert_eq!(
24694 editors.len(),
24695 1,
24696 "After unsuccessful fallback, no other editor should have been opened"
24697 );
24698 });
24699}
24700
24701#[gpui::test]
24702async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
24703 init_test(cx, |_| {});
24704 let mut cx = EditorLspTestContext::new_rust(
24705 lsp::ServerCapabilities {
24706 references_provider: Some(lsp::OneOf::Left(true)),
24707 ..lsp::ServerCapabilities::default()
24708 },
24709 cx,
24710 )
24711 .await;
24712
24713 cx.set_state(
24714 &r#"
24715 fn one() {
24716 let mut a = two();
24717 }
24718
24719 fn ˇtwo() {}"#
24720 .unindent(),
24721 );
24722 cx.lsp
24723 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
24724 Ok(Some(vec![
24725 lsp::Location {
24726 uri: params.text_document_position.text_document.uri.clone(),
24727 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
24728 },
24729 lsp::Location {
24730 uri: params.text_document_position.text_document.uri,
24731 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
24732 },
24733 ]))
24734 });
24735 let navigated = cx
24736 .update_editor(|editor, window, cx| {
24737 editor.find_all_references(&FindAllReferences::default(), window, cx)
24738 })
24739 .unwrap()
24740 .await
24741 .expect("Failed to navigate to references");
24742 assert_eq!(
24743 navigated,
24744 Navigated::Yes,
24745 "Should have navigated to references from the FindAllReferences response"
24746 );
24747 cx.assert_editor_state(
24748 &r#"fn one() {
24749 let mut a = two();
24750 }
24751
24752 fn ˇtwo() {}"#
24753 .unindent(),
24754 );
24755
24756 let editors = cx.update_workspace(|workspace, _, cx| {
24757 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
24758 });
24759 cx.update_editor(|_, _, _| {
24760 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
24761 });
24762
24763 cx.set_state(
24764 &r#"fn one() {
24765 let mut a = ˇtwo();
24766 }
24767
24768 fn two() {}"#
24769 .unindent(),
24770 );
24771 let navigated = cx
24772 .update_editor(|editor, window, cx| {
24773 editor.find_all_references(&FindAllReferences::default(), window, cx)
24774 })
24775 .unwrap()
24776 .await
24777 .expect("Failed to navigate to references");
24778 assert_eq!(
24779 navigated,
24780 Navigated::Yes,
24781 "Should have navigated to references from the FindAllReferences response"
24782 );
24783 cx.assert_editor_state(
24784 &r#"fn one() {
24785 let mut a = ˇtwo();
24786 }
24787
24788 fn two() {}"#
24789 .unindent(),
24790 );
24791 let editors = cx.update_workspace(|workspace, _, cx| {
24792 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
24793 });
24794 cx.update_editor(|_, _, _| {
24795 assert_eq!(
24796 editors.len(),
24797 2,
24798 "should have re-used the previous multibuffer"
24799 );
24800 });
24801
24802 cx.set_state(
24803 &r#"fn one() {
24804 let mut a = ˇtwo();
24805 }
24806 fn three() {}
24807 fn two() {}"#
24808 .unindent(),
24809 );
24810 cx.lsp
24811 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
24812 Ok(Some(vec![
24813 lsp::Location {
24814 uri: params.text_document_position.text_document.uri.clone(),
24815 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
24816 },
24817 lsp::Location {
24818 uri: params.text_document_position.text_document.uri,
24819 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
24820 },
24821 ]))
24822 });
24823 let navigated = cx
24824 .update_editor(|editor, window, cx| {
24825 editor.find_all_references(&FindAllReferences::default(), window, cx)
24826 })
24827 .unwrap()
24828 .await
24829 .expect("Failed to navigate to references");
24830 assert_eq!(
24831 navigated,
24832 Navigated::Yes,
24833 "Should have navigated to references from the FindAllReferences response"
24834 );
24835 cx.assert_editor_state(
24836 &r#"fn one() {
24837 let mut a = ˇtwo();
24838 }
24839 fn three() {}
24840 fn two() {}"#
24841 .unindent(),
24842 );
24843 let editors = cx.update_workspace(|workspace, _, cx| {
24844 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
24845 });
24846 cx.update_editor(|_, _, _| {
24847 assert_eq!(
24848 editors.len(),
24849 3,
24850 "should have used a new multibuffer as offsets changed"
24851 );
24852 });
24853}
24854#[gpui::test]
24855async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
24856 init_test(cx, |_| {});
24857
24858 let language = Arc::new(Language::new(
24859 LanguageConfig::default(),
24860 Some(tree_sitter_rust::LANGUAGE.into()),
24861 ));
24862
24863 let text = r#"
24864 #[cfg(test)]
24865 mod tests() {
24866 #[test]
24867 fn runnable_1() {
24868 let a = 1;
24869 }
24870
24871 #[test]
24872 fn runnable_2() {
24873 let a = 1;
24874 let b = 2;
24875 }
24876 }
24877 "#
24878 .unindent();
24879
24880 let fs = FakeFs::new(cx.executor());
24881 fs.insert_file("/file.rs", Default::default()).await;
24882
24883 let project = Project::test(fs, ["/a".as_ref()], cx).await;
24884 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
24885 let cx = &mut VisualTestContext::from_window(*window, cx);
24886 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
24887 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
24888
24889 let editor = cx.new_window_entity(|window, cx| {
24890 Editor::new(
24891 EditorMode::full(),
24892 multi_buffer,
24893 Some(project.clone()),
24894 window,
24895 cx,
24896 )
24897 });
24898
24899 editor.update_in(cx, |editor, window, cx| {
24900 let snapshot = editor.buffer().read(cx).snapshot(cx);
24901 editor.runnables.insert(
24902 buffer.read(cx).remote_id(),
24903 3,
24904 buffer.read(cx).version(),
24905 RunnableTasks {
24906 templates: Vec::new(),
24907 offset: snapshot.anchor_before(MultiBufferOffset(43)),
24908 column: 0,
24909 extra_variables: HashMap::default(),
24910 context_range: BufferOffset(43)..BufferOffset(85),
24911 },
24912 );
24913 editor.runnables.insert(
24914 buffer.read(cx).remote_id(),
24915 8,
24916 buffer.read(cx).version(),
24917 RunnableTasks {
24918 templates: Vec::new(),
24919 offset: snapshot.anchor_before(MultiBufferOffset(86)),
24920 column: 0,
24921 extra_variables: HashMap::default(),
24922 context_range: BufferOffset(86)..BufferOffset(191),
24923 },
24924 );
24925
24926 // Test finding task when cursor is inside function body
24927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24928 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
24929 });
24930 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
24931 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
24932
24933 // Test finding task when cursor is on function name
24934 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24935 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
24936 });
24937 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
24938 assert_eq!(row, 8, "Should find task when cursor is on function name");
24939 });
24940}
24941
24942#[gpui::test]
24943async fn test_folding_buffers(cx: &mut TestAppContext) {
24944 init_test(cx, |_| {});
24945
24946 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
24947 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
24948 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
24949
24950 let fs = FakeFs::new(cx.executor());
24951 fs.insert_tree(
24952 path!("/a"),
24953 json!({
24954 "first.rs": sample_text_1,
24955 "second.rs": sample_text_2,
24956 "third.rs": sample_text_3,
24957 }),
24958 )
24959 .await;
24960 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24961 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
24962 let cx = &mut VisualTestContext::from_window(*window, cx);
24963 let worktree = project.update(cx, |project, cx| {
24964 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
24965 assert_eq!(worktrees.len(), 1);
24966 worktrees.pop().unwrap()
24967 });
24968 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
24969
24970 let buffer_1 = project
24971 .update(cx, |project, cx| {
24972 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
24973 })
24974 .await
24975 .unwrap();
24976 let buffer_2 = project
24977 .update(cx, |project, cx| {
24978 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
24979 })
24980 .await
24981 .unwrap();
24982 let buffer_3 = project
24983 .update(cx, |project, cx| {
24984 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
24985 })
24986 .await
24987 .unwrap();
24988
24989 let multi_buffer = cx.new(|cx| {
24990 let mut multi_buffer = MultiBuffer::new(ReadWrite);
24991 multi_buffer.set_excerpts_for_path(
24992 PathKey::sorted(0),
24993 buffer_1.clone(),
24994 [
24995 Point::new(0, 0)..Point::new(2, 0),
24996 Point::new(5, 0)..Point::new(6, 0),
24997 Point::new(9, 0)..Point::new(10, 4),
24998 ],
24999 0,
25000 cx,
25001 );
25002 multi_buffer.set_excerpts_for_path(
25003 PathKey::sorted(1),
25004 buffer_2.clone(),
25005 [
25006 Point::new(0, 0)..Point::new(2, 0),
25007 Point::new(5, 0)..Point::new(6, 0),
25008 Point::new(9, 0)..Point::new(10, 4),
25009 ],
25010 0,
25011 cx,
25012 );
25013 multi_buffer.set_excerpts_for_path(
25014 PathKey::sorted(2),
25015 buffer_3.clone(),
25016 [
25017 Point::new(0, 0)..Point::new(2, 0),
25018 Point::new(5, 0)..Point::new(6, 0),
25019 Point::new(9, 0)..Point::new(10, 4),
25020 ],
25021 0,
25022 cx,
25023 );
25024 multi_buffer
25025 });
25026 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
25027 Editor::new(
25028 EditorMode::full(),
25029 multi_buffer.clone(),
25030 Some(project.clone()),
25031 window,
25032 cx,
25033 )
25034 });
25035
25036 assert_eq!(
25037 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25038 "\n\naaaa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n1111\n2222\n\n5555",
25039 );
25040
25041 multi_buffer_editor.update(cx, |editor, cx| {
25042 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
25043 });
25044 assert_eq!(
25045 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25046 "\n\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n1111\n2222\n\n5555",
25047 "After folding the first buffer, its text should not be displayed"
25048 );
25049
25050 multi_buffer_editor.update(cx, |editor, cx| {
25051 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
25052 });
25053 assert_eq!(
25054 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25055 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n1111\n2222\n\n5555",
25056 "After folding the second buffer, its text should not be displayed"
25057 );
25058
25059 multi_buffer_editor.update(cx, |editor, cx| {
25060 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
25061 });
25062 assert_eq!(
25063 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25064 "\n\n\n\n\n",
25065 "After folding the third buffer, its text should not be displayed"
25066 );
25067
25068 // Emulate selection inside the fold logic, that should work
25069 multi_buffer_editor.update_in(cx, |editor, window, cx| {
25070 editor
25071 .snapshot(window, cx)
25072 .next_line_boundary(Point::new(0, 4));
25073 });
25074
25075 multi_buffer_editor.update(cx, |editor, cx| {
25076 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
25077 });
25078 assert_eq!(
25079 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25080 "\n\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n",
25081 "After unfolding the second buffer, its text should be displayed"
25082 );
25083
25084 // Typing inside of buffer 1 causes that buffer to be unfolded.
25085 multi_buffer_editor.update_in(cx, |editor, window, cx| {
25086 assert_eq!(
25087 multi_buffer
25088 .read(cx)
25089 .snapshot(cx)
25090 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
25091 .collect::<String>(),
25092 "bbbb"
25093 );
25094 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
25095 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
25096 });
25097 editor.handle_input("B", window, cx);
25098 });
25099
25100 assert_eq!(
25101 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25102 "\n\naaaa\nBbbbb\ncccc\n\nffff\ngggg\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n",
25103 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
25104 );
25105
25106 multi_buffer_editor.update(cx, |editor, cx| {
25107 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
25108 });
25109 assert_eq!(
25110 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25111 "\n\naaaa\nBbbbb\ncccc\n\nffff\ngggg\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n1111\n2222\n\n5555",
25112 "After unfolding the all buffers, all original text should be displayed"
25113 );
25114}
25115
25116#[gpui::test]
25117async fn test_folded_buffers_cleared_on_excerpts_removed(cx: &mut TestAppContext) {
25118 init_test(cx, |_| {});
25119
25120 let fs = FakeFs::new(cx.executor());
25121 fs.insert_tree(
25122 path!("/root"),
25123 json!({
25124 "file_a.txt": "File A\nFile A\nFile A",
25125 "file_b.txt": "File B\nFile B\nFile B",
25126 }),
25127 )
25128 .await;
25129
25130 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
25131 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25132 let cx = &mut VisualTestContext::from_window(*window, cx);
25133 let worktree = project.update(cx, |project, cx| {
25134 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
25135 assert_eq!(worktrees.len(), 1);
25136 worktrees.pop().unwrap()
25137 });
25138 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
25139
25140 let buffer_a = project
25141 .update(cx, |project, cx| {
25142 project.open_buffer((worktree_id, rel_path("file_a.txt")), cx)
25143 })
25144 .await
25145 .unwrap();
25146 let buffer_b = project
25147 .update(cx, |project, cx| {
25148 project.open_buffer((worktree_id, rel_path("file_b.txt")), cx)
25149 })
25150 .await
25151 .unwrap();
25152
25153 let multi_buffer = cx.new(|cx| {
25154 let mut multi_buffer = MultiBuffer::new(ReadWrite);
25155 let range_a = Point::new(0, 0)..Point::new(2, 4);
25156 let range_b = Point::new(0, 0)..Point::new(2, 4);
25157
25158 multi_buffer.set_excerpts_for_path(PathKey::sorted(0), buffer_a.clone(), [range_a], 0, cx);
25159 multi_buffer.set_excerpts_for_path(PathKey::sorted(1), buffer_b.clone(), [range_b], 0, cx);
25160 multi_buffer
25161 });
25162
25163 let editor = cx.new_window_entity(|window, cx| {
25164 Editor::new(
25165 EditorMode::full(),
25166 multi_buffer.clone(),
25167 Some(project.clone()),
25168 window,
25169 cx,
25170 )
25171 });
25172
25173 editor.update(cx, |editor, cx| {
25174 editor.fold_buffer(buffer_a.read(cx).remote_id(), cx);
25175 });
25176 assert!(editor.update(cx, |editor, cx| editor.has_any_buffer_folded(cx)));
25177
25178 // When the excerpts for `buffer_a` are removed, a
25179 // `multi_buffer::Event::ExcerptsRemoved` event is emitted, which should be
25180 // picked up by the editor and update the display map accordingly.
25181 multi_buffer.update(cx, |multi_buffer, cx| {
25182 multi_buffer.remove_excerpts_for_path(PathKey::sorted(0), cx)
25183 });
25184 assert!(!editor.update(cx, |editor, cx| editor.has_any_buffer_folded(cx)));
25185}
25186
25187#[gpui::test]
25188async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
25189 init_test(cx, |_| {});
25190
25191 let sample_text_1 = "1111\n2222\n3333".to_string();
25192 let sample_text_2 = "4444\n5555\n6666".to_string();
25193 let sample_text_3 = "7777\n8888\n9999".to_string();
25194
25195 let fs = FakeFs::new(cx.executor());
25196 fs.insert_tree(
25197 path!("/a"),
25198 json!({
25199 "first.rs": sample_text_1,
25200 "second.rs": sample_text_2,
25201 "third.rs": sample_text_3,
25202 }),
25203 )
25204 .await;
25205 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25206 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25207 let cx = &mut VisualTestContext::from_window(*window, cx);
25208 let worktree = project.update(cx, |project, cx| {
25209 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
25210 assert_eq!(worktrees.len(), 1);
25211 worktrees.pop().unwrap()
25212 });
25213 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
25214
25215 let buffer_1 = project
25216 .update(cx, |project, cx| {
25217 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
25218 })
25219 .await
25220 .unwrap();
25221 let buffer_2 = project
25222 .update(cx, |project, cx| {
25223 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
25224 })
25225 .await
25226 .unwrap();
25227 let buffer_3 = project
25228 .update(cx, |project, cx| {
25229 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
25230 })
25231 .await
25232 .unwrap();
25233
25234 let multi_buffer = cx.new(|cx| {
25235 let mut multi_buffer = MultiBuffer::new(ReadWrite);
25236 multi_buffer.set_excerpts_for_path(
25237 PathKey::sorted(0),
25238 buffer_1.clone(),
25239 [Point::new(0, 0)..Point::new(3, 0)],
25240 0,
25241 cx,
25242 );
25243 multi_buffer.set_excerpts_for_path(
25244 PathKey::sorted(1),
25245 buffer_2.clone(),
25246 [Point::new(0, 0)..Point::new(3, 0)],
25247 0,
25248 cx,
25249 );
25250 multi_buffer.set_excerpts_for_path(
25251 PathKey::sorted(2),
25252 buffer_3.clone(),
25253 [Point::new(0, 0)..Point::new(3, 0)],
25254 0,
25255 cx,
25256 );
25257 multi_buffer
25258 });
25259
25260 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
25261 Editor::new(
25262 EditorMode::full(),
25263 multi_buffer,
25264 Some(project.clone()),
25265 window,
25266 cx,
25267 )
25268 });
25269
25270 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
25271 assert_eq!(
25272 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25273 full_text,
25274 );
25275
25276 multi_buffer_editor.update(cx, |editor, cx| {
25277 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
25278 });
25279 assert_eq!(
25280 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25281 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
25282 "After folding the first buffer, its text should not be displayed"
25283 );
25284
25285 multi_buffer_editor.update(cx, |editor, cx| {
25286 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
25287 });
25288
25289 assert_eq!(
25290 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25291 "\n\n\n\n\n\n7777\n8888\n9999",
25292 "After folding the second buffer, its text should not be displayed"
25293 );
25294
25295 multi_buffer_editor.update(cx, |editor, cx| {
25296 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
25297 });
25298 assert_eq!(
25299 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25300 "\n\n\n\n\n",
25301 "After folding the third buffer, its text should not be displayed"
25302 );
25303
25304 multi_buffer_editor.update(cx, |editor, cx| {
25305 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
25306 });
25307 assert_eq!(
25308 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25309 "\n\n\n\n4444\n5555\n6666\n\n",
25310 "After unfolding the second buffer, its text should be displayed"
25311 );
25312
25313 multi_buffer_editor.update(cx, |editor, cx| {
25314 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
25315 });
25316 assert_eq!(
25317 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25318 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
25319 "After unfolding the first buffer, its text should be displayed"
25320 );
25321
25322 multi_buffer_editor.update(cx, |editor, cx| {
25323 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
25324 });
25325 assert_eq!(
25326 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25327 full_text,
25328 "After unfolding all buffers, all original text should be displayed"
25329 );
25330}
25331
25332#[gpui::test]
25333async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
25334 init_test(cx, |_| {});
25335
25336 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
25337
25338 let fs = FakeFs::new(cx.executor());
25339 fs.insert_tree(
25340 path!("/a"),
25341 json!({
25342 "main.rs": sample_text,
25343 }),
25344 )
25345 .await;
25346 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25347 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25348 let cx = &mut VisualTestContext::from_window(*window, cx);
25349 let worktree = project.update(cx, |project, cx| {
25350 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
25351 assert_eq!(worktrees.len(), 1);
25352 worktrees.pop().unwrap()
25353 });
25354 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
25355
25356 let buffer_1 = project
25357 .update(cx, |project, cx| {
25358 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
25359 })
25360 .await
25361 .unwrap();
25362
25363 let multi_buffer = cx.new(|cx| {
25364 let mut multi_buffer = MultiBuffer::new(ReadWrite);
25365 multi_buffer.set_excerpts_for_path(
25366 PathKey::sorted(0),
25367 buffer_1.clone(),
25368 [Point::new(0, 0)
25369 ..Point::new(
25370 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
25371 0,
25372 )],
25373 0,
25374 cx,
25375 );
25376 multi_buffer
25377 });
25378 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
25379 Editor::new(
25380 EditorMode::full(),
25381 multi_buffer,
25382 Some(project.clone()),
25383 window,
25384 cx,
25385 )
25386 });
25387
25388 let selection_range = Point::new(1, 0)..Point::new(2, 0);
25389 multi_buffer_editor.update_in(cx, |editor, window, cx| {
25390 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
25391 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
25392 editor.highlight_text(
25393 HighlightKey::Editor,
25394 vec![highlight_range.clone()],
25395 HighlightStyle::color(Hsla::green()),
25396 cx,
25397 );
25398 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25399 s.select_ranges(Some(highlight_range))
25400 });
25401 });
25402
25403 let full_text = format!("\n\n{sample_text}");
25404 assert_eq!(
25405 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25406 full_text,
25407 );
25408}
25409
25410#[gpui::test]
25411async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
25412 init_test(cx, |_| {});
25413 cx.update(|cx| {
25414 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
25415 "keymaps/default-linux.json",
25416 cx,
25417 )
25418 .unwrap();
25419 cx.bind_keys(default_key_bindings);
25420 });
25421
25422 let (editor, cx) = cx.add_window_view(|window, cx| {
25423 let multi_buffer = MultiBuffer::build_multi(
25424 [
25425 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
25426 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
25427 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
25428 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
25429 ],
25430 cx,
25431 );
25432 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
25433
25434 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
25435 // fold all but the second buffer, so that we test navigating between two
25436 // adjacent folded buffers, as well as folded buffers at the start and
25437 // end the multibuffer
25438 editor.fold_buffer(buffer_ids[0], cx);
25439 editor.fold_buffer(buffer_ids[2], cx);
25440 editor.fold_buffer(buffer_ids[3], cx);
25441
25442 editor
25443 });
25444 cx.simulate_resize(size(px(1000.), px(1000.)));
25445
25446 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
25447 cx.assert_excerpts_with_selections(indoc! {"
25448 [EXCERPT]
25449 ˇ[FOLDED]
25450 [EXCERPT]
25451 a1
25452 b1
25453 [EXCERPT]
25454 [FOLDED]
25455 [EXCERPT]
25456 [FOLDED]
25457 "
25458 });
25459 cx.simulate_keystroke("down");
25460 cx.assert_excerpts_with_selections(indoc! {"
25461 [EXCERPT]
25462 [FOLDED]
25463 [EXCERPT]
25464 ˇa1
25465 b1
25466 [EXCERPT]
25467 [FOLDED]
25468 [EXCERPT]
25469 [FOLDED]
25470 "
25471 });
25472 cx.simulate_keystroke("down");
25473 cx.assert_excerpts_with_selections(indoc! {"
25474 [EXCERPT]
25475 [FOLDED]
25476 [EXCERPT]
25477 a1
25478 ˇb1
25479 [EXCERPT]
25480 [FOLDED]
25481 [EXCERPT]
25482 [FOLDED]
25483 "
25484 });
25485 cx.simulate_keystroke("down");
25486 cx.assert_excerpts_with_selections(indoc! {"
25487 [EXCERPT]
25488 [FOLDED]
25489 [EXCERPT]
25490 a1
25491 b1
25492 ˇ[EXCERPT]
25493 [FOLDED]
25494 [EXCERPT]
25495 [FOLDED]
25496 "
25497 });
25498 cx.simulate_keystroke("down");
25499 cx.assert_excerpts_with_selections(indoc! {"
25500 [EXCERPT]
25501 [FOLDED]
25502 [EXCERPT]
25503 a1
25504 b1
25505 [EXCERPT]
25506 ˇ[FOLDED]
25507 [EXCERPT]
25508 [FOLDED]
25509 "
25510 });
25511 for _ in 0..5 {
25512 cx.simulate_keystroke("down");
25513 cx.assert_excerpts_with_selections(indoc! {"
25514 [EXCERPT]
25515 [FOLDED]
25516 [EXCERPT]
25517 a1
25518 b1
25519 [EXCERPT]
25520 [FOLDED]
25521 [EXCERPT]
25522 ˇ[FOLDED]
25523 "
25524 });
25525 }
25526
25527 cx.simulate_keystroke("up");
25528 cx.assert_excerpts_with_selections(indoc! {"
25529 [EXCERPT]
25530 [FOLDED]
25531 [EXCERPT]
25532 a1
25533 b1
25534 [EXCERPT]
25535 ˇ[FOLDED]
25536 [EXCERPT]
25537 [FOLDED]
25538 "
25539 });
25540 cx.simulate_keystroke("up");
25541 cx.assert_excerpts_with_selections(indoc! {"
25542 [EXCERPT]
25543 [FOLDED]
25544 [EXCERPT]
25545 a1
25546 b1
25547 ˇ[EXCERPT]
25548 [FOLDED]
25549 [EXCERPT]
25550 [FOLDED]
25551 "
25552 });
25553 cx.simulate_keystroke("up");
25554 cx.assert_excerpts_with_selections(indoc! {"
25555 [EXCERPT]
25556 [FOLDED]
25557 [EXCERPT]
25558 a1
25559 ˇb1
25560 [EXCERPT]
25561 [FOLDED]
25562 [EXCERPT]
25563 [FOLDED]
25564 "
25565 });
25566 cx.simulate_keystroke("up");
25567 cx.assert_excerpts_with_selections(indoc! {"
25568 [EXCERPT]
25569 [FOLDED]
25570 [EXCERPT]
25571 ˇa1
25572 b1
25573 [EXCERPT]
25574 [FOLDED]
25575 [EXCERPT]
25576 [FOLDED]
25577 "
25578 });
25579 for _ in 0..5 {
25580 cx.simulate_keystroke("up");
25581 cx.assert_excerpts_with_selections(indoc! {"
25582 [EXCERPT]
25583 ˇ[FOLDED]
25584 [EXCERPT]
25585 a1
25586 b1
25587 [EXCERPT]
25588 [FOLDED]
25589 [EXCERPT]
25590 [FOLDED]
25591 "
25592 });
25593 }
25594}
25595
25596#[gpui::test]
25597async fn test_edit_prediction_text(cx: &mut TestAppContext) {
25598 init_test(cx, |_| {});
25599
25600 // Simple insertion
25601 assert_highlighted_edits(
25602 "Hello, world!",
25603 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
25604 true,
25605 cx,
25606 &|highlighted_edits, cx| {
25607 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
25608 assert_eq!(highlighted_edits.highlights.len(), 1);
25609 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
25610 assert_eq!(
25611 highlighted_edits.highlights[0].1.background_color,
25612 Some(cx.theme().status().created_background)
25613 );
25614 },
25615 )
25616 .await;
25617
25618 // Replacement
25619 assert_highlighted_edits(
25620 "This is a test.",
25621 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
25622 false,
25623 cx,
25624 &|highlighted_edits, cx| {
25625 assert_eq!(highlighted_edits.text, "That is a test.");
25626 assert_eq!(highlighted_edits.highlights.len(), 1);
25627 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
25628 assert_eq!(
25629 highlighted_edits.highlights[0].1.background_color,
25630 Some(cx.theme().status().created_background)
25631 );
25632 },
25633 )
25634 .await;
25635
25636 // Multiple edits
25637 assert_highlighted_edits(
25638 "Hello, world!",
25639 vec![
25640 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
25641 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
25642 ],
25643 false,
25644 cx,
25645 &|highlighted_edits, cx| {
25646 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
25647 assert_eq!(highlighted_edits.highlights.len(), 2);
25648 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
25649 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
25650 assert_eq!(
25651 highlighted_edits.highlights[0].1.background_color,
25652 Some(cx.theme().status().created_background)
25653 );
25654 assert_eq!(
25655 highlighted_edits.highlights[1].1.background_color,
25656 Some(cx.theme().status().created_background)
25657 );
25658 },
25659 )
25660 .await;
25661
25662 // Multiple lines with edits
25663 assert_highlighted_edits(
25664 "First line\nSecond line\nThird line\nFourth line",
25665 vec![
25666 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
25667 (
25668 Point::new(2, 0)..Point::new(2, 10),
25669 "New third line".to_string(),
25670 ),
25671 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
25672 ],
25673 false,
25674 cx,
25675 &|highlighted_edits, cx| {
25676 assert_eq!(
25677 highlighted_edits.text,
25678 "Second modified\nNew third line\nFourth updated line"
25679 );
25680 assert_eq!(highlighted_edits.highlights.len(), 3);
25681 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
25682 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
25683 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
25684 for highlight in &highlighted_edits.highlights {
25685 assert_eq!(
25686 highlight.1.background_color,
25687 Some(cx.theme().status().created_background)
25688 );
25689 }
25690 },
25691 )
25692 .await;
25693}
25694
25695#[gpui::test]
25696async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
25697 init_test(cx, |_| {});
25698
25699 // Deletion
25700 assert_highlighted_edits(
25701 "Hello, world!",
25702 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
25703 true,
25704 cx,
25705 &|highlighted_edits, cx| {
25706 assert_eq!(highlighted_edits.text, "Hello, world!");
25707 assert_eq!(highlighted_edits.highlights.len(), 1);
25708 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
25709 assert_eq!(
25710 highlighted_edits.highlights[0].1.background_color,
25711 Some(cx.theme().status().deleted_background)
25712 );
25713 },
25714 )
25715 .await;
25716
25717 // Insertion
25718 assert_highlighted_edits(
25719 "Hello, world!",
25720 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
25721 true,
25722 cx,
25723 &|highlighted_edits, cx| {
25724 assert_eq!(highlighted_edits.highlights.len(), 1);
25725 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
25726 assert_eq!(
25727 highlighted_edits.highlights[0].1.background_color,
25728 Some(cx.theme().status().created_background)
25729 );
25730 },
25731 )
25732 .await;
25733}
25734
25735async fn assert_highlighted_edits(
25736 text: &str,
25737 edits: Vec<(Range<Point>, String)>,
25738 include_deletions: bool,
25739 cx: &mut TestAppContext,
25740 assertion_fn: &dyn Fn(HighlightedText, &App),
25741) {
25742 let window = cx.add_window(|window, cx| {
25743 let buffer = MultiBuffer::build_simple(text, cx);
25744 Editor::new(EditorMode::full(), buffer, None, window, cx)
25745 });
25746 let cx = &mut VisualTestContext::from_window(*window, cx);
25747
25748 let (buffer, snapshot) = window
25749 .update(cx, |editor, _window, cx| {
25750 (
25751 editor.buffer().clone(),
25752 editor.buffer().read(cx).snapshot(cx),
25753 )
25754 })
25755 .unwrap();
25756
25757 let edits = edits
25758 .into_iter()
25759 .map(|(range, edit)| {
25760 (
25761 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
25762 edit,
25763 )
25764 })
25765 .collect::<Vec<_>>();
25766
25767 let text_anchor_edits = edits
25768 .clone()
25769 .into_iter()
25770 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
25771 .collect::<Vec<_>>();
25772
25773 let edit_preview = window
25774 .update(cx, |_, _window, cx| {
25775 buffer
25776 .read(cx)
25777 .as_singleton()
25778 .unwrap()
25779 .read(cx)
25780 .preview_edits(text_anchor_edits.into(), cx)
25781 })
25782 .unwrap()
25783 .await;
25784
25785 cx.update(|_window, cx| {
25786 let highlighted_edits = edit_prediction_edit_text(
25787 snapshot.as_singleton().unwrap().2,
25788 &edits,
25789 &edit_preview,
25790 include_deletions,
25791 cx,
25792 );
25793 assertion_fn(highlighted_edits, cx)
25794 });
25795}
25796
25797#[track_caller]
25798fn assert_breakpoint(
25799 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
25800 path: &Arc<Path>,
25801 expected: Vec<(u32, Breakpoint)>,
25802) {
25803 if expected.is_empty() {
25804 assert!(!breakpoints.contains_key(path), "{}", path.display());
25805 } else {
25806 let mut breakpoint = breakpoints
25807 .get(path)
25808 .unwrap()
25809 .iter()
25810 .map(|breakpoint| {
25811 (
25812 breakpoint.row,
25813 Breakpoint {
25814 message: breakpoint.message.clone(),
25815 state: breakpoint.state,
25816 condition: breakpoint.condition.clone(),
25817 hit_condition: breakpoint.hit_condition.clone(),
25818 },
25819 )
25820 })
25821 .collect::<Vec<_>>();
25822
25823 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
25824
25825 assert_eq!(expected, breakpoint);
25826 }
25827}
25828
25829fn add_log_breakpoint_at_cursor(
25830 editor: &mut Editor,
25831 log_message: &str,
25832 window: &mut Window,
25833 cx: &mut Context<Editor>,
25834) {
25835 let (anchor, bp) = editor
25836 .breakpoints_at_cursors(window, cx)
25837 .first()
25838 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
25839 .unwrap_or_else(|| {
25840 let snapshot = editor.snapshot(window, cx);
25841 let cursor_position: Point =
25842 editor.selections.newest(&snapshot.display_snapshot).head();
25843
25844 let breakpoint_position = snapshot
25845 .buffer_snapshot()
25846 .anchor_before(Point::new(cursor_position.row, 0));
25847
25848 (breakpoint_position, Breakpoint::new_log(log_message))
25849 });
25850
25851 editor.edit_breakpoint_at_anchor(
25852 anchor,
25853 bp,
25854 BreakpointEditAction::EditLogMessage(log_message.into()),
25855 cx,
25856 );
25857}
25858
25859#[gpui::test]
25860async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
25861 init_test(cx, |_| {});
25862
25863 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
25864 let fs = FakeFs::new(cx.executor());
25865 fs.insert_tree(
25866 path!("/a"),
25867 json!({
25868 "main.rs": sample_text,
25869 }),
25870 )
25871 .await;
25872 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25873 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25874 let cx = &mut VisualTestContext::from_window(*window, cx);
25875
25876 let fs = FakeFs::new(cx.executor());
25877 fs.insert_tree(
25878 path!("/a"),
25879 json!({
25880 "main.rs": sample_text,
25881 }),
25882 )
25883 .await;
25884 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25885 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25886 let workspace = window
25887 .read_with(cx, |mw, _| mw.workspace().clone())
25888 .unwrap();
25889 let cx = &mut VisualTestContext::from_window(*window, cx);
25890 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
25891 workspace.project().update(cx, |project, cx| {
25892 project.worktrees(cx).next().unwrap().read(cx).id()
25893 })
25894 });
25895
25896 let buffer = project
25897 .update(cx, |project, cx| {
25898 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
25899 })
25900 .await
25901 .unwrap();
25902
25903 let (editor, cx) = cx.add_window_view(|window, cx| {
25904 Editor::new(
25905 EditorMode::full(),
25906 MultiBuffer::build_from_buffer(buffer, cx),
25907 Some(project.clone()),
25908 window,
25909 cx,
25910 )
25911 });
25912
25913 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
25914 let abs_path = project.read_with(cx, |project, cx| {
25915 project
25916 .absolute_path(&project_path, cx)
25917 .map(Arc::from)
25918 .unwrap()
25919 });
25920
25921 // assert we can add breakpoint on the first line
25922 editor.update_in(cx, |editor, window, cx| {
25923 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25924 editor.move_to_end(&MoveToEnd, window, cx);
25925 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25926 });
25927
25928 let breakpoints = editor.update(cx, |editor, cx| {
25929 editor
25930 .breakpoint_store()
25931 .as_ref()
25932 .unwrap()
25933 .read(cx)
25934 .all_source_breakpoints(cx)
25935 });
25936
25937 assert_eq!(1, breakpoints.len());
25938 assert_breakpoint(
25939 &breakpoints,
25940 &abs_path,
25941 vec![
25942 (0, Breakpoint::new_standard()),
25943 (3, Breakpoint::new_standard()),
25944 ],
25945 );
25946
25947 editor.update_in(cx, |editor, window, cx| {
25948 editor.move_to_beginning(&MoveToBeginning, window, cx);
25949 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25950 });
25951
25952 let breakpoints = editor.update(cx, |editor, cx| {
25953 editor
25954 .breakpoint_store()
25955 .as_ref()
25956 .unwrap()
25957 .read(cx)
25958 .all_source_breakpoints(cx)
25959 });
25960
25961 assert_eq!(1, breakpoints.len());
25962 assert_breakpoint(
25963 &breakpoints,
25964 &abs_path,
25965 vec![(3, Breakpoint::new_standard())],
25966 );
25967
25968 editor.update_in(cx, |editor, window, cx| {
25969 editor.move_to_end(&MoveToEnd, window, cx);
25970 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25971 });
25972
25973 let breakpoints = editor.update(cx, |editor, cx| {
25974 editor
25975 .breakpoint_store()
25976 .as_ref()
25977 .unwrap()
25978 .read(cx)
25979 .all_source_breakpoints(cx)
25980 });
25981
25982 assert_eq!(0, breakpoints.len());
25983 assert_breakpoint(&breakpoints, &abs_path, vec![]);
25984}
25985
25986#[gpui::test]
25987async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
25988 init_test(cx, |_| {});
25989
25990 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
25991
25992 let fs = FakeFs::new(cx.executor());
25993 fs.insert_tree(
25994 path!("/a"),
25995 json!({
25996 "main.rs": sample_text,
25997 }),
25998 )
25999 .await;
26000 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26001 let (multi_workspace, cx) =
26002 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26003 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
26004
26005 let worktree_id = workspace.update(cx, |workspace, cx| {
26006 workspace.project().update(cx, |project, cx| {
26007 project.worktrees(cx).next().unwrap().read(cx).id()
26008 })
26009 });
26010
26011 let buffer = project
26012 .update(cx, |project, cx| {
26013 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
26014 })
26015 .await
26016 .unwrap();
26017
26018 let (editor, cx) = cx.add_window_view(|window, cx| {
26019 Editor::new(
26020 EditorMode::full(),
26021 MultiBuffer::build_from_buffer(buffer, cx),
26022 Some(project.clone()),
26023 window,
26024 cx,
26025 )
26026 });
26027
26028 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
26029 let abs_path = project.read_with(cx, |project, cx| {
26030 project
26031 .absolute_path(&project_path, cx)
26032 .map(Arc::from)
26033 .unwrap()
26034 });
26035
26036 editor.update_in(cx, |editor, window, cx| {
26037 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
26038 });
26039
26040 let breakpoints = editor.update(cx, |editor, cx| {
26041 editor
26042 .breakpoint_store()
26043 .as_ref()
26044 .unwrap()
26045 .read(cx)
26046 .all_source_breakpoints(cx)
26047 });
26048
26049 assert_breakpoint(
26050 &breakpoints,
26051 &abs_path,
26052 vec![(0, Breakpoint::new_log("hello world"))],
26053 );
26054
26055 // Removing a log message from a log breakpoint should remove it
26056 editor.update_in(cx, |editor, window, cx| {
26057 add_log_breakpoint_at_cursor(editor, "", window, cx);
26058 });
26059
26060 let breakpoints = editor.update(cx, |editor, cx| {
26061 editor
26062 .breakpoint_store()
26063 .as_ref()
26064 .unwrap()
26065 .read(cx)
26066 .all_source_breakpoints(cx)
26067 });
26068
26069 assert_breakpoint(&breakpoints, &abs_path, vec![]);
26070
26071 editor.update_in(cx, |editor, window, cx| {
26072 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
26073 editor.move_to_end(&MoveToEnd, window, cx);
26074 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
26075 // Not adding a log message to a standard breakpoint shouldn't remove it
26076 add_log_breakpoint_at_cursor(editor, "", window, cx);
26077 });
26078
26079 let breakpoints = editor.update(cx, |editor, cx| {
26080 editor
26081 .breakpoint_store()
26082 .as_ref()
26083 .unwrap()
26084 .read(cx)
26085 .all_source_breakpoints(cx)
26086 });
26087
26088 assert_breakpoint(
26089 &breakpoints,
26090 &abs_path,
26091 vec![
26092 (0, Breakpoint::new_standard()),
26093 (3, Breakpoint::new_standard()),
26094 ],
26095 );
26096
26097 editor.update_in(cx, |editor, window, cx| {
26098 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
26099 });
26100
26101 let breakpoints = editor.update(cx, |editor, cx| {
26102 editor
26103 .breakpoint_store()
26104 .as_ref()
26105 .unwrap()
26106 .read(cx)
26107 .all_source_breakpoints(cx)
26108 });
26109
26110 assert_breakpoint(
26111 &breakpoints,
26112 &abs_path,
26113 vec![
26114 (0, Breakpoint::new_standard()),
26115 (3, Breakpoint::new_log("hello world")),
26116 ],
26117 );
26118
26119 editor.update_in(cx, |editor, window, cx| {
26120 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
26121 });
26122
26123 let breakpoints = editor.update(cx, |editor, cx| {
26124 editor
26125 .breakpoint_store()
26126 .as_ref()
26127 .unwrap()
26128 .read(cx)
26129 .all_source_breakpoints(cx)
26130 });
26131
26132 assert_breakpoint(
26133 &breakpoints,
26134 &abs_path,
26135 vec![
26136 (0, Breakpoint::new_standard()),
26137 (3, Breakpoint::new_log("hello Earth!!")),
26138 ],
26139 );
26140}
26141
26142/// This also tests that Editor::breakpoint_at_cursor_head is working properly
26143/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
26144/// or when breakpoints were placed out of order. This tests for a regression too
26145#[gpui::test]
26146async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
26147 init_test(cx, |_| {});
26148
26149 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
26150 let fs = FakeFs::new(cx.executor());
26151 fs.insert_tree(
26152 path!("/a"),
26153 json!({
26154 "main.rs": sample_text,
26155 }),
26156 )
26157 .await;
26158 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26159 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26160 let cx = &mut VisualTestContext::from_window(*window, cx);
26161
26162 let fs = FakeFs::new(cx.executor());
26163 fs.insert_tree(
26164 path!("/a"),
26165 json!({
26166 "main.rs": sample_text,
26167 }),
26168 )
26169 .await;
26170 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26171 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26172 let workspace = window
26173 .read_with(cx, |mw, _| mw.workspace().clone())
26174 .unwrap();
26175 let cx = &mut VisualTestContext::from_window(*window, cx);
26176 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
26177 workspace.project().update(cx, |project, cx| {
26178 project.worktrees(cx).next().unwrap().read(cx).id()
26179 })
26180 });
26181
26182 let buffer = project
26183 .update(cx, |project, cx| {
26184 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
26185 })
26186 .await
26187 .unwrap();
26188
26189 let (editor, cx) = cx.add_window_view(|window, cx| {
26190 Editor::new(
26191 EditorMode::full(),
26192 MultiBuffer::build_from_buffer(buffer, cx),
26193 Some(project.clone()),
26194 window,
26195 cx,
26196 )
26197 });
26198
26199 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
26200 let abs_path = project.read_with(cx, |project, cx| {
26201 project
26202 .absolute_path(&project_path, cx)
26203 .map(Arc::from)
26204 .unwrap()
26205 });
26206
26207 // assert we can add breakpoint on the first line
26208 editor.update_in(cx, |editor, window, cx| {
26209 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
26210 editor.move_to_end(&MoveToEnd, window, cx);
26211 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
26212 editor.move_up(&MoveUp, window, cx);
26213 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
26214 });
26215
26216 let breakpoints = editor.update(cx, |editor, cx| {
26217 editor
26218 .breakpoint_store()
26219 .as_ref()
26220 .unwrap()
26221 .read(cx)
26222 .all_source_breakpoints(cx)
26223 });
26224
26225 assert_eq!(1, breakpoints.len());
26226 assert_breakpoint(
26227 &breakpoints,
26228 &abs_path,
26229 vec![
26230 (0, Breakpoint::new_standard()),
26231 (2, Breakpoint::new_standard()),
26232 (3, Breakpoint::new_standard()),
26233 ],
26234 );
26235
26236 editor.update_in(cx, |editor, window, cx| {
26237 editor.move_to_beginning(&MoveToBeginning, window, cx);
26238 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
26239 editor.move_to_end(&MoveToEnd, window, cx);
26240 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
26241 // Disabling a breakpoint that doesn't exist should do nothing
26242 editor.move_up(&MoveUp, window, cx);
26243 editor.move_up(&MoveUp, window, cx);
26244 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
26245 });
26246
26247 let breakpoints = editor.update(cx, |editor, cx| {
26248 editor
26249 .breakpoint_store()
26250 .as_ref()
26251 .unwrap()
26252 .read(cx)
26253 .all_source_breakpoints(cx)
26254 });
26255
26256 let disable_breakpoint = {
26257 let mut bp = Breakpoint::new_standard();
26258 bp.state = BreakpointState::Disabled;
26259 bp
26260 };
26261
26262 assert_eq!(1, breakpoints.len());
26263 assert_breakpoint(
26264 &breakpoints,
26265 &abs_path,
26266 vec![
26267 (0, disable_breakpoint.clone()),
26268 (2, Breakpoint::new_standard()),
26269 (3, disable_breakpoint.clone()),
26270 ],
26271 );
26272
26273 editor.update_in(cx, |editor, window, cx| {
26274 editor.move_to_beginning(&MoveToBeginning, window, cx);
26275 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
26276 editor.move_to_end(&MoveToEnd, window, cx);
26277 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
26278 editor.move_up(&MoveUp, window, cx);
26279 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
26280 });
26281
26282 let breakpoints = editor.update(cx, |editor, cx| {
26283 editor
26284 .breakpoint_store()
26285 .as_ref()
26286 .unwrap()
26287 .read(cx)
26288 .all_source_breakpoints(cx)
26289 });
26290
26291 assert_eq!(1, breakpoints.len());
26292 assert_breakpoint(
26293 &breakpoints,
26294 &abs_path,
26295 vec![
26296 (0, Breakpoint::new_standard()),
26297 (2, disable_breakpoint),
26298 (3, Breakpoint::new_standard()),
26299 ],
26300 );
26301}
26302
26303#[gpui::test]
26304async fn test_breakpoint_phantom_indicator_collision_on_toggle(cx: &mut TestAppContext) {
26305 init_test(cx, |_| {});
26306
26307 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
26308 let fs = FakeFs::new(cx.executor());
26309 fs.insert_tree(
26310 path!("/a"),
26311 json!({
26312 "main.rs": sample_text,
26313 }),
26314 )
26315 .await;
26316 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26317 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26318 let workspace = window
26319 .read_with(cx, |mw, _| mw.workspace().clone())
26320 .unwrap();
26321 let cx = &mut VisualTestContext::from_window(*window, cx);
26322 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
26323 workspace.project().update(cx, |project, cx| {
26324 project.worktrees(cx).next().unwrap().read(cx).id()
26325 })
26326 });
26327
26328 let buffer = project
26329 .update(cx, |project, cx| {
26330 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
26331 })
26332 .await
26333 .unwrap();
26334
26335 let (editor, cx) = cx.add_window_view(|window, cx| {
26336 Editor::new(
26337 EditorMode::full(),
26338 MultiBuffer::build_from_buffer(buffer, cx),
26339 Some(project.clone()),
26340 window,
26341 cx,
26342 )
26343 });
26344
26345 // Simulate hovering over row 0 with no existing breakpoint.
26346 editor.update(cx, |editor, _cx| {
26347 editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
26348 display_row: DisplayRow(0),
26349 is_active: true,
26350 collides_with_existing_breakpoint: false,
26351 });
26352 });
26353
26354 // Toggle breakpoint on the same row (row 0) — collision should flip to true.
26355 editor.update_in(cx, |editor, window, cx| {
26356 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
26357 });
26358 editor.update(cx, |editor, _cx| {
26359 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
26360 assert!(
26361 indicator.collides_with_existing_breakpoint,
26362 "Adding a breakpoint on the hovered row should set collision to true"
26363 );
26364 });
26365
26366 // Toggle again on the same row — breakpoint is removed, collision should flip back to false.
26367 editor.update_in(cx, |editor, window, cx| {
26368 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
26369 });
26370 editor.update(cx, |editor, _cx| {
26371 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
26372 assert!(
26373 !indicator.collides_with_existing_breakpoint,
26374 "Removing a breakpoint on the hovered row should set collision to false"
26375 );
26376 });
26377
26378 // Now move cursor to row 2 while phantom indicator stays on row 0.
26379 editor.update_in(cx, |editor, window, cx| {
26380 editor.move_down(&MoveDown, window, cx);
26381 editor.move_down(&MoveDown, window, cx);
26382 });
26383
26384 // Ensure phantom indicator is still on row 0, not colliding.
26385 editor.update(cx, |editor, _cx| {
26386 editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
26387 display_row: DisplayRow(0),
26388 is_active: true,
26389 collides_with_existing_breakpoint: false,
26390 });
26391 });
26392
26393 // Toggle breakpoint on row 2 (cursor row) — phantom on row 0 should NOT be affected.
26394 editor.update_in(cx, |editor, window, cx| {
26395 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
26396 });
26397 editor.update(cx, |editor, _cx| {
26398 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
26399 assert!(
26400 !indicator.collides_with_existing_breakpoint,
26401 "Toggling a breakpoint on a different row should not affect the phantom indicator"
26402 );
26403 });
26404}
26405
26406#[gpui::test]
26407async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
26408 init_test(cx, |_| {});
26409 let capabilities = lsp::ServerCapabilities {
26410 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
26411 prepare_provider: Some(true),
26412 work_done_progress_options: Default::default(),
26413 })),
26414 ..Default::default()
26415 };
26416 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
26417
26418 cx.set_state(indoc! {"
26419 struct Fˇoo {}
26420 "});
26421
26422 cx.update_editor(|editor, _, cx| {
26423 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
26424 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
26425 editor.highlight_background(
26426 HighlightKey::DocumentHighlightRead,
26427 &[highlight_range],
26428 |_, theme| theme.colors().editor_document_highlight_read_background,
26429 cx,
26430 );
26431 });
26432
26433 let mut prepare_rename_handler = cx
26434 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
26435 move |_, _, _| async move {
26436 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
26437 start: lsp::Position {
26438 line: 0,
26439 character: 7,
26440 },
26441 end: lsp::Position {
26442 line: 0,
26443 character: 10,
26444 },
26445 })))
26446 },
26447 );
26448 let prepare_rename_task = cx
26449 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
26450 .expect("Prepare rename was not started");
26451 prepare_rename_handler.next().await.unwrap();
26452 prepare_rename_task.await.expect("Prepare rename failed");
26453
26454 let mut rename_handler =
26455 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
26456 let edit = lsp::TextEdit {
26457 range: lsp::Range {
26458 start: lsp::Position {
26459 line: 0,
26460 character: 7,
26461 },
26462 end: lsp::Position {
26463 line: 0,
26464 character: 10,
26465 },
26466 },
26467 new_text: "FooRenamed".to_string(),
26468 };
26469 Ok(Some(lsp::WorkspaceEdit::new(
26470 // Specify the same edit twice
26471 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
26472 )))
26473 });
26474 let rename_task = cx
26475 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
26476 .expect("Confirm rename was not started");
26477 rename_handler.next().await.unwrap();
26478 rename_task.await.expect("Confirm rename failed");
26479 cx.run_until_parked();
26480
26481 // Despite two edits, only one is actually applied as those are identical
26482 cx.assert_editor_state(indoc! {"
26483 struct FooRenamedˇ {}
26484 "});
26485}
26486
26487#[gpui::test]
26488async fn test_rename_without_prepare(cx: &mut TestAppContext) {
26489 init_test(cx, |_| {});
26490 // These capabilities indicate that the server does not support prepare rename.
26491 let capabilities = lsp::ServerCapabilities {
26492 rename_provider: Some(lsp::OneOf::Left(true)),
26493 ..Default::default()
26494 };
26495 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
26496
26497 cx.set_state(indoc! {"
26498 struct Fˇoo {}
26499 "});
26500
26501 cx.update_editor(|editor, _window, cx| {
26502 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
26503 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
26504 editor.highlight_background(
26505 HighlightKey::DocumentHighlightRead,
26506 &[highlight_range],
26507 |_, theme| theme.colors().editor_document_highlight_read_background,
26508 cx,
26509 );
26510 });
26511
26512 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
26513 .expect("Prepare rename was not started")
26514 .await
26515 .expect("Prepare rename failed");
26516
26517 let mut rename_handler =
26518 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
26519 let edit = lsp::TextEdit {
26520 range: lsp::Range {
26521 start: lsp::Position {
26522 line: 0,
26523 character: 7,
26524 },
26525 end: lsp::Position {
26526 line: 0,
26527 character: 10,
26528 },
26529 },
26530 new_text: "FooRenamed".to_string(),
26531 };
26532 Ok(Some(lsp::WorkspaceEdit::new(
26533 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
26534 )))
26535 });
26536 let rename_task = cx
26537 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
26538 .expect("Confirm rename was not started");
26539 rename_handler.next().await.unwrap();
26540 rename_task.await.expect("Confirm rename failed");
26541 cx.run_until_parked();
26542
26543 // Correct range is renamed, as `surrounding_word` is used to find it.
26544 cx.assert_editor_state(indoc! {"
26545 struct FooRenamedˇ {}
26546 "});
26547}
26548
26549#[gpui::test]
26550async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
26551 init_test(cx, |_| {});
26552 let mut cx = EditorTestContext::new(cx).await;
26553
26554 let language = Arc::new(
26555 Language::new(
26556 LanguageConfig::default(),
26557 Some(tree_sitter_html::LANGUAGE.into()),
26558 )
26559 .with_brackets_query(
26560 r#"
26561 ("<" @open "/>" @close)
26562 ("</" @open ">" @close)
26563 ("<" @open ">" @close)
26564 ("\"" @open "\"" @close)
26565 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
26566 "#,
26567 )
26568 .unwrap(),
26569 );
26570 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26571
26572 cx.set_state(indoc! {"
26573 <span>ˇ</span>
26574 "});
26575 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
26576 cx.assert_editor_state(indoc! {"
26577 <span>
26578 ˇ
26579 </span>
26580 "});
26581
26582 cx.set_state(indoc! {"
26583 <span><span></span>ˇ</span>
26584 "});
26585 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
26586 cx.assert_editor_state(indoc! {"
26587 <span><span></span>
26588 ˇ</span>
26589 "});
26590
26591 cx.set_state(indoc! {"
26592 <span>ˇ
26593 </span>
26594 "});
26595 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
26596 cx.assert_editor_state(indoc! {"
26597 <span>
26598 ˇ
26599 </span>
26600 "});
26601}
26602
26603#[gpui::test(iterations = 10)]
26604async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
26605 init_test(cx, |_| {});
26606
26607 let fs = FakeFs::new(cx.executor());
26608 fs.insert_tree(
26609 path!("/dir"),
26610 json!({
26611 "a.ts": "a",
26612 }),
26613 )
26614 .await;
26615
26616 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
26617 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26618 let workspace = window
26619 .read_with(cx, |mw, _| mw.workspace().clone())
26620 .unwrap();
26621 let cx = &mut VisualTestContext::from_window(*window, cx);
26622
26623 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26624 language_registry.add(Arc::new(Language::new(
26625 LanguageConfig {
26626 name: "TypeScript".into(),
26627 matcher: LanguageMatcher {
26628 path_suffixes: vec!["ts".to_string()],
26629 ..Default::default()
26630 },
26631 ..Default::default()
26632 },
26633 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
26634 )));
26635 let mut fake_language_servers = language_registry.register_fake_lsp(
26636 "TypeScript",
26637 FakeLspAdapter {
26638 capabilities: lsp::ServerCapabilities {
26639 code_lens_provider: Some(lsp::CodeLensOptions {
26640 resolve_provider: Some(true),
26641 }),
26642 execute_command_provider: Some(lsp::ExecuteCommandOptions {
26643 commands: vec!["_the/command".to_string()],
26644 ..lsp::ExecuteCommandOptions::default()
26645 }),
26646 ..lsp::ServerCapabilities::default()
26647 },
26648 ..FakeLspAdapter::default()
26649 },
26650 );
26651
26652 let editor = workspace
26653 .update_in(cx, |workspace, window, cx| {
26654 workspace.open_abs_path(
26655 PathBuf::from(path!("/dir/a.ts")),
26656 OpenOptions::default(),
26657 window,
26658 cx,
26659 )
26660 })
26661 .await
26662 .unwrap()
26663 .downcast::<Editor>()
26664 .unwrap();
26665 cx.executor().run_until_parked();
26666
26667 let fake_server = fake_language_servers.next().await.unwrap();
26668
26669 let buffer = editor.update(cx, |editor, cx| {
26670 editor
26671 .buffer()
26672 .read(cx)
26673 .as_singleton()
26674 .expect("have opened a single file by path")
26675 });
26676
26677 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
26678 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
26679 drop(buffer_snapshot);
26680 let actions = cx
26681 .update_window(*window, |_, window, cx| {
26682 project.code_actions(&buffer, anchor..anchor, window, cx)
26683 })
26684 .unwrap();
26685
26686 fake_server
26687 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
26688 Ok(Some(vec![
26689 lsp::CodeLens {
26690 range: lsp::Range::default(),
26691 command: Some(lsp::Command {
26692 title: "Code lens command".to_owned(),
26693 command: "_the/command".to_owned(),
26694 arguments: None,
26695 }),
26696 data: None,
26697 },
26698 lsp::CodeLens {
26699 range: lsp::Range::default(),
26700 command: Some(lsp::Command {
26701 title: "Command not in capabilities".to_owned(),
26702 command: "not in capabilities".to_owned(),
26703 arguments: None,
26704 }),
26705 data: None,
26706 },
26707 lsp::CodeLens {
26708 range: lsp::Range {
26709 start: lsp::Position {
26710 line: 1,
26711 character: 1,
26712 },
26713 end: lsp::Position {
26714 line: 1,
26715 character: 1,
26716 },
26717 },
26718 command: Some(lsp::Command {
26719 title: "Command not in range".to_owned(),
26720 command: "_the/command".to_owned(),
26721 arguments: None,
26722 }),
26723 data: None,
26724 },
26725 ]))
26726 })
26727 .next()
26728 .await;
26729
26730 let actions = actions.await.unwrap();
26731 assert_eq!(
26732 actions.len(),
26733 1,
26734 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
26735 );
26736 let action = actions[0].clone();
26737 let apply = project.update(cx, |project, cx| {
26738 project.apply_code_action(buffer.clone(), action, true, cx)
26739 });
26740
26741 // Resolving the code action does not populate its edits. In absence of
26742 // edits, we must execute the given command.
26743 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
26744 |mut lens, _| async move {
26745 let lens_command = lens.command.as_mut().expect("should have a command");
26746 assert_eq!(lens_command.title, "Code lens command");
26747 lens_command.arguments = Some(vec![json!("the-argument")]);
26748 Ok(lens)
26749 },
26750 );
26751
26752 // While executing the command, the language server sends the editor
26753 // a `workspaceEdit` request.
26754 fake_server
26755 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
26756 let fake = fake_server.clone();
26757 move |params, _| {
26758 assert_eq!(params.command, "_the/command");
26759 let fake = fake.clone();
26760 async move {
26761 fake.server
26762 .request::<lsp::request::ApplyWorkspaceEdit>(
26763 lsp::ApplyWorkspaceEditParams {
26764 label: None,
26765 edit: lsp::WorkspaceEdit {
26766 changes: Some(
26767 [(
26768 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
26769 vec![lsp::TextEdit {
26770 range: lsp::Range::new(
26771 lsp::Position::new(0, 0),
26772 lsp::Position::new(0, 0),
26773 ),
26774 new_text: "X".into(),
26775 }],
26776 )]
26777 .into_iter()
26778 .collect(),
26779 ),
26780 ..lsp::WorkspaceEdit::default()
26781 },
26782 },
26783 DEFAULT_LSP_REQUEST_TIMEOUT,
26784 )
26785 .await
26786 .into_response()
26787 .unwrap();
26788 Ok(Some(json!(null)))
26789 }
26790 }
26791 })
26792 .next()
26793 .await;
26794
26795 // Applying the code lens command returns a project transaction containing the edits
26796 // sent by the language server in its `workspaceEdit` request.
26797 let transaction = apply.await.unwrap();
26798 assert!(transaction.0.contains_key(&buffer));
26799 buffer.update(cx, |buffer, cx| {
26800 assert_eq!(buffer.text(), "Xa");
26801 buffer.undo(cx);
26802 assert_eq!(buffer.text(), "a");
26803 });
26804
26805 let actions_after_edits = cx
26806 .update(|window, cx| project.code_actions(&buffer, anchor..anchor, window, cx))
26807 .unwrap()
26808 .await;
26809 assert_eq!(
26810 actions, actions_after_edits,
26811 "For the same selection, same code lens actions should be returned"
26812 );
26813
26814 let _responses =
26815 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
26816 panic!("No more code lens requests are expected");
26817 });
26818 editor.update_in(cx, |editor, window, cx| {
26819 editor.select_all(&SelectAll, window, cx);
26820 });
26821 cx.executor().run_until_parked();
26822 let new_actions = cx
26823 .update(|window, cx| project.code_actions(&buffer, anchor..anchor, window, cx))
26824 .unwrap()
26825 .await;
26826 assert_eq!(
26827 actions, new_actions,
26828 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
26829 );
26830}
26831
26832#[gpui::test]
26833async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
26834 init_test(cx, |_| {});
26835
26836 let fs = FakeFs::new(cx.executor());
26837 let main_text = r#"fn main() {
26838println!("1");
26839println!("2");
26840println!("3");
26841println!("4");
26842println!("5");
26843}"#;
26844 let lib_text = "mod foo {}";
26845 fs.insert_tree(
26846 path!("/a"),
26847 json!({
26848 "lib.rs": lib_text,
26849 "main.rs": main_text,
26850 }),
26851 )
26852 .await;
26853
26854 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26855 let (multi_workspace, cx) =
26856 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26857 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
26858 let worktree_id = workspace.update(cx, |workspace, cx| {
26859 workspace.project().update(cx, |project, cx| {
26860 project.worktrees(cx).next().unwrap().read(cx).id()
26861 })
26862 });
26863
26864 let expected_ranges = vec![
26865 Point::new(0, 0)..Point::new(0, 0),
26866 Point::new(1, 0)..Point::new(1, 1),
26867 Point::new(2, 0)..Point::new(2, 2),
26868 Point::new(3, 0)..Point::new(3, 3),
26869 ];
26870
26871 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
26872 let editor_1 = workspace
26873 .update_in(cx, |workspace, window, cx| {
26874 workspace.open_path(
26875 (worktree_id, rel_path("main.rs")),
26876 Some(pane_1.downgrade()),
26877 true,
26878 window,
26879 cx,
26880 )
26881 })
26882 .unwrap()
26883 .await
26884 .downcast::<Editor>()
26885 .unwrap();
26886 pane_1.update(cx, |pane, cx| {
26887 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26888 open_editor.update(cx, |editor, cx| {
26889 assert_eq!(
26890 editor.display_text(cx),
26891 main_text,
26892 "Original main.rs text on initial open",
26893 );
26894 assert_eq!(
26895 editor
26896 .selections
26897 .all::<Point>(&editor.display_snapshot(cx))
26898 .into_iter()
26899 .map(|s| s.range())
26900 .collect::<Vec<_>>(),
26901 vec![Point::zero()..Point::zero()],
26902 "Default selections on initial open",
26903 );
26904 })
26905 });
26906 editor_1.update_in(cx, |editor, window, cx| {
26907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26908 s.select_ranges(expected_ranges.clone());
26909 });
26910 });
26911
26912 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
26913 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
26914 });
26915 let editor_2 = workspace
26916 .update_in(cx, |workspace, window, cx| {
26917 workspace.open_path(
26918 (worktree_id, rel_path("main.rs")),
26919 Some(pane_2.downgrade()),
26920 true,
26921 window,
26922 cx,
26923 )
26924 })
26925 .unwrap()
26926 .await
26927 .downcast::<Editor>()
26928 .unwrap();
26929 pane_2.update(cx, |pane, cx| {
26930 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26931 open_editor.update(cx, |editor, cx| {
26932 assert_eq!(
26933 editor.display_text(cx),
26934 main_text,
26935 "Original main.rs text on initial open in another panel",
26936 );
26937 assert_eq!(
26938 editor
26939 .selections
26940 .all::<Point>(&editor.display_snapshot(cx))
26941 .into_iter()
26942 .map(|s| s.range())
26943 .collect::<Vec<_>>(),
26944 vec![Point::zero()..Point::zero()],
26945 "Default selections on initial open in another panel",
26946 );
26947 })
26948 });
26949
26950 editor_2.update_in(cx, |editor, window, cx| {
26951 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
26952 });
26953
26954 let _other_editor_1 = workspace
26955 .update_in(cx, |workspace, window, cx| {
26956 workspace.open_path(
26957 (worktree_id, rel_path("lib.rs")),
26958 Some(pane_1.downgrade()),
26959 true,
26960 window,
26961 cx,
26962 )
26963 })
26964 .unwrap()
26965 .await
26966 .downcast::<Editor>()
26967 .unwrap();
26968 pane_1
26969 .update_in(cx, |pane, window, cx| {
26970 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
26971 })
26972 .await
26973 .unwrap();
26974 drop(editor_1);
26975 pane_1.update(cx, |pane, cx| {
26976 pane.active_item()
26977 .unwrap()
26978 .downcast::<Editor>()
26979 .unwrap()
26980 .update(cx, |editor, cx| {
26981 assert_eq!(
26982 editor.display_text(cx),
26983 lib_text,
26984 "Other file should be open and active",
26985 );
26986 });
26987 assert_eq!(pane.items().count(), 1, "No other editors should be open");
26988 });
26989
26990 let _other_editor_2 = workspace
26991 .update_in(cx, |workspace, window, cx| {
26992 workspace.open_path(
26993 (worktree_id, rel_path("lib.rs")),
26994 Some(pane_2.downgrade()),
26995 true,
26996 window,
26997 cx,
26998 )
26999 })
27000 .unwrap()
27001 .await
27002 .downcast::<Editor>()
27003 .unwrap();
27004 pane_2
27005 .update_in(cx, |pane, window, cx| {
27006 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
27007 })
27008 .await
27009 .unwrap();
27010 drop(editor_2);
27011 pane_2.update(cx, |pane, cx| {
27012 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
27013 open_editor.update(cx, |editor, cx| {
27014 assert_eq!(
27015 editor.display_text(cx),
27016 lib_text,
27017 "Other file should be open and active in another panel too",
27018 );
27019 });
27020 assert_eq!(
27021 pane.items().count(),
27022 1,
27023 "No other editors should be open in another pane",
27024 );
27025 });
27026
27027 let _editor_1_reopened = workspace
27028 .update_in(cx, |workspace, window, cx| {
27029 workspace.open_path(
27030 (worktree_id, rel_path("main.rs")),
27031 Some(pane_1.downgrade()),
27032 true,
27033 window,
27034 cx,
27035 )
27036 })
27037 .unwrap()
27038 .await
27039 .downcast::<Editor>()
27040 .unwrap();
27041 let _editor_2_reopened = workspace
27042 .update_in(cx, |workspace, window, cx| {
27043 workspace.open_path(
27044 (worktree_id, rel_path("main.rs")),
27045 Some(pane_2.downgrade()),
27046 true,
27047 window,
27048 cx,
27049 )
27050 })
27051 .unwrap()
27052 .await
27053 .downcast::<Editor>()
27054 .unwrap();
27055 pane_1.update(cx, |pane, cx| {
27056 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
27057 open_editor.update(cx, |editor, cx| {
27058 assert_eq!(
27059 editor.display_text(cx),
27060 main_text,
27061 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
27062 );
27063 assert_eq!(
27064 editor
27065 .selections
27066 .all::<Point>(&editor.display_snapshot(cx))
27067 .into_iter()
27068 .map(|s| s.range())
27069 .collect::<Vec<_>>(),
27070 expected_ranges,
27071 "Previous editor in the 1st panel had selections and should get them restored on reopen",
27072 );
27073 })
27074 });
27075 pane_2.update(cx, |pane, cx| {
27076 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
27077 open_editor.update(cx, |editor, cx| {
27078 assert_eq!(
27079 editor.display_text(cx),
27080 r#"fn main() {
27081⋯rintln!("1");
27082⋯intln!("2");
27083⋯ntln!("3");
27084println!("4");
27085println!("5");
27086}"#,
27087 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
27088 );
27089 assert_eq!(
27090 editor
27091 .selections
27092 .all::<Point>(&editor.display_snapshot(cx))
27093 .into_iter()
27094 .map(|s| s.range())
27095 .collect::<Vec<_>>(),
27096 vec![Point::zero()..Point::zero()],
27097 "Previous editor in the 2nd pane had no selections changed hence should restore none",
27098 );
27099 })
27100 });
27101}
27102
27103#[gpui::test]
27104async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
27105 init_test(cx, |_| {});
27106
27107 let fs = FakeFs::new(cx.executor());
27108 let main_text = r#"fn main() {
27109println!("1");
27110println!("2");
27111println!("3");
27112println!("4");
27113println!("5");
27114}"#;
27115 let lib_text = "mod foo {}";
27116 fs.insert_tree(
27117 path!("/a"),
27118 json!({
27119 "lib.rs": lib_text,
27120 "main.rs": main_text,
27121 }),
27122 )
27123 .await;
27124
27125 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27126 let (multi_workspace, cx) =
27127 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27128 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
27129 let worktree_id = workspace.update(cx, |workspace, cx| {
27130 workspace.project().update(cx, |project, cx| {
27131 project.worktrees(cx).next().unwrap().read(cx).id()
27132 })
27133 });
27134
27135 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
27136 let editor = workspace
27137 .update_in(cx, |workspace, window, cx| {
27138 workspace.open_path(
27139 (worktree_id, rel_path("main.rs")),
27140 Some(pane.downgrade()),
27141 true,
27142 window,
27143 cx,
27144 )
27145 })
27146 .unwrap()
27147 .await
27148 .downcast::<Editor>()
27149 .unwrap();
27150 pane.update(cx, |pane, cx| {
27151 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
27152 open_editor.update(cx, |editor, cx| {
27153 assert_eq!(
27154 editor.display_text(cx),
27155 main_text,
27156 "Original main.rs text on initial open",
27157 );
27158 })
27159 });
27160 editor.update_in(cx, |editor, window, cx| {
27161 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
27162 });
27163
27164 cx.update_global(|store: &mut SettingsStore, cx| {
27165 store.update_user_settings(cx, |s| {
27166 s.workspace.restore_on_file_reopen = Some(false);
27167 });
27168 });
27169 editor.update_in(cx, |editor, window, cx| {
27170 editor.fold_ranges(
27171 vec![
27172 Point::new(1, 0)..Point::new(1, 1),
27173 Point::new(2, 0)..Point::new(2, 2),
27174 Point::new(3, 0)..Point::new(3, 3),
27175 ],
27176 false,
27177 window,
27178 cx,
27179 );
27180 });
27181 pane.update_in(cx, |pane, window, cx| {
27182 pane.close_all_items(&CloseAllItems::default(), window, cx)
27183 })
27184 .await
27185 .unwrap();
27186 pane.update(cx, |pane, _| {
27187 assert!(pane.active_item().is_none());
27188 });
27189 cx.update_global(|store: &mut SettingsStore, cx| {
27190 store.update_user_settings(cx, |s| {
27191 s.workspace.restore_on_file_reopen = Some(true);
27192 });
27193 });
27194
27195 let _editor_reopened = workspace
27196 .update_in(cx, |workspace, window, cx| {
27197 workspace.open_path(
27198 (worktree_id, rel_path("main.rs")),
27199 Some(pane.downgrade()),
27200 true,
27201 window,
27202 cx,
27203 )
27204 })
27205 .unwrap()
27206 .await
27207 .downcast::<Editor>()
27208 .unwrap();
27209 pane.update(cx, |pane, cx| {
27210 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
27211 open_editor.update(cx, |editor, cx| {
27212 assert_eq!(
27213 editor.display_text(cx),
27214 main_text,
27215 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
27216 );
27217 })
27218 });
27219}
27220
27221#[gpui::test]
27222async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
27223 struct EmptyModalView {
27224 focus_handle: gpui::FocusHandle,
27225 }
27226 impl EventEmitter<DismissEvent> for EmptyModalView {}
27227 impl Render for EmptyModalView {
27228 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
27229 div()
27230 }
27231 }
27232 impl Focusable for EmptyModalView {
27233 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
27234 self.focus_handle.clone()
27235 }
27236 }
27237 impl workspace::ModalView for EmptyModalView {}
27238 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
27239 EmptyModalView {
27240 focus_handle: cx.focus_handle(),
27241 }
27242 }
27243
27244 init_test(cx, |_| {});
27245
27246 let fs = FakeFs::new(cx.executor());
27247 let project = Project::test(fs, [], cx).await;
27248 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27249 let workspace = window
27250 .read_with(cx, |mw, _| mw.workspace().clone())
27251 .unwrap();
27252 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
27253 let cx = &mut VisualTestContext::from_window(*window, cx);
27254 let editor = cx.new_window_entity(|window, cx| {
27255 Editor::new(
27256 EditorMode::full(),
27257 buffer,
27258 Some(project.clone()),
27259 window,
27260 cx,
27261 )
27262 });
27263 workspace.update_in(cx, |workspace, window, cx| {
27264 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
27265 });
27266
27267 editor.update_in(cx, |editor, window, cx| {
27268 editor.open_context_menu(&OpenContextMenu, window, cx);
27269 assert!(editor.mouse_context_menu.is_some());
27270 });
27271 workspace.update_in(cx, |workspace, window, cx| {
27272 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
27273 });
27274
27275 cx.read(|cx| {
27276 assert!(editor.read(cx).mouse_context_menu.is_none());
27277 });
27278}
27279
27280fn set_linked_edit_ranges(
27281 opening: (Point, Point),
27282 closing: (Point, Point),
27283 editor: &mut Editor,
27284 cx: &mut Context<Editor>,
27285) {
27286 let Some((buffer, _)) = editor
27287 .buffer
27288 .read(cx)
27289 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
27290 else {
27291 panic!("Failed to get buffer for selection position");
27292 };
27293 let buffer = buffer.read(cx);
27294 let buffer_id = buffer.remote_id();
27295 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
27296 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
27297 let mut linked_ranges = HashMap::default();
27298 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
27299 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
27300}
27301
27302#[gpui::test]
27303async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
27304 init_test(cx, |_| {});
27305
27306 let fs = FakeFs::new(cx.executor());
27307 fs.insert_file(path!("/file.html"), Default::default())
27308 .await;
27309
27310 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
27311
27312 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27313 let html_language = Arc::new(Language::new(
27314 LanguageConfig {
27315 name: "HTML".into(),
27316 matcher: LanguageMatcher {
27317 path_suffixes: vec!["html".to_string()],
27318 ..LanguageMatcher::default()
27319 },
27320 brackets: BracketPairConfig {
27321 pairs: vec![BracketPair {
27322 start: "<".into(),
27323 end: ">".into(),
27324 close: true,
27325 ..Default::default()
27326 }],
27327 ..Default::default()
27328 },
27329 ..Default::default()
27330 },
27331 Some(tree_sitter_html::LANGUAGE.into()),
27332 ));
27333 language_registry.add(html_language);
27334 let mut fake_servers = language_registry.register_fake_lsp(
27335 "HTML",
27336 FakeLspAdapter {
27337 capabilities: lsp::ServerCapabilities {
27338 completion_provider: Some(lsp::CompletionOptions {
27339 resolve_provider: Some(true),
27340 ..Default::default()
27341 }),
27342 ..Default::default()
27343 },
27344 ..Default::default()
27345 },
27346 );
27347
27348 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27349 let workspace = window
27350 .read_with(cx, |mw, _| mw.workspace().clone())
27351 .unwrap();
27352 let cx = &mut VisualTestContext::from_window(*window, cx);
27353
27354 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
27355 workspace.project().update(cx, |project, cx| {
27356 project.worktrees(cx).next().unwrap().read(cx).id()
27357 })
27358 });
27359
27360 project
27361 .update(cx, |project, cx| {
27362 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
27363 })
27364 .await
27365 .unwrap();
27366 let editor = workspace
27367 .update_in(cx, |workspace, window, cx| {
27368 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
27369 })
27370 .await
27371 .unwrap()
27372 .downcast::<Editor>()
27373 .unwrap();
27374
27375 let fake_server = fake_servers.next().await.unwrap();
27376 cx.run_until_parked();
27377 editor.update_in(cx, |editor, window, cx| {
27378 editor.set_text("<ad></ad>", window, cx);
27379 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
27380 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
27381 });
27382 set_linked_edit_ranges(
27383 (Point::new(0, 1), Point::new(0, 3)),
27384 (Point::new(0, 6), Point::new(0, 8)),
27385 editor,
27386 cx,
27387 );
27388 });
27389 let mut completion_handle =
27390 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
27391 Ok(Some(lsp::CompletionResponse::Array(vec![
27392 lsp::CompletionItem {
27393 label: "head".to_string(),
27394 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
27395 lsp::InsertReplaceEdit {
27396 new_text: "head".to_string(),
27397 insert: lsp::Range::new(
27398 lsp::Position::new(0, 1),
27399 lsp::Position::new(0, 3),
27400 ),
27401 replace: lsp::Range::new(
27402 lsp::Position::new(0, 1),
27403 lsp::Position::new(0, 3),
27404 ),
27405 },
27406 )),
27407 ..Default::default()
27408 },
27409 ])))
27410 });
27411 editor.update_in(cx, |editor, window, cx| {
27412 editor.show_completions(&ShowCompletions, window, cx);
27413 });
27414 cx.run_until_parked();
27415 completion_handle.next().await.unwrap();
27416 editor.update(cx, |editor, _| {
27417 assert!(
27418 editor.context_menu_visible(),
27419 "Completion menu should be visible"
27420 );
27421 });
27422 editor.update_in(cx, |editor, window, cx| {
27423 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
27424 });
27425 cx.executor().run_until_parked();
27426 editor.update(cx, |editor, cx| {
27427 assert_eq!(editor.text(cx), "<head></head>");
27428 });
27429}
27430
27431#[gpui::test]
27432async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
27433 init_test(cx, |_| {});
27434
27435 let mut cx = EditorTestContext::new(cx).await;
27436 let language = Arc::new(Language::new(
27437 LanguageConfig {
27438 name: "TSX".into(),
27439 matcher: LanguageMatcher {
27440 path_suffixes: vec!["tsx".to_string()],
27441 ..LanguageMatcher::default()
27442 },
27443 brackets: BracketPairConfig {
27444 pairs: vec![BracketPair {
27445 start: "<".into(),
27446 end: ">".into(),
27447 close: true,
27448 ..Default::default()
27449 }],
27450 ..Default::default()
27451 },
27452 linked_edit_characters: HashSet::from_iter(['.']),
27453 ..Default::default()
27454 },
27455 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
27456 ));
27457 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27458
27459 // Test typing > does not extend linked pair
27460 cx.set_state("<divˇ<div></div>");
27461 cx.update_editor(|editor, _, cx| {
27462 set_linked_edit_ranges(
27463 (Point::new(0, 1), Point::new(0, 4)),
27464 (Point::new(0, 11), Point::new(0, 14)),
27465 editor,
27466 cx,
27467 );
27468 });
27469 cx.update_editor(|editor, window, cx| {
27470 editor.handle_input(">", window, cx);
27471 });
27472 cx.assert_editor_state("<div>ˇ<div></div>");
27473
27474 // Test typing . do extend linked pair
27475 cx.set_state("<Animatedˇ></Animated>");
27476 cx.update_editor(|editor, _, cx| {
27477 set_linked_edit_ranges(
27478 (Point::new(0, 1), Point::new(0, 9)),
27479 (Point::new(0, 12), Point::new(0, 20)),
27480 editor,
27481 cx,
27482 );
27483 });
27484 cx.update_editor(|editor, window, cx| {
27485 editor.handle_input(".", window, cx);
27486 });
27487 cx.assert_editor_state("<Animated.ˇ></Animated.>");
27488 cx.update_editor(|editor, _, cx| {
27489 set_linked_edit_ranges(
27490 (Point::new(0, 1), Point::new(0, 10)),
27491 (Point::new(0, 13), Point::new(0, 21)),
27492 editor,
27493 cx,
27494 );
27495 });
27496 cx.update_editor(|editor, window, cx| {
27497 editor.handle_input("V", window, cx);
27498 });
27499 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
27500}
27501
27502#[gpui::test]
27503async fn test_linked_edits_on_typing_dot_without_language_override(cx: &mut TestAppContext) {
27504 init_test(cx, |_| {});
27505
27506 let mut cx = EditorTestContext::new(cx).await;
27507 let language = Arc::new(Language::new(
27508 LanguageConfig {
27509 name: "HTML".into(),
27510 matcher: LanguageMatcher {
27511 path_suffixes: vec!["html".to_string()],
27512 ..LanguageMatcher::default()
27513 },
27514 brackets: BracketPairConfig {
27515 pairs: vec![BracketPair {
27516 start: "<".into(),
27517 end: ">".into(),
27518 close: true,
27519 ..Default::default()
27520 }],
27521 ..Default::default()
27522 },
27523 ..Default::default()
27524 },
27525 Some(tree_sitter_html::LANGUAGE.into()),
27526 ));
27527 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27528
27529 cx.set_state("<Tableˇ></Table>");
27530 cx.update_editor(|editor, _, cx| {
27531 set_linked_edit_ranges(
27532 (Point::new(0, 1), Point::new(0, 6)),
27533 (Point::new(0, 9), Point::new(0, 14)),
27534 editor,
27535 cx,
27536 );
27537 });
27538 cx.update_editor(|editor, window, cx| {
27539 editor.handle_input(".", window, cx);
27540 });
27541 cx.assert_editor_state("<Table.ˇ></Table.>");
27542}
27543
27544#[gpui::test]
27545async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
27546 init_test(cx, |_| {});
27547
27548 let fs = FakeFs::new(cx.executor());
27549 fs.insert_tree(
27550 path!("/root"),
27551 json!({
27552 "a": {
27553 "main.rs": "fn main() {}",
27554 },
27555 "foo": {
27556 "bar": {
27557 "external_file.rs": "pub mod external {}",
27558 }
27559 }
27560 }),
27561 )
27562 .await;
27563
27564 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
27565 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27566 language_registry.add(rust_lang());
27567 let _fake_servers = language_registry.register_fake_lsp(
27568 "Rust",
27569 FakeLspAdapter {
27570 ..FakeLspAdapter::default()
27571 },
27572 );
27573 let (multi_workspace, cx) =
27574 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27575 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
27576 let worktree_id = workspace.update(cx, |workspace, cx| {
27577 workspace.project().update(cx, |project, cx| {
27578 project.worktrees(cx).next().unwrap().read(cx).id()
27579 })
27580 });
27581
27582 let assert_language_servers_count =
27583 |expected: usize, context: &str, cx: &mut VisualTestContext| {
27584 project.update(cx, |project, cx| {
27585 let current = project
27586 .lsp_store()
27587 .read(cx)
27588 .as_local()
27589 .unwrap()
27590 .language_servers
27591 .len();
27592 assert_eq!(expected, current, "{context}");
27593 });
27594 };
27595
27596 assert_language_servers_count(
27597 0,
27598 "No servers should be running before any file is open",
27599 cx,
27600 );
27601 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
27602 let main_editor = workspace
27603 .update_in(cx, |workspace, window, cx| {
27604 workspace.open_path(
27605 (worktree_id, rel_path("main.rs")),
27606 Some(pane.downgrade()),
27607 true,
27608 window,
27609 cx,
27610 )
27611 })
27612 .unwrap()
27613 .await
27614 .downcast::<Editor>()
27615 .unwrap();
27616 pane.update(cx, |pane, cx| {
27617 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
27618 open_editor.update(cx, |editor, cx| {
27619 assert_eq!(
27620 editor.display_text(cx),
27621 "fn main() {}",
27622 "Original main.rs text on initial open",
27623 );
27624 });
27625 assert_eq!(open_editor, main_editor);
27626 });
27627 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
27628
27629 let external_editor = workspace
27630 .update_in(cx, |workspace, window, cx| {
27631 workspace.open_abs_path(
27632 PathBuf::from("/root/foo/bar/external_file.rs"),
27633 OpenOptions::default(),
27634 window,
27635 cx,
27636 )
27637 })
27638 .await
27639 .expect("opening external file")
27640 .downcast::<Editor>()
27641 .expect("downcasted external file's open element to editor");
27642 pane.update(cx, |pane, cx| {
27643 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
27644 open_editor.update(cx, |editor, cx| {
27645 assert_eq!(
27646 editor.display_text(cx),
27647 "pub mod external {}",
27648 "External file is open now",
27649 );
27650 });
27651 assert_eq!(open_editor, external_editor);
27652 });
27653 assert_language_servers_count(
27654 1,
27655 "Second, external, *.rs file should join the existing server",
27656 cx,
27657 );
27658
27659 pane.update_in(cx, |pane, window, cx| {
27660 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27661 })
27662 .await
27663 .unwrap();
27664 pane.update_in(cx, |pane, window, cx| {
27665 pane.navigate_backward(&Default::default(), window, cx);
27666 });
27667 cx.run_until_parked();
27668 pane.update(cx, |pane, cx| {
27669 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
27670 open_editor.update(cx, |editor, cx| {
27671 assert_eq!(
27672 editor.display_text(cx),
27673 "pub mod external {}",
27674 "External file is open now",
27675 );
27676 });
27677 });
27678 assert_language_servers_count(
27679 1,
27680 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
27681 cx,
27682 );
27683
27684 cx.update(|_, cx| {
27685 workspace::reload(cx);
27686 });
27687 assert_language_servers_count(
27688 1,
27689 "After reloading the worktree with local and external files opened, only one project should be started",
27690 cx,
27691 );
27692}
27693
27694#[gpui::test]
27695async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
27696 init_test(cx, |_| {});
27697
27698 let mut cx = EditorTestContext::new(cx).await;
27699 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
27700 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27701
27702 // test cursor move to start of each line on tab
27703 // for `if`, `elif`, `else`, `while`, `with` and `for`
27704 cx.set_state(indoc! {"
27705 def main():
27706 ˇ for item in items:
27707 ˇ while item.active:
27708 ˇ if item.value > 10:
27709 ˇ continue
27710 ˇ elif item.value < 0:
27711 ˇ break
27712 ˇ else:
27713 ˇ with item.context() as ctx:
27714 ˇ yield count
27715 ˇ else:
27716 ˇ log('while else')
27717 ˇ else:
27718 ˇ log('for else')
27719 "});
27720 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
27721 cx.wait_for_autoindent_applied().await;
27722 cx.assert_editor_state(indoc! {"
27723 def main():
27724 ˇfor item in items:
27725 ˇwhile item.active:
27726 ˇif item.value > 10:
27727 ˇcontinue
27728 ˇelif item.value < 0:
27729 ˇbreak
27730 ˇelse:
27731 ˇwith item.context() as ctx:
27732 ˇyield count
27733 ˇelse:
27734 ˇlog('while else')
27735 ˇelse:
27736 ˇlog('for else')
27737 "});
27738 // test relative indent is preserved when tab
27739 // for `if`, `elif`, `else`, `while`, `with` and `for`
27740 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
27741 cx.wait_for_autoindent_applied().await;
27742 cx.assert_editor_state(indoc! {"
27743 def main():
27744 ˇfor item in items:
27745 ˇwhile item.active:
27746 ˇif item.value > 10:
27747 ˇcontinue
27748 ˇelif item.value < 0:
27749 ˇbreak
27750 ˇelse:
27751 ˇwith item.context() as ctx:
27752 ˇyield count
27753 ˇelse:
27754 ˇlog('while else')
27755 ˇelse:
27756 ˇlog('for else')
27757 "});
27758
27759 // test cursor move to start of each line on tab
27760 // for `try`, `except`, `else`, `finally`, `match` and `def`
27761 cx.set_state(indoc! {"
27762 def main():
27763 ˇ try:
27764 ˇ fetch()
27765 ˇ except ValueError:
27766 ˇ handle_error()
27767 ˇ else:
27768 ˇ match value:
27769 ˇ case _:
27770 ˇ finally:
27771 ˇ def status():
27772 ˇ return 0
27773 "});
27774 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
27775 cx.wait_for_autoindent_applied().await;
27776 cx.assert_editor_state(indoc! {"
27777 def main():
27778 ˇtry:
27779 ˇfetch()
27780 ˇexcept ValueError:
27781 ˇhandle_error()
27782 ˇelse:
27783 ˇmatch value:
27784 ˇcase _:
27785 ˇfinally:
27786 ˇdef status():
27787 ˇreturn 0
27788 "});
27789 // test relative indent is preserved when tab
27790 // for `try`, `except`, `else`, `finally`, `match` and `def`
27791 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
27792 cx.wait_for_autoindent_applied().await;
27793 cx.assert_editor_state(indoc! {"
27794 def main():
27795 ˇtry:
27796 ˇfetch()
27797 ˇexcept ValueError:
27798 ˇhandle_error()
27799 ˇelse:
27800 ˇmatch value:
27801 ˇcase _:
27802 ˇfinally:
27803 ˇdef status():
27804 ˇreturn 0
27805 "});
27806}
27807
27808#[gpui::test]
27809async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
27810 init_test(cx, |_| {});
27811
27812 let mut cx = EditorTestContext::new(cx).await;
27813 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
27814 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27815
27816 // test `else` auto outdents when typed inside `if` block
27817 cx.set_state(indoc! {"
27818 def main():
27819 if i == 2:
27820 return
27821 ˇ
27822 "});
27823 cx.update_editor(|editor, window, cx| {
27824 editor.handle_input("else:", window, cx);
27825 });
27826 cx.wait_for_autoindent_applied().await;
27827 cx.assert_editor_state(indoc! {"
27828 def main():
27829 if i == 2:
27830 return
27831 else:ˇ
27832 "});
27833
27834 // test `except` auto outdents when typed inside `try` block
27835 cx.set_state(indoc! {"
27836 def main():
27837 try:
27838 i = 2
27839 ˇ
27840 "});
27841 cx.update_editor(|editor, window, cx| {
27842 editor.handle_input("except:", window, cx);
27843 });
27844 cx.wait_for_autoindent_applied().await;
27845 cx.assert_editor_state(indoc! {"
27846 def main():
27847 try:
27848 i = 2
27849 except:ˇ
27850 "});
27851
27852 // test `else` auto outdents when typed inside `except` block
27853 cx.set_state(indoc! {"
27854 def main():
27855 try:
27856 i = 2
27857 except:
27858 j = 2
27859 ˇ
27860 "});
27861 cx.update_editor(|editor, window, cx| {
27862 editor.handle_input("else:", window, cx);
27863 });
27864 cx.wait_for_autoindent_applied().await;
27865 cx.assert_editor_state(indoc! {"
27866 def main():
27867 try:
27868 i = 2
27869 except:
27870 j = 2
27871 else:ˇ
27872 "});
27873
27874 // test `finally` auto outdents when typed inside `else` block
27875 cx.set_state(indoc! {"
27876 def main():
27877 try:
27878 i = 2
27879 except:
27880 j = 2
27881 else:
27882 k = 2
27883 ˇ
27884 "});
27885 cx.update_editor(|editor, window, cx| {
27886 editor.handle_input("finally:", window, cx);
27887 });
27888 cx.wait_for_autoindent_applied().await;
27889 cx.assert_editor_state(indoc! {"
27890 def main():
27891 try:
27892 i = 2
27893 except:
27894 j = 2
27895 else:
27896 k = 2
27897 finally:ˇ
27898 "});
27899
27900 // test `else` does not outdents when typed inside `except` block right after for block
27901 cx.set_state(indoc! {"
27902 def main():
27903 try:
27904 i = 2
27905 except:
27906 for i in range(n):
27907 pass
27908 ˇ
27909 "});
27910 cx.update_editor(|editor, window, cx| {
27911 editor.handle_input("else:", window, cx);
27912 });
27913 cx.wait_for_autoindent_applied().await;
27914 cx.assert_editor_state(indoc! {"
27915 def main():
27916 try:
27917 i = 2
27918 except:
27919 for i in range(n):
27920 pass
27921 else:ˇ
27922 "});
27923
27924 // test `finally` auto outdents when typed inside `else` block right after for block
27925 cx.set_state(indoc! {"
27926 def main():
27927 try:
27928 i = 2
27929 except:
27930 j = 2
27931 else:
27932 for i in range(n):
27933 pass
27934 ˇ
27935 "});
27936 cx.update_editor(|editor, window, cx| {
27937 editor.handle_input("finally:", window, cx);
27938 });
27939 cx.wait_for_autoindent_applied().await;
27940 cx.assert_editor_state(indoc! {"
27941 def main():
27942 try:
27943 i = 2
27944 except:
27945 j = 2
27946 else:
27947 for i in range(n):
27948 pass
27949 finally:ˇ
27950 "});
27951
27952 // test `except` outdents to inner "try" block
27953 cx.set_state(indoc! {"
27954 def main():
27955 try:
27956 i = 2
27957 if i == 2:
27958 try:
27959 i = 3
27960 ˇ
27961 "});
27962 cx.update_editor(|editor, window, cx| {
27963 editor.handle_input("except:", window, cx);
27964 });
27965 cx.wait_for_autoindent_applied().await;
27966 cx.assert_editor_state(indoc! {"
27967 def main():
27968 try:
27969 i = 2
27970 if i == 2:
27971 try:
27972 i = 3
27973 except:ˇ
27974 "});
27975
27976 // test `except` outdents to outer "try" block
27977 cx.set_state(indoc! {"
27978 def main():
27979 try:
27980 i = 2
27981 if i == 2:
27982 try:
27983 i = 3
27984 ˇ
27985 "});
27986 cx.update_editor(|editor, window, cx| {
27987 editor.handle_input("except:", window, cx);
27988 });
27989 cx.wait_for_autoindent_applied().await;
27990 cx.assert_editor_state(indoc! {"
27991 def main():
27992 try:
27993 i = 2
27994 if i == 2:
27995 try:
27996 i = 3
27997 except:ˇ
27998 "});
27999
28000 // test `else` stays at correct indent when typed after `for` block
28001 cx.set_state(indoc! {"
28002 def main():
28003 for i in range(10):
28004 if i == 3:
28005 break
28006 ˇ
28007 "});
28008 cx.update_editor(|editor, window, cx| {
28009 editor.handle_input("else:", window, cx);
28010 });
28011 cx.wait_for_autoindent_applied().await;
28012 cx.assert_editor_state(indoc! {"
28013 def main():
28014 for i in range(10):
28015 if i == 3:
28016 break
28017 else:ˇ
28018 "});
28019
28020 // test does not outdent on typing after line with square brackets
28021 cx.set_state(indoc! {"
28022 def f() -> list[str]:
28023 ˇ
28024 "});
28025 cx.update_editor(|editor, window, cx| {
28026 editor.handle_input("a", window, cx);
28027 });
28028 cx.wait_for_autoindent_applied().await;
28029 cx.assert_editor_state(indoc! {"
28030 def f() -> list[str]:
28031 aˇ
28032 "});
28033
28034 // test does not outdent on typing : after case keyword
28035 cx.set_state(indoc! {"
28036 match 1:
28037 caseˇ
28038 "});
28039 cx.update_editor(|editor, window, cx| {
28040 editor.handle_input(":", window, cx);
28041 });
28042 cx.wait_for_autoindent_applied().await;
28043 cx.assert_editor_state(indoc! {"
28044 match 1:
28045 case:ˇ
28046 "});
28047}
28048
28049#[gpui::test]
28050async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
28051 init_test(cx, |_| {});
28052 update_test_language_settings(cx, &|settings| {
28053 settings.defaults.extend_comment_on_newline = Some(false);
28054 });
28055 let mut cx = EditorTestContext::new(cx).await;
28056 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
28057 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
28058
28059 // test correct indent after newline on comment
28060 cx.set_state(indoc! {"
28061 # COMMENT:ˇ
28062 "});
28063 cx.update_editor(|editor, window, cx| {
28064 editor.newline(&Newline, window, cx);
28065 });
28066 cx.wait_for_autoindent_applied().await;
28067 cx.assert_editor_state(indoc! {"
28068 # COMMENT:
28069 ˇ
28070 "});
28071
28072 // test correct indent after newline in brackets
28073 cx.set_state(indoc! {"
28074 {ˇ}
28075 "});
28076 cx.update_editor(|editor, window, cx| {
28077 editor.newline(&Newline, window, cx);
28078 });
28079 cx.wait_for_autoindent_applied().await;
28080 cx.assert_editor_state(indoc! {"
28081 {
28082 ˇ
28083 }
28084 "});
28085
28086 cx.set_state(indoc! {"
28087 (ˇ)
28088 "});
28089 cx.update_editor(|editor, window, cx| {
28090 editor.newline(&Newline, window, cx);
28091 });
28092 cx.run_until_parked();
28093 cx.assert_editor_state(indoc! {"
28094 (
28095 ˇ
28096 )
28097 "});
28098
28099 // do not indent after empty lists or dictionaries
28100 cx.set_state(indoc! {"
28101 a = []ˇ
28102 "});
28103 cx.update_editor(|editor, window, cx| {
28104 editor.newline(&Newline, window, cx);
28105 });
28106 cx.run_until_parked();
28107 cx.assert_editor_state(indoc! {"
28108 a = []
28109 ˇ
28110 "});
28111}
28112
28113#[gpui::test]
28114async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
28115 init_test(cx, |_| {});
28116
28117 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
28118 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
28119 language_registry.add(markdown_lang());
28120 language_registry.add(python_lang);
28121
28122 let mut cx = EditorTestContext::new(cx).await;
28123 cx.update_buffer(|buffer, cx| {
28124 buffer.set_language_registry(language_registry);
28125 buffer.set_language(Some(markdown_lang()), cx);
28126 });
28127
28128 // Test that `else:` correctly outdents to match `if:` inside the Python code block
28129 cx.set_state(indoc! {"
28130 # Heading
28131
28132 ```python
28133 def main():
28134 if condition:
28135 pass
28136 ˇ
28137 ```
28138 "});
28139 cx.update_editor(|editor, window, cx| {
28140 editor.handle_input("else:", window, cx);
28141 });
28142 cx.run_until_parked();
28143 cx.assert_editor_state(indoc! {"
28144 # Heading
28145
28146 ```python
28147 def main():
28148 if condition:
28149 pass
28150 else:ˇ
28151 ```
28152 "});
28153}
28154
28155#[gpui::test]
28156async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
28157 init_test(cx, |_| {});
28158
28159 let mut cx = EditorTestContext::new(cx).await;
28160 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
28161 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
28162
28163 // test cursor move to start of each line on tab
28164 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
28165 cx.set_state(indoc! {"
28166 function main() {
28167 ˇ for item in $items; do
28168 ˇ while [ -n \"$item\" ]; do
28169 ˇ if [ \"$value\" -gt 10 ]; then
28170 ˇ continue
28171 ˇ elif [ \"$value\" -lt 0 ]; then
28172 ˇ break
28173 ˇ else
28174 ˇ echo \"$item\"
28175 ˇ fi
28176 ˇ done
28177 ˇ done
28178 ˇ}
28179 "});
28180 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
28181 cx.wait_for_autoindent_applied().await;
28182 cx.assert_editor_state(indoc! {"
28183 function main() {
28184 ˇfor item in $items; do
28185 ˇwhile [ -n \"$item\" ]; do
28186 ˇif [ \"$value\" -gt 10 ]; then
28187 ˇcontinue
28188 ˇelif [ \"$value\" -lt 0 ]; then
28189 ˇbreak
28190 ˇelse
28191 ˇecho \"$item\"
28192 ˇfi
28193 ˇdone
28194 ˇdone
28195 ˇ}
28196 "});
28197 // test relative indent is preserved when tab
28198 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
28199 cx.wait_for_autoindent_applied().await;
28200 cx.assert_editor_state(indoc! {"
28201 function main() {
28202 ˇfor item in $items; do
28203 ˇwhile [ -n \"$item\" ]; do
28204 ˇif [ \"$value\" -gt 10 ]; then
28205 ˇcontinue
28206 ˇelif [ \"$value\" -lt 0 ]; then
28207 ˇbreak
28208 ˇelse
28209 ˇecho \"$item\"
28210 ˇfi
28211 ˇdone
28212 ˇdone
28213 ˇ}
28214 "});
28215
28216 // test cursor move to start of each line on tab
28217 // for `case` statement with patterns
28218 cx.set_state(indoc! {"
28219 function handle() {
28220 ˇ case \"$1\" in
28221 ˇ start)
28222 ˇ echo \"a\"
28223 ˇ ;;
28224 ˇ stop)
28225 ˇ echo \"b\"
28226 ˇ ;;
28227 ˇ *)
28228 ˇ echo \"c\"
28229 ˇ ;;
28230 ˇ esac
28231 ˇ}
28232 "});
28233 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
28234 cx.wait_for_autoindent_applied().await;
28235 cx.assert_editor_state(indoc! {"
28236 function handle() {
28237 ˇcase \"$1\" in
28238 ˇstart)
28239 ˇecho \"a\"
28240 ˇ;;
28241 ˇstop)
28242 ˇecho \"b\"
28243 ˇ;;
28244 ˇ*)
28245 ˇecho \"c\"
28246 ˇ;;
28247 ˇesac
28248 ˇ}
28249 "});
28250}
28251
28252#[gpui::test]
28253async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
28254 init_test(cx, |_| {});
28255
28256 let mut cx = EditorTestContext::new(cx).await;
28257 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
28258 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
28259
28260 // test indents on comment insert
28261 cx.set_state(indoc! {"
28262 function main() {
28263 ˇ for item in $items; do
28264 ˇ while [ -n \"$item\" ]; do
28265 ˇ if [ \"$value\" -gt 10 ]; then
28266 ˇ continue
28267 ˇ elif [ \"$value\" -lt 0 ]; then
28268 ˇ break
28269 ˇ else
28270 ˇ echo \"$item\"
28271 ˇ fi
28272 ˇ done
28273 ˇ done
28274 ˇ}
28275 "});
28276 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
28277 cx.wait_for_autoindent_applied().await;
28278 cx.assert_editor_state(indoc! {"
28279 function main() {
28280 #ˇ for item in $items; do
28281 #ˇ while [ -n \"$item\" ]; do
28282 #ˇ if [ \"$value\" -gt 10 ]; then
28283 #ˇ continue
28284 #ˇ elif [ \"$value\" -lt 0 ]; then
28285 #ˇ break
28286 #ˇ else
28287 #ˇ echo \"$item\"
28288 #ˇ fi
28289 #ˇ done
28290 #ˇ done
28291 #ˇ}
28292 "});
28293}
28294
28295#[gpui::test]
28296async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
28297 init_test(cx, |_| {});
28298
28299 let mut cx = EditorTestContext::new(cx).await;
28300 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
28301 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
28302
28303 // test `else` auto outdents when typed inside `if` block
28304 cx.set_state(indoc! {"
28305 if [ \"$1\" = \"test\" ]; then
28306 echo \"foo bar\"
28307 ˇ
28308 "});
28309 cx.update_editor(|editor, window, cx| {
28310 editor.handle_input("else", window, cx);
28311 });
28312 cx.wait_for_autoindent_applied().await;
28313 cx.assert_editor_state(indoc! {"
28314 if [ \"$1\" = \"test\" ]; then
28315 echo \"foo bar\"
28316 elseˇ
28317 "});
28318
28319 // test `elif` auto outdents when typed inside `if` block
28320 cx.set_state(indoc! {"
28321 if [ \"$1\" = \"test\" ]; then
28322 echo \"foo bar\"
28323 ˇ
28324 "});
28325 cx.update_editor(|editor, window, cx| {
28326 editor.handle_input("elif", window, cx);
28327 });
28328 cx.wait_for_autoindent_applied().await;
28329 cx.assert_editor_state(indoc! {"
28330 if [ \"$1\" = \"test\" ]; then
28331 echo \"foo bar\"
28332 elifˇ
28333 "});
28334
28335 // test `fi` auto outdents when typed inside `else` block
28336 cx.set_state(indoc! {"
28337 if [ \"$1\" = \"test\" ]; then
28338 echo \"foo bar\"
28339 else
28340 echo \"bar baz\"
28341 ˇ
28342 "});
28343 cx.update_editor(|editor, window, cx| {
28344 editor.handle_input("fi", window, cx);
28345 });
28346 cx.wait_for_autoindent_applied().await;
28347 cx.assert_editor_state(indoc! {"
28348 if [ \"$1\" = \"test\" ]; then
28349 echo \"foo bar\"
28350 else
28351 echo \"bar baz\"
28352 fiˇ
28353 "});
28354
28355 // test `done` auto outdents when typed inside `while` block
28356 cx.set_state(indoc! {"
28357 while read line; do
28358 echo \"$line\"
28359 ˇ
28360 "});
28361 cx.update_editor(|editor, window, cx| {
28362 editor.handle_input("done", window, cx);
28363 });
28364 cx.wait_for_autoindent_applied().await;
28365 cx.assert_editor_state(indoc! {"
28366 while read line; do
28367 echo \"$line\"
28368 doneˇ
28369 "});
28370
28371 // test `done` auto outdents when typed inside `for` block
28372 cx.set_state(indoc! {"
28373 for file in *.txt; do
28374 cat \"$file\"
28375 ˇ
28376 "});
28377 cx.update_editor(|editor, window, cx| {
28378 editor.handle_input("done", window, cx);
28379 });
28380 cx.wait_for_autoindent_applied().await;
28381 cx.assert_editor_state(indoc! {"
28382 for file in *.txt; do
28383 cat \"$file\"
28384 doneˇ
28385 "});
28386
28387 // test `esac` auto outdents when typed inside `case` block
28388 cx.set_state(indoc! {"
28389 case \"$1\" in
28390 start)
28391 echo \"foo bar\"
28392 ;;
28393 stop)
28394 echo \"bar baz\"
28395 ;;
28396 ˇ
28397 "});
28398 cx.update_editor(|editor, window, cx| {
28399 editor.handle_input("esac", window, cx);
28400 });
28401 cx.wait_for_autoindent_applied().await;
28402 cx.assert_editor_state(indoc! {"
28403 case \"$1\" in
28404 start)
28405 echo \"foo bar\"
28406 ;;
28407 stop)
28408 echo \"bar baz\"
28409 ;;
28410 esacˇ
28411 "});
28412
28413 // test `*)` auto outdents when typed inside `case` block
28414 cx.set_state(indoc! {"
28415 case \"$1\" in
28416 start)
28417 echo \"foo bar\"
28418 ;;
28419 ˇ
28420 "});
28421 cx.update_editor(|editor, window, cx| {
28422 editor.handle_input("*)", window, cx);
28423 });
28424 cx.wait_for_autoindent_applied().await;
28425 cx.assert_editor_state(indoc! {"
28426 case \"$1\" in
28427 start)
28428 echo \"foo bar\"
28429 ;;
28430 *)ˇ
28431 "});
28432
28433 // test `fi` outdents to correct level with nested if blocks
28434 cx.set_state(indoc! {"
28435 if [ \"$1\" = \"test\" ]; then
28436 echo \"outer if\"
28437 if [ \"$2\" = \"debug\" ]; then
28438 echo \"inner if\"
28439 ˇ
28440 "});
28441 cx.update_editor(|editor, window, cx| {
28442 editor.handle_input("fi", window, cx);
28443 });
28444 cx.wait_for_autoindent_applied().await;
28445 cx.assert_editor_state(indoc! {"
28446 if [ \"$1\" = \"test\" ]; then
28447 echo \"outer if\"
28448 if [ \"$2\" = \"debug\" ]; then
28449 echo \"inner if\"
28450 fiˇ
28451 "});
28452}
28453
28454#[gpui::test]
28455async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
28456 init_test(cx, |_| {});
28457 update_test_language_settings(cx, &|settings| {
28458 settings.defaults.extend_comment_on_newline = Some(false);
28459 });
28460 let mut cx = EditorTestContext::new(cx).await;
28461 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
28462 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
28463
28464 // test correct indent after newline on comment
28465 cx.set_state(indoc! {"
28466 # COMMENT:ˇ
28467 "});
28468 cx.update_editor(|editor, window, cx| {
28469 editor.newline(&Newline, window, cx);
28470 });
28471 cx.wait_for_autoindent_applied().await;
28472 cx.assert_editor_state(indoc! {"
28473 # COMMENT:
28474 ˇ
28475 "});
28476
28477 // test correct indent after newline after `then`
28478 cx.set_state(indoc! {"
28479
28480 if [ \"$1\" = \"test\" ]; thenˇ
28481 "});
28482 cx.update_editor(|editor, window, cx| {
28483 editor.newline(&Newline, window, cx);
28484 });
28485 cx.wait_for_autoindent_applied().await;
28486 cx.assert_editor_state(indoc! {"
28487
28488 if [ \"$1\" = \"test\" ]; then
28489 ˇ
28490 "});
28491
28492 // test correct indent after newline after `else`
28493 cx.set_state(indoc! {"
28494 if [ \"$1\" = \"test\" ]; then
28495 elseˇ
28496 "});
28497 cx.update_editor(|editor, window, cx| {
28498 editor.newline(&Newline, window, cx);
28499 });
28500 cx.wait_for_autoindent_applied().await;
28501 cx.assert_editor_state(indoc! {"
28502 if [ \"$1\" = \"test\" ]; then
28503 else
28504 ˇ
28505 "});
28506
28507 // test correct indent after newline after `elif`
28508 cx.set_state(indoc! {"
28509 if [ \"$1\" = \"test\" ]; then
28510 elifˇ
28511 "});
28512 cx.update_editor(|editor, window, cx| {
28513 editor.newline(&Newline, window, cx);
28514 });
28515 cx.wait_for_autoindent_applied().await;
28516 cx.assert_editor_state(indoc! {"
28517 if [ \"$1\" = \"test\" ]; then
28518 elif
28519 ˇ
28520 "});
28521
28522 // test correct indent after newline after `do`
28523 cx.set_state(indoc! {"
28524 for file in *.txt; doˇ
28525 "});
28526 cx.update_editor(|editor, window, cx| {
28527 editor.newline(&Newline, window, cx);
28528 });
28529 cx.wait_for_autoindent_applied().await;
28530 cx.assert_editor_state(indoc! {"
28531 for file in *.txt; do
28532 ˇ
28533 "});
28534
28535 // test correct indent after newline after case pattern
28536 cx.set_state(indoc! {"
28537 case \"$1\" in
28538 start)ˇ
28539 "});
28540 cx.update_editor(|editor, window, cx| {
28541 editor.newline(&Newline, window, cx);
28542 });
28543 cx.wait_for_autoindent_applied().await;
28544 cx.assert_editor_state(indoc! {"
28545 case \"$1\" in
28546 start)
28547 ˇ
28548 "});
28549
28550 // test correct indent after newline after case pattern
28551 cx.set_state(indoc! {"
28552 case \"$1\" in
28553 start)
28554 ;;
28555 *)ˇ
28556 "});
28557 cx.update_editor(|editor, window, cx| {
28558 editor.newline(&Newline, window, cx);
28559 });
28560 cx.wait_for_autoindent_applied().await;
28561 cx.assert_editor_state(indoc! {"
28562 case \"$1\" in
28563 start)
28564 ;;
28565 *)
28566 ˇ
28567 "});
28568
28569 // test correct indent after newline after function opening brace
28570 cx.set_state(indoc! {"
28571 function test() {ˇ}
28572 "});
28573 cx.update_editor(|editor, window, cx| {
28574 editor.newline(&Newline, window, cx);
28575 });
28576 cx.wait_for_autoindent_applied().await;
28577 cx.assert_editor_state(indoc! {"
28578 function test() {
28579 ˇ
28580 }
28581 "});
28582
28583 // test no extra indent after semicolon on same line
28584 cx.set_state(indoc! {"
28585 echo \"test\";ˇ
28586 "});
28587 cx.update_editor(|editor, window, cx| {
28588 editor.newline(&Newline, window, cx);
28589 });
28590 cx.wait_for_autoindent_applied().await;
28591 cx.assert_editor_state(indoc! {"
28592 echo \"test\";
28593 ˇ
28594 "});
28595}
28596
28597fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
28598 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
28599 point..point
28600}
28601
28602#[track_caller]
28603fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
28604 let (text, ranges) = marked_text_ranges(marked_text, true);
28605 assert_eq!(editor.text(cx), text);
28606 assert_eq!(
28607 editor.selections.ranges(&editor.display_snapshot(cx)),
28608 ranges
28609 .iter()
28610 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
28611 .collect::<Vec<_>>(),
28612 "Assert selections are {}",
28613 marked_text
28614 );
28615}
28616
28617pub fn handle_signature_help_request(
28618 cx: &mut EditorLspTestContext,
28619 mocked_response: lsp::SignatureHelp,
28620) -> impl Future<Output = ()> + use<> {
28621 let mut request =
28622 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
28623 let mocked_response = mocked_response.clone();
28624 async move { Ok(Some(mocked_response)) }
28625 });
28626
28627 async move {
28628 request.next().await;
28629 }
28630}
28631
28632#[track_caller]
28633pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
28634 cx.update_editor(|editor, _, _| {
28635 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
28636 let entries = menu.entries.borrow();
28637 let entries = entries
28638 .iter()
28639 .map(|entry| entry.string.as_str())
28640 .collect::<Vec<_>>();
28641 assert_eq!(entries, expected);
28642 } else {
28643 panic!("Expected completions menu");
28644 }
28645 });
28646}
28647
28648#[gpui::test]
28649async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
28650 init_test(cx, |_| {});
28651 let mut cx = EditorLspTestContext::new_rust(
28652 lsp::ServerCapabilities {
28653 completion_provider: Some(lsp::CompletionOptions {
28654 ..Default::default()
28655 }),
28656 ..Default::default()
28657 },
28658 cx,
28659 )
28660 .await;
28661 cx.lsp
28662 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
28663 Ok(Some(lsp::CompletionResponse::Array(vec![
28664 lsp::CompletionItem {
28665 label: "unsafe".into(),
28666 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
28667 range: lsp::Range {
28668 start: lsp::Position {
28669 line: 0,
28670 character: 9,
28671 },
28672 end: lsp::Position {
28673 line: 0,
28674 character: 11,
28675 },
28676 },
28677 new_text: "unsafe".to_string(),
28678 })),
28679 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
28680 ..Default::default()
28681 },
28682 ])))
28683 });
28684
28685 cx.update_editor(|editor, _, cx| {
28686 editor.project().unwrap().update(cx, |project, cx| {
28687 project.snippets().update(cx, |snippets, _cx| {
28688 snippets.add_snippet_for_test(
28689 None,
28690 PathBuf::from("test_snippets.json"),
28691 vec![
28692 Arc::new(project::snippet_provider::Snippet {
28693 prefix: vec![
28694 "unlimited word count".to_string(),
28695 "unlimit word count".to_string(),
28696 "unlimited unknown".to_string(),
28697 ],
28698 body: "this is many words".to_string(),
28699 description: Some("description".to_string()),
28700 name: "multi-word snippet test".to_string(),
28701 }),
28702 Arc::new(project::snippet_provider::Snippet {
28703 prefix: vec!["unsnip".to_string(), "@few".to_string()],
28704 body: "fewer words".to_string(),
28705 description: Some("alt description".to_string()),
28706 name: "other name".to_string(),
28707 }),
28708 Arc::new(project::snippet_provider::Snippet {
28709 prefix: vec!["ab aa".to_string()],
28710 body: "abcd".to_string(),
28711 description: None,
28712 name: "alphabet".to_string(),
28713 }),
28714 ],
28715 );
28716 });
28717 })
28718 });
28719
28720 let get_completions = |cx: &mut EditorLspTestContext| {
28721 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
28722 Some(CodeContextMenu::Completions(context_menu)) => {
28723 let entries = context_menu.entries.borrow();
28724 entries
28725 .iter()
28726 .map(|entry| entry.string.clone())
28727 .collect_vec()
28728 }
28729 _ => vec![],
28730 })
28731 };
28732
28733 // snippets:
28734 // @foo
28735 // foo bar
28736 //
28737 // when typing:
28738 //
28739 // when typing:
28740 // - if I type a symbol "open the completions with snippets only"
28741 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
28742 //
28743 // stuff we need:
28744 // - filtering logic change?
28745 // - remember how far back the completion started.
28746
28747 let test_cases: &[(&str, &[&str])] = &[
28748 (
28749 "un",
28750 &[
28751 "unsafe",
28752 "unlimit word count",
28753 "unlimited unknown",
28754 "unlimited word count",
28755 "unsnip",
28756 ],
28757 ),
28758 (
28759 "u ",
28760 &[
28761 "unlimit word count",
28762 "unlimited unknown",
28763 "unlimited word count",
28764 ],
28765 ),
28766 ("u a", &["ab aa", "unsafe"]), // unsAfe
28767 (
28768 "u u",
28769 &[
28770 "unsafe",
28771 "unlimit word count",
28772 "unlimited unknown", // ranked highest among snippets
28773 "unlimited word count",
28774 "unsnip",
28775 ],
28776 ),
28777 ("uw c", &["unlimit word count", "unlimited word count"]),
28778 (
28779 "u w",
28780 &[
28781 "unlimit word count",
28782 "unlimited word count",
28783 "unlimited unknown",
28784 ],
28785 ),
28786 ("u w ", &["unlimit word count", "unlimited word count"]),
28787 (
28788 "u ",
28789 &[
28790 "unlimit word count",
28791 "unlimited unknown",
28792 "unlimited word count",
28793 ],
28794 ),
28795 ("wor", &[]),
28796 ("uf", &["unsafe"]),
28797 ("af", &["unsafe"]),
28798 ("afu", &[]),
28799 (
28800 "ue",
28801 &["unsafe", "unlimited unknown", "unlimited word count"],
28802 ),
28803 ("@", &["@few"]),
28804 ("@few", &["@few"]),
28805 ("@ ", &[]),
28806 ("a@", &["@few"]),
28807 ("a@f", &["@few", "unsafe"]),
28808 ("a@fw", &["@few"]),
28809 ("a", &["ab aa", "unsafe"]),
28810 ("aa", &["ab aa"]),
28811 ("aaa", &["ab aa"]),
28812 ("ab", &["ab aa"]),
28813 ("ab ", &["ab aa"]),
28814 ("ab a", &["ab aa", "unsafe"]),
28815 ("ab ab", &["ab aa"]),
28816 ("ab ab aa", &["ab aa"]),
28817 ];
28818
28819 for &(input_to_simulate, expected_completions) in test_cases {
28820 cx.set_state("fn a() { ˇ }\n");
28821 for c in input_to_simulate.split("") {
28822 cx.simulate_input(c);
28823 cx.run_until_parked();
28824 }
28825 let expected_completions = expected_completions
28826 .iter()
28827 .map(|s| s.to_string())
28828 .collect_vec();
28829 assert_eq!(
28830 get_completions(&mut cx),
28831 expected_completions,
28832 "< actual / expected >, input = {input_to_simulate:?}",
28833 );
28834 }
28835}
28836
28837/// Handle completion request passing a marked string specifying where the completion
28838/// should be triggered from using '|' character, what range should be replaced, and what completions
28839/// should be returned using '<' and '>' to delimit the range.
28840///
28841/// Also see `handle_completion_request_with_insert_and_replace`.
28842#[track_caller]
28843pub fn handle_completion_request(
28844 marked_string: &str,
28845 completions: Vec<&'static str>,
28846 is_incomplete: bool,
28847 counter: Arc<AtomicUsize>,
28848 cx: &mut EditorLspTestContext,
28849) -> impl Future<Output = ()> {
28850 let complete_from_marker: TextRangeMarker = '|'.into();
28851 let replace_range_marker: TextRangeMarker = ('<', '>').into();
28852 let (_, mut marked_ranges) = marked_text_ranges_by(
28853 marked_string,
28854 vec![complete_from_marker.clone(), replace_range_marker.clone()],
28855 );
28856
28857 let complete_from_position = cx.to_lsp(MultiBufferOffset(
28858 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
28859 ));
28860 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
28861 let replace_range =
28862 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
28863
28864 let mut request =
28865 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
28866 let completions = completions.clone();
28867 counter.fetch_add(1, atomic::Ordering::Release);
28868 async move {
28869 assert_eq!(params.text_document_position.text_document.uri, url.clone());
28870 assert_eq!(
28871 params.text_document_position.position,
28872 complete_from_position
28873 );
28874 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
28875 is_incomplete,
28876 item_defaults: None,
28877 items: completions
28878 .iter()
28879 .map(|completion_text| lsp::CompletionItem {
28880 label: completion_text.to_string(),
28881 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
28882 range: replace_range,
28883 new_text: completion_text.to_string(),
28884 })),
28885 ..Default::default()
28886 })
28887 .collect(),
28888 })))
28889 }
28890 });
28891
28892 async move {
28893 request.next().await;
28894 }
28895}
28896
28897/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
28898/// given instead, which also contains an `insert` range.
28899///
28900/// This function uses markers to define ranges:
28901/// - `|` marks the cursor position
28902/// - `<>` marks the replace range
28903/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
28904pub fn handle_completion_request_with_insert_and_replace(
28905 cx: &mut EditorLspTestContext,
28906 marked_string: &str,
28907 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
28908 counter: Arc<AtomicUsize>,
28909) -> impl Future<Output = ()> {
28910 let complete_from_marker: TextRangeMarker = '|'.into();
28911 let replace_range_marker: TextRangeMarker = ('<', '>').into();
28912 let insert_range_marker: TextRangeMarker = ('{', '}').into();
28913
28914 let (_, mut marked_ranges) = marked_text_ranges_by(
28915 marked_string,
28916 vec![
28917 complete_from_marker.clone(),
28918 replace_range_marker.clone(),
28919 insert_range_marker.clone(),
28920 ],
28921 );
28922
28923 let complete_from_position = cx.to_lsp(MultiBufferOffset(
28924 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
28925 ));
28926 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
28927 let replace_range =
28928 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
28929
28930 let insert_range = match marked_ranges.remove(&insert_range_marker) {
28931 Some(ranges) if !ranges.is_empty() => {
28932 let range1 = ranges[0].clone();
28933 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
28934 }
28935 _ => lsp::Range {
28936 start: replace_range.start,
28937 end: complete_from_position,
28938 },
28939 };
28940
28941 let mut request =
28942 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
28943 let completions = completions.clone();
28944 counter.fetch_add(1, atomic::Ordering::Release);
28945 async move {
28946 assert_eq!(params.text_document_position.text_document.uri, url.clone());
28947 assert_eq!(
28948 params.text_document_position.position, complete_from_position,
28949 "marker `|` position doesn't match",
28950 );
28951 Ok(Some(lsp::CompletionResponse::Array(
28952 completions
28953 .iter()
28954 .map(|(label, new_text)| lsp::CompletionItem {
28955 label: label.to_string(),
28956 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
28957 lsp::InsertReplaceEdit {
28958 insert: insert_range,
28959 replace: replace_range,
28960 new_text: new_text.to_string(),
28961 },
28962 )),
28963 ..Default::default()
28964 })
28965 .collect(),
28966 )))
28967 }
28968 });
28969
28970 async move {
28971 request.next().await;
28972 }
28973}
28974
28975fn handle_resolve_completion_request(
28976 cx: &mut EditorLspTestContext,
28977 edits: Option<Vec<(&'static str, &'static str)>>,
28978) -> impl Future<Output = ()> {
28979 let edits = edits.map(|edits| {
28980 edits
28981 .iter()
28982 .map(|(marked_string, new_text)| {
28983 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
28984 let replace_range = cx.to_lsp_range(
28985 MultiBufferOffset(marked_ranges[0].start)
28986 ..MultiBufferOffset(marked_ranges[0].end),
28987 );
28988 lsp::TextEdit::new(replace_range, new_text.to_string())
28989 })
28990 .collect::<Vec<_>>()
28991 });
28992
28993 let mut request =
28994 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
28995 let edits = edits.clone();
28996 async move {
28997 Ok(lsp::CompletionItem {
28998 additional_text_edits: edits,
28999 ..Default::default()
29000 })
29001 }
29002 });
29003
29004 async move {
29005 request.next().await;
29006 }
29007}
29008
29009pub(crate) fn update_test_language_settings(
29010 cx: &mut TestAppContext,
29011 f: &dyn Fn(&mut AllLanguageSettingsContent),
29012) {
29013 cx.update(|cx| {
29014 SettingsStore::update_global(cx, |store, cx| {
29015 store.update_user_settings(cx, &|settings: &mut SettingsContent| {
29016 f(&mut settings.project.all_languages)
29017 });
29018 });
29019 });
29020}
29021
29022pub(crate) fn update_test_project_settings(
29023 cx: &mut TestAppContext,
29024 f: &dyn Fn(&mut ProjectSettingsContent),
29025) {
29026 cx.update(|cx| {
29027 SettingsStore::update_global(cx, |store, cx| {
29028 store.update_user_settings(cx, |settings| f(&mut settings.project));
29029 });
29030 });
29031}
29032
29033pub(crate) fn update_test_editor_settings(
29034 cx: &mut TestAppContext,
29035 f: &dyn Fn(&mut EditorSettingsContent),
29036) {
29037 cx.update(|cx| {
29038 SettingsStore::update_global(cx, |store, cx| {
29039 store.update_user_settings(cx, |settings| f(&mut settings.editor));
29040 })
29041 })
29042}
29043
29044pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
29045 cx.update(|cx| {
29046 assets::Assets.load_test_fonts(cx);
29047 let store = SettingsStore::test(cx);
29048 cx.set_global(store);
29049 theme::init(theme::LoadThemes::JustBase, cx);
29050 release_channel::init(semver::Version::new(0, 0, 0), cx);
29051 crate::init(cx);
29052 });
29053 zlog::init_test();
29054 update_test_language_settings(cx, &f);
29055}
29056
29057#[track_caller]
29058fn assert_hunk_revert(
29059 not_reverted_text_with_selections: &str,
29060 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
29061 expected_reverted_text_with_selections: &str,
29062 base_text: &str,
29063 cx: &mut EditorLspTestContext,
29064) {
29065 cx.set_state(not_reverted_text_with_selections);
29066 cx.set_head_text(base_text);
29067 cx.executor().run_until_parked();
29068
29069 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
29070 let snapshot = editor.snapshot(window, cx);
29071 let reverted_hunk_statuses = snapshot
29072 .buffer_snapshot()
29073 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
29074 .map(|hunk| hunk.status().kind)
29075 .collect::<Vec<_>>();
29076
29077 editor.git_restore(&Default::default(), window, cx);
29078 reverted_hunk_statuses
29079 });
29080 cx.executor().run_until_parked();
29081 cx.assert_editor_state(expected_reverted_text_with_selections);
29082 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
29083}
29084
29085#[gpui::test(iterations = 10)]
29086async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
29087 init_test(cx, |_| {});
29088
29089 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
29090 let counter = diagnostic_requests.clone();
29091
29092 let fs = FakeFs::new(cx.executor());
29093 fs.insert_tree(
29094 path!("/a"),
29095 json!({
29096 "first.rs": "fn main() { let a = 5; }",
29097 "second.rs": "// Test file",
29098 }),
29099 )
29100 .await;
29101
29102 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
29103 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
29104 let workspace = window
29105 .read_with(cx, |mw, _| mw.workspace().clone())
29106 .unwrap();
29107 let cx = &mut VisualTestContext::from_window(*window, cx);
29108
29109 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
29110 language_registry.add(rust_lang());
29111 let mut fake_servers = language_registry.register_fake_lsp(
29112 "Rust",
29113 FakeLspAdapter {
29114 capabilities: lsp::ServerCapabilities {
29115 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
29116 lsp::DiagnosticOptions {
29117 identifier: None,
29118 inter_file_dependencies: true,
29119 workspace_diagnostics: true,
29120 work_done_progress_options: Default::default(),
29121 },
29122 )),
29123 ..Default::default()
29124 },
29125 ..Default::default()
29126 },
29127 );
29128
29129 let editor = workspace
29130 .update_in(cx, |workspace, window, cx| {
29131 workspace.open_abs_path(
29132 PathBuf::from(path!("/a/first.rs")),
29133 OpenOptions::default(),
29134 window,
29135 cx,
29136 )
29137 })
29138 .await
29139 .unwrap()
29140 .downcast::<Editor>()
29141 .unwrap();
29142 let fake_server = fake_servers.next().await.unwrap();
29143 let server_id = fake_server.server.server_id();
29144 let mut first_request = fake_server
29145 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
29146 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
29147 let result_id = Some(new_result_id.to_string());
29148 assert_eq!(
29149 params.text_document.uri,
29150 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
29151 );
29152 async move {
29153 Ok(lsp::DocumentDiagnosticReportResult::Report(
29154 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
29155 related_documents: None,
29156 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
29157 items: Vec::new(),
29158 result_id,
29159 },
29160 }),
29161 ))
29162 }
29163 });
29164
29165 let ensure_result_id = |expected_result_id: Option<SharedString>, cx: &mut TestAppContext| {
29166 project.update(cx, |project, cx| {
29167 let buffer_id = editor
29168 .read(cx)
29169 .buffer()
29170 .read(cx)
29171 .as_singleton()
29172 .expect("created a singleton buffer")
29173 .read(cx)
29174 .remote_id();
29175 let buffer_result_id = project
29176 .lsp_store()
29177 .read(cx)
29178 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
29179 assert_eq!(expected_result_id, buffer_result_id);
29180 });
29181 };
29182
29183 ensure_result_id(None, cx);
29184 cx.executor().advance_clock(Duration::from_millis(60));
29185 cx.executor().run_until_parked();
29186 assert_eq!(
29187 diagnostic_requests.load(atomic::Ordering::Acquire),
29188 1,
29189 "Opening file should trigger diagnostic request"
29190 );
29191 first_request
29192 .next()
29193 .await
29194 .expect("should have sent the first diagnostics pull request");
29195 ensure_result_id(Some(SharedString::new_static("1")), cx);
29196
29197 // Editing should trigger diagnostics
29198 editor.update_in(cx, |editor, window, cx| {
29199 editor.handle_input("2", window, cx)
29200 });
29201 cx.executor().advance_clock(Duration::from_millis(60));
29202 cx.executor().run_until_parked();
29203 assert_eq!(
29204 diagnostic_requests.load(atomic::Ordering::Acquire),
29205 2,
29206 "Editing should trigger diagnostic request"
29207 );
29208 ensure_result_id(Some(SharedString::new_static("2")), cx);
29209
29210 // Moving cursor should not trigger diagnostic request
29211 editor.update_in(cx, |editor, window, cx| {
29212 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
29213 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
29214 });
29215 });
29216 cx.executor().advance_clock(Duration::from_millis(60));
29217 cx.executor().run_until_parked();
29218 assert_eq!(
29219 diagnostic_requests.load(atomic::Ordering::Acquire),
29220 2,
29221 "Cursor movement should not trigger diagnostic request"
29222 );
29223 ensure_result_id(Some(SharedString::new_static("2")), cx);
29224 // Multiple rapid edits should be debounced
29225 for _ in 0..5 {
29226 editor.update_in(cx, |editor, window, cx| {
29227 editor.handle_input("x", window, cx)
29228 });
29229 }
29230 cx.executor().advance_clock(Duration::from_millis(60));
29231 cx.executor().run_until_parked();
29232
29233 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
29234 assert!(
29235 final_requests <= 4,
29236 "Multiple rapid edits should be debounced (got {final_requests} requests)",
29237 );
29238 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
29239}
29240
29241#[gpui::test]
29242async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
29243 // Regression test for issue #11671
29244 // Previously, adding a cursor after moving multiple cursors would reset
29245 // the cursor count instead of adding to the existing cursors.
29246 init_test(cx, |_| {});
29247 let mut cx = EditorTestContext::new(cx).await;
29248
29249 // Create a simple buffer with cursor at start
29250 cx.set_state(indoc! {"
29251 ˇaaaa
29252 bbbb
29253 cccc
29254 dddd
29255 eeee
29256 ffff
29257 gggg
29258 hhhh"});
29259
29260 // Add 2 cursors below (so we have 3 total)
29261 cx.update_editor(|editor, window, cx| {
29262 editor.add_selection_below(&Default::default(), window, cx);
29263 editor.add_selection_below(&Default::default(), window, cx);
29264 });
29265
29266 // Verify we have 3 cursors
29267 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
29268 assert_eq!(
29269 initial_count, 3,
29270 "Should have 3 cursors after adding 2 below"
29271 );
29272
29273 // Move down one line
29274 cx.update_editor(|editor, window, cx| {
29275 editor.move_down(&MoveDown, window, cx);
29276 });
29277
29278 // Add another cursor below
29279 cx.update_editor(|editor, window, cx| {
29280 editor.add_selection_below(&Default::default(), window, cx);
29281 });
29282
29283 // Should now have 4 cursors (3 original + 1 new)
29284 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
29285 assert_eq!(
29286 final_count, 4,
29287 "Should have 4 cursors after moving and adding another"
29288 );
29289}
29290
29291#[gpui::test]
29292async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
29293 init_test(cx, |_| {});
29294
29295 let mut cx = EditorTestContext::new(cx).await;
29296
29297 cx.set_state(indoc!(
29298 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
29299 Second line here"#
29300 ));
29301
29302 cx.update_editor(|editor, window, cx| {
29303 // Enable soft wrapping with a narrow width to force soft wrapping and
29304 // confirm that more than 2 rows are being displayed.
29305 editor.set_wrap_width(Some(100.0.into()), cx);
29306 assert!(editor.display_text(cx).lines().count() > 2);
29307
29308 editor.add_selection_below(
29309 &AddSelectionBelow {
29310 skip_soft_wrap: true,
29311 },
29312 window,
29313 cx,
29314 );
29315
29316 assert_eq!(
29317 display_ranges(editor, cx),
29318 &[
29319 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
29320 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
29321 ]
29322 );
29323
29324 editor.add_selection_above(
29325 &AddSelectionAbove {
29326 skip_soft_wrap: true,
29327 },
29328 window,
29329 cx,
29330 );
29331
29332 assert_eq!(
29333 display_ranges(editor, cx),
29334 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
29335 );
29336
29337 editor.add_selection_below(
29338 &AddSelectionBelow {
29339 skip_soft_wrap: false,
29340 },
29341 window,
29342 cx,
29343 );
29344
29345 assert_eq!(
29346 display_ranges(editor, cx),
29347 &[
29348 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
29349 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
29350 ]
29351 );
29352
29353 editor.add_selection_above(
29354 &AddSelectionAbove {
29355 skip_soft_wrap: false,
29356 },
29357 window,
29358 cx,
29359 );
29360
29361 assert_eq!(
29362 display_ranges(editor, cx),
29363 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
29364 );
29365 });
29366
29367 // Set up text where selections are in the middle of a soft-wrapped line.
29368 // When adding selection below with `skip_soft_wrap` set to `true`, the new
29369 // selection should be at the same buffer column, not the same pixel
29370 // position.
29371 cx.set_state(indoc!(
29372 r#"1. Very long line to show «howˇ» a wrapped line would look
29373 2. Very long line to show how a wrapped line would look"#
29374 ));
29375
29376 cx.update_editor(|editor, window, cx| {
29377 // Enable soft wrapping with a narrow width to force soft wrapping and
29378 // confirm that more than 2 rows are being displayed.
29379 editor.set_wrap_width(Some(100.0.into()), cx);
29380 assert!(editor.display_text(cx).lines().count() > 2);
29381
29382 editor.add_selection_below(
29383 &AddSelectionBelow {
29384 skip_soft_wrap: true,
29385 },
29386 window,
29387 cx,
29388 );
29389
29390 // Assert that there's now 2 selections, both selecting the same column
29391 // range in the buffer row.
29392 let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
29393 let selections = editor.selections.all::<Point>(&display_map);
29394 assert_eq!(selections.len(), 2);
29395 assert_eq!(selections[0].start.column, selections[1].start.column);
29396 assert_eq!(selections[0].end.column, selections[1].end.column);
29397 });
29398}
29399
29400#[gpui::test]
29401async fn test_insert_snippet(cx: &mut TestAppContext) {
29402 init_test(cx, |_| {});
29403 let mut cx = EditorTestContext::new(cx).await;
29404
29405 cx.update_editor(|editor, _, cx| {
29406 editor.project().unwrap().update(cx, |project, cx| {
29407 project.snippets().update(cx, |snippets, _cx| {
29408 let snippet = project::snippet_provider::Snippet {
29409 prefix: vec![], // no prefix needed!
29410 body: "an Unspecified".to_string(),
29411 description: Some("shhhh it's a secret".to_string()),
29412 name: "super secret snippet".to_string(),
29413 };
29414 snippets.add_snippet_for_test(
29415 None,
29416 PathBuf::from("test_snippets.json"),
29417 vec![Arc::new(snippet)],
29418 );
29419
29420 let snippet = project::snippet_provider::Snippet {
29421 prefix: vec![], // no prefix needed!
29422 body: " Location".to_string(),
29423 description: Some("the word 'location'".to_string()),
29424 name: "location word".to_string(),
29425 };
29426 snippets.add_snippet_for_test(
29427 Some("Markdown".to_string()),
29428 PathBuf::from("test_snippets.json"),
29429 vec![Arc::new(snippet)],
29430 );
29431 });
29432 })
29433 });
29434
29435 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
29436
29437 cx.update_editor(|editor, window, cx| {
29438 editor.insert_snippet_at_selections(
29439 &InsertSnippet {
29440 language: None,
29441 name: Some("super secret snippet".to_string()),
29442 snippet: None,
29443 },
29444 window,
29445 cx,
29446 );
29447
29448 // Language is specified in the action,
29449 // so the buffer language does not need to match
29450 editor.insert_snippet_at_selections(
29451 &InsertSnippet {
29452 language: Some("Markdown".to_string()),
29453 name: Some("location word".to_string()),
29454 snippet: None,
29455 },
29456 window,
29457 cx,
29458 );
29459
29460 editor.insert_snippet_at_selections(
29461 &InsertSnippet {
29462 language: None,
29463 name: None,
29464 snippet: Some("$0 after".to_string()),
29465 },
29466 window,
29467 cx,
29468 );
29469 });
29470
29471 cx.assert_editor_state(
29472 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
29473 );
29474}
29475
29476#[gpui::test]
29477async fn test_inlay_hints_request_timeout(cx: &mut TestAppContext) {
29478 use crate::inlays::inlay_hints::InlayHintRefreshReason;
29479 use crate::inlays::inlay_hints::tests::{cached_hint_labels, init_test, visible_hint_labels};
29480 use settings::InlayHintSettingsContent;
29481 use std::sync::atomic::AtomicU32;
29482 use std::time::Duration;
29483
29484 const BASE_TIMEOUT_SECS: u64 = 1;
29485
29486 let request_count = Arc::new(AtomicU32::new(0));
29487 let closure_request_count = request_count.clone();
29488
29489 init_test(cx, &|settings| {
29490 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
29491 enabled: Some(true),
29492 ..InlayHintSettingsContent::default()
29493 })
29494 });
29495 cx.update(|cx| {
29496 SettingsStore::update_global(cx, |store, cx| {
29497 store.update_user_settings(cx, &|settings: &mut SettingsContent| {
29498 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
29499 request_timeout: Some(BASE_TIMEOUT_SECS),
29500 button: Some(true),
29501 notifications: None,
29502 semantic_token_rules: None,
29503 });
29504 });
29505 });
29506 });
29507
29508 let fs = FakeFs::new(cx.executor());
29509 fs.insert_tree(
29510 path!("/a"),
29511 json!({
29512 "main.rs": "fn main() { let a = 5; }",
29513 }),
29514 )
29515 .await;
29516
29517 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
29518 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
29519 language_registry.add(rust_lang());
29520 let mut fake_servers = language_registry.register_fake_lsp(
29521 "Rust",
29522 FakeLspAdapter {
29523 capabilities: lsp::ServerCapabilities {
29524 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
29525 ..lsp::ServerCapabilities::default()
29526 },
29527 initializer: Some(Box::new(move |fake_server| {
29528 let request_count = closure_request_count.clone();
29529 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
29530 move |params, cx| {
29531 let request_count = request_count.clone();
29532 async move {
29533 cx.background_executor()
29534 .timer(Duration::from_secs(BASE_TIMEOUT_SECS * 2))
29535 .await;
29536 let count = request_count.fetch_add(1, atomic::Ordering::Release) + 1;
29537 assert_eq!(
29538 params.text_document.uri,
29539 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
29540 );
29541 Ok(Some(vec![lsp::InlayHint {
29542 position: lsp::Position::new(0, 1),
29543 label: lsp::InlayHintLabel::String(count.to_string()),
29544 kind: None,
29545 text_edits: None,
29546 tooltip: None,
29547 padding_left: None,
29548 padding_right: None,
29549 data: None,
29550 }]))
29551 }
29552 },
29553 );
29554 })),
29555 ..FakeLspAdapter::default()
29556 },
29557 );
29558
29559 let buffer = project
29560 .update(cx, |project, cx| {
29561 project.open_local_buffer(path!("/a/main.rs"), cx)
29562 })
29563 .await
29564 .unwrap();
29565 let editor = cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
29566
29567 cx.executor().run_until_parked();
29568 let fake_server = fake_servers.next().await.unwrap();
29569
29570 cx.executor()
29571 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS) + Duration::from_millis(100));
29572 cx.executor().run_until_parked();
29573 editor
29574 .update(cx, |editor, _window, cx| {
29575 assert!(
29576 cached_hint_labels(editor, cx).is_empty(),
29577 "First request should time out, no hints cached"
29578 );
29579 })
29580 .unwrap();
29581
29582 editor
29583 .update(cx, |editor, _window, cx| {
29584 editor.refresh_inlay_hints(
29585 InlayHintRefreshReason::RefreshRequested {
29586 server_id: fake_server.server.server_id(),
29587 request_id: Some(1),
29588 },
29589 cx,
29590 );
29591 })
29592 .unwrap();
29593 cx.executor()
29594 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS) + Duration::from_millis(100));
29595 cx.executor().run_until_parked();
29596 editor
29597 .update(cx, |editor, _window, cx| {
29598 assert!(
29599 cached_hint_labels(editor, cx).is_empty(),
29600 "Second request should also time out with BASE_TIMEOUT, no hints cached"
29601 );
29602 })
29603 .unwrap();
29604
29605 cx.update(|cx| {
29606 SettingsStore::update_global(cx, |store, cx| {
29607 store.update_user_settings(cx, |settings| {
29608 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
29609 request_timeout: Some(BASE_TIMEOUT_SECS * 4),
29610 button: Some(true),
29611 notifications: None,
29612 semantic_token_rules: None,
29613 });
29614 });
29615 });
29616 });
29617 editor
29618 .update(cx, |editor, _window, cx| {
29619 editor.refresh_inlay_hints(
29620 InlayHintRefreshReason::RefreshRequested {
29621 server_id: fake_server.server.server_id(),
29622 request_id: Some(2),
29623 },
29624 cx,
29625 );
29626 })
29627 .unwrap();
29628 cx.executor()
29629 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS * 4) + Duration::from_millis(100));
29630 cx.executor().run_until_parked();
29631 editor
29632 .update(cx, |editor, _window, cx| {
29633 assert_eq!(
29634 vec!["1".to_string()],
29635 cached_hint_labels(editor, cx),
29636 "With extended timeout (BASE * 4), hints should arrive successfully"
29637 );
29638 assert_eq!(vec!["1".to_string()], visible_hint_labels(editor, cx));
29639 })
29640 .unwrap();
29641}
29642
29643#[gpui::test]
29644async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
29645 init_test(cx, |_| {});
29646 let (editor, cx) = cx.add_window_view(Editor::single_line);
29647 editor.update_in(cx, |editor, window, cx| {
29648 editor.set_text("oops\n\nwow\n", window, cx)
29649 });
29650 cx.run_until_parked();
29651 editor.update(cx, |editor, cx| {
29652 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
29653 });
29654 editor.update(cx, |editor, cx| {
29655 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
29656 });
29657 cx.run_until_parked();
29658 editor.update(cx, |editor, cx| {
29659 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
29660 });
29661}
29662
29663#[gpui::test]
29664async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
29665 init_test(cx, |_| {});
29666
29667 cx.update(|cx| {
29668 register_project_item::<Editor>(cx);
29669 });
29670
29671 let fs = FakeFs::new(cx.executor());
29672 fs.insert_tree("/root1", json!({})).await;
29673 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
29674 .await;
29675
29676 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
29677 let (multi_workspace, cx) =
29678 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
29679 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
29680
29681 let worktree_id = project.update(cx, |project, cx| {
29682 project.worktrees(cx).next().unwrap().read(cx).id()
29683 });
29684
29685 let handle = workspace
29686 .update_in(cx, |workspace, window, cx| {
29687 let project_path = (worktree_id, rel_path("one.pdf"));
29688 workspace.open_path(project_path, None, true, window, cx)
29689 })
29690 .await
29691 .unwrap();
29692 // The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
29693 // Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
29694 // With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
29695 assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
29696}
29697
29698#[gpui::test]
29699async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
29700 init_test(cx, |_| {});
29701
29702 let language = Arc::new(Language::new(
29703 LanguageConfig::default(),
29704 Some(tree_sitter_rust::LANGUAGE.into()),
29705 ));
29706
29707 // Test hierarchical sibling navigation
29708 let text = r#"
29709 fn outer() {
29710 if condition {
29711 let a = 1;
29712 }
29713 let b = 2;
29714 }
29715
29716 fn another() {
29717 let c = 3;
29718 }
29719 "#;
29720
29721 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
29722 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
29723 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
29724
29725 // Wait for parsing to complete
29726 editor
29727 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
29728 .await;
29729
29730 editor.update_in(cx, |editor, window, cx| {
29731 // Start by selecting "let a = 1;" inside the if block
29732 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
29733 s.select_display_ranges([
29734 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
29735 ]);
29736 });
29737
29738 let initial_selection = editor
29739 .selections
29740 .display_ranges(&editor.display_snapshot(cx));
29741 assert_eq!(initial_selection.len(), 1, "Should have one selection");
29742
29743 // Test select next sibling - should move up levels to find the next sibling
29744 // Since "let a = 1;" has no siblings in the if block, it should move up
29745 // to find "let b = 2;" which is a sibling of the if block
29746 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
29747 let next_selection = editor
29748 .selections
29749 .display_ranges(&editor.display_snapshot(cx));
29750
29751 // Should have a selection and it should be different from the initial
29752 assert_eq!(
29753 next_selection.len(),
29754 1,
29755 "Should have one selection after next"
29756 );
29757 assert_ne!(
29758 next_selection[0], initial_selection[0],
29759 "Next sibling selection should be different"
29760 );
29761
29762 // Test hierarchical navigation by going to the end of the current function
29763 // and trying to navigate to the next function
29764 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
29765 s.select_display_ranges([
29766 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
29767 ]);
29768 });
29769
29770 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
29771 let function_next_selection = editor
29772 .selections
29773 .display_ranges(&editor.display_snapshot(cx));
29774
29775 // Should move to the next function
29776 assert_eq!(
29777 function_next_selection.len(),
29778 1,
29779 "Should have one selection after function next"
29780 );
29781
29782 // Test select previous sibling navigation
29783 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
29784 let prev_selection = editor
29785 .selections
29786 .display_ranges(&editor.display_snapshot(cx));
29787
29788 // Should have a selection and it should be different
29789 assert_eq!(
29790 prev_selection.len(),
29791 1,
29792 "Should have one selection after prev"
29793 );
29794 assert_ne!(
29795 prev_selection[0], function_next_selection[0],
29796 "Previous sibling selection should be different from next"
29797 );
29798 });
29799}
29800
29801#[gpui::test]
29802async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
29803 init_test(cx, |_| {});
29804
29805 let mut cx = EditorTestContext::new(cx).await;
29806 cx.set_state(
29807 "let ˇvariable = 42;
29808let another = variable + 1;
29809let result = variable * 2;",
29810 );
29811
29812 // Set up document highlights manually (simulating LSP response)
29813 cx.update_editor(|editor, _window, cx| {
29814 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
29815
29816 // Create highlights for "variable" occurrences
29817 let highlight_ranges = [
29818 Point::new(0, 4)..Point::new(0, 12), // First "variable"
29819 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
29820 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
29821 ];
29822
29823 let anchor_ranges: Vec<_> = highlight_ranges
29824 .iter()
29825 .map(|range| range.clone().to_anchors(&buffer_snapshot))
29826 .collect();
29827
29828 editor.highlight_background(
29829 HighlightKey::DocumentHighlightRead,
29830 &anchor_ranges,
29831 |_, theme| theme.colors().editor_document_highlight_read_background,
29832 cx,
29833 );
29834 });
29835
29836 // Go to next highlight - should move to second "variable"
29837 cx.update_editor(|editor, window, cx| {
29838 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
29839 });
29840 cx.assert_editor_state(
29841 "let variable = 42;
29842let another = ˇvariable + 1;
29843let result = variable * 2;",
29844 );
29845
29846 // Go to next highlight - should move to third "variable"
29847 cx.update_editor(|editor, window, cx| {
29848 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
29849 });
29850 cx.assert_editor_state(
29851 "let variable = 42;
29852let another = variable + 1;
29853let result = ˇvariable * 2;",
29854 );
29855
29856 // Go to next highlight - should stay at third "variable" (no wrap-around)
29857 cx.update_editor(|editor, window, cx| {
29858 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
29859 });
29860 cx.assert_editor_state(
29861 "let variable = 42;
29862let another = variable + 1;
29863let result = ˇvariable * 2;",
29864 );
29865
29866 // Now test going backwards from third position
29867 cx.update_editor(|editor, window, cx| {
29868 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
29869 });
29870 cx.assert_editor_state(
29871 "let variable = 42;
29872let another = ˇvariable + 1;
29873let result = variable * 2;",
29874 );
29875
29876 // Go to previous highlight - should move to first "variable"
29877 cx.update_editor(|editor, window, cx| {
29878 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
29879 });
29880 cx.assert_editor_state(
29881 "let ˇvariable = 42;
29882let another = variable + 1;
29883let result = variable * 2;",
29884 );
29885
29886 // Go to previous highlight - should stay on first "variable"
29887 cx.update_editor(|editor, window, cx| {
29888 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
29889 });
29890 cx.assert_editor_state(
29891 "let ˇvariable = 42;
29892let another = variable + 1;
29893let result = variable * 2;",
29894 );
29895}
29896
29897#[gpui::test]
29898async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
29899 cx: &mut gpui::TestAppContext,
29900) {
29901 init_test(cx, |_| {});
29902
29903 let url = "https://zed.dev";
29904
29905 let markdown_language = Arc::new(Language::new(
29906 LanguageConfig {
29907 name: "Markdown".into(),
29908 ..LanguageConfig::default()
29909 },
29910 None,
29911 ));
29912
29913 let mut cx = EditorTestContext::new(cx).await;
29914 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29915 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
29916
29917 cx.update_editor(|editor, window, cx| {
29918 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
29919 editor.paste(&Paste, window, cx);
29920 });
29921
29922 cx.assert_editor_state(&format!(
29923 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
29924 ));
29925}
29926
29927#[gpui::test]
29928async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
29929 init_test(cx, |_| {});
29930
29931 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29932 let mut cx = EditorTestContext::new(cx).await;
29933
29934 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29935
29936 // Case 1: Test if adding a character with multi cursors preserves nested list indents
29937 cx.set_state(&indoc! {"
29938 - [ ] Item 1
29939 - [ ] Item 1.a
29940 - [ˇ] Item 2
29941 - [ˇ] Item 2.a
29942 - [ˇ] Item 2.b
29943 "
29944 });
29945 cx.update_editor(|editor, window, cx| {
29946 editor.handle_input("x", window, cx);
29947 });
29948 cx.run_until_parked();
29949 cx.assert_editor_state(indoc! {"
29950 - [ ] Item 1
29951 - [ ] Item 1.a
29952 - [xˇ] Item 2
29953 - [xˇ] Item 2.a
29954 - [xˇ] Item 2.b
29955 "
29956 });
29957
29958 // Case 2: Test adding new line after nested list continues the list with unchecked task
29959 cx.set_state(&indoc! {"
29960 - [ ] Item 1
29961 - [ ] Item 1.a
29962 - [x] Item 2
29963 - [x] Item 2.a
29964 - [x] Item 2.bˇ"
29965 });
29966 cx.update_editor(|editor, window, cx| {
29967 editor.newline(&Newline, window, cx);
29968 });
29969 cx.assert_editor_state(indoc! {"
29970 - [ ] Item 1
29971 - [ ] Item 1.a
29972 - [x] Item 2
29973 - [x] Item 2.a
29974 - [x] Item 2.b
29975 - [ ] ˇ"
29976 });
29977
29978 // Case 3: Test adding content to continued list item
29979 cx.update_editor(|editor, window, cx| {
29980 editor.handle_input("Item 2.c", window, cx);
29981 });
29982 cx.run_until_parked();
29983 cx.assert_editor_state(indoc! {"
29984 - [ ] Item 1
29985 - [ ] Item 1.a
29986 - [x] Item 2
29987 - [x] Item 2.a
29988 - [x] Item 2.b
29989 - [ ] Item 2.cˇ"
29990 });
29991
29992 // Case 4: Test adding new line after nested ordered list continues with next number
29993 cx.set_state(indoc! {"
29994 1. Item 1
29995 1. Item 1.a
29996 2. Item 2
29997 1. Item 2.a
29998 2. Item 2.bˇ"
29999 });
30000 cx.update_editor(|editor, window, cx| {
30001 editor.newline(&Newline, window, cx);
30002 });
30003 cx.assert_editor_state(indoc! {"
30004 1. Item 1
30005 1. Item 1.a
30006 2. Item 2
30007 1. Item 2.a
30008 2. Item 2.b
30009 3. ˇ"
30010 });
30011
30012 // Case 5: Adding content to continued ordered list item
30013 cx.update_editor(|editor, window, cx| {
30014 editor.handle_input("Item 2.c", window, cx);
30015 });
30016 cx.run_until_parked();
30017 cx.assert_editor_state(indoc! {"
30018 1. Item 1
30019 1. Item 1.a
30020 2. Item 2
30021 1. Item 2.a
30022 2. Item 2.b
30023 3. Item 2.cˇ"
30024 });
30025
30026 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
30027 cx.set_state(indoc! {"
30028 - Item 1
30029 - Item 1.a
30030 - Item 1.a
30031 ˇ"});
30032 cx.update_editor(|editor, window, cx| {
30033 editor.handle_input("-", window, cx);
30034 });
30035 cx.run_until_parked();
30036 cx.assert_editor_state(indoc! {"
30037 - Item 1
30038 - Item 1.a
30039 - Item 1.a
30040 -ˇ"});
30041
30042 // Case 7: Test blockquote newline preserves something
30043 cx.set_state(indoc! {"
30044 > Item 1ˇ"
30045 });
30046 cx.update_editor(|editor, window, cx| {
30047 editor.newline(&Newline, window, cx);
30048 });
30049 cx.assert_editor_state(indoc! {"
30050 > Item 1
30051 ˇ"
30052 });
30053}
30054
30055#[gpui::test]
30056async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
30057 cx: &mut gpui::TestAppContext,
30058) {
30059 init_test(cx, |_| {});
30060
30061 let url = "https://zed.dev";
30062
30063 let markdown_language = Arc::new(Language::new(
30064 LanguageConfig {
30065 name: "Markdown".into(),
30066 ..LanguageConfig::default()
30067 },
30068 None,
30069 ));
30070
30071 let mut cx = EditorTestContext::new(cx).await;
30072 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30073 cx.set_state(&format!(
30074 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
30075 ));
30076
30077 cx.update_editor(|editor, window, cx| {
30078 editor.copy(&Copy, window, cx);
30079 });
30080
30081 cx.set_state(&format!(
30082 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
30083 ));
30084
30085 cx.update_editor(|editor, window, cx| {
30086 editor.paste(&Paste, window, cx);
30087 });
30088
30089 cx.assert_editor_state(&format!(
30090 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
30091 ));
30092}
30093
30094#[gpui::test]
30095async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
30096 cx: &mut gpui::TestAppContext,
30097) {
30098 init_test(cx, |_| {});
30099
30100 let url = "https://zed.dev";
30101
30102 let markdown_language = Arc::new(Language::new(
30103 LanguageConfig {
30104 name: "Markdown".into(),
30105 ..LanguageConfig::default()
30106 },
30107 None,
30108 ));
30109
30110 let mut cx = EditorTestContext::new(cx).await;
30111 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30112 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
30113
30114 cx.update_editor(|editor, window, cx| {
30115 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
30116 editor.paste(&Paste, window, cx);
30117 });
30118
30119 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
30120}
30121
30122#[gpui::test]
30123async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
30124 cx: &mut gpui::TestAppContext,
30125) {
30126 init_test(cx, |_| {});
30127
30128 let text = "Awesome";
30129
30130 let markdown_language = Arc::new(Language::new(
30131 LanguageConfig {
30132 name: "Markdown".into(),
30133 ..LanguageConfig::default()
30134 },
30135 None,
30136 ));
30137
30138 let mut cx = EditorTestContext::new(cx).await;
30139 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30140 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
30141
30142 cx.update_editor(|editor, window, cx| {
30143 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
30144 editor.paste(&Paste, window, cx);
30145 });
30146
30147 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
30148}
30149
30150#[gpui::test]
30151async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
30152 cx: &mut gpui::TestAppContext,
30153) {
30154 init_test(cx, |_| {});
30155
30156 let url = "https://zed.dev";
30157
30158 let markdown_language = Arc::new(Language::new(
30159 LanguageConfig {
30160 name: "Rust".into(),
30161 ..LanguageConfig::default()
30162 },
30163 None,
30164 ));
30165
30166 let mut cx = EditorTestContext::new(cx).await;
30167 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30168 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
30169
30170 cx.update_editor(|editor, window, cx| {
30171 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
30172 editor.paste(&Paste, window, cx);
30173 });
30174
30175 cx.assert_editor_state(&format!(
30176 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
30177 ));
30178}
30179
30180#[gpui::test]
30181async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
30182 cx: &mut TestAppContext,
30183) {
30184 init_test(cx, |_| {});
30185
30186 let url = "https://zed.dev";
30187
30188 let markdown_language = Arc::new(Language::new(
30189 LanguageConfig {
30190 name: "Markdown".into(),
30191 ..LanguageConfig::default()
30192 },
30193 None,
30194 ));
30195
30196 let (editor, cx) = cx.add_window_view(|window, cx| {
30197 let multi_buffer = MultiBuffer::build_multi(
30198 [
30199 ("this will embed -> link", vec![Point::row_range(0..1)]),
30200 ("this will replace -> link", vec![Point::row_range(0..1)]),
30201 ],
30202 cx,
30203 );
30204 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
30205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30206 s.select_ranges(vec![
30207 Point::new(0, 19)..Point::new(0, 23),
30208 Point::new(1, 21)..Point::new(1, 25),
30209 ])
30210 });
30211 let first_buffer_id = multi_buffer
30212 .read(cx)
30213 .excerpt_buffer_ids()
30214 .into_iter()
30215 .next()
30216 .unwrap();
30217 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
30218 first_buffer.update(cx, |buffer, cx| {
30219 buffer.set_language(Some(markdown_language.clone()), cx);
30220 });
30221
30222 editor
30223 });
30224 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
30225
30226 cx.update_editor(|editor, window, cx| {
30227 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
30228 editor.paste(&Paste, window, cx);
30229 });
30230
30231 cx.assert_editor_state(&format!(
30232 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
30233 ));
30234}
30235
30236#[gpui::test]
30237async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
30238 init_test(cx, |_| {});
30239
30240 let fs = FakeFs::new(cx.executor());
30241 fs.insert_tree(
30242 path!("/project"),
30243 json!({
30244 "first.rs": "# First Document\nSome content here.",
30245 "second.rs": "Plain text content for second file.",
30246 }),
30247 )
30248 .await;
30249
30250 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
30251 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
30252 let cx = &mut VisualTestContext::from_window(*window, cx);
30253
30254 let language = rust_lang();
30255 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
30256 language_registry.add(language.clone());
30257 let mut fake_servers = language_registry.register_fake_lsp(
30258 "Rust",
30259 FakeLspAdapter {
30260 ..FakeLspAdapter::default()
30261 },
30262 );
30263
30264 let buffer1 = project
30265 .update(cx, |project, cx| {
30266 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
30267 })
30268 .await
30269 .unwrap();
30270 let buffer2 = project
30271 .update(cx, |project, cx| {
30272 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
30273 })
30274 .await
30275 .unwrap();
30276
30277 let multi_buffer = cx.new(|cx| {
30278 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
30279 multi_buffer.set_excerpts_for_path(
30280 PathKey::for_buffer(&buffer1, cx),
30281 buffer1.clone(),
30282 [Point::zero()..buffer1.read(cx).max_point()],
30283 3,
30284 cx,
30285 );
30286 multi_buffer.set_excerpts_for_path(
30287 PathKey::for_buffer(&buffer2, cx),
30288 buffer2.clone(),
30289 [Point::zero()..buffer1.read(cx).max_point()],
30290 3,
30291 cx,
30292 );
30293 multi_buffer
30294 });
30295
30296 let (editor, cx) = cx.add_window_view(|window, cx| {
30297 Editor::new(
30298 EditorMode::full(),
30299 multi_buffer,
30300 Some(project.clone()),
30301 window,
30302 cx,
30303 )
30304 });
30305
30306 let fake_language_server = fake_servers.next().await.unwrap();
30307
30308 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
30309
30310 let save = editor.update_in(cx, |editor, window, cx| {
30311 assert!(editor.is_dirty(cx));
30312
30313 editor.save(
30314 SaveOptions {
30315 format: true,
30316 autosave: true,
30317 },
30318 project,
30319 window,
30320 cx,
30321 )
30322 });
30323 let (start_edit_tx, start_edit_rx) = oneshot::channel();
30324 let (done_edit_tx, done_edit_rx) = oneshot::channel();
30325 let mut done_edit_rx = Some(done_edit_rx);
30326 let mut start_edit_tx = Some(start_edit_tx);
30327
30328 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
30329 start_edit_tx.take().unwrap().send(()).unwrap();
30330 let done_edit_rx = done_edit_rx.take().unwrap();
30331 async move {
30332 done_edit_rx.await.unwrap();
30333 Ok(None)
30334 }
30335 });
30336
30337 start_edit_rx.await.unwrap();
30338 buffer2
30339 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
30340 .unwrap();
30341
30342 done_edit_tx.send(()).unwrap();
30343
30344 save.await.unwrap();
30345 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
30346}
30347
30348#[gpui::test]
30349fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
30350 init_test(cx, |_| {});
30351
30352 let editor = cx.add_window(|window, cx| {
30353 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
30354 build_editor(buffer, window, cx)
30355 });
30356
30357 editor
30358 .update(cx, |editor, window, cx| {
30359 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30360 s.select_display_ranges([
30361 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
30362 ])
30363 });
30364
30365 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
30366
30367 assert_eq!(
30368 editor.display_text(cx),
30369 "line1\nline2\nline2",
30370 "Duplicating last line upward should create duplicate above, not on same line"
30371 );
30372
30373 assert_eq!(
30374 editor
30375 .selections
30376 .display_ranges(&editor.display_snapshot(cx)),
30377 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
30378 "Selection should move to the duplicated line"
30379 );
30380 })
30381 .unwrap();
30382}
30383
30384#[gpui::test]
30385async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
30386 init_test(cx, |_| {});
30387
30388 let mut cx = EditorTestContext::new(cx).await;
30389
30390 cx.set_state("line1\nline2ˇ");
30391
30392 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
30393
30394 let clipboard_text = cx
30395 .read_from_clipboard()
30396 .and_then(|item| item.text().as_deref().map(str::to_string));
30397
30398 assert_eq!(
30399 clipboard_text,
30400 Some("line2\n".to_string()),
30401 "Copying a line without trailing newline should include a newline"
30402 );
30403
30404 cx.set_state("line1\nˇ");
30405
30406 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
30407
30408 cx.assert_editor_state("line1\nline2\nˇ");
30409}
30410
30411#[gpui::test]
30412async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
30413 init_test(cx, |_| {});
30414
30415 let mut cx = EditorTestContext::new(cx).await;
30416
30417 cx.set_state("ˇline1\nˇline2\nˇline3\n");
30418
30419 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
30420
30421 let clipboard_text = cx
30422 .read_from_clipboard()
30423 .and_then(|item| item.text().as_deref().map(str::to_string));
30424
30425 assert_eq!(
30426 clipboard_text,
30427 Some("line1\nline2\nline3\n".to_string()),
30428 "Copying multiple lines should include a single newline between lines"
30429 );
30430
30431 cx.set_state("lineA\nˇ");
30432
30433 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
30434
30435 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
30436}
30437
30438#[gpui::test]
30439async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
30440 init_test(cx, |_| {});
30441
30442 let mut cx = EditorTestContext::new(cx).await;
30443
30444 cx.set_state("ˇline1\nˇline2\nˇline3\n");
30445
30446 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
30447
30448 let clipboard_text = cx
30449 .read_from_clipboard()
30450 .and_then(|item| item.text().as_deref().map(str::to_string));
30451
30452 assert_eq!(
30453 clipboard_text,
30454 Some("line1\nline2\nline3\n".to_string()),
30455 "Copying multiple lines should include a single newline between lines"
30456 );
30457
30458 cx.set_state("lineA\nˇ");
30459
30460 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
30461
30462 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
30463}
30464
30465#[gpui::test]
30466async fn test_end_of_editor_context(cx: &mut TestAppContext) {
30467 init_test(cx, |_| {});
30468
30469 let mut cx = EditorTestContext::new(cx).await;
30470
30471 cx.set_state("line1\nline2ˇ");
30472 cx.update_editor(|e, window, cx| {
30473 e.set_mode(EditorMode::SingleLine);
30474 assert!(e.key_context(window, cx).contains("end_of_input"));
30475 });
30476 cx.set_state("ˇline1\nline2");
30477 cx.update_editor(|e, window, cx| {
30478 assert!(!e.key_context(window, cx).contains("end_of_input"));
30479 });
30480 cx.set_state("line1ˇ\nline2");
30481 cx.update_editor(|e, window, cx| {
30482 assert!(!e.key_context(window, cx).contains("end_of_input"));
30483 });
30484}
30485
30486#[gpui::test]
30487async fn test_sticky_scroll(cx: &mut TestAppContext) {
30488 init_test(cx, |_| {});
30489 let mut cx = EditorTestContext::new(cx).await;
30490
30491 let buffer = indoc! {"
30492 ˇfn foo() {
30493 let abc = 123;
30494 }
30495 struct Bar;
30496 impl Bar {
30497 fn new() -> Self {
30498 Self
30499 }
30500 }
30501 fn baz() {
30502 }
30503 "};
30504 cx.set_state(&buffer);
30505
30506 cx.update_editor(|e, _, cx| {
30507 e.buffer()
30508 .read(cx)
30509 .as_singleton()
30510 .unwrap()
30511 .update(cx, |buffer, cx| {
30512 buffer.set_language(Some(rust_lang()), cx);
30513 })
30514 });
30515
30516 let mut sticky_headers = |offset: ScrollOffset| {
30517 cx.update_editor(|e, window, cx| {
30518 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
30519 });
30520 cx.run_until_parked();
30521 cx.update_editor(|e, window, cx| {
30522 EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
30523 .into_iter()
30524 .map(
30525 |StickyHeader {
30526 start_point,
30527 offset,
30528 ..
30529 }| { (start_point, offset) },
30530 )
30531 .collect::<Vec<_>>()
30532 })
30533 };
30534
30535 let fn_foo = Point { row: 0, column: 0 };
30536 let impl_bar = Point { row: 4, column: 0 };
30537 let fn_new = Point { row: 5, column: 4 };
30538
30539 assert_eq!(sticky_headers(0.0), vec![]);
30540 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
30541 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
30542 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
30543 assert_eq!(sticky_headers(2.0), vec![]);
30544 assert_eq!(sticky_headers(2.5), vec![]);
30545 assert_eq!(sticky_headers(3.0), vec![]);
30546 assert_eq!(sticky_headers(3.5), vec![]);
30547 assert_eq!(sticky_headers(4.0), vec![]);
30548 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
30549 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
30550 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
30551 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
30552 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
30553 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
30554 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
30555 assert_eq!(sticky_headers(8.0), vec![]);
30556 assert_eq!(sticky_headers(8.5), vec![]);
30557 assert_eq!(sticky_headers(9.0), vec![]);
30558 assert_eq!(sticky_headers(9.5), vec![]);
30559 assert_eq!(sticky_headers(10.0), vec![]);
30560}
30561
30562#[gpui::test]
30563async fn test_sticky_scroll_with_expanded_deleted_diff_hunks(
30564 executor: BackgroundExecutor,
30565 cx: &mut TestAppContext,
30566) {
30567 init_test(cx, |_| {});
30568 let mut cx = EditorTestContext::new(cx).await;
30569
30570 let diff_base = indoc! {"
30571 fn foo() {
30572 let a = 1;
30573 let b = 2;
30574 let c = 3;
30575 let d = 4;
30576 let e = 5;
30577 }
30578 "};
30579
30580 let buffer = indoc! {"
30581 ˇfn foo() {
30582 }
30583 "};
30584
30585 cx.set_state(&buffer);
30586
30587 cx.update_editor(|e, _, cx| {
30588 e.buffer()
30589 .read(cx)
30590 .as_singleton()
30591 .unwrap()
30592 .update(cx, |buffer, cx| {
30593 buffer.set_language(Some(rust_lang()), cx);
30594 })
30595 });
30596
30597 cx.set_head_text(diff_base);
30598 executor.run_until_parked();
30599
30600 cx.update_editor(|editor, window, cx| {
30601 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
30602 });
30603 executor.run_until_parked();
30604
30605 // After expanding, the display should look like:
30606 // row 0: fn foo() {
30607 // row 1: - let a = 1; (deleted)
30608 // row 2: - let b = 2; (deleted)
30609 // row 3: - let c = 3; (deleted)
30610 // row 4: - let d = 4; (deleted)
30611 // row 5: - let e = 5; (deleted)
30612 // row 6: }
30613 //
30614 // fn foo() spans display rows 0-6. Scrolling into the deleted region
30615 // (rows 1-5) should still show fn foo() as a sticky header.
30616
30617 let fn_foo = Point { row: 0, column: 0 };
30618
30619 let mut sticky_headers = |offset: ScrollOffset| {
30620 cx.update_editor(|e, window, cx| {
30621 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
30622 });
30623 cx.run_until_parked();
30624 cx.update_editor(|e, window, cx| {
30625 EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
30626 .into_iter()
30627 .map(
30628 |StickyHeader {
30629 start_point,
30630 offset,
30631 ..
30632 }| { (start_point, offset) },
30633 )
30634 .collect::<Vec<_>>()
30635 })
30636 };
30637
30638 assert_eq!(sticky_headers(0.0), vec![]);
30639 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
30640 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
30641 // Scrolling into deleted lines: fn foo() should still be a sticky header.
30642 assert_eq!(sticky_headers(2.0), vec![(fn_foo, 0.0)]);
30643 assert_eq!(sticky_headers(3.0), vec![(fn_foo, 0.0)]);
30644 assert_eq!(sticky_headers(4.0), vec![(fn_foo, 0.0)]);
30645 assert_eq!(sticky_headers(5.0), vec![(fn_foo, 0.0)]);
30646 assert_eq!(sticky_headers(5.5), vec![(fn_foo, -0.5)]);
30647 // Past the closing brace: no more sticky header.
30648 assert_eq!(sticky_headers(6.0), vec![]);
30649}
30650
30651#[gpui::test]
30652fn test_relative_line_numbers(cx: &mut TestAppContext) {
30653 init_test(cx, |_| {});
30654
30655 let buffer_1 = cx.new(|cx| Buffer::local("aaaaaaaaaa\nbbb\n", cx));
30656 let buffer_2 = cx.new(|cx| Buffer::local("cccccccccc\nddd\n", cx));
30657 let buffer_3 = cx.new(|cx| Buffer::local("eee\nffffffffff\n", cx));
30658
30659 let multibuffer = cx.new(|cx| {
30660 let mut multibuffer = MultiBuffer::new(ReadWrite);
30661 multibuffer.set_excerpts_for_path(
30662 PathKey::sorted(0),
30663 buffer_1.clone(),
30664 [Point::new(0, 0)..Point::new(2, 0)],
30665 0,
30666 cx,
30667 );
30668 multibuffer.set_excerpts_for_path(
30669 PathKey::sorted(1),
30670 buffer_2.clone(),
30671 [Point::new(0, 0)..Point::new(2, 0)],
30672 0,
30673 cx,
30674 );
30675 multibuffer.set_excerpts_for_path(
30676 PathKey::sorted(2),
30677 buffer_3.clone(),
30678 [Point::new(0, 0)..Point::new(2, 0)],
30679 0,
30680 cx,
30681 );
30682 multibuffer
30683 });
30684
30685 // wrapped contents of multibuffer:
30686 // aaa
30687 // aaa
30688 // aaa
30689 // a
30690 // bbb
30691 //
30692 // ccc
30693 // ccc
30694 // ccc
30695 // c
30696 // ddd
30697 //
30698 // eee
30699 // fff
30700 // fff
30701 // fff
30702 // f
30703
30704 let editor = cx.add_window(|window, cx| build_editor(multibuffer, window, cx));
30705 _ = editor.update(cx, |editor, window, cx| {
30706 editor.set_wrap_width(Some(30.0.into()), cx); // every 3 characters
30707
30708 // includes trailing newlines.
30709 let expected_line_numbers = [2, 6, 7, 10, 14, 15, 18, 19, 23];
30710 let expected_wrapped_line_numbers = [
30711 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23,
30712 ];
30713
30714 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30715 s.select_ranges([
30716 Point::new(7, 0)..Point::new(7, 1), // second row of `ccc`
30717 ]);
30718 });
30719
30720 let snapshot = editor.snapshot(window, cx);
30721
30722 // these are all 0-indexed
30723 let base_display_row = DisplayRow(11);
30724 let base_row = 3;
30725 let wrapped_base_row = 7;
30726
30727 // test not counting wrapped lines
30728 let expected_relative_numbers = expected_line_numbers
30729 .into_iter()
30730 .enumerate()
30731 .map(|(i, row)| (DisplayRow(row), i.abs_diff(base_row) as u32))
30732 .filter(|(_, relative_line_number)| *relative_line_number != 0)
30733 .collect_vec();
30734 let actual_relative_numbers = snapshot
30735 .calculate_relative_line_numbers(
30736 &(DisplayRow(0)..DisplayRow(24)),
30737 base_display_row,
30738 false,
30739 )
30740 .into_iter()
30741 .sorted()
30742 .collect_vec();
30743 assert_eq!(expected_relative_numbers, actual_relative_numbers);
30744 // check `calculate_relative_line_numbers()` against `relative_line_delta()` for each line
30745 for (display_row, relative_number) in expected_relative_numbers {
30746 assert_eq!(
30747 relative_number,
30748 snapshot
30749 .relative_line_delta(display_row, base_display_row, false)
30750 .unsigned_abs() as u32,
30751 );
30752 }
30753
30754 // test counting wrapped lines
30755 let expected_wrapped_relative_numbers = expected_wrapped_line_numbers
30756 .into_iter()
30757 .enumerate()
30758 .map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32))
30759 .filter(|(row, _)| *row != base_display_row)
30760 .collect_vec();
30761 let actual_relative_numbers = snapshot
30762 .calculate_relative_line_numbers(
30763 &(DisplayRow(0)..DisplayRow(24)),
30764 base_display_row,
30765 true,
30766 )
30767 .into_iter()
30768 .sorted()
30769 .collect_vec();
30770 assert_eq!(expected_wrapped_relative_numbers, actual_relative_numbers);
30771 // check `calculate_relative_line_numbers()` against `relative_wrapped_line_delta()` for each line
30772 for (display_row, relative_number) in expected_wrapped_relative_numbers {
30773 assert_eq!(
30774 relative_number,
30775 snapshot
30776 .relative_line_delta(display_row, base_display_row, true)
30777 .unsigned_abs() as u32,
30778 );
30779 }
30780 });
30781}
30782
30783#[gpui::test]
30784async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
30785 init_test(cx, |_| {});
30786 cx.update(|cx| {
30787 SettingsStore::update_global(cx, |store, cx| {
30788 store.update_user_settings(cx, |settings| {
30789 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
30790 enabled: Some(true),
30791 })
30792 });
30793 });
30794 });
30795 let mut cx = EditorTestContext::new(cx).await;
30796
30797 let line_height = cx.update_editor(|editor, window, cx| {
30798 editor
30799 .style(cx)
30800 .text
30801 .line_height_in_pixels(window.rem_size())
30802 });
30803
30804 let buffer = indoc! {"
30805 ˇfn foo() {
30806 let abc = 123;
30807 }
30808 struct Bar;
30809 impl Bar {
30810 fn new() -> Self {
30811 Self
30812 }
30813 }
30814 fn baz() {
30815 }
30816 "};
30817 cx.set_state(&buffer);
30818
30819 cx.update_editor(|e, _, cx| {
30820 e.buffer()
30821 .read(cx)
30822 .as_singleton()
30823 .unwrap()
30824 .update(cx, |buffer, cx| {
30825 buffer.set_language(Some(rust_lang()), cx);
30826 })
30827 });
30828
30829 let fn_foo = || empty_range(0, 0);
30830 let impl_bar = || empty_range(4, 0);
30831 let fn_new = || empty_range(5, 4);
30832
30833 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
30834 cx.update_editor(|e, window, cx| {
30835 e.scroll(
30836 gpui::Point {
30837 x: 0.,
30838 y: scroll_offset,
30839 },
30840 None,
30841 window,
30842 cx,
30843 );
30844 });
30845 cx.run_until_parked();
30846 cx.simulate_click(
30847 gpui::Point {
30848 x: px(0.),
30849 y: click_offset as f32 * line_height,
30850 },
30851 Modifiers::none(),
30852 );
30853 cx.run_until_parked();
30854 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
30855 };
30856 assert_eq!(
30857 scroll_and_click(
30858 4.5, // impl Bar is halfway off the screen
30859 0.0 // click top of screen
30860 ),
30861 // scrolled to impl Bar
30862 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
30863 );
30864
30865 assert_eq!(
30866 scroll_and_click(
30867 4.5, // impl Bar is halfway off the screen
30868 0.25 // click middle of impl Bar
30869 ),
30870 // scrolled to impl Bar
30871 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
30872 );
30873
30874 assert_eq!(
30875 scroll_and_click(
30876 4.5, // impl Bar is halfway off the screen
30877 1.5 // click below impl Bar (e.g. fn new())
30878 ),
30879 // scrolled to fn new() - this is below the impl Bar header which has persisted
30880 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
30881 );
30882
30883 assert_eq!(
30884 scroll_and_click(
30885 5.5, // fn new is halfway underneath impl Bar
30886 0.75 // click on the overlap of impl Bar and fn new()
30887 ),
30888 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
30889 );
30890
30891 assert_eq!(
30892 scroll_and_click(
30893 5.5, // fn new is halfway underneath impl Bar
30894 1.25 // click on the visible part of fn new()
30895 ),
30896 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
30897 );
30898
30899 assert_eq!(
30900 scroll_and_click(
30901 1.5, // fn foo is halfway off the screen
30902 0.0 // click top of screen
30903 ),
30904 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
30905 );
30906
30907 assert_eq!(
30908 scroll_and_click(
30909 1.5, // fn foo is halfway off the screen
30910 0.75 // click visible part of let abc...
30911 )
30912 .0,
30913 // no change in scroll
30914 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
30915 (gpui::Point { x: 0., y: 1.5 })
30916 );
30917}
30918
30919#[gpui::test]
30920async fn test_next_prev_reference(cx: &mut TestAppContext) {
30921 const CYCLE_POSITIONS: &[&'static str] = &[
30922 indoc! {"
30923 fn foo() {
30924 let ˇabc = 123;
30925 let x = abc + 1;
30926 let y = abc + 2;
30927 let z = abc + 2;
30928 }
30929 "},
30930 indoc! {"
30931 fn foo() {
30932 let abc = 123;
30933 let x = ˇabc + 1;
30934 let y = abc + 2;
30935 let z = abc + 2;
30936 }
30937 "},
30938 indoc! {"
30939 fn foo() {
30940 let abc = 123;
30941 let x = abc + 1;
30942 let y = ˇabc + 2;
30943 let z = abc + 2;
30944 }
30945 "},
30946 indoc! {"
30947 fn foo() {
30948 let abc = 123;
30949 let x = abc + 1;
30950 let y = abc + 2;
30951 let z = ˇabc + 2;
30952 }
30953 "},
30954 ];
30955
30956 init_test(cx, |_| {});
30957
30958 let mut cx = EditorLspTestContext::new_rust(
30959 lsp::ServerCapabilities {
30960 references_provider: Some(lsp::OneOf::Left(true)),
30961 ..Default::default()
30962 },
30963 cx,
30964 )
30965 .await;
30966
30967 // importantly, the cursor is in the middle
30968 cx.set_state(indoc! {"
30969 fn foo() {
30970 let aˇbc = 123;
30971 let x = abc + 1;
30972 let y = abc + 2;
30973 let z = abc + 2;
30974 }
30975 "});
30976
30977 let reference_ranges = [
30978 lsp::Position::new(1, 8),
30979 lsp::Position::new(2, 12),
30980 lsp::Position::new(3, 12),
30981 lsp::Position::new(4, 12),
30982 ]
30983 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
30984
30985 cx.lsp
30986 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
30987 Ok(Some(
30988 reference_ranges
30989 .map(|range| lsp::Location {
30990 uri: params.text_document_position.text_document.uri.clone(),
30991 range,
30992 })
30993 .to_vec(),
30994 ))
30995 });
30996
30997 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
30998 cx.update_editor(|editor, window, cx| {
30999 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
31000 })
31001 .unwrap()
31002 .await
31003 .unwrap()
31004 };
31005
31006 _move(Direction::Next, 1, &mut cx).await;
31007 cx.assert_editor_state(CYCLE_POSITIONS[1]);
31008
31009 _move(Direction::Next, 1, &mut cx).await;
31010 cx.assert_editor_state(CYCLE_POSITIONS[2]);
31011
31012 _move(Direction::Next, 1, &mut cx).await;
31013 cx.assert_editor_state(CYCLE_POSITIONS[3]);
31014
31015 // loops back to the start
31016 _move(Direction::Next, 1, &mut cx).await;
31017 cx.assert_editor_state(CYCLE_POSITIONS[0]);
31018
31019 // loops back to the end
31020 _move(Direction::Prev, 1, &mut cx).await;
31021 cx.assert_editor_state(CYCLE_POSITIONS[3]);
31022
31023 _move(Direction::Prev, 1, &mut cx).await;
31024 cx.assert_editor_state(CYCLE_POSITIONS[2]);
31025
31026 _move(Direction::Prev, 1, &mut cx).await;
31027 cx.assert_editor_state(CYCLE_POSITIONS[1]);
31028
31029 _move(Direction::Prev, 1, &mut cx).await;
31030 cx.assert_editor_state(CYCLE_POSITIONS[0]);
31031
31032 _move(Direction::Next, 3, &mut cx).await;
31033 cx.assert_editor_state(CYCLE_POSITIONS[3]);
31034
31035 _move(Direction::Prev, 2, &mut cx).await;
31036 cx.assert_editor_state(CYCLE_POSITIONS[1]);
31037}
31038
31039#[gpui::test]
31040async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
31041 init_test(cx, |_| {});
31042
31043 let (editor, cx) = cx.add_window_view(|window, cx| {
31044 let multi_buffer = MultiBuffer::build_multi(
31045 [
31046 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
31047 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
31048 ],
31049 cx,
31050 );
31051 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31052 });
31053
31054 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
31055 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
31056
31057 cx.assert_excerpts_with_selections(indoc! {"
31058 [EXCERPT]
31059 ˇ1
31060 2
31061 3
31062 [EXCERPT]
31063 1
31064 2
31065 3
31066 "});
31067
31068 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
31069 cx.update_editor(|editor, window, cx| {
31070 editor.change_selections(None.into(), window, cx, |s| {
31071 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
31072 });
31073 });
31074 cx.assert_excerpts_with_selections(indoc! {"
31075 [EXCERPT]
31076 1
31077 2ˇ
31078 3
31079 [EXCERPT]
31080 1
31081 2
31082 3
31083 "});
31084
31085 cx.update_editor(|editor, window, cx| {
31086 editor
31087 .select_all_matches(&SelectAllMatches, window, cx)
31088 .unwrap();
31089 });
31090 cx.assert_excerpts_with_selections(indoc! {"
31091 [EXCERPT]
31092 1
31093 2ˇ
31094 3
31095 [EXCERPT]
31096 1
31097 2ˇ
31098 3
31099 "});
31100
31101 cx.update_editor(|editor, window, cx| {
31102 editor.handle_input("X", window, cx);
31103 });
31104 cx.assert_excerpts_with_selections(indoc! {"
31105 [EXCERPT]
31106 1
31107 Xˇ
31108 3
31109 [EXCERPT]
31110 1
31111 Xˇ
31112 3
31113 "});
31114
31115 // Scenario 2: Select "2", then fold second buffer before insertion
31116 cx.update_multibuffer(|mb, cx| {
31117 for buffer_id in buffer_ids.iter() {
31118 let buffer = mb.buffer(*buffer_id).unwrap();
31119 buffer.update(cx, |buffer, cx| {
31120 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
31121 });
31122 }
31123 });
31124
31125 // Select "2" and select all matches
31126 cx.update_editor(|editor, window, cx| {
31127 editor.change_selections(None.into(), window, cx, |s| {
31128 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
31129 });
31130 editor
31131 .select_all_matches(&SelectAllMatches, window, cx)
31132 .unwrap();
31133 });
31134
31135 // Fold second buffer - should remove selections from folded buffer
31136 cx.update_editor(|editor, _, cx| {
31137 editor.fold_buffer(buffer_ids[1], cx);
31138 });
31139 cx.assert_excerpts_with_selections(indoc! {"
31140 [EXCERPT]
31141 1
31142 2ˇ
31143 3
31144 [EXCERPT]
31145 [FOLDED]
31146 "});
31147
31148 // Insert text - should only affect first buffer
31149 cx.update_editor(|editor, window, cx| {
31150 editor.handle_input("Y", window, cx);
31151 });
31152 cx.update_editor(|editor, _, cx| {
31153 editor.unfold_buffer(buffer_ids[1], cx);
31154 });
31155 cx.assert_excerpts_with_selections(indoc! {"
31156 [EXCERPT]
31157 1
31158 Yˇ
31159 3
31160 [EXCERPT]
31161 1
31162 2
31163 3
31164 "});
31165
31166 // Scenario 3: Select "2", then fold first buffer before insertion
31167 cx.update_multibuffer(|mb, cx| {
31168 for buffer_id in buffer_ids.iter() {
31169 let buffer = mb.buffer(*buffer_id).unwrap();
31170 buffer.update(cx, |buffer, cx| {
31171 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
31172 });
31173 }
31174 });
31175
31176 // Select "2" and select all matches
31177 cx.update_editor(|editor, window, cx| {
31178 editor.change_selections(None.into(), window, cx, |s| {
31179 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
31180 });
31181 editor
31182 .select_all_matches(&SelectAllMatches, window, cx)
31183 .unwrap();
31184 });
31185
31186 // Fold first buffer - should remove selections from folded buffer
31187 cx.update_editor(|editor, _, cx| {
31188 editor.fold_buffer(buffer_ids[0], cx);
31189 });
31190 cx.assert_excerpts_with_selections(indoc! {"
31191 [EXCERPT]
31192 [FOLDED]
31193 [EXCERPT]
31194 1
31195 2ˇ
31196 3
31197 "});
31198
31199 // Insert text - should only affect second buffer
31200 cx.update_editor(|editor, window, cx| {
31201 editor.handle_input("Z", window, cx);
31202 });
31203 cx.update_editor(|editor, _, cx| {
31204 editor.unfold_buffer(buffer_ids[0], cx);
31205 });
31206 cx.assert_excerpts_with_selections(indoc! {"
31207 [EXCERPT]
31208 1
31209 2
31210 3
31211 [EXCERPT]
31212 1
31213 Zˇ
31214 3
31215 "});
31216
31217 // Test correct folded header is selected upon fold
31218 cx.update_editor(|editor, _, cx| {
31219 editor.fold_buffer(buffer_ids[0], cx);
31220 editor.fold_buffer(buffer_ids[1], cx);
31221 });
31222 cx.assert_excerpts_with_selections(indoc! {"
31223 [EXCERPT]
31224 [FOLDED]
31225 [EXCERPT]
31226 ˇ[FOLDED]
31227 "});
31228
31229 // Test selection inside folded buffer unfolds it on type
31230 cx.update_editor(|editor, window, cx| {
31231 editor.handle_input("W", window, cx);
31232 });
31233 cx.update_editor(|editor, _, cx| {
31234 editor.unfold_buffer(buffer_ids[0], cx);
31235 });
31236 cx.assert_excerpts_with_selections(indoc! {"
31237 [EXCERPT]
31238 1
31239 2
31240 3
31241 [EXCERPT]
31242 Wˇ1
31243 Z
31244 3
31245 "});
31246}
31247
31248#[gpui::test]
31249async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
31250 init_test(cx, |_| {});
31251
31252 let (editor, cx) = cx.add_window_view(|window, cx| {
31253 let multi_buffer = MultiBuffer::build_multi(
31254 [
31255 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
31256 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
31257 ],
31258 cx,
31259 );
31260 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31261 });
31262
31263 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
31264
31265 cx.assert_excerpts_with_selections(indoc! {"
31266 [EXCERPT]
31267 ˇ1
31268 2
31269 3
31270 [EXCERPT]
31271 1
31272 2
31273 3
31274 4
31275 5
31276 6
31277 7
31278 8
31279 9
31280 "});
31281
31282 cx.update_editor(|editor, window, cx| {
31283 editor.change_selections(None.into(), window, cx, |s| {
31284 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
31285 });
31286 });
31287
31288 cx.assert_excerpts_with_selections(indoc! {"
31289 [EXCERPT]
31290 1
31291 2
31292 3
31293 [EXCERPT]
31294 1
31295 2
31296 3
31297 4
31298 5
31299 6
31300 ˇ7
31301 8
31302 9
31303 "});
31304
31305 cx.update_editor(|editor, _window, cx| {
31306 editor.set_vertical_scroll_margin(0, cx);
31307 });
31308
31309 cx.update_editor(|editor, window, cx| {
31310 assert_eq!(editor.vertical_scroll_margin(), 0);
31311 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
31312 assert_eq!(
31313 editor.snapshot(window, cx).scroll_position(),
31314 gpui::Point::new(0., 12.0)
31315 );
31316 });
31317
31318 cx.update_editor(|editor, _window, cx| {
31319 editor.set_vertical_scroll_margin(3, cx);
31320 });
31321
31322 cx.update_editor(|editor, window, cx| {
31323 assert_eq!(editor.vertical_scroll_margin(), 3);
31324 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
31325 assert_eq!(
31326 editor.snapshot(window, cx).scroll_position(),
31327 gpui::Point::new(0., 9.0)
31328 );
31329 });
31330}
31331
31332#[gpui::test]
31333async fn test_find_references_single_case(cx: &mut TestAppContext) {
31334 init_test(cx, |_| {});
31335 let mut cx = EditorLspTestContext::new_rust(
31336 lsp::ServerCapabilities {
31337 references_provider: Some(lsp::OneOf::Left(true)),
31338 ..lsp::ServerCapabilities::default()
31339 },
31340 cx,
31341 )
31342 .await;
31343
31344 let before = indoc!(
31345 r#"
31346 fn main() {
31347 let aˇbc = 123;
31348 let xyz = abc;
31349 }
31350 "#
31351 );
31352 let after = indoc!(
31353 r#"
31354 fn main() {
31355 let abc = 123;
31356 let xyz = ˇabc;
31357 }
31358 "#
31359 );
31360
31361 cx.lsp
31362 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
31363 Ok(Some(vec![
31364 lsp::Location {
31365 uri: params.text_document_position.text_document.uri.clone(),
31366 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
31367 },
31368 lsp::Location {
31369 uri: params.text_document_position.text_document.uri,
31370 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
31371 },
31372 ]))
31373 });
31374
31375 cx.set_state(before);
31376
31377 let action = FindAllReferences {
31378 always_open_multibuffer: false,
31379 };
31380
31381 let navigated = cx
31382 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
31383 .expect("should have spawned a task")
31384 .await
31385 .unwrap();
31386
31387 assert_eq!(navigated, Navigated::No);
31388
31389 cx.run_until_parked();
31390
31391 cx.assert_editor_state(after);
31392}
31393
31394#[gpui::test]
31395async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
31396 init_test(cx, |settings| {
31397 settings.defaults.tab_size = Some(2.try_into().unwrap());
31398 });
31399
31400 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
31401 let mut cx = EditorTestContext::new(cx).await;
31402 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31403
31404 // Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
31405 cx.set_state(indoc! {"
31406 - [ ] taskˇ
31407 "});
31408 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31409 cx.wait_for_autoindent_applied().await;
31410 cx.assert_editor_state(indoc! {"
31411 - [ ] task
31412 - [ ] ˇ
31413 "});
31414
31415 // Case 2: Works with checked task items too
31416 cx.set_state(indoc! {"
31417 - [x] completed taskˇ
31418 "});
31419 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31420 cx.wait_for_autoindent_applied().await;
31421 cx.assert_editor_state(indoc! {"
31422 - [x] completed task
31423 - [ ] ˇ
31424 "});
31425
31426 // Case 2.1: Works with uppercase checked marker too
31427 cx.set_state(indoc! {"
31428 - [X] completed taskˇ
31429 "});
31430 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31431 cx.wait_for_autoindent_applied().await;
31432 cx.assert_editor_state(indoc! {"
31433 - [X] completed task
31434 - [ ] ˇ
31435 "});
31436
31437 // Case 3: Cursor position doesn't matter - content after marker is what counts
31438 cx.set_state(indoc! {"
31439 - [ ] taˇsk
31440 "});
31441 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31442 cx.wait_for_autoindent_applied().await;
31443 cx.assert_editor_state(indoc! {"
31444 - [ ] ta
31445 - [ ] ˇsk
31446 "});
31447
31448 // Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
31449 cx.set_state(indoc! {"
31450 - [ ] ˇ
31451 "});
31452 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31453 cx.wait_for_autoindent_applied().await;
31454 cx.assert_editor_state(
31455 indoc! {"
31456 - [ ]$$
31457 ˇ
31458 "}
31459 .replace("$", " ")
31460 .as_str(),
31461 );
31462
31463 // Case 5: Adding newline with content adds marker preserving indentation
31464 cx.set_state(indoc! {"
31465 - [ ] task
31466 - [ ] indentedˇ
31467 "});
31468 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31469 cx.wait_for_autoindent_applied().await;
31470 cx.assert_editor_state(indoc! {"
31471 - [ ] task
31472 - [ ] indented
31473 - [ ] ˇ
31474 "});
31475
31476 // Case 6: Adding newline with cursor right after prefix, unindents
31477 cx.set_state(indoc! {"
31478 - [ ] task
31479 - [ ] sub task
31480 - [ ] ˇ
31481 "});
31482 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31483 cx.wait_for_autoindent_applied().await;
31484 cx.assert_editor_state(indoc! {"
31485 - [ ] task
31486 - [ ] sub task
31487 - [ ] ˇ
31488 "});
31489 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31490 cx.wait_for_autoindent_applied().await;
31491
31492 // Case 7: Adding newline with cursor right after prefix, removes marker
31493 cx.assert_editor_state(indoc! {"
31494 - [ ] task
31495 - [ ] sub task
31496 - [ ] ˇ
31497 "});
31498 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31499 cx.wait_for_autoindent_applied().await;
31500 cx.assert_editor_state(indoc! {"
31501 - [ ] task
31502 - [ ] sub task
31503 ˇ
31504 "});
31505
31506 // Case 8: Cursor before or inside prefix does not add marker
31507 cx.set_state(indoc! {"
31508 ˇ- [ ] task
31509 "});
31510 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31511 cx.wait_for_autoindent_applied().await;
31512 cx.assert_editor_state(indoc! {"
31513
31514 ˇ- [ ] task
31515 "});
31516
31517 cx.set_state(indoc! {"
31518 - [ˇ ] task
31519 "});
31520 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31521 cx.wait_for_autoindent_applied().await;
31522 cx.assert_editor_state(indoc! {"
31523 - [
31524 ˇ
31525 ] task
31526 "});
31527}
31528
31529#[gpui::test]
31530async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
31531 init_test(cx, |settings| {
31532 settings.defaults.tab_size = Some(2.try_into().unwrap());
31533 });
31534
31535 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
31536 let mut cx = EditorTestContext::new(cx).await;
31537 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31538
31539 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
31540 cx.set_state(indoc! {"
31541 - itemˇ
31542 "});
31543 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31544 cx.wait_for_autoindent_applied().await;
31545 cx.assert_editor_state(indoc! {"
31546 - item
31547 - ˇ
31548 "});
31549
31550 // Case 2: Works with different markers
31551 cx.set_state(indoc! {"
31552 * starred itemˇ
31553 "});
31554 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31555 cx.wait_for_autoindent_applied().await;
31556 cx.assert_editor_state(indoc! {"
31557 * starred item
31558 * ˇ
31559 "});
31560
31561 cx.set_state(indoc! {"
31562 + plus itemˇ
31563 "});
31564 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31565 cx.wait_for_autoindent_applied().await;
31566 cx.assert_editor_state(indoc! {"
31567 + plus item
31568 + ˇ
31569 "});
31570
31571 // Case 3: Cursor position doesn't matter - content after marker is what counts
31572 cx.set_state(indoc! {"
31573 - itˇem
31574 "});
31575 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31576 cx.wait_for_autoindent_applied().await;
31577 cx.assert_editor_state(indoc! {"
31578 - it
31579 - ˇem
31580 "});
31581
31582 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
31583 cx.set_state(indoc! {"
31584 - ˇ
31585 "});
31586 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31587 cx.wait_for_autoindent_applied().await;
31588 cx.assert_editor_state(
31589 indoc! {"
31590 - $
31591 ˇ
31592 "}
31593 .replace("$", " ")
31594 .as_str(),
31595 );
31596
31597 // Case 5: Adding newline with content adds marker preserving indentation
31598 cx.set_state(indoc! {"
31599 - item
31600 - indentedˇ
31601 "});
31602 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31603 cx.wait_for_autoindent_applied().await;
31604 cx.assert_editor_state(indoc! {"
31605 - item
31606 - indented
31607 - ˇ
31608 "});
31609
31610 // Case 6: Adding newline with cursor right after marker, unindents
31611 cx.set_state(indoc! {"
31612 - item
31613 - sub item
31614 - ˇ
31615 "});
31616 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31617 cx.wait_for_autoindent_applied().await;
31618 cx.assert_editor_state(indoc! {"
31619 - item
31620 - sub item
31621 - ˇ
31622 "});
31623 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31624 cx.wait_for_autoindent_applied().await;
31625
31626 // Case 7: Adding newline with cursor right after marker, removes marker
31627 cx.assert_editor_state(indoc! {"
31628 - item
31629 - sub item
31630 - ˇ
31631 "});
31632 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31633 cx.wait_for_autoindent_applied().await;
31634 cx.assert_editor_state(indoc! {"
31635 - item
31636 - sub item
31637 ˇ
31638 "});
31639
31640 // Case 8: Cursor before or inside prefix does not add marker
31641 cx.set_state(indoc! {"
31642 ˇ- item
31643 "});
31644 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31645 cx.wait_for_autoindent_applied().await;
31646 cx.assert_editor_state(indoc! {"
31647
31648 ˇ- item
31649 "});
31650
31651 cx.set_state(indoc! {"
31652 -ˇ item
31653 "});
31654 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31655 cx.wait_for_autoindent_applied().await;
31656 cx.assert_editor_state(indoc! {"
31657 -
31658 ˇitem
31659 "});
31660}
31661
31662#[gpui::test]
31663async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
31664 init_test(cx, |settings| {
31665 settings.defaults.tab_size = Some(2.try_into().unwrap());
31666 });
31667
31668 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
31669 let mut cx = EditorTestContext::new(cx).await;
31670 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31671
31672 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
31673 cx.set_state(indoc! {"
31674 1. first itemˇ
31675 "});
31676 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31677 cx.wait_for_autoindent_applied().await;
31678 cx.assert_editor_state(indoc! {"
31679 1. first item
31680 2. ˇ
31681 "});
31682
31683 // Case 2: Works with larger numbers
31684 cx.set_state(indoc! {"
31685 10. tenth itemˇ
31686 "});
31687 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31688 cx.wait_for_autoindent_applied().await;
31689 cx.assert_editor_state(indoc! {"
31690 10. tenth item
31691 11. ˇ
31692 "});
31693
31694 // Case 3: Cursor position doesn't matter - content after marker is what counts
31695 cx.set_state(indoc! {"
31696 1. itˇem
31697 "});
31698 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31699 cx.wait_for_autoindent_applied().await;
31700 cx.assert_editor_state(indoc! {"
31701 1. it
31702 2. ˇem
31703 "});
31704
31705 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
31706 cx.set_state(indoc! {"
31707 1. ˇ
31708 "});
31709 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31710 cx.wait_for_autoindent_applied().await;
31711 cx.assert_editor_state(
31712 indoc! {"
31713 1. $
31714 ˇ
31715 "}
31716 .replace("$", " ")
31717 .as_str(),
31718 );
31719
31720 // Case 5: Adding newline with content adds marker preserving indentation
31721 cx.set_state(indoc! {"
31722 1. item
31723 2. indentedˇ
31724 "});
31725 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31726 cx.wait_for_autoindent_applied().await;
31727 cx.assert_editor_state(indoc! {"
31728 1. item
31729 2. indented
31730 3. ˇ
31731 "});
31732
31733 // Case 6: Adding newline with cursor right after marker, unindents
31734 cx.set_state(indoc! {"
31735 1. item
31736 2. sub item
31737 3. ˇ
31738 "});
31739 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31740 cx.wait_for_autoindent_applied().await;
31741 cx.assert_editor_state(indoc! {"
31742 1. item
31743 2. sub item
31744 1. ˇ
31745 "});
31746 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31747 cx.wait_for_autoindent_applied().await;
31748
31749 // Case 7: Adding newline with cursor right after marker, removes marker
31750 cx.assert_editor_state(indoc! {"
31751 1. item
31752 2. sub item
31753 1. ˇ
31754 "});
31755 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31756 cx.wait_for_autoindent_applied().await;
31757 cx.assert_editor_state(indoc! {"
31758 1. item
31759 2. sub item
31760 ˇ
31761 "});
31762
31763 // Case 8: Cursor before or inside prefix does not add marker
31764 cx.set_state(indoc! {"
31765 ˇ1. item
31766 "});
31767 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31768 cx.wait_for_autoindent_applied().await;
31769 cx.assert_editor_state(indoc! {"
31770
31771 ˇ1. item
31772 "});
31773
31774 cx.set_state(indoc! {"
31775 1ˇ. item
31776 "});
31777 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31778 cx.wait_for_autoindent_applied().await;
31779 cx.assert_editor_state(indoc! {"
31780 1
31781 ˇ. item
31782 "});
31783}
31784
31785#[gpui::test]
31786async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
31787 init_test(cx, |settings| {
31788 settings.defaults.tab_size = Some(2.try_into().unwrap());
31789 });
31790
31791 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
31792 let mut cx = EditorTestContext::new(cx).await;
31793 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31794
31795 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
31796 cx.set_state(indoc! {"
31797 1. first item
31798 1. sub first item
31799 2. sub second item
31800 3. ˇ
31801 "});
31802 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31803 cx.wait_for_autoindent_applied().await;
31804 cx.assert_editor_state(indoc! {"
31805 1. first item
31806 1. sub first item
31807 2. sub second item
31808 1. ˇ
31809 "});
31810}
31811
31812#[gpui::test]
31813async fn test_tab_list_indent(cx: &mut TestAppContext) {
31814 init_test(cx, |settings| {
31815 settings.defaults.tab_size = Some(2.try_into().unwrap());
31816 });
31817
31818 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
31819 let mut cx = EditorTestContext::new(cx).await;
31820 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31821
31822 // Case 1: Unordered list - cursor after prefix, adds indent before prefix
31823 cx.set_state(indoc! {"
31824 - ˇitem
31825 "});
31826 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31827 cx.wait_for_autoindent_applied().await;
31828 let expected = indoc! {"
31829 $$- ˇitem
31830 "};
31831 cx.assert_editor_state(expected.replace("$", " ").as_str());
31832
31833 // Case 2: Task list - cursor after prefix
31834 cx.set_state(indoc! {"
31835 - [ ] ˇtask
31836 "});
31837 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31838 cx.wait_for_autoindent_applied().await;
31839 let expected = indoc! {"
31840 $$- [ ] ˇtask
31841 "};
31842 cx.assert_editor_state(expected.replace("$", " ").as_str());
31843
31844 // Case 3: Ordered list - cursor after prefix
31845 cx.set_state(indoc! {"
31846 1. ˇfirst
31847 "});
31848 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31849 cx.wait_for_autoindent_applied().await;
31850 let expected = indoc! {"
31851 $$1. ˇfirst
31852 "};
31853 cx.assert_editor_state(expected.replace("$", " ").as_str());
31854
31855 // Case 4: With existing indentation - adds more indent
31856 let initial = indoc! {"
31857 $$- ˇitem
31858 "};
31859 cx.set_state(initial.replace("$", " ").as_str());
31860 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31861 cx.wait_for_autoindent_applied().await;
31862 let expected = indoc! {"
31863 $$$$- ˇitem
31864 "};
31865 cx.assert_editor_state(expected.replace("$", " ").as_str());
31866
31867 // Case 5: Empty list item
31868 cx.set_state(indoc! {"
31869 - ˇ
31870 "});
31871 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31872 cx.wait_for_autoindent_applied().await;
31873 let expected = indoc! {"
31874 $$- ˇ
31875 "};
31876 cx.assert_editor_state(expected.replace("$", " ").as_str());
31877
31878 // Case 6: Cursor at end of line with content
31879 cx.set_state(indoc! {"
31880 - itemˇ
31881 "});
31882 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31883 cx.wait_for_autoindent_applied().await;
31884 let expected = indoc! {"
31885 $$- itemˇ
31886 "};
31887 cx.assert_editor_state(expected.replace("$", " ").as_str());
31888
31889 // Case 7: Cursor at start of list item, indents it
31890 cx.set_state(indoc! {"
31891 - item
31892 ˇ - sub item
31893 "});
31894 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31895 cx.wait_for_autoindent_applied().await;
31896 let expected = indoc! {"
31897 - item
31898 ˇ - sub item
31899 "};
31900 cx.assert_editor_state(expected);
31901
31902 // Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
31903 cx.update_editor(|_, _, cx| {
31904 SettingsStore::update_global(cx, |store, cx| {
31905 store.update_user_settings(cx, |settings| {
31906 settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
31907 });
31908 });
31909 });
31910 cx.set_state(indoc! {"
31911 - item
31912 ˇ - sub item
31913 "});
31914 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31915 cx.wait_for_autoindent_applied().await;
31916 let expected = indoc! {"
31917 - item
31918 ˇ- sub item
31919 "};
31920 cx.assert_editor_state(expected);
31921}
31922
31923#[gpui::test]
31924async fn test_local_worktree_trust(cx: &mut TestAppContext) {
31925 init_test(cx, |_| {});
31926 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), cx));
31927
31928 cx.update(|cx| {
31929 SettingsStore::update_global(cx, |store, cx| {
31930 store.update_user_settings(cx, |settings| {
31931 settings.project.all_languages.defaults.inlay_hints =
31932 Some(InlayHintSettingsContent {
31933 enabled: Some(true),
31934 ..InlayHintSettingsContent::default()
31935 });
31936 });
31937 });
31938 });
31939
31940 let fs = FakeFs::new(cx.executor());
31941 fs.insert_tree(
31942 path!("/project"),
31943 json!({
31944 ".zed": {
31945 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
31946 },
31947 "main.rs": "fn main() {}"
31948 }),
31949 )
31950 .await;
31951
31952 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
31953 let server_name = "override-rust-analyzer";
31954 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
31955
31956 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
31957 language_registry.add(rust_lang());
31958
31959 let capabilities = lsp::ServerCapabilities {
31960 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
31961 ..lsp::ServerCapabilities::default()
31962 };
31963 let mut fake_language_servers = language_registry.register_fake_lsp(
31964 "Rust",
31965 FakeLspAdapter {
31966 name: server_name,
31967 capabilities,
31968 initializer: Some(Box::new({
31969 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
31970 move |fake_server| {
31971 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
31972 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
31973 move |_params, _| {
31974 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
31975 async move {
31976 Ok(Some(vec![lsp::InlayHint {
31977 position: lsp::Position::new(0, 0),
31978 label: lsp::InlayHintLabel::String("hint".to_string()),
31979 kind: None,
31980 text_edits: None,
31981 tooltip: None,
31982 padding_left: None,
31983 padding_right: None,
31984 data: None,
31985 }]))
31986 }
31987 },
31988 );
31989 }
31990 })),
31991 ..FakeLspAdapter::default()
31992 },
31993 );
31994
31995 cx.run_until_parked();
31996
31997 let worktree_id = project.read_with(cx, |project, cx| {
31998 project
31999 .worktrees(cx)
32000 .next()
32001 .map(|wt| wt.read(cx).id())
32002 .expect("should have a worktree")
32003 });
32004 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
32005
32006 let trusted_worktrees =
32007 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
32008
32009 let can_trust = trusted_worktrees.update(cx, |store, cx| {
32010 store.can_trust(&worktree_store, worktree_id, cx)
32011 });
32012 assert!(!can_trust, "worktree should be restricted initially");
32013
32014 let buffer_before_approval = project
32015 .update(cx, |project, cx| {
32016 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
32017 })
32018 .await
32019 .unwrap();
32020
32021 let (editor, cx) = cx.add_window_view(|window, cx| {
32022 Editor::new(
32023 EditorMode::full(),
32024 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
32025 Some(project.clone()),
32026 window,
32027 cx,
32028 )
32029 });
32030 cx.run_until_parked();
32031 let fake_language_server = fake_language_servers.next();
32032
32033 cx.read(|cx| {
32034 let file = buffer_before_approval.read(cx).file();
32035 assert_eq!(
32036 language::language_settings::language_settings(Some("Rust".into()), file, cx)
32037 .language_servers,
32038 ["...".to_string()],
32039 "local .zed/settings.json must not apply before trust approval"
32040 )
32041 });
32042
32043 editor.update_in(cx, |editor, window, cx| {
32044 editor.handle_input("1", window, cx);
32045 });
32046 cx.run_until_parked();
32047 cx.executor()
32048 .advance_clock(std::time::Duration::from_secs(1));
32049 assert_eq!(
32050 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
32051 0,
32052 "inlay hints must not be queried before trust approval"
32053 );
32054
32055 trusted_worktrees.update(cx, |store, cx| {
32056 store.trust(
32057 &worktree_store,
32058 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
32059 cx,
32060 );
32061 });
32062 cx.run_until_parked();
32063
32064 cx.read(|cx| {
32065 let file = buffer_before_approval.read(cx).file();
32066 assert_eq!(
32067 language::language_settings::language_settings(Some("Rust".into()), file, cx)
32068 .language_servers,
32069 ["override-rust-analyzer".to_string()],
32070 "local .zed/settings.json should apply after trust approval"
32071 )
32072 });
32073 let _fake_language_server = fake_language_server.await.unwrap();
32074 editor.update_in(cx, |editor, window, cx| {
32075 editor.handle_input("1", window, cx);
32076 });
32077 cx.run_until_parked();
32078 cx.executor()
32079 .advance_clock(std::time::Duration::from_secs(1));
32080 assert!(
32081 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
32082 "inlay hints should be queried after trust approval"
32083 );
32084
32085 let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
32086 store.can_trust(&worktree_store, worktree_id, cx)
32087 });
32088 assert!(can_trust_after, "worktree should be trusted after trust()");
32089}
32090
32091#[gpui::test]
32092fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
32093 // This test reproduces a bug where drawing an editor at a position above the viewport
32094 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
32095 // causes an infinite loop in blocks_in_range.
32096 //
32097 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
32098 // the content mask intersection produces visible_bounds with origin at the viewport top.
32099 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
32100 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
32101 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
32102 init_test(cx, |_| {});
32103
32104 let window = cx.add_window(|_, _| gpui::Empty);
32105 let mut cx = VisualTestContext::from_window(*window, cx);
32106
32107 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
32108 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
32109
32110 // Simulate a small viewport (500x500 pixels at origin 0,0)
32111 cx.simulate_resize(gpui::size(px(500.), px(500.)));
32112
32113 // Draw the editor at a very negative Y position, simulating an editor that's been
32114 // scrolled way above the visible viewport (like in a List that has scrolled past it).
32115 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
32116 // This should NOT hang - it should just render nothing.
32117 cx.draw(
32118 gpui::point(px(0.), px(-10000.)),
32119 gpui::size(px(500.), px(3000.)),
32120 |_, _| editor.clone().into_any_element(),
32121 );
32122
32123 // If we get here without hanging, the test passes
32124}
32125
32126#[gpui::test]
32127async fn test_diff_review_indicator_created_on_gutter_hover(cx: &mut TestAppContext) {
32128 init_test(cx, |_| {});
32129
32130 let fs = FakeFs::new(cx.executor());
32131 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
32132 .await;
32133
32134 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
32135 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
32136 let workspace = window
32137 .read_with(cx, |mw, _| mw.workspace().clone())
32138 .unwrap();
32139 let cx = &mut VisualTestContext::from_window(*window, cx);
32140
32141 let editor = workspace
32142 .update_in(cx, |workspace, window, cx| {
32143 workspace.open_abs_path(
32144 PathBuf::from(path!("/root/file.txt")),
32145 OpenOptions::default(),
32146 window,
32147 cx,
32148 )
32149 })
32150 .await
32151 .unwrap()
32152 .downcast::<Editor>()
32153 .unwrap();
32154
32155 // Enable diff review button mode
32156 editor.update(cx, |editor, cx| {
32157 editor.set_show_diff_review_button(true, cx);
32158 });
32159
32160 // Initially, no indicator should be present
32161 editor.update(cx, |editor, _cx| {
32162 assert!(
32163 editor.gutter_diff_review_indicator.0.is_none(),
32164 "Indicator should be None initially"
32165 );
32166 });
32167}
32168
32169#[gpui::test]
32170async fn test_diff_review_button_hidden_when_ai_disabled(cx: &mut TestAppContext) {
32171 init_test(cx, |_| {});
32172
32173 // Register DisableAiSettings and set disable_ai to true
32174 cx.update(|cx| {
32175 project::DisableAiSettings::register(cx);
32176 project::DisableAiSettings::override_global(
32177 project::DisableAiSettings { disable_ai: true },
32178 cx,
32179 );
32180 });
32181
32182 let fs = FakeFs::new(cx.executor());
32183 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
32184 .await;
32185
32186 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
32187 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
32188 let workspace = window
32189 .read_with(cx, |mw, _| mw.workspace().clone())
32190 .unwrap();
32191 let cx = &mut VisualTestContext::from_window(*window, cx);
32192
32193 let editor = workspace
32194 .update_in(cx, |workspace, window, cx| {
32195 workspace.open_abs_path(
32196 PathBuf::from(path!("/root/file.txt")),
32197 OpenOptions::default(),
32198 window,
32199 cx,
32200 )
32201 })
32202 .await
32203 .unwrap()
32204 .downcast::<Editor>()
32205 .unwrap();
32206
32207 // Enable diff review button mode
32208 editor.update(cx, |editor, cx| {
32209 editor.set_show_diff_review_button(true, cx);
32210 });
32211
32212 // Verify AI is disabled
32213 cx.read(|cx| {
32214 assert!(
32215 project::DisableAiSettings::get_global(cx).disable_ai,
32216 "AI should be disabled"
32217 );
32218 });
32219
32220 // The indicator should not be created when AI is disabled
32221 // (The mouse_moved handler checks DisableAiSettings before creating the indicator)
32222 editor.update(cx, |editor, _cx| {
32223 assert!(
32224 editor.gutter_diff_review_indicator.0.is_none(),
32225 "Indicator should be None when AI is disabled"
32226 );
32227 });
32228}
32229
32230#[gpui::test]
32231async fn test_diff_review_button_shown_when_ai_enabled(cx: &mut TestAppContext) {
32232 init_test(cx, |_| {});
32233
32234 // Register DisableAiSettings and set disable_ai to false
32235 cx.update(|cx| {
32236 project::DisableAiSettings::register(cx);
32237 project::DisableAiSettings::override_global(
32238 project::DisableAiSettings { disable_ai: false },
32239 cx,
32240 );
32241 });
32242
32243 let fs = FakeFs::new(cx.executor());
32244 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
32245 .await;
32246
32247 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
32248 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
32249 let workspace = window
32250 .read_with(cx, |mw, _| mw.workspace().clone())
32251 .unwrap();
32252 let cx = &mut VisualTestContext::from_window(*window, cx);
32253
32254 let editor = workspace
32255 .update_in(cx, |workspace, window, cx| {
32256 workspace.open_abs_path(
32257 PathBuf::from(path!("/root/file.txt")),
32258 OpenOptions::default(),
32259 window,
32260 cx,
32261 )
32262 })
32263 .await
32264 .unwrap()
32265 .downcast::<Editor>()
32266 .unwrap();
32267
32268 // Enable diff review button mode
32269 editor.update(cx, |editor, cx| {
32270 editor.set_show_diff_review_button(true, cx);
32271 });
32272
32273 // Verify AI is enabled
32274 cx.read(|cx| {
32275 assert!(
32276 !project::DisableAiSettings::get_global(cx).disable_ai,
32277 "AI should be enabled"
32278 );
32279 });
32280
32281 // The show_diff_review_button flag should be true
32282 editor.update(cx, |editor, _cx| {
32283 assert!(
32284 editor.show_diff_review_button(),
32285 "show_diff_review_button should be true"
32286 );
32287 });
32288}
32289
32290/// Helper function to create a DiffHunkKey for testing.
32291/// Uses Anchor::min() as a placeholder anchor since these tests don't need
32292/// real buffer positioning.
32293fn test_hunk_key(file_path: &str) -> DiffHunkKey {
32294 DiffHunkKey {
32295 file_path: if file_path.is_empty() {
32296 Arc::from(util::rel_path::RelPath::empty())
32297 } else {
32298 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
32299 },
32300 hunk_start_anchor: Anchor::min(),
32301 }
32302}
32303
32304/// Helper function to create a DiffHunkKey with a specific anchor for testing.
32305fn test_hunk_key_with_anchor(file_path: &str, anchor: Anchor) -> DiffHunkKey {
32306 DiffHunkKey {
32307 file_path: if file_path.is_empty() {
32308 Arc::from(util::rel_path::RelPath::empty())
32309 } else {
32310 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
32311 },
32312 hunk_start_anchor: anchor,
32313 }
32314}
32315
32316/// Helper function to add a review comment with default anchors for testing.
32317fn add_test_comment(
32318 editor: &mut Editor,
32319 key: DiffHunkKey,
32320 comment: &str,
32321 cx: &mut Context<Editor>,
32322) -> usize {
32323 editor.add_review_comment(key, comment.to_string(), Anchor::min()..Anchor::max(), cx)
32324}
32325
32326#[gpui::test]
32327fn test_review_comment_add_to_hunk(cx: &mut TestAppContext) {
32328 init_test(cx, |_| {});
32329
32330 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32331
32332 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
32333 let key = test_hunk_key("");
32334
32335 let id = add_test_comment(editor, key.clone(), "Test comment", cx);
32336
32337 let snapshot = editor.buffer().read(cx).snapshot(cx);
32338 assert_eq!(editor.total_review_comment_count(), 1);
32339 assert_eq!(editor.hunk_comment_count(&key, &snapshot), 1);
32340
32341 let comments = editor.comments_for_hunk(&key, &snapshot);
32342 assert_eq!(comments.len(), 1);
32343 assert_eq!(comments[0].comment, "Test comment");
32344 assert_eq!(comments[0].id, id);
32345 });
32346}
32347
32348#[gpui::test]
32349fn test_review_comments_are_per_hunk(cx: &mut TestAppContext) {
32350 init_test(cx, |_| {});
32351
32352 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32353
32354 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
32355 let snapshot = editor.buffer().read(cx).snapshot(cx);
32356 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
32357 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
32358 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
32359 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
32360
32361 add_test_comment(editor, key1.clone(), "Comment for file1", cx);
32362 add_test_comment(editor, key2.clone(), "Comment for file2", cx);
32363
32364 let snapshot = editor.buffer().read(cx).snapshot(cx);
32365 assert_eq!(editor.total_review_comment_count(), 2);
32366 assert_eq!(editor.hunk_comment_count(&key1, &snapshot), 1);
32367 assert_eq!(editor.hunk_comment_count(&key2, &snapshot), 1);
32368
32369 assert_eq!(
32370 editor.comments_for_hunk(&key1, &snapshot)[0].comment,
32371 "Comment for file1"
32372 );
32373 assert_eq!(
32374 editor.comments_for_hunk(&key2, &snapshot)[0].comment,
32375 "Comment for file2"
32376 );
32377 });
32378}
32379
32380#[gpui::test]
32381fn test_review_comment_remove(cx: &mut TestAppContext) {
32382 init_test(cx, |_| {});
32383
32384 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32385
32386 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
32387 let key = test_hunk_key("");
32388
32389 let id = add_test_comment(editor, key, "To be removed", cx);
32390
32391 assert_eq!(editor.total_review_comment_count(), 1);
32392
32393 let removed = editor.remove_review_comment(id, cx);
32394 assert!(removed);
32395 assert_eq!(editor.total_review_comment_count(), 0);
32396
32397 // Try to remove again
32398 let removed_again = editor.remove_review_comment(id, cx);
32399 assert!(!removed_again);
32400 });
32401}
32402
32403#[gpui::test]
32404fn test_review_comment_update(cx: &mut TestAppContext) {
32405 init_test(cx, |_| {});
32406
32407 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32408
32409 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
32410 let key = test_hunk_key("");
32411
32412 let id = add_test_comment(editor, key.clone(), "Original text", cx);
32413
32414 let updated = editor.update_review_comment(id, "Updated text".to_string(), cx);
32415 assert!(updated);
32416
32417 let snapshot = editor.buffer().read(cx).snapshot(cx);
32418 let comments = editor.comments_for_hunk(&key, &snapshot);
32419 assert_eq!(comments[0].comment, "Updated text");
32420 assert!(!comments[0].is_editing); // Should clear editing flag
32421 });
32422}
32423
32424#[gpui::test]
32425fn test_review_comment_take_all(cx: &mut TestAppContext) {
32426 init_test(cx, |_| {});
32427
32428 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32429
32430 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
32431 let snapshot = editor.buffer().read(cx).snapshot(cx);
32432 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
32433 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
32434 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
32435 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
32436
32437 let id1 = add_test_comment(editor, key1.clone(), "Comment 1", cx);
32438 let id2 = add_test_comment(editor, key1.clone(), "Comment 2", cx);
32439 let id3 = add_test_comment(editor, key2.clone(), "Comment 3", cx);
32440
32441 // IDs should be sequential starting from 0
32442 assert_eq!(id1, 0);
32443 assert_eq!(id2, 1);
32444 assert_eq!(id3, 2);
32445
32446 assert_eq!(editor.total_review_comment_count(), 3);
32447
32448 let taken = editor.take_all_review_comments(cx);
32449
32450 // Should have 2 entries (one per hunk)
32451 assert_eq!(taken.len(), 2);
32452
32453 // Total comments should be 3
32454 let total: usize = taken
32455 .iter()
32456 .map(|(_, comments): &(DiffHunkKey, Vec<StoredReviewComment>)| comments.len())
32457 .sum();
32458 assert_eq!(total, 3);
32459
32460 // Storage should be empty
32461 assert_eq!(editor.total_review_comment_count(), 0);
32462
32463 // After taking all comments, ID counter should reset
32464 // New comments should get IDs starting from 0 again
32465 let new_id1 = add_test_comment(editor, key1, "New Comment 1", cx);
32466 let new_id2 = add_test_comment(editor, key2, "New Comment 2", cx);
32467
32468 assert_eq!(new_id1, 0, "ID counter should reset after take_all");
32469 assert_eq!(new_id2, 1, "IDs should be sequential after reset");
32470 });
32471}
32472
32473#[gpui::test]
32474fn test_diff_review_overlay_show_and_dismiss(cx: &mut TestAppContext) {
32475 init_test(cx, |_| {});
32476
32477 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32478
32479 // Show overlay
32480 editor
32481 .update(cx, |editor, window, cx| {
32482 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
32483 })
32484 .unwrap();
32485
32486 // Verify overlay is shown
32487 editor
32488 .update(cx, |editor, _window, cx| {
32489 assert!(!editor.diff_review_overlays.is_empty());
32490 assert_eq!(editor.diff_review_line_range(cx), Some((0, 0)));
32491 assert!(editor.diff_review_prompt_editor().is_some());
32492 })
32493 .unwrap();
32494
32495 // Dismiss overlay
32496 editor
32497 .update(cx, |editor, _window, cx| {
32498 editor.dismiss_all_diff_review_overlays(cx);
32499 })
32500 .unwrap();
32501
32502 // Verify overlay is dismissed
32503 editor
32504 .update(cx, |editor, _window, cx| {
32505 assert!(editor.diff_review_overlays.is_empty());
32506 assert_eq!(editor.diff_review_line_range(cx), None);
32507 assert!(editor.diff_review_prompt_editor().is_none());
32508 })
32509 .unwrap();
32510}
32511
32512#[gpui::test]
32513fn test_diff_review_overlay_dismiss_via_cancel(cx: &mut TestAppContext) {
32514 init_test(cx, |_| {});
32515
32516 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32517
32518 // Show overlay
32519 editor
32520 .update(cx, |editor, window, cx| {
32521 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
32522 })
32523 .unwrap();
32524
32525 // Verify overlay is shown
32526 editor
32527 .update(cx, |editor, _window, _cx| {
32528 assert!(!editor.diff_review_overlays.is_empty());
32529 })
32530 .unwrap();
32531
32532 // Dismiss via dismiss_menus_and_popups (which is called by cancel action)
32533 editor
32534 .update(cx, |editor, window, cx| {
32535 editor.dismiss_menus_and_popups(true, window, cx);
32536 })
32537 .unwrap();
32538
32539 // Verify overlay is dismissed
32540 editor
32541 .update(cx, |editor, _window, _cx| {
32542 assert!(editor.diff_review_overlays.is_empty());
32543 })
32544 .unwrap();
32545}
32546
32547#[gpui::test]
32548fn test_diff_review_empty_comment_not_submitted(cx: &mut TestAppContext) {
32549 init_test(cx, |_| {});
32550
32551 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32552
32553 // Show overlay
32554 editor
32555 .update(cx, |editor, window, cx| {
32556 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
32557 })
32558 .unwrap();
32559
32560 // Try to submit without typing anything (empty comment)
32561 editor
32562 .update(cx, |editor, window, cx| {
32563 editor.submit_diff_review_comment(window, cx);
32564 })
32565 .unwrap();
32566
32567 // Verify no comment was added
32568 editor
32569 .update(cx, |editor, _window, _cx| {
32570 assert_eq!(editor.total_review_comment_count(), 0);
32571 })
32572 .unwrap();
32573
32574 // Try to submit with whitespace-only comment
32575 editor
32576 .update(cx, |editor, window, cx| {
32577 if let Some(prompt_editor) = editor.diff_review_prompt_editor().cloned() {
32578 prompt_editor.update(cx, |pe, cx| {
32579 pe.insert(" \n\t ", window, cx);
32580 });
32581 }
32582 editor.submit_diff_review_comment(window, cx);
32583 })
32584 .unwrap();
32585
32586 // Verify still no comment was added
32587 editor
32588 .update(cx, |editor, _window, _cx| {
32589 assert_eq!(editor.total_review_comment_count(), 0);
32590 })
32591 .unwrap();
32592}
32593
32594#[gpui::test]
32595fn test_diff_review_inline_edit_flow(cx: &mut TestAppContext) {
32596 init_test(cx, |_| {});
32597
32598 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32599
32600 // Add a comment directly
32601 let comment_id = editor
32602 .update(cx, |editor, _window, cx| {
32603 let key = test_hunk_key("");
32604 add_test_comment(editor, key, "Original comment", cx)
32605 })
32606 .unwrap();
32607
32608 // Set comment to editing mode
32609 editor
32610 .update(cx, |editor, _window, cx| {
32611 editor.set_comment_editing(comment_id, true, cx);
32612 })
32613 .unwrap();
32614
32615 // Verify editing flag is set
32616 editor
32617 .update(cx, |editor, _window, cx| {
32618 let key = test_hunk_key("");
32619 let snapshot = editor.buffer().read(cx).snapshot(cx);
32620 let comments = editor.comments_for_hunk(&key, &snapshot);
32621 assert_eq!(comments.len(), 1);
32622 assert!(comments[0].is_editing);
32623 })
32624 .unwrap();
32625
32626 // Update the comment
32627 editor
32628 .update(cx, |editor, _window, cx| {
32629 let updated =
32630 editor.update_review_comment(comment_id, "Updated comment".to_string(), cx);
32631 assert!(updated);
32632 })
32633 .unwrap();
32634
32635 // Verify comment was updated and editing flag is cleared
32636 editor
32637 .update(cx, |editor, _window, cx| {
32638 let key = test_hunk_key("");
32639 let snapshot = editor.buffer().read(cx).snapshot(cx);
32640 let comments = editor.comments_for_hunk(&key, &snapshot);
32641 assert_eq!(comments[0].comment, "Updated comment");
32642 assert!(!comments[0].is_editing);
32643 })
32644 .unwrap();
32645}
32646
32647#[gpui::test]
32648fn test_orphaned_comments_are_cleaned_up(cx: &mut TestAppContext) {
32649 init_test(cx, |_| {});
32650
32651 // Create an editor with some text
32652 let editor = cx.add_window(|window, cx| {
32653 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
32654 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32655 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
32656 });
32657
32658 // Add a comment with an anchor on line 2
32659 editor
32660 .update(cx, |editor, _window, cx| {
32661 let snapshot = editor.buffer().read(cx).snapshot(cx);
32662 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
32663 let key = DiffHunkKey {
32664 file_path: Arc::from(util::rel_path::RelPath::empty()),
32665 hunk_start_anchor: anchor,
32666 };
32667 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
32668 assert_eq!(editor.total_review_comment_count(), 1);
32669 })
32670 .unwrap();
32671
32672 // Delete all content (this should orphan the comment's anchor)
32673 editor
32674 .update(cx, |editor, window, cx| {
32675 editor.select_all(&SelectAll, window, cx);
32676 editor.insert("completely new content", window, cx);
32677 })
32678 .unwrap();
32679
32680 // Trigger cleanup
32681 editor
32682 .update(cx, |editor, _window, cx| {
32683 editor.cleanup_orphaned_review_comments(cx);
32684 // Comment should be removed because its anchor is invalid
32685 assert_eq!(editor.total_review_comment_count(), 0);
32686 })
32687 .unwrap();
32688}
32689
32690#[gpui::test]
32691fn test_orphaned_comments_cleanup_called_on_buffer_edit(cx: &mut TestAppContext) {
32692 init_test(cx, |_| {});
32693
32694 // Create an editor with some text
32695 let editor = cx.add_window(|window, cx| {
32696 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
32697 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32698 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
32699 });
32700
32701 // Add a comment with an anchor on line 2
32702 editor
32703 .update(cx, |editor, _window, cx| {
32704 let snapshot = editor.buffer().read(cx).snapshot(cx);
32705 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
32706 let key = DiffHunkKey {
32707 file_path: Arc::from(util::rel_path::RelPath::empty()),
32708 hunk_start_anchor: anchor,
32709 };
32710 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
32711 assert_eq!(editor.total_review_comment_count(), 1);
32712 })
32713 .unwrap();
32714
32715 // Edit the buffer - this should trigger cleanup via on_buffer_event
32716 // Delete all content which orphans the anchor
32717 editor
32718 .update(cx, |editor, window, cx| {
32719 editor.select_all(&SelectAll, window, cx);
32720 editor.insert("completely new content", window, cx);
32721 // The cleanup is called automatically in on_buffer_event when Edited fires
32722 })
32723 .unwrap();
32724
32725 // Verify cleanup happened automatically (not manually triggered)
32726 editor
32727 .update(cx, |editor, _window, _cx| {
32728 // Comment should be removed because its anchor became invalid
32729 // and cleanup was called automatically on buffer edit
32730 assert_eq!(editor.total_review_comment_count(), 0);
32731 })
32732 .unwrap();
32733}
32734
32735#[gpui::test]
32736fn test_comments_stored_for_multiple_hunks(cx: &mut TestAppContext) {
32737 init_test(cx, |_| {});
32738
32739 // This test verifies that comments can be stored for multiple different hunks
32740 // and that hunk_comment_count correctly identifies comments per hunk.
32741 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32742
32743 _ = editor.update(cx, |editor, _window, cx| {
32744 let snapshot = editor.buffer().read(cx).snapshot(cx);
32745
32746 // Create two different hunk keys (simulating two different files)
32747 let anchor = snapshot.anchor_before(Point::new(0, 0));
32748 let key1 = DiffHunkKey {
32749 file_path: Arc::from(util::rel_path::RelPath::unix("file1.rs").unwrap()),
32750 hunk_start_anchor: anchor,
32751 };
32752 let key2 = DiffHunkKey {
32753 file_path: Arc::from(util::rel_path::RelPath::unix("file2.rs").unwrap()),
32754 hunk_start_anchor: anchor,
32755 };
32756
32757 // Add comments to first hunk
32758 editor.add_review_comment(
32759 key1.clone(),
32760 "Comment 1 for file1".to_string(),
32761 anchor..anchor,
32762 cx,
32763 );
32764 editor.add_review_comment(
32765 key1.clone(),
32766 "Comment 2 for file1".to_string(),
32767 anchor..anchor,
32768 cx,
32769 );
32770
32771 // Add comment to second hunk
32772 editor.add_review_comment(
32773 key2.clone(),
32774 "Comment for file2".to_string(),
32775 anchor..anchor,
32776 cx,
32777 );
32778
32779 // Verify total count
32780 assert_eq!(editor.total_review_comment_count(), 3);
32781
32782 // Verify per-hunk counts
32783 let snapshot = editor.buffer().read(cx).snapshot(cx);
32784 assert_eq!(
32785 editor.hunk_comment_count(&key1, &snapshot),
32786 2,
32787 "file1 should have 2 comments"
32788 );
32789 assert_eq!(
32790 editor.hunk_comment_count(&key2, &snapshot),
32791 1,
32792 "file2 should have 1 comment"
32793 );
32794
32795 // Verify comments_for_hunk returns correct comments
32796 let file1_comments = editor.comments_for_hunk(&key1, &snapshot);
32797 assert_eq!(file1_comments.len(), 2);
32798 assert_eq!(file1_comments[0].comment, "Comment 1 for file1");
32799 assert_eq!(file1_comments[1].comment, "Comment 2 for file1");
32800
32801 let file2_comments = editor.comments_for_hunk(&key2, &snapshot);
32802 assert_eq!(file2_comments.len(), 1);
32803 assert_eq!(file2_comments[0].comment, "Comment for file2");
32804 });
32805}
32806
32807#[gpui::test]
32808fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
32809 init_test(cx, |_| {});
32810
32811 // This test verifies that hunk_keys_match correctly identifies when two
32812 // DiffHunkKeys refer to the same hunk (same file path and anchor point).
32813 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32814
32815 _ = editor.update(cx, |editor, _window, cx| {
32816 let snapshot = editor.buffer().read(cx).snapshot(cx);
32817 let anchor = snapshot.anchor_before(Point::new(0, 0));
32818
32819 // Create two keys with the same file path and anchor
32820 let key1 = DiffHunkKey {
32821 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
32822 hunk_start_anchor: anchor,
32823 };
32824 let key2 = DiffHunkKey {
32825 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
32826 hunk_start_anchor: anchor,
32827 };
32828
32829 // Add comment to first key
32830 editor.add_review_comment(key1, "Test comment".to_string(), anchor..anchor, cx);
32831
32832 // Verify second key (same hunk) finds the comment
32833 let snapshot = editor.buffer().read(cx).snapshot(cx);
32834 assert_eq!(
32835 editor.hunk_comment_count(&key2, &snapshot),
32836 1,
32837 "Same hunk should find the comment"
32838 );
32839
32840 // Create a key with different file path
32841 let different_file_key = DiffHunkKey {
32842 file_path: Arc::from(util::rel_path::RelPath::unix("other.rs").unwrap()),
32843 hunk_start_anchor: anchor,
32844 };
32845
32846 // Different file should not find the comment
32847 assert_eq!(
32848 editor.hunk_comment_count(&different_file_key, &snapshot),
32849 0,
32850 "Different file should not find the comment"
32851 );
32852 });
32853}
32854
32855#[gpui::test]
32856fn test_overlay_comments_expanded_state(cx: &mut TestAppContext) {
32857 init_test(cx, |_| {});
32858
32859 // This test verifies that set_diff_review_comments_expanded correctly
32860 // updates the expanded state of overlays.
32861 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32862
32863 // Show overlay
32864 editor
32865 .update(cx, |editor, window, cx| {
32866 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
32867 })
32868 .unwrap();
32869
32870 // Verify initially expanded (default)
32871 editor
32872 .update(cx, |editor, _window, _cx| {
32873 assert!(
32874 editor.diff_review_overlays[0].comments_expanded,
32875 "Should be expanded by default"
32876 );
32877 })
32878 .unwrap();
32879
32880 // Set to collapsed using the public method
32881 editor
32882 .update(cx, |editor, _window, cx| {
32883 editor.set_diff_review_comments_expanded(false, cx);
32884 })
32885 .unwrap();
32886
32887 // Verify collapsed
32888 editor
32889 .update(cx, |editor, _window, _cx| {
32890 assert!(
32891 !editor.diff_review_overlays[0].comments_expanded,
32892 "Should be collapsed after setting to false"
32893 );
32894 })
32895 .unwrap();
32896
32897 // Set back to expanded
32898 editor
32899 .update(cx, |editor, _window, cx| {
32900 editor.set_diff_review_comments_expanded(true, cx);
32901 })
32902 .unwrap();
32903
32904 // Verify expanded again
32905 editor
32906 .update(cx, |editor, _window, _cx| {
32907 assert!(
32908 editor.diff_review_overlays[0].comments_expanded,
32909 "Should be expanded after setting to true"
32910 );
32911 })
32912 .unwrap();
32913}
32914
32915#[gpui::test]
32916fn test_diff_review_multiline_selection(cx: &mut TestAppContext) {
32917 init_test(cx, |_| {});
32918
32919 // Create an editor with multiple lines of text
32920 let editor = cx.add_window(|window, cx| {
32921 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\nline 4\nline 5\n", cx));
32922 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32923 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
32924 });
32925
32926 // Test showing overlay with a multi-line selection (lines 1-3, which are rows 0-2)
32927 editor
32928 .update(cx, |editor, window, cx| {
32929 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(2), window, cx);
32930 })
32931 .unwrap();
32932
32933 // Verify line range
32934 editor
32935 .update(cx, |editor, _window, cx| {
32936 assert!(!editor.diff_review_overlays.is_empty());
32937 assert_eq!(editor.diff_review_line_range(cx), Some((0, 2)));
32938 })
32939 .unwrap();
32940
32941 // Dismiss and test with reversed range (end < start)
32942 editor
32943 .update(cx, |editor, _window, cx| {
32944 editor.dismiss_all_diff_review_overlays(cx);
32945 })
32946 .unwrap();
32947
32948 // Show overlay with reversed range - should normalize it
32949 editor
32950 .update(cx, |editor, window, cx| {
32951 editor.show_diff_review_overlay(DisplayRow(3)..DisplayRow(1), window, cx);
32952 })
32953 .unwrap();
32954
32955 // Verify range is normalized (start <= end)
32956 editor
32957 .update(cx, |editor, _window, cx| {
32958 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
32959 })
32960 .unwrap();
32961}
32962
32963#[gpui::test]
32964fn test_diff_review_drag_state(cx: &mut TestAppContext) {
32965 init_test(cx, |_| {});
32966
32967 let editor = cx.add_window(|window, cx| {
32968 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
32969 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32970 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
32971 });
32972
32973 // Initially no drag state
32974 editor
32975 .update(cx, |editor, _window, _cx| {
32976 assert!(editor.diff_review_drag_state.is_none());
32977 })
32978 .unwrap();
32979
32980 // Start drag at row 1
32981 editor
32982 .update(cx, |editor, window, cx| {
32983 editor.start_diff_review_drag(DisplayRow(1), window, cx);
32984 })
32985 .unwrap();
32986
32987 // Verify drag state is set
32988 editor
32989 .update(cx, |editor, window, cx| {
32990 assert!(editor.diff_review_drag_state.is_some());
32991 let snapshot = editor.snapshot(window, cx);
32992 let range = editor
32993 .diff_review_drag_state
32994 .as_ref()
32995 .unwrap()
32996 .row_range(&snapshot.display_snapshot);
32997 assert_eq!(*range.start(), DisplayRow(1));
32998 assert_eq!(*range.end(), DisplayRow(1));
32999 })
33000 .unwrap();
33001
33002 // Update drag to row 3
33003 editor
33004 .update(cx, |editor, window, cx| {
33005 editor.update_diff_review_drag(DisplayRow(3), window, cx);
33006 })
33007 .unwrap();
33008
33009 // Verify drag state is updated
33010 editor
33011 .update(cx, |editor, window, cx| {
33012 assert!(editor.diff_review_drag_state.is_some());
33013 let snapshot = editor.snapshot(window, cx);
33014 let range = editor
33015 .diff_review_drag_state
33016 .as_ref()
33017 .unwrap()
33018 .row_range(&snapshot.display_snapshot);
33019 assert_eq!(*range.start(), DisplayRow(1));
33020 assert_eq!(*range.end(), DisplayRow(3));
33021 })
33022 .unwrap();
33023
33024 // End drag - should show overlay
33025 editor
33026 .update(cx, |editor, window, cx| {
33027 editor.end_diff_review_drag(window, cx);
33028 })
33029 .unwrap();
33030
33031 // Verify drag state is cleared and overlay is shown
33032 editor
33033 .update(cx, |editor, _window, cx| {
33034 assert!(editor.diff_review_drag_state.is_none());
33035 assert!(!editor.diff_review_overlays.is_empty());
33036 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
33037 })
33038 .unwrap();
33039}
33040
33041#[gpui::test]
33042fn test_diff_review_drag_cancel(cx: &mut TestAppContext) {
33043 init_test(cx, |_| {});
33044
33045 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33046
33047 // Start drag
33048 editor
33049 .update(cx, |editor, window, cx| {
33050 editor.start_diff_review_drag(DisplayRow(0), window, cx);
33051 })
33052 .unwrap();
33053
33054 // Verify drag state is set
33055 editor
33056 .update(cx, |editor, _window, _cx| {
33057 assert!(editor.diff_review_drag_state.is_some());
33058 })
33059 .unwrap();
33060
33061 // Cancel drag
33062 editor
33063 .update(cx, |editor, _window, cx| {
33064 editor.cancel_diff_review_drag(cx);
33065 })
33066 .unwrap();
33067
33068 // Verify drag state is cleared and no overlay was created
33069 editor
33070 .update(cx, |editor, _window, _cx| {
33071 assert!(editor.diff_review_drag_state.is_none());
33072 assert!(editor.diff_review_overlays.is_empty());
33073 })
33074 .unwrap();
33075}
33076
33077#[gpui::test]
33078fn test_calculate_overlay_height(cx: &mut TestAppContext) {
33079 init_test(cx, |_| {});
33080
33081 // This test verifies that calculate_overlay_height returns correct heights
33082 // based on comment count and expanded state.
33083 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
33084
33085 _ = editor.update(cx, |editor, _window, cx| {
33086 let snapshot = editor.buffer().read(cx).snapshot(cx);
33087 let anchor = snapshot.anchor_before(Point::new(0, 0));
33088 let key = DiffHunkKey {
33089 file_path: Arc::from(util::rel_path::RelPath::empty()),
33090 hunk_start_anchor: anchor,
33091 };
33092
33093 // No comments: base height of 2
33094 let height_no_comments = editor.calculate_overlay_height(&key, true, &snapshot);
33095 assert_eq!(
33096 height_no_comments, 2,
33097 "Base height should be 2 with no comments"
33098 );
33099
33100 // Add one comment
33101 editor.add_review_comment(key.clone(), "Comment 1".to_string(), anchor..anchor, cx);
33102
33103 let snapshot = editor.buffer().read(cx).snapshot(cx);
33104
33105 // With comments expanded: base (2) + header (1) + 2 per comment
33106 let height_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
33107 assert_eq!(
33108 height_expanded,
33109 2 + 1 + 2, // base + header + 1 comment * 2
33110 "Height with 1 comment expanded"
33111 );
33112
33113 // With comments collapsed: base (2) + header (1)
33114 let height_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
33115 assert_eq!(
33116 height_collapsed,
33117 2 + 1, // base + header only
33118 "Height with comments collapsed"
33119 );
33120
33121 // Add more comments
33122 editor.add_review_comment(key.clone(), "Comment 2".to_string(), anchor..anchor, cx);
33123 editor.add_review_comment(key.clone(), "Comment 3".to_string(), anchor..anchor, cx);
33124
33125 let snapshot = editor.buffer().read(cx).snapshot(cx);
33126
33127 // With 3 comments expanded
33128 let height_3_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
33129 assert_eq!(
33130 height_3_expanded,
33131 2 + 1 + (3 * 2), // base + header + 3 comments * 2
33132 "Height with 3 comments expanded"
33133 );
33134
33135 // Collapsed height stays the same regardless of comment count
33136 let height_3_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
33137 assert_eq!(
33138 height_3_collapsed,
33139 2 + 1, // base + header only
33140 "Height with 3 comments collapsed should be same as 1 comment collapsed"
33141 );
33142 });
33143}
33144
33145#[gpui::test]
33146async fn test_move_to_start_end_of_larger_syntax_node_single_cursor(cx: &mut TestAppContext) {
33147 init_test(cx, |_| {});
33148
33149 let language = Arc::new(Language::new(
33150 LanguageConfig::default(),
33151 Some(tree_sitter_rust::LANGUAGE.into()),
33152 ));
33153
33154 let text = r#"
33155 fn main() {
33156 let x = foo(1, 2);
33157 }
33158 "#
33159 .unindent();
33160
33161 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
33162 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33163 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33164
33165 editor
33166 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33167 .await;
33168
33169 // Test case 1: Move to end of syntax nodes
33170 editor.update_in(cx, |editor, window, cx| {
33171 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33172 s.select_display_ranges([
33173 DisplayPoint::new(DisplayRow(1), 16)..DisplayPoint::new(DisplayRow(1), 16)
33174 ]);
33175 });
33176 });
33177 editor.update(cx, |editor, cx| {
33178 assert_text_with_selections(
33179 editor,
33180 indoc! {r#"
33181 fn main() {
33182 let x = foo(ˇ1, 2);
33183 }
33184 "#},
33185 cx,
33186 );
33187 });
33188 editor.update_in(cx, |editor, window, cx| {
33189 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
33190 });
33191 editor.update(cx, |editor, cx| {
33192 assert_text_with_selections(
33193 editor,
33194 indoc! {r#"
33195 fn main() {
33196 let x = foo(1ˇ, 2);
33197 }
33198 "#},
33199 cx,
33200 );
33201 });
33202 editor.update_in(cx, |editor, window, cx| {
33203 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
33204 });
33205 editor.update(cx, |editor, cx| {
33206 assert_text_with_selections(
33207 editor,
33208 indoc! {r#"
33209 fn main() {
33210 let x = foo(1, 2)ˇ;
33211 }
33212 "#},
33213 cx,
33214 );
33215 });
33216 editor.update_in(cx, |editor, window, cx| {
33217 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
33218 });
33219 editor.update(cx, |editor, cx| {
33220 assert_text_with_selections(
33221 editor,
33222 indoc! {r#"
33223 fn main() {
33224 let x = foo(1, 2);ˇ
33225 }
33226 "#},
33227 cx,
33228 );
33229 });
33230 editor.update_in(cx, |editor, window, cx| {
33231 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
33232 });
33233 editor.update(cx, |editor, cx| {
33234 assert_text_with_selections(
33235 editor,
33236 indoc! {r#"
33237 fn main() {
33238 let x = foo(1, 2);
33239 }ˇ
33240 "#},
33241 cx,
33242 );
33243 });
33244
33245 // Test case 2: Move to start of syntax nodes
33246 editor.update_in(cx, |editor, window, cx| {
33247 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33248 s.select_display_ranges([
33249 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20)
33250 ]);
33251 });
33252 });
33253 editor.update(cx, |editor, cx| {
33254 assert_text_with_selections(
33255 editor,
33256 indoc! {r#"
33257 fn main() {
33258 let x = foo(1, 2ˇ);
33259 }
33260 "#},
33261 cx,
33262 );
33263 });
33264 editor.update_in(cx, |editor, window, cx| {
33265 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
33266 });
33267 editor.update(cx, |editor, cx| {
33268 assert_text_with_selections(
33269 editor,
33270 indoc! {r#"
33271 fn main() {
33272 let x = fooˇ(1, 2);
33273 }
33274 "#},
33275 cx,
33276 );
33277 });
33278 editor.update_in(cx, |editor, window, cx| {
33279 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
33280 });
33281 editor.update(cx, |editor, cx| {
33282 assert_text_with_selections(
33283 editor,
33284 indoc! {r#"
33285 fn main() {
33286 let x = ˇfoo(1, 2);
33287 }
33288 "#},
33289 cx,
33290 );
33291 });
33292 editor.update_in(cx, |editor, window, cx| {
33293 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
33294 });
33295 editor.update(cx, |editor, cx| {
33296 assert_text_with_selections(
33297 editor,
33298 indoc! {r#"
33299 fn main() {
33300 ˇlet x = foo(1, 2);
33301 }
33302 "#},
33303 cx,
33304 );
33305 });
33306 editor.update_in(cx, |editor, window, cx| {
33307 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
33308 });
33309 editor.update(cx, |editor, cx| {
33310 assert_text_with_selections(
33311 editor,
33312 indoc! {r#"
33313 fn main() ˇ{
33314 let x = foo(1, 2);
33315 }
33316 "#},
33317 cx,
33318 );
33319 });
33320 editor.update_in(cx, |editor, window, cx| {
33321 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
33322 });
33323 editor.update(cx, |editor, cx| {
33324 assert_text_with_selections(
33325 editor,
33326 indoc! {r#"
33327 ˇfn main() {
33328 let x = foo(1, 2);
33329 }
33330 "#},
33331 cx,
33332 );
33333 });
33334}
33335
33336#[gpui::test]
33337async fn test_move_to_start_end_of_larger_syntax_node_two_cursors(cx: &mut TestAppContext) {
33338 init_test(cx, |_| {});
33339
33340 let language = Arc::new(Language::new(
33341 LanguageConfig::default(),
33342 Some(tree_sitter_rust::LANGUAGE.into()),
33343 ));
33344
33345 let text = r#"
33346 fn main() {
33347 let x = foo(1, 2);
33348 let y = bar(3, 4);
33349 }
33350 "#
33351 .unindent();
33352
33353 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
33354 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33355 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33356
33357 editor
33358 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33359 .await;
33360
33361 // Test case 1: Move to end of syntax nodes with two cursors
33362 editor.update_in(cx, |editor, window, cx| {
33363 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33364 s.select_display_ranges([
33365 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20),
33366 DisplayPoint::new(DisplayRow(2), 20)..DisplayPoint::new(DisplayRow(2), 20),
33367 ]);
33368 });
33369 });
33370 editor.update(cx, |editor, cx| {
33371 assert_text_with_selections(
33372 editor,
33373 indoc! {r#"
33374 fn main() {
33375 let x = foo(1, 2ˇ);
33376 let y = bar(3, 4ˇ);
33377 }
33378 "#},
33379 cx,
33380 );
33381 });
33382 editor.update_in(cx, |editor, window, cx| {
33383 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
33384 });
33385 editor.update(cx, |editor, cx| {
33386 assert_text_with_selections(
33387 editor,
33388 indoc! {r#"
33389 fn main() {
33390 let x = foo(1, 2)ˇ;
33391 let y = bar(3, 4)ˇ;
33392 }
33393 "#},
33394 cx,
33395 );
33396 });
33397 editor.update_in(cx, |editor, window, cx| {
33398 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
33399 });
33400 editor.update(cx, |editor, cx| {
33401 assert_text_with_selections(
33402 editor,
33403 indoc! {r#"
33404 fn main() {
33405 let x = foo(1, 2);ˇ
33406 let y = bar(3, 4);ˇ
33407 }
33408 "#},
33409 cx,
33410 );
33411 });
33412
33413 // Test case 2: Move to start of syntax nodes with two cursors
33414 editor.update_in(cx, |editor, window, cx| {
33415 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33416 s.select_display_ranges([
33417 DisplayPoint::new(DisplayRow(1), 19)..DisplayPoint::new(DisplayRow(1), 19),
33418 DisplayPoint::new(DisplayRow(2), 19)..DisplayPoint::new(DisplayRow(2), 19),
33419 ]);
33420 });
33421 });
33422 editor.update(cx, |editor, cx| {
33423 assert_text_with_selections(
33424 editor,
33425 indoc! {r#"
33426 fn main() {
33427 let x = foo(1, ˇ2);
33428 let y = bar(3, ˇ4);
33429 }
33430 "#},
33431 cx,
33432 );
33433 });
33434 editor.update_in(cx, |editor, window, cx| {
33435 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
33436 });
33437 editor.update(cx, |editor, cx| {
33438 assert_text_with_selections(
33439 editor,
33440 indoc! {r#"
33441 fn main() {
33442 let x = fooˇ(1, 2);
33443 let y = barˇ(3, 4);
33444 }
33445 "#},
33446 cx,
33447 );
33448 });
33449 editor.update_in(cx, |editor, window, cx| {
33450 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
33451 });
33452 editor.update(cx, |editor, cx| {
33453 assert_text_with_selections(
33454 editor,
33455 indoc! {r#"
33456 fn main() {
33457 let x = ˇfoo(1, 2);
33458 let y = ˇbar(3, 4);
33459 }
33460 "#},
33461 cx,
33462 );
33463 });
33464 editor.update_in(cx, |editor, window, cx| {
33465 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
33466 });
33467 editor.update(cx, |editor, cx| {
33468 assert_text_with_selections(
33469 editor,
33470 indoc! {r#"
33471 fn main() {
33472 ˇlet x = foo(1, 2);
33473 ˇlet y = bar(3, 4);
33474 }
33475 "#},
33476 cx,
33477 );
33478 });
33479}
33480
33481#[gpui::test]
33482async fn test_move_to_start_end_of_larger_syntax_node_with_selections_and_strings(
33483 cx: &mut TestAppContext,
33484) {
33485 init_test(cx, |_| {});
33486
33487 let language = Arc::new(Language::new(
33488 LanguageConfig::default(),
33489 Some(tree_sitter_rust::LANGUAGE.into()),
33490 ));
33491
33492 let text = r#"
33493 fn main() {
33494 let x = foo(1, 2);
33495 let msg = "hello world";
33496 }
33497 "#
33498 .unindent();
33499
33500 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
33501 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33502 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33503
33504 editor
33505 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33506 .await;
33507
33508 // Test case 1: With existing selection, move_to_end keeps selection
33509 editor.update_in(cx, |editor, window, cx| {
33510 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33511 s.select_display_ranges([
33512 DisplayPoint::new(DisplayRow(1), 12)..DisplayPoint::new(DisplayRow(1), 21)
33513 ]);
33514 });
33515 });
33516 editor.update(cx, |editor, cx| {
33517 assert_text_with_selections(
33518 editor,
33519 indoc! {r#"
33520 fn main() {
33521 let x = «foo(1, 2)ˇ»;
33522 let msg = "hello world";
33523 }
33524 "#},
33525 cx,
33526 );
33527 });
33528 editor.update_in(cx, |editor, window, cx| {
33529 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
33530 });
33531 editor.update(cx, |editor, cx| {
33532 assert_text_with_selections(
33533 editor,
33534 indoc! {r#"
33535 fn main() {
33536 let x = «foo(1, 2)ˇ»;
33537 let msg = "hello world";
33538 }
33539 "#},
33540 cx,
33541 );
33542 });
33543
33544 // Test case 2: Move to end within a string
33545 editor.update_in(cx, |editor, window, cx| {
33546 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33547 s.select_display_ranges([
33548 DisplayPoint::new(DisplayRow(2), 15)..DisplayPoint::new(DisplayRow(2), 15)
33549 ]);
33550 });
33551 });
33552 editor.update(cx, |editor, cx| {
33553 assert_text_with_selections(
33554 editor,
33555 indoc! {r#"
33556 fn main() {
33557 let x = foo(1, 2);
33558 let msg = "ˇhello world";
33559 }
33560 "#},
33561 cx,
33562 );
33563 });
33564 editor.update_in(cx, |editor, window, cx| {
33565 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
33566 });
33567 editor.update(cx, |editor, cx| {
33568 assert_text_with_selections(
33569 editor,
33570 indoc! {r#"
33571 fn main() {
33572 let x = foo(1, 2);
33573 let msg = "hello worldˇ";
33574 }
33575 "#},
33576 cx,
33577 );
33578 });
33579
33580 // Test case 3: Move to start within a string
33581 editor.update_in(cx, |editor, window, cx| {
33582 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33583 s.select_display_ranges([
33584 DisplayPoint::new(DisplayRow(2), 21)..DisplayPoint::new(DisplayRow(2), 21)
33585 ]);
33586 });
33587 });
33588 editor.update(cx, |editor, cx| {
33589 assert_text_with_selections(
33590 editor,
33591 indoc! {r#"
33592 fn main() {
33593 let x = foo(1, 2);
33594 let msg = "hello ˇworld";
33595 }
33596 "#},
33597 cx,
33598 );
33599 });
33600 editor.update_in(cx, |editor, window, cx| {
33601 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
33602 });
33603 editor.update(cx, |editor, cx| {
33604 assert_text_with_selections(
33605 editor,
33606 indoc! {r#"
33607 fn main() {
33608 let x = foo(1, 2);
33609 let msg = "ˇhello world";
33610 }
33611 "#},
33612 cx,
33613 );
33614 });
33615}
33616
33617#[gpui::test]
33618async fn test_select_to_start_end_of_larger_syntax_node(cx: &mut TestAppContext) {
33619 init_test(cx, |_| {});
33620
33621 let language = Arc::new(Language::new(
33622 LanguageConfig::default(),
33623 Some(tree_sitter_rust::LANGUAGE.into()),
33624 ));
33625
33626 // Test Group 1.1: Cursor in String - First Jump (Select to End)
33627 let text = r#"let msg = "foo bar baz";"#.unindent();
33628
33629 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33630 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33631 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33632
33633 editor
33634 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33635 .await;
33636
33637 editor.update_in(cx, |editor, window, cx| {
33638 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33639 s.select_display_ranges([
33640 DisplayPoint::new(DisplayRow(0), 14)..DisplayPoint::new(DisplayRow(0), 14)
33641 ]);
33642 });
33643 });
33644 editor.update(cx, |editor, cx| {
33645 assert_text_with_selections(editor, indoc! {r#"let msg = "fooˇ bar baz";"#}, cx);
33646 });
33647 editor.update_in(cx, |editor, window, cx| {
33648 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33649 });
33650 editor.update(cx, |editor, cx| {
33651 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar bazˇ»";"#}, cx);
33652 });
33653
33654 // Test Group 1.2: Cursor in String - Second Jump (Select to End)
33655 editor.update_in(cx, |editor, window, cx| {
33656 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33657 });
33658 editor.update(cx, |editor, cx| {
33659 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz"ˇ»;"#}, cx);
33660 });
33661
33662 // Test Group 1.3: Cursor in String - Third Jump (Select to End)
33663 editor.update_in(cx, |editor, window, cx| {
33664 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33665 });
33666 editor.update(cx, |editor, cx| {
33667 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz";ˇ»"#}, cx);
33668 });
33669
33670 // Test Group 1.4: Cursor in String - First Jump (Select to Start)
33671 editor.update_in(cx, |editor, window, cx| {
33672 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33673 s.select_display_ranges([
33674 DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18)
33675 ]);
33676 });
33677 });
33678 editor.update(cx, |editor, cx| {
33679 assert_text_with_selections(editor, indoc! {r#"let msg = "foo barˇ baz";"#}, cx);
33680 });
33681 editor.update_in(cx, |editor, window, cx| {
33682 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
33683 });
33684 editor.update(cx, |editor, cx| {
33685 assert_text_with_selections(editor, indoc! {r#"let msg = "«ˇfoo bar» baz";"#}, cx);
33686 });
33687
33688 // Test Group 1.5: Cursor in String - Second Jump (Select to Start)
33689 editor.update_in(cx, |editor, window, cx| {
33690 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
33691 });
33692 editor.update(cx, |editor, cx| {
33693 assert_text_with_selections(editor, indoc! {r#"let msg = «ˇ"foo bar» baz";"#}, cx);
33694 });
33695
33696 // Test Group 1.6: Cursor in String - Third Jump (Select to Start)
33697 editor.update_in(cx, |editor, window, cx| {
33698 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
33699 });
33700 editor.update(cx, |editor, cx| {
33701 assert_text_with_selections(editor, indoc! {r#"«ˇlet msg = "foo bar» baz";"#}, cx);
33702 });
33703
33704 // Test Group 2.1: Let Statement Progression (Select to End)
33705 let text = r#"
33706fn main() {
33707 let x = "hello";
33708}
33709"#
33710 .unindent();
33711
33712 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33713 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33714 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33715
33716 editor
33717 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33718 .await;
33719
33720 editor.update_in(cx, |editor, window, cx| {
33721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33722 s.select_display_ranges([
33723 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)
33724 ]);
33725 });
33726 });
33727 editor.update(cx, |editor, cx| {
33728 assert_text_with_selections(
33729 editor,
33730 indoc! {r#"
33731 fn main() {
33732 let xˇ = "hello";
33733 }
33734 "#},
33735 cx,
33736 );
33737 });
33738 editor.update_in(cx, |editor, window, cx| {
33739 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33740 });
33741 editor.update(cx, |editor, cx| {
33742 assert_text_with_selections(
33743 editor,
33744 indoc! {r##"
33745 fn main() {
33746 let x« = "hello";ˇ»
33747 }
33748 "##},
33749 cx,
33750 );
33751 });
33752 editor.update_in(cx, |editor, window, cx| {
33753 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33754 });
33755 editor.update(cx, |editor, cx| {
33756 assert_text_with_selections(
33757 editor,
33758 indoc! {r#"
33759 fn main() {
33760 let x« = "hello";
33761 }ˇ»
33762 "#},
33763 cx,
33764 );
33765 });
33766
33767 // Test Group 2.2a: From Inside String Content Node To String Content Boundary
33768 let text = r#"let x = "hello";"#.unindent();
33769
33770 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33771 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33772 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33773
33774 editor
33775 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33776 .await;
33777
33778 editor.update_in(cx, |editor, window, cx| {
33779 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33780 s.select_display_ranges([
33781 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12)
33782 ]);
33783 });
33784 });
33785 editor.update(cx, |editor, cx| {
33786 assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo";"#}, cx);
33787 });
33788 editor.update_in(cx, |editor, window, cx| {
33789 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
33790 });
33791 editor.update(cx, |editor, cx| {
33792 assert_text_with_selections(editor, indoc! {r#"let x = "«ˇhel»lo";"#}, cx);
33793 });
33794
33795 // Test Group 2.2b: From Edge of String Content Node To String Literal Boundary
33796 editor.update_in(cx, |editor, window, cx| {
33797 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33798 s.select_display_ranges([
33799 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
33800 ]);
33801 });
33802 });
33803 editor.update(cx, |editor, cx| {
33804 assert_text_with_selections(editor, indoc! {r#"let x = "ˇhello";"#}, cx);
33805 });
33806 editor.update_in(cx, |editor, window, cx| {
33807 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
33808 });
33809 editor.update(cx, |editor, cx| {
33810 assert_text_with_selections(editor, indoc! {r#"let x = «ˇ"»hello";"#}, cx);
33811 });
33812
33813 // Test Group 3.1: Create Selection from Cursor (Select to End)
33814 let text = r#"let x = "hello world";"#.unindent();
33815
33816 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33817 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33818 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33819
33820 editor
33821 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33822 .await;
33823
33824 editor.update_in(cx, |editor, window, cx| {
33825 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33826 s.select_display_ranges([
33827 DisplayPoint::new(DisplayRow(0), 14)..DisplayPoint::new(DisplayRow(0), 14)
33828 ]);
33829 });
33830 });
33831 editor.update(cx, |editor, cx| {
33832 assert_text_with_selections(editor, indoc! {r#"let x = "helloˇ world";"#}, cx);
33833 });
33834 editor.update_in(cx, |editor, window, cx| {
33835 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33836 });
33837 editor.update(cx, |editor, cx| {
33838 assert_text_with_selections(editor, indoc! {r#"let x = "hello« worldˇ»";"#}, cx);
33839 });
33840
33841 // Test Group 3.2: Extend Existing Selection (Select to End)
33842 editor.update_in(cx, |editor, window, cx| {
33843 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33844 s.select_display_ranges([
33845 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 17)
33846 ]);
33847 });
33848 });
33849 editor.update(cx, |editor, cx| {
33850 assert_text_with_selections(editor, indoc! {r#"let x = "he«llo woˇ»rld";"#}, cx);
33851 });
33852 editor.update_in(cx, |editor, window, cx| {
33853 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33854 });
33855 editor.update(cx, |editor, cx| {
33856 assert_text_with_selections(editor, indoc! {r#"let x = "he«llo worldˇ»";"#}, cx);
33857 });
33858
33859 // Test Group 4.1: Multiple Cursors - All Expand to Different Syntax Nodes
33860 let text = r#"let x = "hello"; let y = 42;"#.unindent();
33861
33862 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33863 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33864 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33865
33866 editor
33867 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33868 .await;
33869
33870 editor.update_in(cx, |editor, window, cx| {
33871 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33872 s.select_display_ranges([
33873 // Cursor inside string content
33874 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12),
33875 // Cursor at let statement semicolon
33876 DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18),
33877 // Cursor inside integer literal
33878 DisplayPoint::new(DisplayRow(0), 26)..DisplayPoint::new(DisplayRow(0), 26),
33879 ]);
33880 });
33881 });
33882 editor.update(cx, |editor, cx| {
33883 assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo"; lˇet y = 4ˇ2;"#}, cx);
33884 });
33885 editor.update_in(cx, |editor, window, cx| {
33886 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33887 });
33888 editor.update(cx, |editor, cx| {
33889 assert_text_with_selections(editor, indoc! {r#"let x = "hel«loˇ»"; l«et y = 42;ˇ»"#}, cx);
33890 });
33891
33892 // Test Group 4.2: Multiple Cursors on Separate Lines
33893 let text = r#"
33894let x = "hello";
33895let y = 42;
33896"#
33897 .unindent();
33898
33899 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33900 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33901 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33902
33903 editor
33904 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33905 .await;
33906
33907 editor.update_in(cx, |editor, window, cx| {
33908 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33909 s.select_display_ranges([
33910 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12),
33911 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9),
33912 ]);
33913 });
33914 });
33915
33916 editor.update(cx, |editor, cx| {
33917 assert_text_with_selections(
33918 editor,
33919 indoc! {r#"
33920 let x = "helˇlo";
33921 let y = 4ˇ2;
33922 "#},
33923 cx,
33924 );
33925 });
33926 editor.update_in(cx, |editor, window, cx| {
33927 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33928 });
33929 editor.update(cx, |editor, cx| {
33930 assert_text_with_selections(
33931 editor,
33932 indoc! {r#"
33933 let x = "hel«loˇ»";
33934 let y = 4«2ˇ»;
33935 "#},
33936 cx,
33937 );
33938 });
33939
33940 // Test Group 5.1: Nested Function Calls
33941 let text = r#"let result = foo(bar("arg"));"#.unindent();
33942
33943 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33944 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33945 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33946
33947 editor
33948 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33949 .await;
33950
33951 editor.update_in(cx, |editor, window, cx| {
33952 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33953 s.select_display_ranges([
33954 DisplayPoint::new(DisplayRow(0), 22)..DisplayPoint::new(DisplayRow(0), 22)
33955 ]);
33956 });
33957 });
33958 editor.update(cx, |editor, cx| {
33959 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("ˇarg"));"#}, cx);
33960 });
33961 editor.update_in(cx, |editor, window, cx| {
33962 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33963 });
33964 editor.update(cx, |editor, cx| {
33965 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«argˇ»"));"#}, cx);
33966 });
33967 editor.update_in(cx, |editor, window, cx| {
33968 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33969 });
33970 editor.update(cx, |editor, cx| {
33971 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg"ˇ»));"#}, cx);
33972 });
33973 editor.update_in(cx, |editor, window, cx| {
33974 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33975 });
33976 editor.update(cx, |editor, cx| {
33977 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg")ˇ»);"#}, cx);
33978 });
33979
33980 // Test Group 6.1: Block Comments
33981 let text = r#"let x = /* multi
33982 line
33983 comment */;"#
33984 .unindent();
33985
33986 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33987 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33988 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33989
33990 editor
33991 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33992 .await;
33993
33994 editor.update_in(cx, |editor, window, cx| {
33995 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33996 s.select_display_ranges([
33997 DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16)
33998 ]);
33999 });
34000 });
34001 editor.update(cx, |editor, cx| {
34002 assert_text_with_selections(
34003 editor,
34004 indoc! {r#"
34005let x = /* multiˇ
34006line
34007comment */;"#},
34008 cx,
34009 );
34010 });
34011 editor.update_in(cx, |editor, window, cx| {
34012 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
34013 });
34014 editor.update(cx, |editor, cx| {
34015 assert_text_with_selections(
34016 editor,
34017 indoc! {r#"
34018let x = /* multi«
34019line
34020comment */ˇ»;"#},
34021 cx,
34022 );
34023 });
34024
34025 // Test Group 6.2: Array/Vector Literals
34026 let text = r#"let arr = [1, 2, 3];"#.unindent();
34027
34028 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
34029 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34030 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
34031
34032 editor
34033 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
34034 .await;
34035
34036 editor.update_in(cx, |editor, window, cx| {
34037 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
34038 s.select_display_ranges([
34039 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
34040 ]);
34041 });
34042 });
34043 editor.update(cx, |editor, cx| {
34044 assert_text_with_selections(editor, indoc! {r#"let arr = [ˇ1, 2, 3];"#}, cx);
34045 });
34046 editor.update_in(cx, |editor, window, cx| {
34047 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
34048 });
34049 editor.update(cx, |editor, cx| {
34050 assert_text_with_selections(editor, indoc! {r#"let arr = [«1ˇ», 2, 3];"#}, cx);
34051 });
34052 editor.update_in(cx, |editor, window, cx| {
34053 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
34054 });
34055 editor.update(cx, |editor, cx| {
34056 assert_text_with_selections(editor, indoc! {r#"let arr = [«1, 2, 3]ˇ»;"#}, cx);
34057 });
34058}
34059
34060#[gpui::test]
34061async fn test_restore_and_next(cx: &mut TestAppContext) {
34062 init_test(cx, |_| {});
34063 let mut cx = EditorTestContext::new(cx).await;
34064
34065 let diff_base = r#"
34066 one
34067 two
34068 three
34069 four
34070 five
34071 "#
34072 .unindent();
34073
34074 cx.set_state(
34075 &r#"
34076 ONE
34077 two
34078 ˇTHREE
34079 four
34080 FIVE
34081 "#
34082 .unindent(),
34083 );
34084 cx.set_head_text(&diff_base);
34085
34086 cx.update_editor(|editor, window, cx| {
34087 editor.set_expand_all_diff_hunks(cx);
34088 editor.restore_and_next(&Default::default(), window, cx);
34089 });
34090 cx.run_until_parked();
34091
34092 cx.assert_state_with_diff(
34093 r#"
34094 - one
34095 + ONE
34096 two
34097 three
34098 four
34099 - ˇfive
34100 + FIVE
34101 "#
34102 .unindent(),
34103 );
34104
34105 cx.update_editor(|editor, window, cx| {
34106 editor.restore_and_next(&Default::default(), window, cx);
34107 });
34108 cx.run_until_parked();
34109
34110 cx.assert_state_with_diff(
34111 r#"
34112 - one
34113 + ONE
34114 two
34115 three
34116 four
34117 ˇfive
34118 "#
34119 .unindent(),
34120 );
34121}
34122
34123#[gpui::test]
34124async fn test_align_selections(cx: &mut TestAppContext) {
34125 init_test(cx, |_| {});
34126 let mut cx = EditorTestContext::new(cx).await;
34127
34128 // 1) one cursor, no action
34129 let before = " abc\n abc\nabc\n ˇabc";
34130 cx.set_state(before);
34131 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
34132 cx.assert_editor_state(before);
34133
34134 // 2) multiple cursors at different rows
34135 let before = indoc!(
34136 r#"
34137 let aˇbc = 123;
34138 let xˇyz = 456;
34139 let fˇoo = 789;
34140 let bˇar = 0;
34141 "#
34142 );
34143 let after = indoc!(
34144 r#"
34145 let a ˇbc = 123;
34146 let x ˇyz = 456;
34147 let f ˇoo = 789;
34148 let bˇar = 0;
34149 "#
34150 );
34151 cx.set_state(before);
34152 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
34153 cx.assert_editor_state(after);
34154
34155 // 3) multiple selections at different rows
34156 let before = indoc!(
34157 r#"
34158 let «ˇabc» = 123;
34159 let «ˇxyz» = 456;
34160 let «ˇfoo» = 789;
34161 let «ˇbar» = 0;
34162 "#
34163 );
34164 let after = indoc!(
34165 r#"
34166 let «ˇabc» = 123;
34167 let «ˇxyz» = 456;
34168 let «ˇfoo» = 789;
34169 let «ˇbar» = 0;
34170 "#
34171 );
34172 cx.set_state(before);
34173 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
34174 cx.assert_editor_state(after);
34175
34176 // 4) multiple selections at different rows, inverted head
34177 let before = indoc!(
34178 r#"
34179 let «abcˇ» = 123;
34180 // comment
34181 let «xyzˇ» = 456;
34182 let «fooˇ» = 789;
34183 let «barˇ» = 0;
34184 "#
34185 );
34186 let after = indoc!(
34187 r#"
34188 let «abcˇ» = 123;
34189 // comment
34190 let «xyzˇ» = 456;
34191 let «fooˇ» = 789;
34192 let «barˇ» = 0;
34193 "#
34194 );
34195 cx.set_state(before);
34196 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
34197 cx.assert_editor_state(after);
34198}
34199
34200#[gpui::test]
34201async fn test_align_selections_multicolumn(cx: &mut TestAppContext) {
34202 init_test(cx, |_| {});
34203 let mut cx = EditorTestContext::new(cx).await;
34204
34205 // 1) Multicolumn, one non affected editor row
34206 let before = indoc!(
34207 r#"
34208 name «|ˇ» age «|ˇ» height «|ˇ» note
34209 Matthew «|ˇ» 7 «|ˇ» 2333 «|ˇ» smart
34210 Mike «|ˇ» 1234 «|ˇ» 567 «|ˇ» lazy
34211 Anything that is not selected
34212 Miles «|ˇ» 88 «|ˇ» 99 «|ˇ» funny
34213 "#
34214 );
34215 let after = indoc!(
34216 r#"
34217 name «|ˇ» age «|ˇ» height «|ˇ» note
34218 Matthew «|ˇ» 7 «|ˇ» 2333 «|ˇ» smart
34219 Mike «|ˇ» 1234 «|ˇ» 567 «|ˇ» lazy
34220 Anything that is not selected
34221 Miles «|ˇ» 88 «|ˇ» 99 «|ˇ» funny
34222 "#
34223 );
34224 cx.set_state(before);
34225 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
34226 cx.assert_editor_state(after);
34227
34228 // 2) not all alignment rows has the number of alignment columns
34229 let before = indoc!(
34230 r#"
34231 name «|ˇ» age «|ˇ» height
34232 Matthew «|ˇ» 7 «|ˇ» 2333
34233 Mike «|ˇ» 1234
34234 Miles «|ˇ» 88 «|ˇ» 99
34235 "#
34236 );
34237 let after = indoc!(
34238 r#"
34239 name «|ˇ» age «|ˇ» height
34240 Matthew «|ˇ» 7 «|ˇ» 2333
34241 Mike «|ˇ» 1234
34242 Miles «|ˇ» 88 «|ˇ» 99
34243 "#
34244 );
34245 cx.set_state(before);
34246 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
34247 cx.assert_editor_state(after);
34248
34249 // 3) A aligned column shall stay aligned
34250 let before = indoc!(
34251 r#"
34252 $ ˇa ˇa
34253 $ ˇa ˇa
34254 $ ˇa ˇa
34255 $ ˇa ˇa
34256 "#
34257 );
34258 let after = indoc!(
34259 r#"
34260 $ ˇa ˇa
34261 $ ˇa ˇa
34262 $ ˇa ˇa
34263 $ ˇa ˇa
34264 "#
34265 );
34266 cx.set_state(before);
34267 cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
34268 cx.assert_editor_state(after);
34269}