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
10209#[gpui::test]
10210async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
10211 init_test(cx, |_| {});
10212
10213 let language = Arc::new(Language::new(
10214 LanguageConfig::default(),
10215 Some(tree_sitter_rust::LANGUAGE.into()),
10216 ));
10217
10218 let text = "let a = 2;";
10219
10220 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10221 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10222 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10223
10224 editor
10225 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10226 .await;
10227
10228 // Test case 1: Cursor at end of word
10229 editor.update_in(cx, |editor, window, cx| {
10230 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10231 s.select_display_ranges([
10232 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
10233 ]);
10234 });
10235 });
10236 editor.update(cx, |editor, cx| {
10237 assert_text_with_selections(editor, "let aˇ = 2;", cx);
10238 });
10239 editor.update_in(cx, |editor, window, cx| {
10240 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10241 });
10242 editor.update(cx, |editor, cx| {
10243 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
10244 });
10245 editor.update_in(cx, |editor, window, cx| {
10246 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10247 });
10248 editor.update(cx, |editor, cx| {
10249 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
10250 });
10251
10252 // Test case 2: Cursor at end of statement
10253 editor.update_in(cx, |editor, window, cx| {
10254 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10255 s.select_display_ranges([
10256 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
10257 ]);
10258 });
10259 });
10260 editor.update(cx, |editor, cx| {
10261 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
10262 });
10263 editor.update_in(cx, |editor, window, cx| {
10264 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10265 });
10266 editor.update(cx, |editor, cx| {
10267 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
10268 });
10269}
10270
10271#[gpui::test]
10272async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
10273 init_test(cx, |_| {});
10274
10275 let language = Arc::new(Language::new(
10276 LanguageConfig {
10277 name: "JavaScript".into(),
10278 ..Default::default()
10279 },
10280 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10281 ));
10282
10283 let text = r#"
10284 let a = {
10285 key: "value",
10286 };
10287 "#
10288 .unindent();
10289
10290 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10291 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10292 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10293
10294 editor
10295 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10296 .await;
10297
10298 // Test case 1: Cursor after '{'
10299 editor.update_in(cx, |editor, window, cx| {
10300 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10301 s.select_display_ranges([
10302 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
10303 ]);
10304 });
10305 });
10306 editor.update(cx, |editor, cx| {
10307 assert_text_with_selections(
10308 editor,
10309 indoc! {r#"
10310 let a = {ˇ
10311 key: "value",
10312 };
10313 "#},
10314 cx,
10315 );
10316 });
10317 editor.update_in(cx, |editor, window, cx| {
10318 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10319 });
10320 editor.update(cx, |editor, cx| {
10321 assert_text_with_selections(
10322 editor,
10323 indoc! {r#"
10324 let a = «ˇ{
10325 key: "value",
10326 }»;
10327 "#},
10328 cx,
10329 );
10330 });
10331
10332 // Test case 2: Cursor after ':'
10333 editor.update_in(cx, |editor, window, cx| {
10334 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10335 s.select_display_ranges([
10336 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
10337 ]);
10338 });
10339 });
10340 editor.update(cx, |editor, cx| {
10341 assert_text_with_selections(
10342 editor,
10343 indoc! {r#"
10344 let a = {
10345 key:ˇ "value",
10346 };
10347 "#},
10348 cx,
10349 );
10350 });
10351 editor.update_in(cx, |editor, window, cx| {
10352 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10353 });
10354 editor.update(cx, |editor, cx| {
10355 assert_text_with_selections(
10356 editor,
10357 indoc! {r#"
10358 let a = {
10359 «ˇkey: "value"»,
10360 };
10361 "#},
10362 cx,
10363 );
10364 });
10365 editor.update_in(cx, |editor, window, cx| {
10366 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10367 });
10368 editor.update(cx, |editor, cx| {
10369 assert_text_with_selections(
10370 editor,
10371 indoc! {r#"
10372 let a = «ˇ{
10373 key: "value",
10374 }»;
10375 "#},
10376 cx,
10377 );
10378 });
10379
10380 // Test case 3: Cursor after ','
10381 editor.update_in(cx, |editor, window, cx| {
10382 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10383 s.select_display_ranges([
10384 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
10385 ]);
10386 });
10387 });
10388 editor.update(cx, |editor, cx| {
10389 assert_text_with_selections(
10390 editor,
10391 indoc! {r#"
10392 let a = {
10393 key: "value",ˇ
10394 };
10395 "#},
10396 cx,
10397 );
10398 });
10399 editor.update_in(cx, |editor, window, cx| {
10400 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10401 });
10402 editor.update(cx, |editor, cx| {
10403 assert_text_with_selections(
10404 editor,
10405 indoc! {r#"
10406 let a = «ˇ{
10407 key: "value",
10408 }»;
10409 "#},
10410 cx,
10411 );
10412 });
10413
10414 // Test case 4: Cursor after ';'
10415 editor.update_in(cx, |editor, window, cx| {
10416 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10417 s.select_display_ranges([
10418 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
10419 ]);
10420 });
10421 });
10422 editor.update(cx, |editor, cx| {
10423 assert_text_with_selections(
10424 editor,
10425 indoc! {r#"
10426 let a = {
10427 key: "value",
10428 };ˇ
10429 "#},
10430 cx,
10431 );
10432 });
10433 editor.update_in(cx, |editor, window, cx| {
10434 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10435 });
10436 editor.update(cx, |editor, cx| {
10437 assert_text_with_selections(
10438 editor,
10439 indoc! {r#"
10440 «ˇlet a = {
10441 key: "value",
10442 };
10443 »"#},
10444 cx,
10445 );
10446 });
10447}
10448
10449#[gpui::test]
10450async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
10451 init_test(cx, |_| {});
10452
10453 let language = Arc::new(Language::new(
10454 LanguageConfig::default(),
10455 Some(tree_sitter_rust::LANGUAGE.into()),
10456 ));
10457
10458 let text = r#"
10459 use mod1::mod2::{mod3, mod4};
10460
10461 fn fn_1(param1: bool, param2: &str) {
10462 let var1 = "hello world";
10463 }
10464 "#
10465 .unindent();
10466
10467 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10468 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10469 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10470
10471 editor
10472 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10473 .await;
10474
10475 // Test 1: Cursor on a letter of a string word
10476 editor.update_in(cx, |editor, window, cx| {
10477 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10478 s.select_display_ranges([
10479 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
10480 ]);
10481 });
10482 });
10483 editor.update_in(cx, |editor, window, cx| {
10484 assert_text_with_selections(
10485 editor,
10486 indoc! {r#"
10487 use mod1::mod2::{mod3, mod4};
10488
10489 fn fn_1(param1: bool, param2: &str) {
10490 let var1 = "hˇello world";
10491 }
10492 "#},
10493 cx,
10494 );
10495 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10496 assert_text_with_selections(
10497 editor,
10498 indoc! {r#"
10499 use mod1::mod2::{mod3, mod4};
10500
10501 fn fn_1(param1: bool, param2: &str) {
10502 let var1 = "«ˇhello» world";
10503 }
10504 "#},
10505 cx,
10506 );
10507 });
10508
10509 // Test 2: Partial selection within a word
10510 editor.update_in(cx, |editor, window, cx| {
10511 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10512 s.select_display_ranges([
10513 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
10514 ]);
10515 });
10516 });
10517 editor.update_in(cx, |editor, window, cx| {
10518 assert_text_with_selections(
10519 editor,
10520 indoc! {r#"
10521 use mod1::mod2::{mod3, mod4};
10522
10523 fn fn_1(param1: bool, param2: &str) {
10524 let var1 = "h«elˇ»lo world";
10525 }
10526 "#},
10527 cx,
10528 );
10529 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10530 assert_text_with_selections(
10531 editor,
10532 indoc! {r#"
10533 use mod1::mod2::{mod3, mod4};
10534
10535 fn fn_1(param1: bool, param2: &str) {
10536 let var1 = "«ˇhello» world";
10537 }
10538 "#},
10539 cx,
10540 );
10541 });
10542
10543 // Test 3: Complete word already selected
10544 editor.update_in(cx, |editor, window, cx| {
10545 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10546 s.select_display_ranges([
10547 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
10548 ]);
10549 });
10550 });
10551 editor.update_in(cx, |editor, window, cx| {
10552 assert_text_with_selections(
10553 editor,
10554 indoc! {r#"
10555 use mod1::mod2::{mod3, mod4};
10556
10557 fn fn_1(param1: bool, param2: &str) {
10558 let var1 = "«helloˇ» world";
10559 }
10560 "#},
10561 cx,
10562 );
10563 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10564 assert_text_with_selections(
10565 editor,
10566 indoc! {r#"
10567 use mod1::mod2::{mod3, mod4};
10568
10569 fn fn_1(param1: bool, param2: &str) {
10570 let var1 = "«hello worldˇ»";
10571 }
10572 "#},
10573 cx,
10574 );
10575 });
10576
10577 // Test 4: Selection spanning across words
10578 editor.update_in(cx, |editor, window, cx| {
10579 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10580 s.select_display_ranges([
10581 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
10582 ]);
10583 });
10584 });
10585 editor.update_in(cx, |editor, window, cx| {
10586 assert_text_with_selections(
10587 editor,
10588 indoc! {r#"
10589 use mod1::mod2::{mod3, mod4};
10590
10591 fn fn_1(param1: bool, param2: &str) {
10592 let var1 = "hel«lo woˇ»rld";
10593 }
10594 "#},
10595 cx,
10596 );
10597 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10598 assert_text_with_selections(
10599 editor,
10600 indoc! {r#"
10601 use mod1::mod2::{mod3, mod4};
10602
10603 fn fn_1(param1: bool, param2: &str) {
10604 let var1 = "«ˇhello world»";
10605 }
10606 "#},
10607 cx,
10608 );
10609 });
10610
10611 // Test 5: Expansion beyond string
10612 editor.update_in(cx, |editor, window, cx| {
10613 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10614 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10615 assert_text_with_selections(
10616 editor,
10617 indoc! {r#"
10618 use mod1::mod2::{mod3, mod4};
10619
10620 fn fn_1(param1: bool, param2: &str) {
10621 «ˇlet var1 = "hello world";»
10622 }
10623 "#},
10624 cx,
10625 );
10626 });
10627}
10628
10629#[gpui::test]
10630async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
10631 init_test(cx, |_| {});
10632
10633 let mut cx = EditorTestContext::new(cx).await;
10634
10635 let language = Arc::new(Language::new(
10636 LanguageConfig::default(),
10637 Some(tree_sitter_rust::LANGUAGE.into()),
10638 ));
10639
10640 cx.update_buffer(|buffer, cx| {
10641 buffer.set_language(Some(language), cx);
10642 });
10643
10644 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
10645 cx.update_editor(|editor, window, cx| {
10646 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
10647 });
10648
10649 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
10650
10651 cx.set_state(indoc! { r#"fn a() {
10652 // what
10653 // a
10654 // ˇlong
10655 // method
10656 // I
10657 // sure
10658 // hope
10659 // it
10660 // works
10661 }"# });
10662
10663 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
10664 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
10665 cx.update(|_, cx| {
10666 multi_buffer.update(cx, |multi_buffer, cx| {
10667 multi_buffer.set_excerpts_for_path(
10668 PathKey::for_buffer(&buffer, cx),
10669 buffer,
10670 [Point::new(1, 0)..Point::new(1, 0)],
10671 3,
10672 cx,
10673 );
10674 });
10675 });
10676
10677 let editor2 = cx.new_window_entity(|window, cx| {
10678 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
10679 });
10680
10681 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
10682 cx.update_editor(|editor, window, cx| {
10683 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
10684 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
10685 })
10686 });
10687
10688 cx.assert_editor_state(indoc! { "
10689 fn a() {
10690 // what
10691 // a
10692 ˇ // long
10693 // method"});
10694
10695 cx.update_editor(|editor, window, cx| {
10696 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
10697 });
10698
10699 // Although we could potentially make the action work when the syntax node
10700 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
10701 // did. Maybe we could also expand the excerpt to contain the range?
10702 cx.assert_editor_state(indoc! { "
10703 fn a() {
10704 // what
10705 // a
10706 ˇ // long
10707 // method"});
10708}
10709
10710#[gpui::test]
10711async fn test_fold_function_bodies(cx: &mut TestAppContext) {
10712 init_test(cx, |_| {});
10713
10714 let base_text = r#"
10715 impl A {
10716 // this is an uncommitted comment
10717
10718 fn b() {
10719 c();
10720 }
10721
10722 // this is another uncommitted comment
10723
10724 fn d() {
10725 // e
10726 // f
10727 }
10728 }
10729
10730 fn g() {
10731 // h
10732 }
10733 "#
10734 .unindent();
10735
10736 let text = r#"
10737 ˇimpl A {
10738
10739 fn b() {
10740 c();
10741 }
10742
10743 fn d() {
10744 // e
10745 // f
10746 }
10747 }
10748
10749 fn g() {
10750 // h
10751 }
10752 "#
10753 .unindent();
10754
10755 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10756 cx.set_state(&text);
10757 cx.set_head_text(&base_text);
10758 cx.update_editor(|editor, window, cx| {
10759 editor.expand_all_diff_hunks(&Default::default(), window, cx);
10760 });
10761
10762 cx.assert_state_with_diff(
10763 "
10764 ˇimpl A {
10765 - // this is an uncommitted comment
10766
10767 fn b() {
10768 c();
10769 }
10770
10771 - // this is another uncommitted comment
10772 -
10773 fn d() {
10774 // e
10775 // f
10776 }
10777 }
10778
10779 fn g() {
10780 // h
10781 }
10782 "
10783 .unindent(),
10784 );
10785
10786 let expected_display_text = "
10787 impl A {
10788 // this is an uncommitted comment
10789
10790 fn b() {
10791 ⋯
10792 }
10793
10794 // this is another uncommitted comment
10795
10796 fn d() {
10797 ⋯
10798 }
10799 }
10800
10801 fn g() {
10802 ⋯
10803 }
10804 "
10805 .unindent();
10806
10807 cx.update_editor(|editor, window, cx| {
10808 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
10809 assert_eq!(editor.display_text(cx), expected_display_text);
10810 });
10811}
10812
10813#[gpui::test]
10814async fn test_autoindent(cx: &mut TestAppContext) {
10815 init_test(cx, |_| {});
10816
10817 let language = Arc::new(
10818 Language::new(
10819 LanguageConfig {
10820 brackets: BracketPairConfig {
10821 pairs: vec![
10822 BracketPair {
10823 start: "{".to_string(),
10824 end: "}".to_string(),
10825 close: false,
10826 surround: false,
10827 newline: true,
10828 },
10829 BracketPair {
10830 start: "(".to_string(),
10831 end: ")".to_string(),
10832 close: false,
10833 surround: false,
10834 newline: true,
10835 },
10836 ],
10837 ..Default::default()
10838 },
10839 ..Default::default()
10840 },
10841 Some(tree_sitter_rust::LANGUAGE.into()),
10842 )
10843 .with_indents_query(
10844 r#"
10845 (_ "(" ")" @end) @indent
10846 (_ "{" "}" @end) @indent
10847 "#,
10848 )
10849 .unwrap(),
10850 );
10851
10852 let text = "fn a() {}";
10853
10854 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10855 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10856 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10857 editor
10858 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10859 .await;
10860
10861 editor.update_in(cx, |editor, window, cx| {
10862 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10863 s.select_ranges([
10864 MultiBufferOffset(5)..MultiBufferOffset(5),
10865 MultiBufferOffset(8)..MultiBufferOffset(8),
10866 MultiBufferOffset(9)..MultiBufferOffset(9),
10867 ])
10868 });
10869 editor.newline(&Newline, window, cx);
10870 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
10871 assert_eq!(
10872 editor.selections.ranges(&editor.display_snapshot(cx)),
10873 &[
10874 Point::new(1, 4)..Point::new(1, 4),
10875 Point::new(3, 4)..Point::new(3, 4),
10876 Point::new(5, 0)..Point::new(5, 0)
10877 ]
10878 );
10879 });
10880}
10881
10882#[gpui::test]
10883async fn test_autoindent_disabled(cx: &mut TestAppContext) {
10884 init_test(cx, |settings| {
10885 settings.defaults.auto_indent = Some(settings::AutoIndentMode::None)
10886 });
10887
10888 let language = Arc::new(
10889 Language::new(
10890 LanguageConfig {
10891 brackets: BracketPairConfig {
10892 pairs: vec![
10893 BracketPair {
10894 start: "{".to_string(),
10895 end: "}".to_string(),
10896 close: false,
10897 surround: false,
10898 newline: true,
10899 },
10900 BracketPair {
10901 start: "(".to_string(),
10902 end: ")".to_string(),
10903 close: false,
10904 surround: false,
10905 newline: true,
10906 },
10907 ],
10908 ..Default::default()
10909 },
10910 ..Default::default()
10911 },
10912 Some(tree_sitter_rust::LANGUAGE.into()),
10913 )
10914 .with_indents_query(
10915 r#"
10916 (_ "(" ")" @end) @indent
10917 (_ "{" "}" @end) @indent
10918 "#,
10919 )
10920 .unwrap(),
10921 );
10922
10923 let text = "fn a() {}";
10924
10925 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10926 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10927 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10928 editor
10929 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10930 .await;
10931
10932 editor.update_in(cx, |editor, window, cx| {
10933 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10934 s.select_ranges([
10935 MultiBufferOffset(5)..MultiBufferOffset(5),
10936 MultiBufferOffset(8)..MultiBufferOffset(8),
10937 MultiBufferOffset(9)..MultiBufferOffset(9),
10938 ])
10939 });
10940 editor.newline(&Newline, window, cx);
10941 assert_eq!(
10942 editor.text(cx),
10943 indoc!(
10944 "
10945 fn a(
10946
10947 ) {
10948
10949 }
10950 "
10951 )
10952 );
10953 assert_eq!(
10954 editor.selections.ranges(&editor.display_snapshot(cx)),
10955 &[
10956 Point::new(1, 0)..Point::new(1, 0),
10957 Point::new(3, 0)..Point::new(3, 0),
10958 Point::new(5, 0)..Point::new(5, 0)
10959 ]
10960 );
10961 });
10962}
10963
10964#[gpui::test]
10965async fn test_autoindent_none_does_not_preserve_indentation_on_newline(cx: &mut TestAppContext) {
10966 init_test(cx, |settings| {
10967 settings.defaults.auto_indent = Some(settings::AutoIndentMode::None)
10968 });
10969
10970 let mut cx = EditorTestContext::new(cx).await;
10971
10972 cx.set_state(indoc! {"
10973 hello
10974 indented lineˇ
10975 world
10976 "});
10977
10978 cx.update_editor(|editor, window, cx| {
10979 editor.newline(&Newline, window, cx);
10980 });
10981
10982 cx.assert_editor_state(indoc! {"
10983 hello
10984 indented line
10985 ˇ
10986 world
10987 "});
10988}
10989
10990#[gpui::test]
10991async fn test_autoindent_preserve_indent_maintains_indentation_on_newline(cx: &mut TestAppContext) {
10992 // When auto_indent is "preserve_indent", pressing Enter on an indented line
10993 // should preserve the indentation but not adjust based on syntax.
10994 init_test(cx, |settings| {
10995 settings.defaults.auto_indent = Some(settings::AutoIndentMode::PreserveIndent)
10996 });
10997
10998 let mut cx = EditorTestContext::new(cx).await;
10999
11000 cx.set_state(indoc! {"
11001 hello
11002 indented lineˇ
11003 world
11004 "});
11005
11006 cx.update_editor(|editor, window, cx| {
11007 editor.newline(&Newline, window, cx);
11008 });
11009
11010 // The new line SHOULD have the same indentation as the previous line
11011 cx.assert_editor_state(indoc! {"
11012 hello
11013 indented line
11014 ˇ
11015 world
11016 "});
11017}
11018
11019#[gpui::test]
11020async fn test_autoindent_preserve_indent_does_not_apply_syntax_indent(cx: &mut TestAppContext) {
11021 init_test(cx, |settings| {
11022 settings.defaults.auto_indent = Some(settings::AutoIndentMode::PreserveIndent)
11023 });
11024
11025 let language = Arc::new(
11026 Language::new(
11027 LanguageConfig {
11028 brackets: BracketPairConfig {
11029 pairs: vec![BracketPair {
11030 start: "{".to_string(),
11031 end: "}".to_string(),
11032 close: false,
11033 surround: false,
11034 newline: false, // Disable extra newline behavior to isolate syntax indent test
11035 }],
11036 ..Default::default()
11037 },
11038 ..Default::default()
11039 },
11040 Some(tree_sitter_rust::LANGUAGE.into()),
11041 )
11042 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
11043 .unwrap(),
11044 );
11045
11046 let buffer =
11047 cx.new(|cx| Buffer::local("fn foo() {\n}", cx).with_language(language.clone(), cx));
11048 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11049 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11050 editor
11051 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11052 .await;
11053
11054 // Position cursor at end of line containing `{`
11055 editor.update_in(cx, |editor, window, cx| {
11056 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11057 s.select_ranges([MultiBufferOffset(10)..MultiBufferOffset(10)]) // After "fn foo() {"
11058 });
11059 editor.newline(&Newline, window, cx);
11060
11061 // With PreserveIndent, the new line should have 0 indentation (same as the fn line)
11062 // NOT 4 spaces (which tree-sitter would add for being inside `{}`)
11063 assert_eq!(editor.text(cx), "fn foo() {\n\n}");
11064 });
11065}
11066
11067#[gpui::test]
11068async fn test_autoindent_syntax_aware_applies_syntax_indent(cx: &mut TestAppContext) {
11069 // Companion test to show that SyntaxAware DOES apply tree-sitter indentation
11070 init_test(cx, |settings| {
11071 settings.defaults.auto_indent = Some(settings::AutoIndentMode::SyntaxAware)
11072 });
11073
11074 let language = Arc::new(
11075 Language::new(
11076 LanguageConfig {
11077 brackets: BracketPairConfig {
11078 pairs: vec![BracketPair {
11079 start: "{".to_string(),
11080 end: "}".to_string(),
11081 close: false,
11082 surround: false,
11083 newline: false, // Disable extra newline behavior to isolate syntax indent test
11084 }],
11085 ..Default::default()
11086 },
11087 ..Default::default()
11088 },
11089 Some(tree_sitter_rust::LANGUAGE.into()),
11090 )
11091 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
11092 .unwrap(),
11093 );
11094
11095 let buffer =
11096 cx.new(|cx| Buffer::local("fn foo() {\n}", cx).with_language(language.clone(), cx));
11097 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11098 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11099 editor
11100 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11101 .await;
11102
11103 // Position cursor at end of line containing `{`
11104 editor.update_in(cx, |editor, window, cx| {
11105 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11106 s.select_ranges([MultiBufferOffset(10)..MultiBufferOffset(10)]) // After "fn foo() {"
11107 });
11108 editor.newline(&Newline, window, cx);
11109
11110 // With SyntaxAware, tree-sitter adds indentation for being inside `{}`
11111 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
11112 });
11113}
11114
11115#[gpui::test]
11116async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
11117 init_test(cx, |settings| {
11118 settings.defaults.auto_indent = Some(settings::AutoIndentMode::SyntaxAware);
11119 settings.languages.0.insert(
11120 "python".into(),
11121 LanguageSettingsContent {
11122 auto_indent: Some(settings::AutoIndentMode::None),
11123 ..Default::default()
11124 },
11125 );
11126 });
11127
11128 let mut cx = EditorTestContext::new(cx).await;
11129
11130 let injected_language = Arc::new(
11131 Language::new(
11132 LanguageConfig {
11133 brackets: BracketPairConfig {
11134 pairs: vec![
11135 BracketPair {
11136 start: "{".to_string(),
11137 end: "}".to_string(),
11138 close: false,
11139 surround: false,
11140 newline: true,
11141 },
11142 BracketPair {
11143 start: "(".to_string(),
11144 end: ")".to_string(),
11145 close: true,
11146 surround: false,
11147 newline: true,
11148 },
11149 ],
11150 ..Default::default()
11151 },
11152 name: "python".into(),
11153 ..Default::default()
11154 },
11155 Some(tree_sitter_python::LANGUAGE.into()),
11156 )
11157 .with_indents_query(
11158 r#"
11159 (_ "(" ")" @end) @indent
11160 (_ "{" "}" @end) @indent
11161 "#,
11162 )
11163 .unwrap(),
11164 );
11165
11166 let language = Arc::new(
11167 Language::new(
11168 LanguageConfig {
11169 brackets: BracketPairConfig {
11170 pairs: vec![
11171 BracketPair {
11172 start: "{".to_string(),
11173 end: "}".to_string(),
11174 close: false,
11175 surround: false,
11176 newline: true,
11177 },
11178 BracketPair {
11179 start: "(".to_string(),
11180 end: ")".to_string(),
11181 close: true,
11182 surround: false,
11183 newline: true,
11184 },
11185 ],
11186 ..Default::default()
11187 },
11188 name: LanguageName::new_static("rust"),
11189 ..Default::default()
11190 },
11191 Some(tree_sitter_rust::LANGUAGE.into()),
11192 )
11193 .with_indents_query(
11194 r#"
11195 (_ "(" ")" @end) @indent
11196 (_ "{" "}" @end) @indent
11197 "#,
11198 )
11199 .unwrap()
11200 .with_injection_query(
11201 r#"
11202 (macro_invocation
11203 macro: (identifier) @_macro_name
11204 (token_tree) @injection.content
11205 (#set! injection.language "python"))
11206 "#,
11207 )
11208 .unwrap(),
11209 );
11210
11211 cx.language_registry().add(injected_language);
11212 cx.language_registry().add(language.clone());
11213
11214 cx.update_buffer(|buffer, cx| {
11215 buffer.set_language(Some(language), cx);
11216 });
11217
11218 cx.set_state(r#"struct A {ˇ}"#);
11219
11220 cx.update_editor(|editor, window, cx| {
11221 editor.newline(&Default::default(), window, cx);
11222 });
11223
11224 cx.assert_editor_state(indoc!(
11225 "struct A {
11226 ˇ
11227 }"
11228 ));
11229
11230 cx.set_state(r#"select_biased!(ˇ)"#);
11231
11232 cx.update_editor(|editor, window, cx| {
11233 editor.newline(&Default::default(), window, cx);
11234 editor.handle_input("def ", window, cx);
11235 editor.handle_input("(", window, cx);
11236 editor.newline(&Default::default(), window, cx);
11237 editor.handle_input("a", window, cx);
11238 });
11239
11240 cx.assert_editor_state(indoc!(
11241 "select_biased!(
11242 def (
11243 aˇ
11244 )
11245 )"
11246 ));
11247}
11248
11249#[gpui::test]
11250async fn test_autoindent_selections(cx: &mut TestAppContext) {
11251 init_test(cx, |_| {});
11252
11253 {
11254 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
11255 cx.set_state(indoc! {"
11256 impl A {
11257
11258 fn b() {}
11259
11260 «fn c() {
11261
11262 }ˇ»
11263 }
11264 "});
11265
11266 cx.update_editor(|editor, window, cx| {
11267 editor.autoindent(&Default::default(), window, cx);
11268 });
11269 cx.wait_for_autoindent_applied().await;
11270
11271 cx.assert_editor_state(indoc! {"
11272 impl A {
11273
11274 fn b() {}
11275
11276 «fn c() {
11277
11278 }ˇ»
11279 }
11280 "});
11281 }
11282
11283 {
11284 let mut cx = EditorTestContext::new_multibuffer(
11285 cx,
11286 [indoc! { "
11287 impl A {
11288 «
11289 // a
11290 fn b(){}
11291 »
11292 «
11293 }
11294 fn c(){}
11295 »
11296 "}],
11297 );
11298
11299 let buffer = cx.update_editor(|editor, _, cx| {
11300 let buffer = editor.buffer().update(cx, |buffer, _| {
11301 buffer.all_buffers().iter().next().unwrap().clone()
11302 });
11303 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
11304 buffer
11305 });
11306
11307 cx.run_until_parked();
11308 cx.update_editor(|editor, window, cx| {
11309 editor.select_all(&Default::default(), window, cx);
11310 editor.autoindent(&Default::default(), window, cx)
11311 });
11312 cx.run_until_parked();
11313
11314 cx.update(|_, cx| {
11315 assert_eq!(
11316 buffer.read(cx).text(),
11317 indoc! { "
11318 impl A {
11319
11320 // a
11321 fn b(){}
11322
11323
11324 }
11325 fn c(){}
11326
11327 " }
11328 )
11329 });
11330 }
11331}
11332
11333#[gpui::test]
11334async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
11335 init_test(cx, |_| {});
11336
11337 let mut cx = EditorTestContext::new(cx).await;
11338
11339 let language = Arc::new(Language::new(
11340 LanguageConfig {
11341 brackets: BracketPairConfig {
11342 pairs: vec![
11343 BracketPair {
11344 start: "{".to_string(),
11345 end: "}".to_string(),
11346 close: true,
11347 surround: true,
11348 newline: true,
11349 },
11350 BracketPair {
11351 start: "(".to_string(),
11352 end: ")".to_string(),
11353 close: true,
11354 surround: true,
11355 newline: true,
11356 },
11357 BracketPair {
11358 start: "/*".to_string(),
11359 end: " */".to_string(),
11360 close: true,
11361 surround: true,
11362 newline: true,
11363 },
11364 BracketPair {
11365 start: "[".to_string(),
11366 end: "]".to_string(),
11367 close: false,
11368 surround: false,
11369 newline: true,
11370 },
11371 BracketPair {
11372 start: "\"".to_string(),
11373 end: "\"".to_string(),
11374 close: true,
11375 surround: true,
11376 newline: false,
11377 },
11378 BracketPair {
11379 start: "<".to_string(),
11380 end: ">".to_string(),
11381 close: false,
11382 surround: true,
11383 newline: true,
11384 },
11385 ],
11386 ..Default::default()
11387 },
11388 autoclose_before: "})]".to_string(),
11389 ..Default::default()
11390 },
11391 Some(tree_sitter_rust::LANGUAGE.into()),
11392 ));
11393
11394 cx.language_registry().add(language.clone());
11395 cx.update_buffer(|buffer, cx| {
11396 buffer.set_language(Some(language), cx);
11397 });
11398
11399 cx.set_state(
11400 &r#"
11401 🏀ˇ
11402 εˇ
11403 ❤️ˇ
11404 "#
11405 .unindent(),
11406 );
11407
11408 // autoclose multiple nested brackets at multiple cursors
11409 cx.update_editor(|editor, window, cx| {
11410 editor.handle_input("{", window, cx);
11411 editor.handle_input("{", window, cx);
11412 editor.handle_input("{", window, cx);
11413 });
11414 cx.assert_editor_state(
11415 &"
11416 🏀{{{ˇ}}}
11417 ε{{{ˇ}}}
11418 ❤️{{{ˇ}}}
11419 "
11420 .unindent(),
11421 );
11422
11423 // insert a different closing bracket
11424 cx.update_editor(|editor, window, cx| {
11425 editor.handle_input(")", window, cx);
11426 });
11427 cx.assert_editor_state(
11428 &"
11429 🏀{{{)ˇ}}}
11430 ε{{{)ˇ}}}
11431 ❤️{{{)ˇ}}}
11432 "
11433 .unindent(),
11434 );
11435
11436 // skip over the auto-closed brackets when typing a closing bracket
11437 cx.update_editor(|editor, window, cx| {
11438 editor.move_right(&MoveRight, window, cx);
11439 editor.handle_input("}", window, cx);
11440 editor.handle_input("}", window, cx);
11441 editor.handle_input("}", window, cx);
11442 });
11443 cx.assert_editor_state(
11444 &"
11445 🏀{{{)}}}}ˇ
11446 ε{{{)}}}}ˇ
11447 ❤️{{{)}}}}ˇ
11448 "
11449 .unindent(),
11450 );
11451
11452 // autoclose multi-character pairs
11453 cx.set_state(
11454 &"
11455 ˇ
11456 ˇ
11457 "
11458 .unindent(),
11459 );
11460 cx.update_editor(|editor, window, cx| {
11461 editor.handle_input("/", window, cx);
11462 editor.handle_input("*", window, cx);
11463 });
11464 cx.assert_editor_state(
11465 &"
11466 /*ˇ */
11467 /*ˇ */
11468 "
11469 .unindent(),
11470 );
11471
11472 // one cursor autocloses a multi-character pair, one cursor
11473 // does not autoclose.
11474 cx.set_state(
11475 &"
11476 /ˇ
11477 ˇ
11478 "
11479 .unindent(),
11480 );
11481 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
11482 cx.assert_editor_state(
11483 &"
11484 /*ˇ */
11485 *ˇ
11486 "
11487 .unindent(),
11488 );
11489
11490 // Don't autoclose if the next character isn't whitespace and isn't
11491 // listed in the language's "autoclose_before" section.
11492 cx.set_state("ˇa b");
11493 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
11494 cx.assert_editor_state("{ˇa b");
11495
11496 // Don't autoclose if `close` is false for the bracket pair
11497 cx.set_state("ˇ");
11498 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
11499 cx.assert_editor_state("[ˇ");
11500
11501 // Surround with brackets if text is selected
11502 cx.set_state("«aˇ» b");
11503 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
11504 cx.assert_editor_state("{«aˇ»} b");
11505
11506 // Autoclose when not immediately after a word character
11507 cx.set_state("a ˇ");
11508 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
11509 cx.assert_editor_state("a \"ˇ\"");
11510
11511 // Autoclose pair where the start and end characters are the same
11512 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
11513 cx.assert_editor_state("a \"\"ˇ");
11514
11515 // Don't autoclose when immediately after a word character
11516 cx.set_state("aˇ");
11517 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
11518 cx.assert_editor_state("a\"ˇ");
11519
11520 // Do autoclose when after a non-word character
11521 cx.set_state("{ˇ");
11522 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
11523 cx.assert_editor_state("{\"ˇ\"");
11524
11525 // Non identical pairs autoclose regardless of preceding character
11526 cx.set_state("aˇ");
11527 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
11528 cx.assert_editor_state("a{ˇ}");
11529
11530 // Don't autoclose pair if autoclose is disabled
11531 cx.set_state("ˇ");
11532 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
11533 cx.assert_editor_state("<ˇ");
11534
11535 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
11536 cx.set_state("«aˇ» b");
11537 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
11538 cx.assert_editor_state("<«aˇ»> b");
11539}
11540
11541#[gpui::test]
11542async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
11543 init_test(cx, |settings| {
11544 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11545 });
11546
11547 let mut cx = EditorTestContext::new(cx).await;
11548
11549 let language = Arc::new(Language::new(
11550 LanguageConfig {
11551 brackets: BracketPairConfig {
11552 pairs: vec![
11553 BracketPair {
11554 start: "{".to_string(),
11555 end: "}".to_string(),
11556 close: true,
11557 surround: true,
11558 newline: true,
11559 },
11560 BracketPair {
11561 start: "(".to_string(),
11562 end: ")".to_string(),
11563 close: true,
11564 surround: true,
11565 newline: true,
11566 },
11567 BracketPair {
11568 start: "[".to_string(),
11569 end: "]".to_string(),
11570 close: false,
11571 surround: false,
11572 newline: true,
11573 },
11574 ],
11575 ..Default::default()
11576 },
11577 autoclose_before: "})]".to_string(),
11578 ..Default::default()
11579 },
11580 Some(tree_sitter_rust::LANGUAGE.into()),
11581 ));
11582
11583 cx.language_registry().add(language.clone());
11584 cx.update_buffer(|buffer, cx| {
11585 buffer.set_language(Some(language), cx);
11586 });
11587
11588 cx.set_state(
11589 &"
11590 ˇ
11591 ˇ
11592 ˇ
11593 "
11594 .unindent(),
11595 );
11596
11597 // ensure only matching closing brackets are skipped over
11598 cx.update_editor(|editor, window, cx| {
11599 editor.handle_input("}", window, cx);
11600 editor.move_left(&MoveLeft, window, cx);
11601 editor.handle_input(")", window, cx);
11602 editor.move_left(&MoveLeft, window, cx);
11603 });
11604 cx.assert_editor_state(
11605 &"
11606 ˇ)}
11607 ˇ)}
11608 ˇ)}
11609 "
11610 .unindent(),
11611 );
11612
11613 // skip-over closing brackets at multiple cursors
11614 cx.update_editor(|editor, window, cx| {
11615 editor.handle_input(")", window, cx);
11616 editor.handle_input("}", window, cx);
11617 });
11618 cx.assert_editor_state(
11619 &"
11620 )}ˇ
11621 )}ˇ
11622 )}ˇ
11623 "
11624 .unindent(),
11625 );
11626
11627 // ignore non-close brackets
11628 cx.update_editor(|editor, window, cx| {
11629 editor.handle_input("]", window, cx);
11630 editor.move_left(&MoveLeft, window, cx);
11631 editor.handle_input("]", window, cx);
11632 });
11633 cx.assert_editor_state(
11634 &"
11635 )}]ˇ]
11636 )}]ˇ]
11637 )}]ˇ]
11638 "
11639 .unindent(),
11640 );
11641}
11642
11643#[gpui::test]
11644async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
11645 init_test(cx, |_| {});
11646
11647 let mut cx = EditorTestContext::new(cx).await;
11648
11649 let html_language = Arc::new(
11650 Language::new(
11651 LanguageConfig {
11652 name: "HTML".into(),
11653 brackets: BracketPairConfig {
11654 pairs: vec![
11655 BracketPair {
11656 start: "<".into(),
11657 end: ">".into(),
11658 close: true,
11659 ..Default::default()
11660 },
11661 BracketPair {
11662 start: "{".into(),
11663 end: "}".into(),
11664 close: true,
11665 ..Default::default()
11666 },
11667 BracketPair {
11668 start: "(".into(),
11669 end: ")".into(),
11670 close: true,
11671 ..Default::default()
11672 },
11673 ],
11674 ..Default::default()
11675 },
11676 autoclose_before: "})]>".into(),
11677 ..Default::default()
11678 },
11679 Some(tree_sitter_html::LANGUAGE.into()),
11680 )
11681 .with_injection_query(
11682 r#"
11683 (script_element
11684 (raw_text) @injection.content
11685 (#set! injection.language "javascript"))
11686 "#,
11687 )
11688 .unwrap(),
11689 );
11690
11691 let javascript_language = Arc::new(Language::new(
11692 LanguageConfig {
11693 name: "JavaScript".into(),
11694 brackets: BracketPairConfig {
11695 pairs: vec![
11696 BracketPair {
11697 start: "/*".into(),
11698 end: " */".into(),
11699 close: true,
11700 ..Default::default()
11701 },
11702 BracketPair {
11703 start: "{".into(),
11704 end: "}".into(),
11705 close: true,
11706 ..Default::default()
11707 },
11708 BracketPair {
11709 start: "(".into(),
11710 end: ")".into(),
11711 close: true,
11712 ..Default::default()
11713 },
11714 ],
11715 ..Default::default()
11716 },
11717 autoclose_before: "})]>".into(),
11718 ..Default::default()
11719 },
11720 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11721 ));
11722
11723 cx.language_registry().add(html_language.clone());
11724 cx.language_registry().add(javascript_language);
11725 cx.executor().run_until_parked();
11726
11727 cx.update_buffer(|buffer, cx| {
11728 buffer.set_language(Some(html_language), cx);
11729 });
11730
11731 cx.set_state(
11732 &r#"
11733 <body>ˇ
11734 <script>
11735 var x = 1;ˇ
11736 </script>
11737 </body>ˇ
11738 "#
11739 .unindent(),
11740 );
11741
11742 // Precondition: different languages are active at different locations.
11743 cx.update_editor(|editor, window, cx| {
11744 let snapshot = editor.snapshot(window, cx);
11745 let cursors = editor
11746 .selections
11747 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
11748 let languages = cursors
11749 .iter()
11750 .map(|c| snapshot.language_at(c.start).unwrap().name())
11751 .collect::<Vec<_>>();
11752 assert_eq!(
11753 languages,
11754 &[
11755 LanguageName::from("HTML"),
11756 LanguageName::from("JavaScript"),
11757 LanguageName::from("HTML"),
11758 ]
11759 );
11760 });
11761
11762 // Angle brackets autoclose in HTML, but not JavaScript.
11763 cx.update_editor(|editor, window, cx| {
11764 editor.handle_input("<", window, cx);
11765 editor.handle_input("a", window, cx);
11766 });
11767 cx.assert_editor_state(
11768 &r#"
11769 <body><aˇ>
11770 <script>
11771 var x = 1;<aˇ
11772 </script>
11773 </body><aˇ>
11774 "#
11775 .unindent(),
11776 );
11777
11778 // Curly braces and parens autoclose in both HTML and JavaScript.
11779 cx.update_editor(|editor, window, cx| {
11780 editor.handle_input(" b=", window, cx);
11781 editor.handle_input("{", window, cx);
11782 editor.handle_input("c", window, cx);
11783 editor.handle_input("(", window, cx);
11784 });
11785 cx.assert_editor_state(
11786 &r#"
11787 <body><a b={c(ˇ)}>
11788 <script>
11789 var x = 1;<a b={c(ˇ)}
11790 </script>
11791 </body><a b={c(ˇ)}>
11792 "#
11793 .unindent(),
11794 );
11795
11796 // Brackets that were already autoclosed are skipped.
11797 cx.update_editor(|editor, window, cx| {
11798 editor.handle_input(")", window, cx);
11799 editor.handle_input("d", window, cx);
11800 editor.handle_input("}", window, cx);
11801 });
11802 cx.assert_editor_state(
11803 &r#"
11804 <body><a b={c()d}ˇ>
11805 <script>
11806 var x = 1;<a b={c()d}ˇ
11807 </script>
11808 </body><a b={c()d}ˇ>
11809 "#
11810 .unindent(),
11811 );
11812 cx.update_editor(|editor, window, cx| {
11813 editor.handle_input(">", window, cx);
11814 });
11815 cx.assert_editor_state(
11816 &r#"
11817 <body><a b={c()d}>ˇ
11818 <script>
11819 var x = 1;<a b={c()d}>ˇ
11820 </script>
11821 </body><a b={c()d}>ˇ
11822 "#
11823 .unindent(),
11824 );
11825
11826 // Reset
11827 cx.set_state(
11828 &r#"
11829 <body>ˇ
11830 <script>
11831 var x = 1;ˇ
11832 </script>
11833 </body>ˇ
11834 "#
11835 .unindent(),
11836 );
11837
11838 cx.update_editor(|editor, window, cx| {
11839 editor.handle_input("<", window, cx);
11840 });
11841 cx.assert_editor_state(
11842 &r#"
11843 <body><ˇ>
11844 <script>
11845 var x = 1;<ˇ
11846 </script>
11847 </body><ˇ>
11848 "#
11849 .unindent(),
11850 );
11851
11852 // When backspacing, the closing angle brackets are removed.
11853 cx.update_editor(|editor, window, cx| {
11854 editor.backspace(&Backspace, window, cx);
11855 });
11856 cx.assert_editor_state(
11857 &r#"
11858 <body>ˇ
11859 <script>
11860 var x = 1;ˇ
11861 </script>
11862 </body>ˇ
11863 "#
11864 .unindent(),
11865 );
11866
11867 // Block comments autoclose in JavaScript, but not HTML.
11868 cx.update_editor(|editor, window, cx| {
11869 editor.handle_input("/", window, cx);
11870 editor.handle_input("*", window, cx);
11871 });
11872 cx.assert_editor_state(
11873 &r#"
11874 <body>/*ˇ
11875 <script>
11876 var x = 1;/*ˇ */
11877 </script>
11878 </body>/*ˇ
11879 "#
11880 .unindent(),
11881 );
11882}
11883
11884#[gpui::test]
11885async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
11886 init_test(cx, |_| {});
11887
11888 let mut cx = EditorTestContext::new(cx).await;
11889
11890 let rust_language = Arc::new(
11891 Language::new(
11892 LanguageConfig {
11893 name: "Rust".into(),
11894 brackets: serde_json::from_value(json!([
11895 { "start": "{", "end": "}", "close": true, "newline": true },
11896 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
11897 ]))
11898 .unwrap(),
11899 autoclose_before: "})]>".into(),
11900 ..Default::default()
11901 },
11902 Some(tree_sitter_rust::LANGUAGE.into()),
11903 )
11904 .with_override_query("(string_literal) @string")
11905 .unwrap(),
11906 );
11907
11908 cx.language_registry().add(rust_language.clone());
11909 cx.update_buffer(|buffer, cx| {
11910 buffer.set_language(Some(rust_language), cx);
11911 });
11912
11913 cx.set_state(
11914 &r#"
11915 let x = ˇ
11916 "#
11917 .unindent(),
11918 );
11919
11920 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
11921 cx.update_editor(|editor, window, cx| {
11922 editor.handle_input("\"", window, cx);
11923 });
11924 cx.assert_editor_state(
11925 &r#"
11926 let x = "ˇ"
11927 "#
11928 .unindent(),
11929 );
11930
11931 // Inserting another quotation mark. The cursor moves across the existing
11932 // automatically-inserted quotation mark.
11933 cx.update_editor(|editor, window, cx| {
11934 editor.handle_input("\"", window, cx);
11935 });
11936 cx.assert_editor_state(
11937 &r#"
11938 let x = ""ˇ
11939 "#
11940 .unindent(),
11941 );
11942
11943 // Reset
11944 cx.set_state(
11945 &r#"
11946 let x = ˇ
11947 "#
11948 .unindent(),
11949 );
11950
11951 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
11952 cx.update_editor(|editor, window, cx| {
11953 editor.handle_input("\"", window, cx);
11954 editor.handle_input(" ", window, cx);
11955 editor.move_left(&Default::default(), window, cx);
11956 editor.handle_input("\\", window, cx);
11957 editor.handle_input("\"", window, cx);
11958 });
11959 cx.assert_editor_state(
11960 &r#"
11961 let x = "\"ˇ "
11962 "#
11963 .unindent(),
11964 );
11965
11966 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
11967 // mark. Nothing is inserted.
11968 cx.update_editor(|editor, window, cx| {
11969 editor.move_right(&Default::default(), window, cx);
11970 editor.handle_input("\"", window, cx);
11971 });
11972 cx.assert_editor_state(
11973 &r#"
11974 let x = "\" "ˇ
11975 "#
11976 .unindent(),
11977 );
11978}
11979
11980#[gpui::test]
11981async fn test_autoclose_quotes_with_scope_awareness(cx: &mut TestAppContext) {
11982 init_test(cx, |_| {});
11983
11984 let mut cx = EditorTestContext::new(cx).await;
11985 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
11986
11987 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11988
11989 // Double quote inside single-quoted string
11990 cx.set_state(indoc! {r#"
11991 def main():
11992 items = ['"', ˇ]
11993 "#});
11994 cx.update_editor(|editor, window, cx| {
11995 editor.handle_input("\"", window, cx);
11996 });
11997 cx.assert_editor_state(indoc! {r#"
11998 def main():
11999 items = ['"', "ˇ"]
12000 "#});
12001
12002 // Two double quotes inside single-quoted string
12003 cx.set_state(indoc! {r#"
12004 def main():
12005 items = ['""', ˇ]
12006 "#});
12007 cx.update_editor(|editor, window, cx| {
12008 editor.handle_input("\"", window, cx);
12009 });
12010 cx.assert_editor_state(indoc! {r#"
12011 def main():
12012 items = ['""', "ˇ"]
12013 "#});
12014
12015 // Single quote inside double-quoted string
12016 cx.set_state(indoc! {r#"
12017 def main():
12018 items = ["'", ˇ]
12019 "#});
12020 cx.update_editor(|editor, window, cx| {
12021 editor.handle_input("'", window, cx);
12022 });
12023 cx.assert_editor_state(indoc! {r#"
12024 def main():
12025 items = ["'", 'ˇ']
12026 "#});
12027
12028 // Two single quotes inside double-quoted string
12029 cx.set_state(indoc! {r#"
12030 def main():
12031 items = ["''", ˇ]
12032 "#});
12033 cx.update_editor(|editor, window, cx| {
12034 editor.handle_input("'", window, cx);
12035 });
12036 cx.assert_editor_state(indoc! {r#"
12037 def main():
12038 items = ["''", 'ˇ']
12039 "#});
12040
12041 // Mixed quotes on same line
12042 cx.set_state(indoc! {r#"
12043 def main():
12044 items = ['"""', "'''''", ˇ]
12045 "#});
12046 cx.update_editor(|editor, window, cx| {
12047 editor.handle_input("\"", window, cx);
12048 });
12049 cx.assert_editor_state(indoc! {r#"
12050 def main():
12051 items = ['"""', "'''''", "ˇ"]
12052 "#});
12053 cx.update_editor(|editor, window, cx| {
12054 editor.move_right(&MoveRight, window, cx);
12055 });
12056 cx.update_editor(|editor, window, cx| {
12057 editor.handle_input(", ", window, cx);
12058 });
12059 cx.update_editor(|editor, window, cx| {
12060 editor.handle_input("'", window, cx);
12061 });
12062 cx.assert_editor_state(indoc! {r#"
12063 def main():
12064 items = ['"""', "'''''", "", 'ˇ']
12065 "#});
12066}
12067
12068#[gpui::test]
12069async fn test_autoclose_quotes_with_multibyte_characters(cx: &mut TestAppContext) {
12070 init_test(cx, |_| {});
12071
12072 let mut cx = EditorTestContext::new(cx).await;
12073 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
12074 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12075
12076 cx.set_state(indoc! {r#"
12077 def main():
12078 items = ["🎉", ˇ]
12079 "#});
12080 cx.update_editor(|editor, window, cx| {
12081 editor.handle_input("\"", window, cx);
12082 });
12083 cx.assert_editor_state(indoc! {r#"
12084 def main():
12085 items = ["🎉", "ˇ"]
12086 "#});
12087}
12088
12089#[gpui::test]
12090async fn test_surround_with_pair(cx: &mut TestAppContext) {
12091 init_test(cx, |_| {});
12092
12093 let language = Arc::new(Language::new(
12094 LanguageConfig {
12095 brackets: BracketPairConfig {
12096 pairs: vec![
12097 BracketPair {
12098 start: "{".to_string(),
12099 end: "}".to_string(),
12100 close: true,
12101 surround: true,
12102 newline: true,
12103 },
12104 BracketPair {
12105 start: "/* ".to_string(),
12106 end: "*/".to_string(),
12107 close: true,
12108 surround: true,
12109 ..Default::default()
12110 },
12111 ],
12112 ..Default::default()
12113 },
12114 ..Default::default()
12115 },
12116 Some(tree_sitter_rust::LANGUAGE.into()),
12117 ));
12118
12119 let text = r#"
12120 a
12121 b
12122 c
12123 "#
12124 .unindent();
12125
12126 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12127 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12128 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12129 editor
12130 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12131 .await;
12132
12133 editor.update_in(cx, |editor, window, cx| {
12134 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12135 s.select_display_ranges([
12136 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
12137 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
12138 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
12139 ])
12140 });
12141
12142 editor.handle_input("{", window, cx);
12143 editor.handle_input("{", window, cx);
12144 editor.handle_input("{", window, cx);
12145 assert_eq!(
12146 editor.text(cx),
12147 "
12148 {{{a}}}
12149 {{{b}}}
12150 {{{c}}}
12151 "
12152 .unindent()
12153 );
12154 assert_eq!(
12155 display_ranges(editor, cx),
12156 [
12157 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
12158 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
12159 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
12160 ]
12161 );
12162
12163 editor.undo(&Undo, window, cx);
12164 editor.undo(&Undo, window, cx);
12165 editor.undo(&Undo, window, cx);
12166 assert_eq!(
12167 editor.text(cx),
12168 "
12169 a
12170 b
12171 c
12172 "
12173 .unindent()
12174 );
12175 assert_eq!(
12176 display_ranges(editor, cx),
12177 [
12178 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
12179 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
12180 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
12181 ]
12182 );
12183
12184 // Ensure inserting the first character of a multi-byte bracket pair
12185 // doesn't surround the selections with the bracket.
12186 editor.handle_input("/", window, cx);
12187 assert_eq!(
12188 editor.text(cx),
12189 "
12190 /
12191 /
12192 /
12193 "
12194 .unindent()
12195 );
12196 assert_eq!(
12197 display_ranges(editor, cx),
12198 [
12199 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
12200 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
12201 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
12202 ]
12203 );
12204
12205 editor.undo(&Undo, window, cx);
12206 assert_eq!(
12207 editor.text(cx),
12208 "
12209 a
12210 b
12211 c
12212 "
12213 .unindent()
12214 );
12215 assert_eq!(
12216 display_ranges(editor, cx),
12217 [
12218 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
12219 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
12220 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
12221 ]
12222 );
12223
12224 // Ensure inserting the last character of a multi-byte bracket pair
12225 // doesn't surround the selections with the bracket.
12226 editor.handle_input("*", window, cx);
12227 assert_eq!(
12228 editor.text(cx),
12229 "
12230 *
12231 *
12232 *
12233 "
12234 .unindent()
12235 );
12236 assert_eq!(
12237 display_ranges(editor, cx),
12238 [
12239 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
12240 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
12241 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
12242 ]
12243 );
12244 });
12245}
12246
12247#[gpui::test]
12248async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
12249 init_test(cx, |_| {});
12250
12251 let language = Arc::new(Language::new(
12252 LanguageConfig {
12253 brackets: BracketPairConfig {
12254 pairs: vec![BracketPair {
12255 start: "{".to_string(),
12256 end: "}".to_string(),
12257 close: true,
12258 surround: true,
12259 newline: true,
12260 }],
12261 ..Default::default()
12262 },
12263 autoclose_before: "}".to_string(),
12264 ..Default::default()
12265 },
12266 Some(tree_sitter_rust::LANGUAGE.into()),
12267 ));
12268
12269 let text = r#"
12270 a
12271 b
12272 c
12273 "#
12274 .unindent();
12275
12276 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12277 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12278 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12279 editor
12280 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12281 .await;
12282
12283 editor.update_in(cx, |editor, window, cx| {
12284 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12285 s.select_ranges([
12286 Point::new(0, 1)..Point::new(0, 1),
12287 Point::new(1, 1)..Point::new(1, 1),
12288 Point::new(2, 1)..Point::new(2, 1),
12289 ])
12290 });
12291
12292 editor.handle_input("{", window, cx);
12293 editor.handle_input("{", window, cx);
12294 editor.handle_input("_", window, cx);
12295 assert_eq!(
12296 editor.text(cx),
12297 "
12298 a{{_}}
12299 b{{_}}
12300 c{{_}}
12301 "
12302 .unindent()
12303 );
12304 assert_eq!(
12305 editor
12306 .selections
12307 .ranges::<Point>(&editor.display_snapshot(cx)),
12308 [
12309 Point::new(0, 4)..Point::new(0, 4),
12310 Point::new(1, 4)..Point::new(1, 4),
12311 Point::new(2, 4)..Point::new(2, 4)
12312 ]
12313 );
12314
12315 editor.backspace(&Default::default(), window, cx);
12316 editor.backspace(&Default::default(), window, cx);
12317 assert_eq!(
12318 editor.text(cx),
12319 "
12320 a{}
12321 b{}
12322 c{}
12323 "
12324 .unindent()
12325 );
12326 assert_eq!(
12327 editor
12328 .selections
12329 .ranges::<Point>(&editor.display_snapshot(cx)),
12330 [
12331 Point::new(0, 2)..Point::new(0, 2),
12332 Point::new(1, 2)..Point::new(1, 2),
12333 Point::new(2, 2)..Point::new(2, 2)
12334 ]
12335 );
12336
12337 editor.delete_to_previous_word_start(&Default::default(), window, cx);
12338 assert_eq!(
12339 editor.text(cx),
12340 "
12341 a
12342 b
12343 c
12344 "
12345 .unindent()
12346 );
12347 assert_eq!(
12348 editor
12349 .selections
12350 .ranges::<Point>(&editor.display_snapshot(cx)),
12351 [
12352 Point::new(0, 1)..Point::new(0, 1),
12353 Point::new(1, 1)..Point::new(1, 1),
12354 Point::new(2, 1)..Point::new(2, 1)
12355 ]
12356 );
12357 });
12358}
12359
12360#[gpui::test]
12361async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
12362 init_test(cx, |settings| {
12363 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
12364 });
12365
12366 let mut cx = EditorTestContext::new(cx).await;
12367
12368 let language = Arc::new(Language::new(
12369 LanguageConfig {
12370 brackets: BracketPairConfig {
12371 pairs: vec![
12372 BracketPair {
12373 start: "{".to_string(),
12374 end: "}".to_string(),
12375 close: true,
12376 surround: true,
12377 newline: true,
12378 },
12379 BracketPair {
12380 start: "(".to_string(),
12381 end: ")".to_string(),
12382 close: true,
12383 surround: true,
12384 newline: true,
12385 },
12386 BracketPair {
12387 start: "[".to_string(),
12388 end: "]".to_string(),
12389 close: false,
12390 surround: true,
12391 newline: true,
12392 },
12393 ],
12394 ..Default::default()
12395 },
12396 autoclose_before: "})]".to_string(),
12397 ..Default::default()
12398 },
12399 Some(tree_sitter_rust::LANGUAGE.into()),
12400 ));
12401
12402 cx.language_registry().add(language.clone());
12403 cx.update_buffer(|buffer, cx| {
12404 buffer.set_language(Some(language), cx);
12405 });
12406
12407 cx.set_state(
12408 &"
12409 {(ˇ)}
12410 [[ˇ]]
12411 {(ˇ)}
12412 "
12413 .unindent(),
12414 );
12415
12416 cx.update_editor(|editor, window, cx| {
12417 editor.backspace(&Default::default(), window, cx);
12418 editor.backspace(&Default::default(), window, cx);
12419 });
12420
12421 cx.assert_editor_state(
12422 &"
12423 ˇ
12424 ˇ]]
12425 ˇ
12426 "
12427 .unindent(),
12428 );
12429
12430 cx.update_editor(|editor, window, cx| {
12431 editor.handle_input("{", window, cx);
12432 editor.handle_input("{", window, cx);
12433 editor.move_right(&MoveRight, window, cx);
12434 editor.move_right(&MoveRight, window, cx);
12435 editor.move_left(&MoveLeft, window, cx);
12436 editor.move_left(&MoveLeft, window, cx);
12437 editor.backspace(&Default::default(), window, cx);
12438 });
12439
12440 cx.assert_editor_state(
12441 &"
12442 {ˇ}
12443 {ˇ}]]
12444 {ˇ}
12445 "
12446 .unindent(),
12447 );
12448
12449 cx.update_editor(|editor, window, cx| {
12450 editor.backspace(&Default::default(), window, cx);
12451 });
12452
12453 cx.assert_editor_state(
12454 &"
12455 ˇ
12456 ˇ]]
12457 ˇ
12458 "
12459 .unindent(),
12460 );
12461}
12462
12463#[gpui::test]
12464async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
12465 init_test(cx, |_| {});
12466
12467 let language = Arc::new(Language::new(
12468 LanguageConfig::default(),
12469 Some(tree_sitter_rust::LANGUAGE.into()),
12470 ));
12471
12472 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
12473 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12474 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12475 editor
12476 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12477 .await;
12478
12479 editor.update_in(cx, |editor, window, cx| {
12480 editor.set_auto_replace_emoji_shortcode(true);
12481
12482 editor.handle_input("Hello ", window, cx);
12483 editor.handle_input(":wave", window, cx);
12484 assert_eq!(editor.text(cx), "Hello :wave".unindent());
12485
12486 editor.handle_input(":", window, cx);
12487 assert_eq!(editor.text(cx), "Hello 👋".unindent());
12488
12489 editor.handle_input(" :smile", window, cx);
12490 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
12491
12492 editor.handle_input(":", window, cx);
12493 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
12494
12495 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
12496 editor.handle_input(":wave", window, cx);
12497 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
12498
12499 editor.handle_input(":", window, cx);
12500 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
12501
12502 editor.handle_input(":1", window, cx);
12503 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
12504
12505 editor.handle_input(":", window, cx);
12506 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
12507
12508 // Ensure shortcode does not get replaced when it is part of a word
12509 editor.handle_input(" Test:wave", window, cx);
12510 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
12511
12512 editor.handle_input(":", window, cx);
12513 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
12514
12515 editor.set_auto_replace_emoji_shortcode(false);
12516
12517 // Ensure shortcode does not get replaced when auto replace is off
12518 editor.handle_input(" :wave", window, cx);
12519 assert_eq!(
12520 editor.text(cx),
12521 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
12522 );
12523
12524 editor.handle_input(":", window, cx);
12525 assert_eq!(
12526 editor.text(cx),
12527 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
12528 );
12529 });
12530}
12531
12532#[gpui::test]
12533async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
12534 init_test(cx, |_| {});
12535
12536 let (text, insertion_ranges) = marked_text_ranges(
12537 indoc! {"
12538 ˇ
12539 "},
12540 false,
12541 );
12542
12543 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
12544 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12545
12546 _ = editor.update_in(cx, |editor, window, cx| {
12547 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
12548
12549 editor
12550 .insert_snippet(
12551 &insertion_ranges
12552 .iter()
12553 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
12554 .collect::<Vec<_>>(),
12555 snippet,
12556 window,
12557 cx,
12558 )
12559 .unwrap();
12560
12561 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
12562 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
12563 assert_eq!(editor.text(cx), expected_text);
12564 assert_eq!(
12565 editor.selections.ranges(&editor.display_snapshot(cx)),
12566 selection_ranges
12567 .iter()
12568 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
12569 .collect::<Vec<_>>()
12570 );
12571 }
12572
12573 assert(
12574 editor,
12575 cx,
12576 indoc! {"
12577 type «» =•
12578 "},
12579 );
12580
12581 assert!(editor.context_menu_visible(), "There should be a matches");
12582 });
12583}
12584
12585#[gpui::test]
12586async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
12587 init_test(cx, |_| {});
12588
12589 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
12590 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
12591 assert_eq!(editor.text(cx), expected_text);
12592 assert_eq!(
12593 editor.selections.ranges(&editor.display_snapshot(cx)),
12594 selection_ranges
12595 .iter()
12596 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
12597 .collect::<Vec<_>>()
12598 );
12599 }
12600
12601 let (text, insertion_ranges) = marked_text_ranges(
12602 indoc! {"
12603 ˇ
12604 "},
12605 false,
12606 );
12607
12608 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
12609 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12610
12611 _ = editor.update_in(cx, |editor, window, cx| {
12612 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
12613
12614 editor
12615 .insert_snippet(
12616 &insertion_ranges
12617 .iter()
12618 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
12619 .collect::<Vec<_>>(),
12620 snippet,
12621 window,
12622 cx,
12623 )
12624 .unwrap();
12625
12626 assert_state(
12627 editor,
12628 cx,
12629 indoc! {"
12630 type «» = ;•
12631 "},
12632 );
12633
12634 assert!(
12635 editor.context_menu_visible(),
12636 "Context menu should be visible for placeholder choices"
12637 );
12638
12639 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
12640
12641 assert_state(
12642 editor,
12643 cx,
12644 indoc! {"
12645 type = «»;•
12646 "},
12647 );
12648
12649 assert!(
12650 !editor.context_menu_visible(),
12651 "Context menu should be hidden after moving to next tabstop"
12652 );
12653
12654 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
12655
12656 assert_state(
12657 editor,
12658 cx,
12659 indoc! {"
12660 type = ; ˇ
12661 "},
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
12675 _ = editor.update_in(cx, |editor, window, cx| {
12676 editor.select_all(&SelectAll, window, cx);
12677 editor.backspace(&Backspace, window, cx);
12678
12679 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
12680 let insertion_ranges = editor
12681 .selections
12682 .all(&editor.display_snapshot(cx))
12683 .iter()
12684 .map(|s| s.range())
12685 .collect::<Vec<_>>();
12686
12687 editor
12688 .insert_snippet(&insertion_ranges, snippet, window, cx)
12689 .unwrap();
12690
12691 assert_state(editor, cx, "fn «» = value;•");
12692
12693 assert!(
12694 editor.context_menu_visible(),
12695 "Context menu should be visible for placeholder choices"
12696 );
12697
12698 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
12699
12700 assert_state(editor, cx, "fn = «valueˇ»;•");
12701
12702 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
12703
12704 assert_state(editor, cx, "fn «» = value;•");
12705
12706 assert!(
12707 editor.context_menu_visible(),
12708 "Context menu should be visible again after returning to first tabstop"
12709 );
12710
12711 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
12712
12713 assert_state(editor, cx, "fn «» = value;•");
12714 });
12715}
12716
12717#[gpui::test]
12718async fn test_snippets(cx: &mut TestAppContext) {
12719 init_test(cx, |_| {});
12720
12721 let mut cx = EditorTestContext::new(cx).await;
12722
12723 cx.set_state(indoc! {"
12724 a.ˇ b
12725 a.ˇ b
12726 a.ˇ b
12727 "});
12728
12729 cx.update_editor(|editor, window, cx| {
12730 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
12731 let insertion_ranges = editor
12732 .selections
12733 .all(&editor.display_snapshot(cx))
12734 .iter()
12735 .map(|s| s.range())
12736 .collect::<Vec<_>>();
12737 editor
12738 .insert_snippet(&insertion_ranges, snippet, window, cx)
12739 .unwrap();
12740 });
12741
12742 cx.assert_editor_state(indoc! {"
12743 a.f(«oneˇ», two, «threeˇ») b
12744 a.f(«oneˇ», two, «threeˇ») b
12745 a.f(«oneˇ», two, «threeˇ») b
12746 "});
12747
12748 // Can't move earlier than the first tab stop
12749 cx.update_editor(|editor, window, cx| {
12750 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
12751 });
12752 cx.assert_editor_state(indoc! {"
12753 a.f(«oneˇ», two, «threeˇ») b
12754 a.f(«oneˇ», two, «threeˇ») b
12755 a.f(«oneˇ», two, «threeˇ») b
12756 "});
12757
12758 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
12759 cx.assert_editor_state(indoc! {"
12760 a.f(one, «twoˇ», three) b
12761 a.f(one, «twoˇ», three) b
12762 a.f(one, «twoˇ», three) b
12763 "});
12764
12765 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
12766 cx.assert_editor_state(indoc! {"
12767 a.f(«oneˇ», two, «threeˇ») b
12768 a.f(«oneˇ», two, «threeˇ») b
12769 a.f(«oneˇ», two, «threeˇ») b
12770 "});
12771
12772 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
12773 cx.assert_editor_state(indoc! {"
12774 a.f(one, «twoˇ», three) b
12775 a.f(one, «twoˇ», three) b
12776 a.f(one, «twoˇ», three) b
12777 "});
12778 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
12779 cx.assert_editor_state(indoc! {"
12780 a.f(one, two, three)ˇ b
12781 a.f(one, two, three)ˇ b
12782 a.f(one, two, three)ˇ b
12783 "});
12784
12785 // As soon as the last tab stop is reached, snippet state is gone
12786 cx.update_editor(|editor, window, cx| {
12787 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
12788 });
12789 cx.assert_editor_state(indoc! {"
12790 a.f(one, two, three)ˇ b
12791 a.f(one, two, three)ˇ b
12792 a.f(one, two, three)ˇ b
12793 "});
12794}
12795
12796#[gpui::test]
12797async fn test_snippet_indentation(cx: &mut TestAppContext) {
12798 init_test(cx, |_| {});
12799
12800 let mut cx = EditorTestContext::new(cx).await;
12801
12802 cx.update_editor(|editor, window, cx| {
12803 let snippet = Snippet::parse(indoc! {"
12804 /*
12805 * Multiline comment with leading indentation
12806 *
12807 * $1
12808 */
12809 $0"})
12810 .unwrap();
12811 let insertion_ranges = editor
12812 .selections
12813 .all(&editor.display_snapshot(cx))
12814 .iter()
12815 .map(|s| s.range())
12816 .collect::<Vec<_>>();
12817 editor
12818 .insert_snippet(&insertion_ranges, snippet, window, cx)
12819 .unwrap();
12820 });
12821
12822 cx.assert_editor_state(indoc! {"
12823 /*
12824 * Multiline comment with leading indentation
12825 *
12826 * ˇ
12827 */
12828 "});
12829
12830 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
12831 cx.assert_editor_state(indoc! {"
12832 /*
12833 * Multiline comment with leading indentation
12834 *
12835 *•
12836 */
12837 ˇ"});
12838}
12839
12840#[gpui::test]
12841async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
12842 init_test(cx, |_| {});
12843
12844 let mut cx = EditorTestContext::new(cx).await;
12845 cx.update_editor(|editor, _, cx| {
12846 editor.project().unwrap().update(cx, |project, cx| {
12847 project.snippets().update(cx, |snippets, _cx| {
12848 let snippet = project::snippet_provider::Snippet {
12849 prefix: vec!["multi word".to_string()],
12850 body: "this is many words".to_string(),
12851 description: Some("description".to_string()),
12852 name: "multi-word snippet test".to_string(),
12853 };
12854 snippets.add_snippet_for_test(
12855 None,
12856 PathBuf::from("test_snippets.json"),
12857 vec![Arc::new(snippet)],
12858 );
12859 });
12860 })
12861 });
12862
12863 for (input_to_simulate, should_match_snippet) in [
12864 ("m", true),
12865 ("m ", true),
12866 ("m w", true),
12867 ("aa m w", true),
12868 ("aa m g", false),
12869 ] {
12870 cx.set_state("ˇ");
12871 cx.simulate_input(input_to_simulate); // fails correctly
12872
12873 cx.update_editor(|editor, _, _| {
12874 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
12875 else {
12876 assert!(!should_match_snippet); // no completions! don't even show the menu
12877 return;
12878 };
12879 assert!(context_menu.visible());
12880 let completions = context_menu.completions.borrow();
12881
12882 assert_eq!(!completions.is_empty(), should_match_snippet);
12883 });
12884 }
12885}
12886
12887#[gpui::test]
12888async fn test_document_format_during_save(cx: &mut TestAppContext) {
12889 init_test(cx, |_| {});
12890
12891 let fs = FakeFs::new(cx.executor());
12892 fs.insert_file(path!("/file.rs"), Default::default()).await;
12893
12894 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
12895
12896 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12897 language_registry.add(rust_lang());
12898 let mut fake_servers = language_registry.register_fake_lsp(
12899 "Rust",
12900 FakeLspAdapter {
12901 capabilities: lsp::ServerCapabilities {
12902 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12903 ..Default::default()
12904 },
12905 ..Default::default()
12906 },
12907 );
12908
12909 let buffer = project
12910 .update(cx, |project, cx| {
12911 project.open_local_buffer(path!("/file.rs"), cx)
12912 })
12913 .await
12914 .unwrap();
12915
12916 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12917 let (editor, cx) = cx.add_window_view(|window, cx| {
12918 build_editor_with_project(project.clone(), buffer, window, cx)
12919 });
12920 editor.update_in(cx, |editor, window, cx| {
12921 editor.set_text("one\ntwo\nthree\n", window, cx)
12922 });
12923 assert!(cx.read(|cx| editor.is_dirty(cx)));
12924
12925 let fake_server = fake_servers.next().await.unwrap();
12926
12927 {
12928 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12929 move |params, _| async move {
12930 assert_eq!(
12931 params.text_document.uri,
12932 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12933 );
12934 assert_eq!(params.options.tab_size, 4);
12935 Ok(Some(vec![lsp::TextEdit::new(
12936 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12937 ", ".to_string(),
12938 )]))
12939 },
12940 );
12941 let save = editor
12942 .update_in(cx, |editor, window, cx| {
12943 editor.save(
12944 SaveOptions {
12945 format: true,
12946 autosave: false,
12947 },
12948 project.clone(),
12949 window,
12950 cx,
12951 )
12952 })
12953 .unwrap();
12954 save.await;
12955
12956 assert_eq!(
12957 editor.update(cx, |editor, cx| editor.text(cx)),
12958 "one, two\nthree\n"
12959 );
12960 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12961 }
12962
12963 {
12964 editor.update_in(cx, |editor, window, cx| {
12965 editor.set_text("one\ntwo\nthree\n", window, cx)
12966 });
12967 assert!(cx.read(|cx| editor.is_dirty(cx)));
12968
12969 // Ensure we can still save even if formatting hangs.
12970 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12971 move |params, _| async move {
12972 assert_eq!(
12973 params.text_document.uri,
12974 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12975 );
12976 futures::future::pending::<()>().await;
12977 unreachable!()
12978 },
12979 );
12980 let save = editor
12981 .update_in(cx, |editor, window, cx| {
12982 editor.save(
12983 SaveOptions {
12984 format: true,
12985 autosave: false,
12986 },
12987 project.clone(),
12988 window,
12989 cx,
12990 )
12991 })
12992 .unwrap();
12993 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12994 save.await;
12995 assert_eq!(
12996 editor.update(cx, |editor, cx| editor.text(cx)),
12997 "one\ntwo\nthree\n"
12998 );
12999 }
13000
13001 // Set rust language override and assert overridden tabsize is sent to language server
13002 update_test_language_settings(cx, &|settings| {
13003 settings.languages.0.insert(
13004 "Rust".into(),
13005 LanguageSettingsContent {
13006 tab_size: NonZeroU32::new(8),
13007 ..Default::default()
13008 },
13009 );
13010 });
13011
13012 {
13013 editor.update_in(cx, |editor, window, cx| {
13014 editor.set_text("somehting_new\n", window, cx)
13015 });
13016 assert!(cx.read(|cx| editor.is_dirty(cx)));
13017 let _formatting_request_signal = fake_server
13018 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
13019 assert_eq!(
13020 params.text_document.uri,
13021 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13022 );
13023 assert_eq!(params.options.tab_size, 8);
13024 Ok(Some(vec![]))
13025 });
13026 let save = editor
13027 .update_in(cx, |editor, window, cx| {
13028 editor.save(
13029 SaveOptions {
13030 format: true,
13031 autosave: false,
13032 },
13033 project.clone(),
13034 window,
13035 cx,
13036 )
13037 })
13038 .unwrap();
13039 save.await;
13040 }
13041}
13042
13043#[gpui::test]
13044async fn test_auto_formatter_skips_server_without_formatting(cx: &mut TestAppContext) {
13045 init_test(cx, |_| {});
13046
13047 let fs = FakeFs::new(cx.executor());
13048 fs.insert_file(path!("/file.rs"), Default::default()).await;
13049
13050 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
13051
13052 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13053 language_registry.add(rust_lang());
13054
13055 // First server: no formatting capability
13056 let mut no_format_servers = language_registry.register_fake_lsp(
13057 "Rust",
13058 FakeLspAdapter {
13059 name: "no-format-server",
13060 capabilities: lsp::ServerCapabilities {
13061 completion_provider: Some(lsp::CompletionOptions::default()),
13062 ..Default::default()
13063 },
13064 ..Default::default()
13065 },
13066 );
13067
13068 // Second server: has formatting capability
13069 let mut format_servers = language_registry.register_fake_lsp(
13070 "Rust",
13071 FakeLspAdapter {
13072 name: "format-server",
13073 capabilities: lsp::ServerCapabilities {
13074 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13075 ..Default::default()
13076 },
13077 ..Default::default()
13078 },
13079 );
13080
13081 let buffer = project
13082 .update(cx, |project, cx| {
13083 project.open_local_buffer(path!("/file.rs"), cx)
13084 })
13085 .await
13086 .unwrap();
13087
13088 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13089 let (editor, cx) = cx.add_window_view(|window, cx| {
13090 build_editor_with_project(project.clone(), buffer, window, cx)
13091 });
13092 editor.update_in(cx, |editor, window, cx| {
13093 editor.set_text("one\ntwo\nthree\n", window, cx)
13094 });
13095
13096 let _no_format_server = no_format_servers.next().await.unwrap();
13097 let format_server = format_servers.next().await.unwrap();
13098
13099 format_server.set_request_handler::<lsp::request::Formatting, _, _>(
13100 move |params, _| async move {
13101 assert_eq!(
13102 params.text_document.uri,
13103 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13104 );
13105 Ok(Some(vec![lsp::TextEdit::new(
13106 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
13107 ", ".to_string(),
13108 )]))
13109 },
13110 );
13111
13112 let save = editor
13113 .update_in(cx, |editor, window, cx| {
13114 editor.save(
13115 SaveOptions {
13116 format: true,
13117 autosave: false,
13118 },
13119 project.clone(),
13120 window,
13121 cx,
13122 )
13123 })
13124 .unwrap();
13125 save.await;
13126
13127 assert_eq!(
13128 editor.update(cx, |editor, cx| editor.text(cx)),
13129 "one, two\nthree\n"
13130 );
13131}
13132
13133#[gpui::test]
13134async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
13135 init_test(cx, |settings| {
13136 settings.defaults.ensure_final_newline_on_save = Some(false);
13137 });
13138
13139 let fs = FakeFs::new(cx.executor());
13140 fs.insert_file(path!("/file.txt"), "foo".into()).await;
13141
13142 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
13143
13144 let buffer = project
13145 .update(cx, |project, cx| {
13146 project.open_local_buffer(path!("/file.txt"), cx)
13147 })
13148 .await
13149 .unwrap();
13150
13151 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13152 let (editor, cx) = cx.add_window_view(|window, cx| {
13153 build_editor_with_project(project.clone(), buffer, window, cx)
13154 });
13155 editor.update_in(cx, |editor, window, cx| {
13156 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
13157 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13158 });
13159 });
13160 assert!(!cx.read(|cx| editor.is_dirty(cx)));
13161
13162 editor.update_in(cx, |editor, window, cx| {
13163 editor.handle_input("\n", window, cx)
13164 });
13165 cx.run_until_parked();
13166 save(&editor, &project, cx).await;
13167 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
13168
13169 editor.update_in(cx, |editor, window, cx| {
13170 editor.undo(&Default::default(), window, cx);
13171 });
13172 save(&editor, &project, cx).await;
13173 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
13174
13175 editor.update_in(cx, |editor, window, cx| {
13176 editor.redo(&Default::default(), window, cx);
13177 });
13178 cx.run_until_parked();
13179 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
13180
13181 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
13182 let save = editor
13183 .update_in(cx, |editor, window, cx| {
13184 editor.save(
13185 SaveOptions {
13186 format: true,
13187 autosave: false,
13188 },
13189 project.clone(),
13190 window,
13191 cx,
13192 )
13193 })
13194 .unwrap();
13195 save.await;
13196 assert!(!cx.read(|cx| editor.is_dirty(cx)));
13197 }
13198}
13199
13200#[gpui::test]
13201async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
13202 init_test(cx, |_| {});
13203
13204 let cols = 4;
13205 let rows = 10;
13206 let sample_text_1 = sample_text(rows, cols, 'a');
13207 assert_eq!(
13208 sample_text_1,
13209 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
13210 );
13211 let sample_text_2 = sample_text(rows, cols, 'l');
13212 assert_eq!(
13213 sample_text_2,
13214 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
13215 );
13216 let sample_text_3 = sample_text(rows, cols, 'v').replace('\u{7f}', ".");
13217 assert_eq!(
13218 sample_text_3,
13219 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n...."
13220 );
13221
13222 let fs = FakeFs::new(cx.executor());
13223 fs.insert_tree(
13224 path!("/a"),
13225 json!({
13226 "main.rs": sample_text_1,
13227 "other.rs": sample_text_2,
13228 "lib.rs": sample_text_3,
13229 }),
13230 )
13231 .await;
13232
13233 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13234 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
13235 let cx = &mut VisualTestContext::from_window(*window, cx);
13236
13237 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13238 language_registry.add(rust_lang());
13239 let mut fake_servers = language_registry.register_fake_lsp(
13240 "Rust",
13241 FakeLspAdapter {
13242 capabilities: lsp::ServerCapabilities {
13243 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13244 ..Default::default()
13245 },
13246 ..Default::default()
13247 },
13248 );
13249
13250 let worktree = project.update(cx, |project, cx| {
13251 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
13252 assert_eq!(worktrees.len(), 1);
13253 worktrees.pop().unwrap()
13254 });
13255 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
13256
13257 let buffer_1 = project
13258 .update(cx, |project, cx| {
13259 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
13260 })
13261 .await
13262 .unwrap();
13263 let buffer_2 = project
13264 .update(cx, |project, cx| {
13265 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
13266 })
13267 .await
13268 .unwrap();
13269 let buffer_3 = project
13270 .update(cx, |project, cx| {
13271 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
13272 })
13273 .await
13274 .unwrap();
13275
13276 let multi_buffer = cx.new(|cx| {
13277 let mut multi_buffer = MultiBuffer::new(ReadWrite);
13278 multi_buffer.set_excerpts_for_path(
13279 PathKey::sorted(0),
13280 buffer_1.clone(),
13281 [
13282 Point::new(0, 0)..Point::new(2, 4),
13283 Point::new(5, 0)..Point::new(6, 4),
13284 Point::new(9, 0)..Point::new(9, 4),
13285 ],
13286 0,
13287 cx,
13288 );
13289 multi_buffer.set_excerpts_for_path(
13290 PathKey::sorted(1),
13291 buffer_2.clone(),
13292 [
13293 Point::new(0, 0)..Point::new(2, 4),
13294 Point::new(5, 0)..Point::new(6, 4),
13295 Point::new(9, 0)..Point::new(9, 4),
13296 ],
13297 0,
13298 cx,
13299 );
13300 multi_buffer.set_excerpts_for_path(
13301 PathKey::sorted(2),
13302 buffer_3.clone(),
13303 [
13304 Point::new(0, 0)..Point::new(2, 4),
13305 Point::new(5, 0)..Point::new(6, 4),
13306 Point::new(9, 0)..Point::new(9, 4),
13307 ],
13308 0,
13309 cx,
13310 );
13311 assert_eq!(multi_buffer.excerpt_ids().len(), 9);
13312 multi_buffer
13313 });
13314 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
13315 Editor::new(
13316 EditorMode::full(),
13317 multi_buffer,
13318 Some(project.clone()),
13319 window,
13320 cx,
13321 )
13322 });
13323
13324 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13325 let a = editor.text(cx).find("aaaa").unwrap();
13326 editor.change_selections(
13327 SelectionEffects::scroll(Autoscroll::Next),
13328 window,
13329 cx,
13330 |s| s.select_ranges(Some(MultiBufferOffset(a + 1)..MultiBufferOffset(a + 2))),
13331 );
13332 editor.insert("|one|two|three|", window, cx);
13333 });
13334 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
13335 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13336 let n = editor.text(cx).find("nnnn").unwrap();
13337 editor.change_selections(
13338 SelectionEffects::scroll(Autoscroll::Next),
13339 window,
13340 cx,
13341 |s| s.select_ranges(Some(MultiBufferOffset(n + 4)..MultiBufferOffset(n + 14))),
13342 );
13343 editor.insert("|four|five|six|", window, cx);
13344 });
13345 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
13346
13347 // First two buffers should be edited, but not the third one.
13348 pretty_assertions::assert_eq!(
13349 editor_content_with_blocks(&multi_buffer_editor, cx),
13350 indoc! {"
13351 § main.rs
13352 § -----
13353 a|one|two|three|aa
13354 bbbb
13355 cccc
13356 § -----
13357 ffff
13358 gggg
13359 § -----
13360 jjjj
13361 § other.rs
13362 § -----
13363 llll
13364 mmmm
13365 nnnn|four|five|six|
13366 § -----
13367
13368 § -----
13369 uuuu
13370 § lib.rs
13371 § -----
13372 vvvv
13373 wwww
13374 xxxx
13375 § -----
13376 {{{{
13377 ||||
13378 § -----
13379 ...."}
13380 );
13381 buffer_1.update(cx, |buffer, _| {
13382 assert!(buffer.is_dirty());
13383 assert_eq!(
13384 buffer.text(),
13385 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
13386 )
13387 });
13388 buffer_2.update(cx, |buffer, _| {
13389 assert!(buffer.is_dirty());
13390 assert_eq!(
13391 buffer.text(),
13392 "llll\nmmmm\nnnnn|four|five|six|\noooo\npppp\n\nssss\ntttt\nuuuu",
13393 )
13394 });
13395 buffer_3.update(cx, |buffer, _| {
13396 assert!(!buffer.is_dirty());
13397 assert_eq!(buffer.text(), sample_text_3,)
13398 });
13399 cx.executor().run_until_parked();
13400
13401 let save = multi_buffer_editor
13402 .update_in(cx, |editor, window, cx| {
13403 editor.save(
13404 SaveOptions {
13405 format: true,
13406 autosave: false,
13407 },
13408 project.clone(),
13409 window,
13410 cx,
13411 )
13412 })
13413 .unwrap();
13414
13415 let fake_server = fake_servers.next().await.unwrap();
13416 fake_server
13417 .server
13418 .on_request::<lsp::request::Formatting, _, _>(move |_params, _| async move {
13419 Ok(Some(vec![lsp::TextEdit::new(
13420 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
13421 "[formatted]".to_string(),
13422 )]))
13423 })
13424 .detach();
13425 save.await;
13426
13427 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
13428 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
13429 assert_eq!(
13430 editor_content_with_blocks(&multi_buffer_editor, cx),
13431 indoc! {"
13432 § main.rs
13433 § -----
13434 a|o[formatted]bbbb
13435 cccc
13436 § -----
13437 ffff
13438 gggg
13439 § -----
13440 jjjj
13441
13442 § other.rs
13443 § -----
13444 lll[formatted]mmmm
13445 nnnn|four|five|six|
13446 § -----
13447
13448 § -----
13449 uuuu
13450
13451 § lib.rs
13452 § -----
13453 vvvv
13454 wwww
13455 xxxx
13456 § -----
13457 {{{{
13458 ||||
13459 § -----
13460 ...."}
13461 );
13462 buffer_1.update(cx, |buffer, _| {
13463 assert!(!buffer.is_dirty());
13464 assert_eq!(
13465 buffer.text(),
13466 "a|o[formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n",
13467 )
13468 });
13469 // Diff < left / right > :
13470 // lll[formatted]mmmm
13471 // <nnnn|four|five|six|
13472 // <oooo
13473 // >nnnn|four|five|six|oooo
13474 // pppp
13475 // <
13476 // ssss
13477 // tttt
13478 // uuuu
13479
13480 buffer_2.update(cx, |buffer, _| {
13481 assert!(!buffer.is_dirty());
13482 assert_eq!(
13483 buffer.text(),
13484 "lll[formatted]mmmm\nnnnn|four|five|six|\noooo\npppp\n\nssss\ntttt\nuuuu\n",
13485 )
13486 });
13487 buffer_3.update(cx, |buffer, _| {
13488 assert!(!buffer.is_dirty());
13489 assert_eq!(buffer.text(), sample_text_3,)
13490 });
13491}
13492
13493#[gpui::test]
13494async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
13495 init_test(cx, |_| {});
13496
13497 let fs = FakeFs::new(cx.executor());
13498 fs.insert_tree(
13499 path!("/dir"),
13500 json!({
13501 "file1.rs": "fn main() { println!(\"hello\"); }",
13502 "file2.rs": "fn test() { println!(\"test\"); }",
13503 "file3.rs": "fn other() { println!(\"other\"); }\n",
13504 }),
13505 )
13506 .await;
13507
13508 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
13509 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
13510 let cx = &mut VisualTestContext::from_window(*window, cx);
13511
13512 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13513 language_registry.add(rust_lang());
13514
13515 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
13516 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
13517
13518 // Open three buffers
13519 let buffer_1 = project
13520 .update(cx, |project, cx| {
13521 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
13522 })
13523 .await
13524 .unwrap();
13525 let buffer_2 = project
13526 .update(cx, |project, cx| {
13527 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
13528 })
13529 .await
13530 .unwrap();
13531 let buffer_3 = project
13532 .update(cx, |project, cx| {
13533 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
13534 })
13535 .await
13536 .unwrap();
13537
13538 // Create a multi-buffer with all three buffers
13539 let multi_buffer = cx.new(|cx| {
13540 let mut multi_buffer = MultiBuffer::new(ReadWrite);
13541 multi_buffer.set_excerpts_for_path(
13542 PathKey::sorted(0),
13543 buffer_1.clone(),
13544 [Point::new(0, 0)..Point::new(1, 0)],
13545 0,
13546 cx,
13547 );
13548 multi_buffer.set_excerpts_for_path(
13549 PathKey::sorted(1),
13550 buffer_2.clone(),
13551 [Point::new(0, 0)..Point::new(1, 0)],
13552 0,
13553 cx,
13554 );
13555 multi_buffer.set_excerpts_for_path(
13556 PathKey::sorted(2),
13557 buffer_3.clone(),
13558 [Point::new(0, 0)..Point::new(1, 0)],
13559 0,
13560 cx,
13561 );
13562 multi_buffer
13563 });
13564
13565 let editor = cx.new_window_entity(|window, cx| {
13566 Editor::new(
13567 EditorMode::full(),
13568 multi_buffer,
13569 Some(project.clone()),
13570 window,
13571 cx,
13572 )
13573 });
13574
13575 // Edit only the first buffer
13576 editor.update_in(cx, |editor, window, cx| {
13577 editor.change_selections(
13578 SelectionEffects::scroll(Autoscroll::Next),
13579 window,
13580 cx,
13581 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
13582 );
13583 editor.insert("// edited", window, cx);
13584 });
13585
13586 // Verify that only buffer 1 is dirty
13587 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
13588 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
13589 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
13590
13591 // Get write counts after file creation (files were created with initial content)
13592 // We expect each file to have been written once during creation
13593 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
13594 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
13595 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
13596
13597 // Perform autosave
13598 let save_task = editor.update_in(cx, |editor, window, cx| {
13599 editor.save(
13600 SaveOptions {
13601 format: true,
13602 autosave: true,
13603 },
13604 project.clone(),
13605 window,
13606 cx,
13607 )
13608 });
13609 save_task.await.unwrap();
13610
13611 // Only the dirty buffer should have been saved
13612 assert_eq!(
13613 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
13614 1,
13615 "Buffer 1 was dirty, so it should have been written once during autosave"
13616 );
13617 assert_eq!(
13618 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
13619 0,
13620 "Buffer 2 was clean, so it should not have been written during autosave"
13621 );
13622 assert_eq!(
13623 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
13624 0,
13625 "Buffer 3 was clean, so it should not have been written during autosave"
13626 );
13627
13628 // Verify buffer states after autosave
13629 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
13630 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
13631 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
13632
13633 // Now perform a manual save (format = true)
13634 let save_task = editor.update_in(cx, |editor, window, cx| {
13635 editor.save(
13636 SaveOptions {
13637 format: true,
13638 autosave: false,
13639 },
13640 project.clone(),
13641 window,
13642 cx,
13643 )
13644 });
13645 save_task.await.unwrap();
13646
13647 // During manual save, clean buffers don't get written to disk
13648 // They just get did_save called for language server notifications
13649 assert_eq!(
13650 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
13651 1,
13652 "Buffer 1 should only have been written once total (during autosave, not manual save)"
13653 );
13654 assert_eq!(
13655 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
13656 0,
13657 "Buffer 2 should not have been written at all"
13658 );
13659 assert_eq!(
13660 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
13661 0,
13662 "Buffer 3 should not have been written at all"
13663 );
13664}
13665
13666async fn setup_range_format_test(
13667 cx: &mut TestAppContext,
13668) -> (
13669 Entity<Project>,
13670 Entity<Editor>,
13671 &mut gpui::VisualTestContext,
13672 lsp::FakeLanguageServer,
13673) {
13674 init_test(cx, |_| {});
13675
13676 let fs = FakeFs::new(cx.executor());
13677 fs.insert_file(path!("/file.rs"), Default::default()).await;
13678
13679 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13680
13681 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13682 language_registry.add(rust_lang());
13683 let mut fake_servers = language_registry.register_fake_lsp(
13684 "Rust",
13685 FakeLspAdapter {
13686 capabilities: lsp::ServerCapabilities {
13687 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
13688 ..lsp::ServerCapabilities::default()
13689 },
13690 ..FakeLspAdapter::default()
13691 },
13692 );
13693
13694 let buffer = project
13695 .update(cx, |project, cx| {
13696 project.open_local_buffer(path!("/file.rs"), cx)
13697 })
13698 .await
13699 .unwrap();
13700
13701 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13702 let (editor, cx) = cx.add_window_view(|window, cx| {
13703 build_editor_with_project(project.clone(), buffer, window, cx)
13704 });
13705
13706 let fake_server = fake_servers.next().await.unwrap();
13707
13708 (project, editor, cx, fake_server)
13709}
13710
13711#[gpui::test]
13712async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
13713 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
13714
13715 editor.update_in(cx, |editor, window, cx| {
13716 editor.set_text("one\ntwo\nthree\n", window, cx)
13717 });
13718 assert!(cx.read(|cx| editor.is_dirty(cx)));
13719
13720 let save = editor
13721 .update_in(cx, |editor, window, cx| {
13722 editor.save(
13723 SaveOptions {
13724 format: true,
13725 autosave: false,
13726 },
13727 project.clone(),
13728 window,
13729 cx,
13730 )
13731 })
13732 .unwrap();
13733 fake_server
13734 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
13735 assert_eq!(
13736 params.text_document.uri,
13737 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13738 );
13739 assert_eq!(params.options.tab_size, 4);
13740 Ok(Some(vec![lsp::TextEdit::new(
13741 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
13742 ", ".to_string(),
13743 )]))
13744 })
13745 .next()
13746 .await;
13747 save.await;
13748 assert_eq!(
13749 editor.update(cx, |editor, cx| editor.text(cx)),
13750 "one, two\nthree\n"
13751 );
13752 assert!(!cx.read(|cx| editor.is_dirty(cx)));
13753}
13754
13755#[gpui::test]
13756async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
13757 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
13758
13759 editor.update_in(cx, |editor, window, cx| {
13760 editor.set_text("one\ntwo\nthree\n", window, cx)
13761 });
13762 assert!(cx.read(|cx| editor.is_dirty(cx)));
13763
13764 // Test that save still works when formatting hangs
13765 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
13766 move |params, _| async move {
13767 assert_eq!(
13768 params.text_document.uri,
13769 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13770 );
13771 futures::future::pending::<()>().await;
13772 unreachable!()
13773 },
13774 );
13775 let save = editor
13776 .update_in(cx, |editor, window, cx| {
13777 editor.save(
13778 SaveOptions {
13779 format: true,
13780 autosave: false,
13781 },
13782 project.clone(),
13783 window,
13784 cx,
13785 )
13786 })
13787 .unwrap();
13788 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
13789 save.await;
13790 assert_eq!(
13791 editor.update(cx, |editor, cx| editor.text(cx)),
13792 "one\ntwo\nthree\n"
13793 );
13794 assert!(!cx.read(|cx| editor.is_dirty(cx)));
13795}
13796
13797#[gpui::test]
13798async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
13799 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
13800
13801 // Buffer starts clean, no formatting should be requested
13802 let save = editor
13803 .update_in(cx, |editor, window, cx| {
13804 editor.save(
13805 SaveOptions {
13806 format: false,
13807 autosave: false,
13808 },
13809 project.clone(),
13810 window,
13811 cx,
13812 )
13813 })
13814 .unwrap();
13815 let _pending_format_request = fake_server
13816 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
13817 panic!("Should not be invoked");
13818 })
13819 .next();
13820 save.await;
13821 cx.run_until_parked();
13822}
13823
13824#[gpui::test]
13825async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
13826 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
13827
13828 // Set Rust language override and assert overridden tabsize is sent to language server
13829 update_test_language_settings(cx, &|settings| {
13830 settings.languages.0.insert(
13831 "Rust".into(),
13832 LanguageSettingsContent {
13833 tab_size: NonZeroU32::new(8),
13834 ..Default::default()
13835 },
13836 );
13837 });
13838
13839 editor.update_in(cx, |editor, window, cx| {
13840 editor.set_text("something_new\n", window, cx)
13841 });
13842 assert!(cx.read(|cx| editor.is_dirty(cx)));
13843 let save = editor
13844 .update_in(cx, |editor, window, cx| {
13845 editor.save(
13846 SaveOptions {
13847 format: true,
13848 autosave: false,
13849 },
13850 project.clone(),
13851 window,
13852 cx,
13853 )
13854 })
13855 .unwrap();
13856 fake_server
13857 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
13858 assert_eq!(
13859 params.text_document.uri,
13860 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13861 );
13862 assert_eq!(params.options.tab_size, 8);
13863 Ok(Some(Vec::new()))
13864 })
13865 .next()
13866 .await;
13867 save.await;
13868}
13869
13870#[gpui::test]
13871async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
13872 init_test(cx, |settings| {
13873 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
13874 settings::LanguageServerFormatterSpecifier::Current,
13875 )))
13876 });
13877
13878 let fs = FakeFs::new(cx.executor());
13879 fs.insert_file(path!("/file.rs"), Default::default()).await;
13880
13881 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13882
13883 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13884 language_registry.add(Arc::new(Language::new(
13885 LanguageConfig {
13886 name: "Rust".into(),
13887 matcher: LanguageMatcher {
13888 path_suffixes: vec!["rs".to_string()],
13889 ..Default::default()
13890 },
13891 ..LanguageConfig::default()
13892 },
13893 Some(tree_sitter_rust::LANGUAGE.into()),
13894 )));
13895 update_test_language_settings(cx, &|settings| {
13896 // Enable Prettier formatting for the same buffer, and ensure
13897 // LSP is called instead of Prettier.
13898 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
13899 });
13900 let mut fake_servers = language_registry.register_fake_lsp(
13901 "Rust",
13902 FakeLspAdapter {
13903 capabilities: lsp::ServerCapabilities {
13904 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13905 ..Default::default()
13906 },
13907 ..Default::default()
13908 },
13909 );
13910
13911 let buffer = project
13912 .update(cx, |project, cx| {
13913 project.open_local_buffer(path!("/file.rs"), cx)
13914 })
13915 .await
13916 .unwrap();
13917
13918 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13919 let (editor, cx) = cx.add_window_view(|window, cx| {
13920 build_editor_with_project(project.clone(), buffer, window, cx)
13921 });
13922 editor.update_in(cx, |editor, window, cx| {
13923 editor.set_text("one\ntwo\nthree\n", window, cx)
13924 });
13925
13926 let fake_server = fake_servers.next().await.unwrap();
13927
13928 let format = editor
13929 .update_in(cx, |editor, window, cx| {
13930 editor.perform_format(
13931 project.clone(),
13932 FormatTrigger::Manual,
13933 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
13934 window,
13935 cx,
13936 )
13937 })
13938 .unwrap();
13939 fake_server
13940 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
13941 assert_eq!(
13942 params.text_document.uri,
13943 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13944 );
13945 assert_eq!(params.options.tab_size, 4);
13946 Ok(Some(vec![lsp::TextEdit::new(
13947 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
13948 ", ".to_string(),
13949 )]))
13950 })
13951 .next()
13952 .await;
13953 format.await;
13954 assert_eq!(
13955 editor.update(cx, |editor, cx| editor.text(cx)),
13956 "one, two\nthree\n"
13957 );
13958
13959 editor.update_in(cx, |editor, window, cx| {
13960 editor.set_text("one\ntwo\nthree\n", window, cx)
13961 });
13962 // Ensure we don't lock if formatting hangs.
13963 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
13964 move |params, _| async move {
13965 assert_eq!(
13966 params.text_document.uri,
13967 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13968 );
13969 futures::future::pending::<()>().await;
13970 unreachable!()
13971 },
13972 );
13973 let format = editor
13974 .update_in(cx, |editor, window, cx| {
13975 editor.perform_format(
13976 project,
13977 FormatTrigger::Manual,
13978 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
13979 window,
13980 cx,
13981 )
13982 })
13983 .unwrap();
13984 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
13985 format.await;
13986 assert_eq!(
13987 editor.update(cx, |editor, cx| editor.text(cx)),
13988 "one\ntwo\nthree\n"
13989 );
13990}
13991
13992#[gpui::test]
13993async fn test_multiple_formatters(cx: &mut TestAppContext) {
13994 init_test(cx, |settings| {
13995 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
13996 settings.defaults.formatter = Some(FormatterList::Vec(vec![
13997 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
13998 Formatter::CodeAction("code-action-1".into()),
13999 Formatter::CodeAction("code-action-2".into()),
14000 ]))
14001 });
14002
14003 let fs = FakeFs::new(cx.executor());
14004 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
14005 .await;
14006
14007 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14008 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14009 language_registry.add(rust_lang());
14010
14011 let mut fake_servers = language_registry.register_fake_lsp(
14012 "Rust",
14013 FakeLspAdapter {
14014 capabilities: lsp::ServerCapabilities {
14015 document_formatting_provider: Some(lsp::OneOf::Left(true)),
14016 execute_command_provider: Some(lsp::ExecuteCommandOptions {
14017 commands: vec!["the-command-for-code-action-1".into()],
14018 ..Default::default()
14019 }),
14020 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14021 ..Default::default()
14022 },
14023 ..Default::default()
14024 },
14025 );
14026
14027 let buffer = project
14028 .update(cx, |project, cx| {
14029 project.open_local_buffer(path!("/file.rs"), cx)
14030 })
14031 .await
14032 .unwrap();
14033
14034 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14035 let (editor, cx) = cx.add_window_view(|window, cx| {
14036 build_editor_with_project(project.clone(), buffer, window, cx)
14037 });
14038
14039 let fake_server = fake_servers.next().await.unwrap();
14040 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
14041 move |_params, _| async move {
14042 Ok(Some(vec![lsp::TextEdit::new(
14043 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
14044 "applied-formatting\n".to_string(),
14045 )]))
14046 },
14047 );
14048 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14049 move |params, _| async move {
14050 let requested_code_actions = params.context.only.expect("Expected code action request");
14051 assert_eq!(requested_code_actions.len(), 1);
14052
14053 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
14054 let code_action = match requested_code_actions[0].as_str() {
14055 "code-action-1" => lsp::CodeAction {
14056 kind: Some("code-action-1".into()),
14057 edit: Some(lsp::WorkspaceEdit::new(
14058 [(
14059 uri,
14060 vec![lsp::TextEdit::new(
14061 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
14062 "applied-code-action-1-edit\n".to_string(),
14063 )],
14064 )]
14065 .into_iter()
14066 .collect(),
14067 )),
14068 command: Some(lsp::Command {
14069 command: "the-command-for-code-action-1".into(),
14070 ..Default::default()
14071 }),
14072 ..Default::default()
14073 },
14074 "code-action-2" => lsp::CodeAction {
14075 kind: Some("code-action-2".into()),
14076 edit: Some(lsp::WorkspaceEdit::new(
14077 [(
14078 uri,
14079 vec![lsp::TextEdit::new(
14080 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
14081 "applied-code-action-2-edit\n".to_string(),
14082 )],
14083 )]
14084 .into_iter()
14085 .collect(),
14086 )),
14087 ..Default::default()
14088 },
14089 req => panic!("Unexpected code action request: {:?}", req),
14090 };
14091 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14092 code_action,
14093 )]))
14094 },
14095 );
14096
14097 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
14098 move |params, _| async move { Ok(params) }
14099 });
14100
14101 let command_lock = Arc::new(futures::lock::Mutex::new(()));
14102 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
14103 let fake = fake_server.clone();
14104 let lock = command_lock.clone();
14105 move |params, _| {
14106 assert_eq!(params.command, "the-command-for-code-action-1");
14107 let fake = fake.clone();
14108 let lock = lock.clone();
14109 async move {
14110 lock.lock().await;
14111 fake.server
14112 .request::<lsp::request::ApplyWorkspaceEdit>(
14113 lsp::ApplyWorkspaceEditParams {
14114 label: None,
14115 edit: lsp::WorkspaceEdit {
14116 changes: Some(
14117 [(
14118 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
14119 vec![lsp::TextEdit {
14120 range: lsp::Range::new(
14121 lsp::Position::new(0, 0),
14122 lsp::Position::new(0, 0),
14123 ),
14124 new_text: "applied-code-action-1-command\n".into(),
14125 }],
14126 )]
14127 .into_iter()
14128 .collect(),
14129 ),
14130 ..Default::default()
14131 },
14132 },
14133 DEFAULT_LSP_REQUEST_TIMEOUT,
14134 )
14135 .await
14136 .into_response()
14137 .unwrap();
14138 Ok(Some(json!(null)))
14139 }
14140 }
14141 });
14142
14143 editor
14144 .update_in(cx, |editor, window, cx| {
14145 editor.perform_format(
14146 project.clone(),
14147 FormatTrigger::Manual,
14148 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
14149 window,
14150 cx,
14151 )
14152 })
14153 .unwrap()
14154 .await;
14155 editor.update(cx, |editor, cx| {
14156 assert_eq!(
14157 editor.text(cx),
14158 r#"
14159 applied-code-action-2-edit
14160 applied-code-action-1-command
14161 applied-code-action-1-edit
14162 applied-formatting
14163 one
14164 two
14165 three
14166 "#
14167 .unindent()
14168 );
14169 });
14170
14171 editor.update_in(cx, |editor, window, cx| {
14172 editor.undo(&Default::default(), window, cx);
14173 assert_eq!(editor.text(cx), "one \ntwo \nthree");
14174 });
14175
14176 // Perform a manual edit while waiting for an LSP command
14177 // that's being run as part of a formatting code action.
14178 let lock_guard = command_lock.lock().await;
14179 let format = editor
14180 .update_in(cx, |editor, window, cx| {
14181 editor.perform_format(
14182 project.clone(),
14183 FormatTrigger::Manual,
14184 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
14185 window,
14186 cx,
14187 )
14188 })
14189 .unwrap();
14190 cx.run_until_parked();
14191 editor.update(cx, |editor, cx| {
14192 assert_eq!(
14193 editor.text(cx),
14194 r#"
14195 applied-code-action-1-edit
14196 applied-formatting
14197 one
14198 two
14199 three
14200 "#
14201 .unindent()
14202 );
14203
14204 editor.buffer.update(cx, |buffer, cx| {
14205 let ix = buffer.len(cx);
14206 buffer.edit([(ix..ix, "edited\n")], None, cx);
14207 });
14208 });
14209
14210 // Allow the LSP command to proceed. Because the buffer was edited,
14211 // the second code action will not be run.
14212 drop(lock_guard);
14213 format.await;
14214 editor.update_in(cx, |editor, window, cx| {
14215 assert_eq!(
14216 editor.text(cx),
14217 r#"
14218 applied-code-action-1-command
14219 applied-code-action-1-edit
14220 applied-formatting
14221 one
14222 two
14223 three
14224 edited
14225 "#
14226 .unindent()
14227 );
14228
14229 // The manual edit is undone first, because it is the last thing the user did
14230 // (even though the command completed afterwards).
14231 editor.undo(&Default::default(), window, cx);
14232 assert_eq!(
14233 editor.text(cx),
14234 r#"
14235 applied-code-action-1-command
14236 applied-code-action-1-edit
14237 applied-formatting
14238 one
14239 two
14240 three
14241 "#
14242 .unindent()
14243 );
14244
14245 // All the formatting (including the command, which completed after the manual edit)
14246 // is undone together.
14247 editor.undo(&Default::default(), window, cx);
14248 assert_eq!(editor.text(cx), "one \ntwo \nthree");
14249 });
14250}
14251
14252#[gpui::test]
14253async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
14254 init_test(cx, |settings| {
14255 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
14256 settings::LanguageServerFormatterSpecifier::Current,
14257 )]))
14258 });
14259
14260 let fs = FakeFs::new(cx.executor());
14261 fs.insert_file(path!("/file.ts"), Default::default()).await;
14262
14263 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14264
14265 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14266 language_registry.add(Arc::new(Language::new(
14267 LanguageConfig {
14268 name: "TypeScript".into(),
14269 matcher: LanguageMatcher {
14270 path_suffixes: vec!["ts".to_string()],
14271 ..Default::default()
14272 },
14273 ..LanguageConfig::default()
14274 },
14275 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14276 )));
14277 update_test_language_settings(cx, &|settings| {
14278 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
14279 });
14280 let mut fake_servers = language_registry.register_fake_lsp(
14281 "TypeScript",
14282 FakeLspAdapter {
14283 capabilities: lsp::ServerCapabilities {
14284 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14285 ..Default::default()
14286 },
14287 ..Default::default()
14288 },
14289 );
14290
14291 let buffer = project
14292 .update(cx, |project, cx| {
14293 project.open_local_buffer(path!("/file.ts"), cx)
14294 })
14295 .await
14296 .unwrap();
14297
14298 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14299 let (editor, cx) = cx.add_window_view(|window, cx| {
14300 build_editor_with_project(project.clone(), buffer, window, cx)
14301 });
14302 editor.update_in(cx, |editor, window, cx| {
14303 editor.set_text(
14304 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
14305 window,
14306 cx,
14307 )
14308 });
14309
14310 let fake_server = fake_servers.next().await.unwrap();
14311
14312 let format = editor
14313 .update_in(cx, |editor, window, cx| {
14314 editor.perform_code_action_kind(
14315 project.clone(),
14316 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
14317 window,
14318 cx,
14319 )
14320 })
14321 .unwrap();
14322 fake_server
14323 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
14324 assert_eq!(
14325 params.text_document.uri,
14326 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
14327 );
14328 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14329 lsp::CodeAction {
14330 title: "Organize Imports".to_string(),
14331 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
14332 edit: Some(lsp::WorkspaceEdit {
14333 changes: Some(
14334 [(
14335 params.text_document.uri.clone(),
14336 vec![lsp::TextEdit::new(
14337 lsp::Range::new(
14338 lsp::Position::new(1, 0),
14339 lsp::Position::new(2, 0),
14340 ),
14341 "".to_string(),
14342 )],
14343 )]
14344 .into_iter()
14345 .collect(),
14346 ),
14347 ..Default::default()
14348 }),
14349 ..Default::default()
14350 },
14351 )]))
14352 })
14353 .next()
14354 .await;
14355 format.await;
14356 assert_eq!(
14357 editor.update(cx, |editor, cx| editor.text(cx)),
14358 "import { a } from 'module';\n\nconst x = a;\n"
14359 );
14360
14361 editor.update_in(cx, |editor, window, cx| {
14362 editor.set_text(
14363 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
14364 window,
14365 cx,
14366 )
14367 });
14368 // Ensure we don't lock if code action hangs.
14369 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14370 move |params, _| async move {
14371 assert_eq!(
14372 params.text_document.uri,
14373 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
14374 );
14375 futures::future::pending::<()>().await;
14376 unreachable!()
14377 },
14378 );
14379 let format = editor
14380 .update_in(cx, |editor, window, cx| {
14381 editor.perform_code_action_kind(
14382 project,
14383 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
14384 window,
14385 cx,
14386 )
14387 })
14388 .unwrap();
14389 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
14390 format.await;
14391 assert_eq!(
14392 editor.update(cx, |editor, cx| editor.text(cx)),
14393 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
14394 );
14395}
14396
14397#[gpui::test]
14398async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
14399 init_test(cx, |_| {});
14400
14401 let mut cx = EditorLspTestContext::new_rust(
14402 lsp::ServerCapabilities {
14403 document_formatting_provider: Some(lsp::OneOf::Left(true)),
14404 ..Default::default()
14405 },
14406 cx,
14407 )
14408 .await;
14409
14410 cx.set_state(indoc! {"
14411 one.twoˇ
14412 "});
14413
14414 // The format request takes a long time. When it completes, it inserts
14415 // a newline and an indent before the `.`
14416 cx.lsp
14417 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
14418 let executor = cx.background_executor().clone();
14419 async move {
14420 executor.timer(Duration::from_millis(100)).await;
14421 Ok(Some(vec![lsp::TextEdit {
14422 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
14423 new_text: "\n ".into(),
14424 }]))
14425 }
14426 });
14427
14428 // Submit a format request.
14429 let format_1 = cx
14430 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
14431 .unwrap();
14432 cx.executor().run_until_parked();
14433
14434 // Submit a second format request.
14435 let format_2 = cx
14436 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
14437 .unwrap();
14438 cx.executor().run_until_parked();
14439
14440 // Wait for both format requests to complete
14441 cx.executor().advance_clock(Duration::from_millis(200));
14442 format_1.await.unwrap();
14443 format_2.await.unwrap();
14444
14445 // The formatting edits only happens once.
14446 cx.assert_editor_state(indoc! {"
14447 one
14448 .twoˇ
14449 "});
14450}
14451
14452#[gpui::test]
14453async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
14454 init_test(cx, |settings| {
14455 settings.defaults.formatter = Some(FormatterList::default())
14456 });
14457
14458 let mut cx = EditorLspTestContext::new_rust(
14459 lsp::ServerCapabilities {
14460 document_formatting_provider: Some(lsp::OneOf::Left(true)),
14461 ..Default::default()
14462 },
14463 cx,
14464 )
14465 .await;
14466
14467 // Record which buffer changes have been sent to the language server
14468 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
14469 cx.lsp
14470 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
14471 let buffer_changes = buffer_changes.clone();
14472 move |params, _| {
14473 buffer_changes.lock().extend(
14474 params
14475 .content_changes
14476 .into_iter()
14477 .map(|e| (e.range.unwrap(), e.text)),
14478 );
14479 }
14480 });
14481 // Handle formatting requests to the language server.
14482 cx.lsp
14483 .set_request_handler::<lsp::request::Formatting, _, _>({
14484 move |_, _| {
14485 // Insert blank lines between each line of the buffer.
14486 async move {
14487 // TODO: this assertion is not reliably true. Currently nothing guarantees that we deliver
14488 // DidChangedTextDocument to the LSP before sending the formatting request.
14489 // assert_eq!(
14490 // &buffer_changes.lock()[1..],
14491 // &[
14492 // (
14493 // lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
14494 // "".into()
14495 // ),
14496 // (
14497 // lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
14498 // "".into()
14499 // ),
14500 // (
14501 // lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
14502 // "\n".into()
14503 // ),
14504 // ]
14505 // );
14506
14507 Ok(Some(vec![
14508 lsp::TextEdit {
14509 range: lsp::Range::new(
14510 lsp::Position::new(1, 0),
14511 lsp::Position::new(1, 0),
14512 ),
14513 new_text: "\n".into(),
14514 },
14515 lsp::TextEdit {
14516 range: lsp::Range::new(
14517 lsp::Position::new(2, 0),
14518 lsp::Position::new(2, 0),
14519 ),
14520 new_text: "\n".into(),
14521 },
14522 ]))
14523 }
14524 }
14525 });
14526
14527 // Set up a buffer white some trailing whitespace and no trailing newline.
14528 cx.set_state(
14529 &[
14530 "one ", //
14531 "twoˇ", //
14532 "three ", //
14533 "four", //
14534 ]
14535 .join("\n"),
14536 );
14537
14538 // Submit a format request.
14539 let format = cx
14540 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
14541 .unwrap();
14542
14543 cx.run_until_parked();
14544 // After formatting the buffer, the trailing whitespace is stripped,
14545 // a newline is appended, and the edits provided by the language server
14546 // have been applied.
14547 format.await.unwrap();
14548
14549 cx.assert_editor_state(
14550 &[
14551 "one", //
14552 "", //
14553 "twoˇ", //
14554 "", //
14555 "three", //
14556 "four", //
14557 "", //
14558 ]
14559 .join("\n"),
14560 );
14561
14562 // Undoing the formatting undoes the trailing whitespace removal, the
14563 // trailing newline, and the LSP edits.
14564 cx.update_buffer(|buffer, cx| buffer.undo(cx));
14565 cx.assert_editor_state(
14566 &[
14567 "one ", //
14568 "twoˇ", //
14569 "three ", //
14570 "four", //
14571 ]
14572 .join("\n"),
14573 );
14574}
14575
14576#[gpui::test]
14577async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
14578 cx: &mut TestAppContext,
14579) {
14580 init_test(cx, |_| {});
14581
14582 cx.update(|cx| {
14583 cx.update_global::<SettingsStore, _>(|settings, cx| {
14584 settings.update_user_settings(cx, |settings| {
14585 settings.editor.auto_signature_help = Some(true);
14586 settings.editor.hover_popover_delay = Some(DelayMs(300));
14587 });
14588 });
14589 });
14590
14591 let mut cx = EditorLspTestContext::new_rust(
14592 lsp::ServerCapabilities {
14593 signature_help_provider: Some(lsp::SignatureHelpOptions {
14594 ..Default::default()
14595 }),
14596 ..Default::default()
14597 },
14598 cx,
14599 )
14600 .await;
14601
14602 let language = Language::new(
14603 LanguageConfig {
14604 name: "Rust".into(),
14605 brackets: BracketPairConfig {
14606 pairs: vec![
14607 BracketPair {
14608 start: "{".to_string(),
14609 end: "}".to_string(),
14610 close: true,
14611 surround: true,
14612 newline: true,
14613 },
14614 BracketPair {
14615 start: "(".to_string(),
14616 end: ")".to_string(),
14617 close: true,
14618 surround: true,
14619 newline: true,
14620 },
14621 BracketPair {
14622 start: "/*".to_string(),
14623 end: " */".to_string(),
14624 close: true,
14625 surround: true,
14626 newline: true,
14627 },
14628 BracketPair {
14629 start: "[".to_string(),
14630 end: "]".to_string(),
14631 close: false,
14632 surround: false,
14633 newline: true,
14634 },
14635 BracketPair {
14636 start: "\"".to_string(),
14637 end: "\"".to_string(),
14638 close: true,
14639 surround: true,
14640 newline: false,
14641 },
14642 BracketPair {
14643 start: "<".to_string(),
14644 end: ">".to_string(),
14645 close: false,
14646 surround: true,
14647 newline: true,
14648 },
14649 ],
14650 ..Default::default()
14651 },
14652 autoclose_before: "})]".to_string(),
14653 ..Default::default()
14654 },
14655 Some(tree_sitter_rust::LANGUAGE.into()),
14656 );
14657 let language = Arc::new(language);
14658
14659 cx.language_registry().add(language.clone());
14660 cx.update_buffer(|buffer, cx| {
14661 buffer.set_language(Some(language), cx);
14662 });
14663
14664 cx.set_state(
14665 &r#"
14666 fn main() {
14667 sampleˇ
14668 }
14669 "#
14670 .unindent(),
14671 );
14672
14673 cx.update_editor(|editor, window, cx| {
14674 editor.handle_input("(", window, cx);
14675 });
14676 cx.assert_editor_state(
14677 &"
14678 fn main() {
14679 sample(ˇ)
14680 }
14681 "
14682 .unindent(),
14683 );
14684
14685 let mocked_response = lsp::SignatureHelp {
14686 signatures: vec![lsp::SignatureInformation {
14687 label: "fn sample(param1: u8, param2: u8)".to_string(),
14688 documentation: None,
14689 parameters: Some(vec![
14690 lsp::ParameterInformation {
14691 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14692 documentation: None,
14693 },
14694 lsp::ParameterInformation {
14695 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14696 documentation: None,
14697 },
14698 ]),
14699 active_parameter: None,
14700 }],
14701 active_signature: Some(0),
14702 active_parameter: Some(0),
14703 };
14704 handle_signature_help_request(&mut cx, mocked_response).await;
14705
14706 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14707 .await;
14708
14709 cx.editor(|editor, _, _| {
14710 let signature_help_state = editor.signature_help_state.popover().cloned();
14711 let signature = signature_help_state.unwrap();
14712 assert_eq!(
14713 signature.signatures[signature.current_signature].label,
14714 "fn sample(param1: u8, param2: u8)"
14715 );
14716 });
14717}
14718
14719#[gpui::test]
14720async fn test_signature_help_delay_only_for_auto(cx: &mut TestAppContext) {
14721 init_test(cx, |_| {});
14722
14723 let delay_ms = 500;
14724 cx.update(|cx| {
14725 cx.update_global::<SettingsStore, _>(|settings, cx| {
14726 settings.update_user_settings(cx, |settings| {
14727 settings.editor.auto_signature_help = Some(true);
14728 settings.editor.show_signature_help_after_edits = Some(false);
14729 settings.editor.hover_popover_delay = Some(DelayMs(delay_ms));
14730 });
14731 });
14732 });
14733
14734 let mut cx = EditorLspTestContext::new_rust(
14735 lsp::ServerCapabilities {
14736 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14737 ..lsp::ServerCapabilities::default()
14738 },
14739 cx,
14740 )
14741 .await;
14742
14743 let mocked_response = lsp::SignatureHelp {
14744 signatures: vec![lsp::SignatureInformation {
14745 label: "fn sample(param1: u8)".to_string(),
14746 documentation: None,
14747 parameters: Some(vec![lsp::ParameterInformation {
14748 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14749 documentation: None,
14750 }]),
14751 active_parameter: None,
14752 }],
14753 active_signature: Some(0),
14754 active_parameter: Some(0),
14755 };
14756
14757 cx.set_state(indoc! {"
14758 fn main() {
14759 sample(ˇ);
14760 }
14761
14762 fn sample(param1: u8) {}
14763 "});
14764
14765 // Manual trigger should show immediately without delay
14766 cx.update_editor(|editor, window, cx| {
14767 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14768 });
14769 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14770 cx.run_until_parked();
14771 cx.editor(|editor, _, _| {
14772 assert!(
14773 editor.signature_help_state.is_shown(),
14774 "Manual trigger should show signature help without delay"
14775 );
14776 });
14777
14778 cx.update_editor(|editor, _, cx| {
14779 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
14780 });
14781 cx.run_until_parked();
14782 cx.editor(|editor, _, _| {
14783 assert!(!editor.signature_help_state.is_shown());
14784 });
14785
14786 // Auto trigger (cursor movement into brackets) should respect delay
14787 cx.set_state(indoc! {"
14788 fn main() {
14789 sampleˇ();
14790 }
14791
14792 fn sample(param1: u8) {}
14793 "});
14794 cx.update_editor(|editor, window, cx| {
14795 editor.move_right(&MoveRight, window, cx);
14796 });
14797 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14798 cx.run_until_parked();
14799 cx.editor(|editor, _, _| {
14800 assert!(
14801 !editor.signature_help_state.is_shown(),
14802 "Auto trigger should wait for delay before showing signature help"
14803 );
14804 });
14805
14806 cx.executor()
14807 .advance_clock(Duration::from_millis(delay_ms + 50));
14808 cx.run_until_parked();
14809 cx.editor(|editor, _, _| {
14810 assert!(
14811 editor.signature_help_state.is_shown(),
14812 "Auto trigger should show signature help after delay elapsed"
14813 );
14814 });
14815}
14816
14817#[gpui::test]
14818async fn test_signature_help_after_edits_no_delay(cx: &mut TestAppContext) {
14819 init_test(cx, |_| {});
14820
14821 let delay_ms = 500;
14822 cx.update(|cx| {
14823 cx.update_global::<SettingsStore, _>(|settings, cx| {
14824 settings.update_user_settings(cx, |settings| {
14825 settings.editor.auto_signature_help = Some(false);
14826 settings.editor.show_signature_help_after_edits = Some(true);
14827 settings.editor.hover_popover_delay = Some(DelayMs(delay_ms));
14828 });
14829 });
14830 });
14831
14832 let mut cx = EditorLspTestContext::new_rust(
14833 lsp::ServerCapabilities {
14834 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14835 ..lsp::ServerCapabilities::default()
14836 },
14837 cx,
14838 )
14839 .await;
14840
14841 let language = Arc::new(Language::new(
14842 LanguageConfig {
14843 name: "Rust".into(),
14844 brackets: BracketPairConfig {
14845 pairs: vec![BracketPair {
14846 start: "(".to_string(),
14847 end: ")".to_string(),
14848 close: true,
14849 surround: true,
14850 newline: true,
14851 }],
14852 ..BracketPairConfig::default()
14853 },
14854 autoclose_before: "})".to_string(),
14855 ..LanguageConfig::default()
14856 },
14857 Some(tree_sitter_rust::LANGUAGE.into()),
14858 ));
14859 cx.language_registry().add(language.clone());
14860 cx.update_buffer(|buffer, cx| {
14861 buffer.set_language(Some(language), cx);
14862 });
14863
14864 let mocked_response = lsp::SignatureHelp {
14865 signatures: vec![lsp::SignatureInformation {
14866 label: "fn sample(param1: u8)".to_string(),
14867 documentation: None,
14868 parameters: Some(vec![lsp::ParameterInformation {
14869 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14870 documentation: None,
14871 }]),
14872 active_parameter: None,
14873 }],
14874 active_signature: Some(0),
14875 active_parameter: Some(0),
14876 };
14877
14878 cx.set_state(indoc! {"
14879 fn main() {
14880 sampleˇ
14881 }
14882 "});
14883
14884 // Typing bracket should show signature help immediately without delay
14885 cx.update_editor(|editor, window, cx| {
14886 editor.handle_input("(", window, cx);
14887 });
14888 handle_signature_help_request(&mut cx, mocked_response).await;
14889 cx.run_until_parked();
14890 cx.editor(|editor, _, _| {
14891 assert!(
14892 editor.signature_help_state.is_shown(),
14893 "show_signature_help_after_edits should show signature help without delay"
14894 );
14895 });
14896}
14897
14898#[gpui::test]
14899async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
14900 init_test(cx, |_| {});
14901
14902 cx.update(|cx| {
14903 cx.update_global::<SettingsStore, _>(|settings, cx| {
14904 settings.update_user_settings(cx, |settings| {
14905 settings.editor.auto_signature_help = Some(false);
14906 settings.editor.show_signature_help_after_edits = Some(false);
14907 });
14908 });
14909 });
14910
14911 let mut cx = EditorLspTestContext::new_rust(
14912 lsp::ServerCapabilities {
14913 signature_help_provider: Some(lsp::SignatureHelpOptions {
14914 ..Default::default()
14915 }),
14916 ..Default::default()
14917 },
14918 cx,
14919 )
14920 .await;
14921
14922 let language = Language::new(
14923 LanguageConfig {
14924 name: "Rust".into(),
14925 brackets: BracketPairConfig {
14926 pairs: vec![
14927 BracketPair {
14928 start: "{".to_string(),
14929 end: "}".to_string(),
14930 close: true,
14931 surround: true,
14932 newline: true,
14933 },
14934 BracketPair {
14935 start: "(".to_string(),
14936 end: ")".to_string(),
14937 close: true,
14938 surround: true,
14939 newline: true,
14940 },
14941 BracketPair {
14942 start: "/*".to_string(),
14943 end: " */".to_string(),
14944 close: true,
14945 surround: true,
14946 newline: true,
14947 },
14948 BracketPair {
14949 start: "[".to_string(),
14950 end: "]".to_string(),
14951 close: false,
14952 surround: false,
14953 newline: true,
14954 },
14955 BracketPair {
14956 start: "\"".to_string(),
14957 end: "\"".to_string(),
14958 close: true,
14959 surround: true,
14960 newline: false,
14961 },
14962 BracketPair {
14963 start: "<".to_string(),
14964 end: ">".to_string(),
14965 close: false,
14966 surround: true,
14967 newline: true,
14968 },
14969 ],
14970 ..Default::default()
14971 },
14972 autoclose_before: "})]".to_string(),
14973 ..Default::default()
14974 },
14975 Some(tree_sitter_rust::LANGUAGE.into()),
14976 );
14977 let language = Arc::new(language);
14978
14979 cx.language_registry().add(language.clone());
14980 cx.update_buffer(|buffer, cx| {
14981 buffer.set_language(Some(language), cx);
14982 });
14983
14984 // Ensure that signature_help is not called when no signature help is enabled.
14985 cx.set_state(
14986 &r#"
14987 fn main() {
14988 sampleˇ
14989 }
14990 "#
14991 .unindent(),
14992 );
14993 cx.update_editor(|editor, window, cx| {
14994 editor.handle_input("(", window, cx);
14995 });
14996 cx.assert_editor_state(
14997 &"
14998 fn main() {
14999 sample(ˇ)
15000 }
15001 "
15002 .unindent(),
15003 );
15004 cx.editor(|editor, _, _| {
15005 assert!(editor.signature_help_state.task().is_none());
15006 });
15007
15008 let mocked_response = lsp::SignatureHelp {
15009 signatures: vec![lsp::SignatureInformation {
15010 label: "fn sample(param1: u8, param2: u8)".to_string(),
15011 documentation: None,
15012 parameters: Some(vec![
15013 lsp::ParameterInformation {
15014 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15015 documentation: None,
15016 },
15017 lsp::ParameterInformation {
15018 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15019 documentation: None,
15020 },
15021 ]),
15022 active_parameter: None,
15023 }],
15024 active_signature: Some(0),
15025 active_parameter: Some(0),
15026 };
15027
15028 // Ensure that signature_help is called when enabled afte edits
15029 cx.update(|_, cx| {
15030 cx.update_global::<SettingsStore, _>(|settings, cx| {
15031 settings.update_user_settings(cx, |settings| {
15032 settings.editor.auto_signature_help = Some(false);
15033 settings.editor.show_signature_help_after_edits = Some(true);
15034 });
15035 });
15036 });
15037 cx.set_state(
15038 &r#"
15039 fn main() {
15040 sampleˇ
15041 }
15042 "#
15043 .unindent(),
15044 );
15045 cx.update_editor(|editor, window, cx| {
15046 editor.handle_input("(", window, cx);
15047 });
15048 cx.assert_editor_state(
15049 &"
15050 fn main() {
15051 sample(ˇ)
15052 }
15053 "
15054 .unindent(),
15055 );
15056 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
15057 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15058 .await;
15059 cx.update_editor(|editor, _, _| {
15060 let signature_help_state = editor.signature_help_state.popover().cloned();
15061 assert!(signature_help_state.is_some());
15062 let signature = signature_help_state.unwrap();
15063 assert_eq!(
15064 signature.signatures[signature.current_signature].label,
15065 "fn sample(param1: u8, param2: u8)"
15066 );
15067 editor.signature_help_state = SignatureHelpState::default();
15068 });
15069
15070 // Ensure that signature_help is called when auto signature help override is enabled
15071 cx.update(|_, cx| {
15072 cx.update_global::<SettingsStore, _>(|settings, cx| {
15073 settings.update_user_settings(cx, |settings| {
15074 settings.editor.auto_signature_help = Some(true);
15075 settings.editor.show_signature_help_after_edits = Some(false);
15076 });
15077 });
15078 });
15079 cx.set_state(
15080 &r#"
15081 fn main() {
15082 sampleˇ
15083 }
15084 "#
15085 .unindent(),
15086 );
15087 cx.update_editor(|editor, window, cx| {
15088 editor.handle_input("(", window, cx);
15089 });
15090 cx.assert_editor_state(
15091 &"
15092 fn main() {
15093 sample(ˇ)
15094 }
15095 "
15096 .unindent(),
15097 );
15098 handle_signature_help_request(&mut cx, mocked_response).await;
15099 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15100 .await;
15101 cx.editor(|editor, _, _| {
15102 let signature_help_state = editor.signature_help_state.popover().cloned();
15103 assert!(signature_help_state.is_some());
15104 let signature = signature_help_state.unwrap();
15105 assert_eq!(
15106 signature.signatures[signature.current_signature].label,
15107 "fn sample(param1: u8, param2: u8)"
15108 );
15109 });
15110}
15111
15112#[gpui::test]
15113async fn test_signature_help(cx: &mut TestAppContext) {
15114 init_test(cx, |_| {});
15115 cx.update(|cx| {
15116 cx.update_global::<SettingsStore, _>(|settings, cx| {
15117 settings.update_user_settings(cx, |settings| {
15118 settings.editor.auto_signature_help = Some(true);
15119 });
15120 });
15121 });
15122
15123 let mut cx = EditorLspTestContext::new_rust(
15124 lsp::ServerCapabilities {
15125 signature_help_provider: Some(lsp::SignatureHelpOptions {
15126 ..Default::default()
15127 }),
15128 ..Default::default()
15129 },
15130 cx,
15131 )
15132 .await;
15133
15134 // A test that directly calls `show_signature_help`
15135 cx.update_editor(|editor, window, cx| {
15136 editor.show_signature_help(&ShowSignatureHelp, window, cx);
15137 });
15138
15139 let mocked_response = lsp::SignatureHelp {
15140 signatures: vec![lsp::SignatureInformation {
15141 label: "fn sample(param1: u8, param2: u8)".to_string(),
15142 documentation: None,
15143 parameters: Some(vec![
15144 lsp::ParameterInformation {
15145 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15146 documentation: None,
15147 },
15148 lsp::ParameterInformation {
15149 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15150 documentation: None,
15151 },
15152 ]),
15153 active_parameter: None,
15154 }],
15155 active_signature: Some(0),
15156 active_parameter: Some(0),
15157 };
15158 handle_signature_help_request(&mut cx, mocked_response).await;
15159
15160 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15161 .await;
15162
15163 cx.editor(|editor, _, _| {
15164 let signature_help_state = editor.signature_help_state.popover().cloned();
15165 assert!(signature_help_state.is_some());
15166 let signature = signature_help_state.unwrap();
15167 assert_eq!(
15168 signature.signatures[signature.current_signature].label,
15169 "fn sample(param1: u8, param2: u8)"
15170 );
15171 });
15172
15173 // When exiting outside from inside the brackets, `signature_help` is closed.
15174 cx.set_state(indoc! {"
15175 fn main() {
15176 sample(ˇ);
15177 }
15178
15179 fn sample(param1: u8, param2: u8) {}
15180 "});
15181
15182 cx.update_editor(|editor, window, cx| {
15183 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15184 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
15185 });
15186 });
15187
15188 let mocked_response = lsp::SignatureHelp {
15189 signatures: Vec::new(),
15190 active_signature: None,
15191 active_parameter: None,
15192 };
15193 handle_signature_help_request(&mut cx, mocked_response).await;
15194
15195 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
15196 .await;
15197
15198 cx.editor(|editor, _, _| {
15199 assert!(!editor.signature_help_state.is_shown());
15200 });
15201
15202 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
15203 cx.set_state(indoc! {"
15204 fn main() {
15205 sample(ˇ);
15206 }
15207
15208 fn sample(param1: u8, param2: u8) {}
15209 "});
15210
15211 let mocked_response = lsp::SignatureHelp {
15212 signatures: vec![lsp::SignatureInformation {
15213 label: "fn sample(param1: u8, param2: u8)".to_string(),
15214 documentation: None,
15215 parameters: Some(vec![
15216 lsp::ParameterInformation {
15217 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15218 documentation: None,
15219 },
15220 lsp::ParameterInformation {
15221 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15222 documentation: None,
15223 },
15224 ]),
15225 active_parameter: None,
15226 }],
15227 active_signature: Some(0),
15228 active_parameter: Some(0),
15229 };
15230 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
15231 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15232 .await;
15233 cx.editor(|editor, _, _| {
15234 assert!(editor.signature_help_state.is_shown());
15235 });
15236
15237 // Restore the popover with more parameter input
15238 cx.set_state(indoc! {"
15239 fn main() {
15240 sample(param1, param2ˇ);
15241 }
15242
15243 fn sample(param1: u8, param2: u8) {}
15244 "});
15245
15246 let mocked_response = lsp::SignatureHelp {
15247 signatures: vec![lsp::SignatureInformation {
15248 label: "fn sample(param1: u8, param2: u8)".to_string(),
15249 documentation: None,
15250 parameters: Some(vec![
15251 lsp::ParameterInformation {
15252 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15253 documentation: None,
15254 },
15255 lsp::ParameterInformation {
15256 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15257 documentation: None,
15258 },
15259 ]),
15260 active_parameter: None,
15261 }],
15262 active_signature: Some(0),
15263 active_parameter: Some(1),
15264 };
15265 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
15266 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15267 .await;
15268
15269 // When selecting a range, the popover is gone.
15270 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
15271 cx.update_editor(|editor, window, cx| {
15272 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15273 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
15274 })
15275 });
15276 cx.assert_editor_state(indoc! {"
15277 fn main() {
15278 sample(param1, «ˇparam2»);
15279 }
15280
15281 fn sample(param1: u8, param2: u8) {}
15282 "});
15283 cx.editor(|editor, _, _| {
15284 assert!(!editor.signature_help_state.is_shown());
15285 });
15286
15287 // When unselecting again, the popover is back if within the brackets.
15288 cx.update_editor(|editor, window, cx| {
15289 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15290 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
15291 })
15292 });
15293 cx.assert_editor_state(indoc! {"
15294 fn main() {
15295 sample(param1, ˇparam2);
15296 }
15297
15298 fn sample(param1: u8, param2: u8) {}
15299 "});
15300 handle_signature_help_request(&mut cx, mocked_response).await;
15301 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15302 .await;
15303 cx.editor(|editor, _, _| {
15304 assert!(editor.signature_help_state.is_shown());
15305 });
15306
15307 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
15308 cx.update_editor(|editor, window, cx| {
15309 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15310 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
15311 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
15312 })
15313 });
15314 cx.assert_editor_state(indoc! {"
15315 fn main() {
15316 sample(param1, ˇparam2);
15317 }
15318
15319 fn sample(param1: u8, param2: u8) {}
15320 "});
15321
15322 let mocked_response = lsp::SignatureHelp {
15323 signatures: vec![lsp::SignatureInformation {
15324 label: "fn sample(param1: u8, param2: u8)".to_string(),
15325 documentation: None,
15326 parameters: Some(vec![
15327 lsp::ParameterInformation {
15328 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
15329 documentation: None,
15330 },
15331 lsp::ParameterInformation {
15332 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
15333 documentation: None,
15334 },
15335 ]),
15336 active_parameter: None,
15337 }],
15338 active_signature: Some(0),
15339 active_parameter: Some(1),
15340 };
15341 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
15342 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15343 .await;
15344 cx.update_editor(|editor, _, cx| {
15345 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
15346 });
15347 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
15348 .await;
15349 cx.update_editor(|editor, window, cx| {
15350 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15351 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
15352 })
15353 });
15354 cx.assert_editor_state(indoc! {"
15355 fn main() {
15356 sample(param1, «ˇparam2»);
15357 }
15358
15359 fn sample(param1: u8, param2: u8) {}
15360 "});
15361 cx.update_editor(|editor, window, cx| {
15362 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15363 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
15364 })
15365 });
15366 cx.assert_editor_state(indoc! {"
15367 fn main() {
15368 sample(param1, ˇparam2);
15369 }
15370
15371 fn sample(param1: u8, param2: u8) {}
15372 "});
15373 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
15374 .await;
15375}
15376
15377#[gpui::test]
15378async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
15379 init_test(cx, |_| {});
15380
15381 let mut cx = EditorLspTestContext::new_rust(
15382 lsp::ServerCapabilities {
15383 signature_help_provider: Some(lsp::SignatureHelpOptions {
15384 ..Default::default()
15385 }),
15386 ..Default::default()
15387 },
15388 cx,
15389 )
15390 .await;
15391
15392 cx.set_state(indoc! {"
15393 fn main() {
15394 overloadedˇ
15395 }
15396 "});
15397
15398 cx.update_editor(|editor, window, cx| {
15399 editor.handle_input("(", window, cx);
15400 editor.show_signature_help(&ShowSignatureHelp, window, cx);
15401 });
15402
15403 // Mock response with 3 signatures
15404 let mocked_response = lsp::SignatureHelp {
15405 signatures: vec![
15406 lsp::SignatureInformation {
15407 label: "fn overloaded(x: i32)".to_string(),
15408 documentation: None,
15409 parameters: Some(vec![lsp::ParameterInformation {
15410 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
15411 documentation: None,
15412 }]),
15413 active_parameter: None,
15414 },
15415 lsp::SignatureInformation {
15416 label: "fn overloaded(x: i32, y: i32)".to_string(),
15417 documentation: None,
15418 parameters: Some(vec![
15419 lsp::ParameterInformation {
15420 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
15421 documentation: None,
15422 },
15423 lsp::ParameterInformation {
15424 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
15425 documentation: None,
15426 },
15427 ]),
15428 active_parameter: None,
15429 },
15430 lsp::SignatureInformation {
15431 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
15432 documentation: None,
15433 parameters: Some(vec![
15434 lsp::ParameterInformation {
15435 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
15436 documentation: None,
15437 },
15438 lsp::ParameterInformation {
15439 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
15440 documentation: None,
15441 },
15442 lsp::ParameterInformation {
15443 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
15444 documentation: None,
15445 },
15446 ]),
15447 active_parameter: None,
15448 },
15449 ],
15450 active_signature: Some(1),
15451 active_parameter: Some(0),
15452 };
15453 handle_signature_help_request(&mut cx, mocked_response).await;
15454
15455 cx.condition(|editor, _| editor.signature_help_state.is_shown())
15456 .await;
15457
15458 // Verify we have multiple signatures and the right one is selected
15459 cx.editor(|editor, _, _| {
15460 let popover = editor.signature_help_state.popover().cloned().unwrap();
15461 assert_eq!(popover.signatures.len(), 3);
15462 // active_signature was 1, so that should be the current
15463 assert_eq!(popover.current_signature, 1);
15464 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
15465 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
15466 assert_eq!(
15467 popover.signatures[2].label,
15468 "fn overloaded(x: i32, y: i32, z: i32)"
15469 );
15470 });
15471
15472 // Test navigation functionality
15473 cx.update_editor(|editor, window, cx| {
15474 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
15475 });
15476
15477 cx.editor(|editor, _, _| {
15478 let popover = editor.signature_help_state.popover().cloned().unwrap();
15479 assert_eq!(popover.current_signature, 2);
15480 });
15481
15482 // Test wrap around
15483 cx.update_editor(|editor, window, cx| {
15484 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
15485 });
15486
15487 cx.editor(|editor, _, _| {
15488 let popover = editor.signature_help_state.popover().cloned().unwrap();
15489 assert_eq!(popover.current_signature, 0);
15490 });
15491
15492 // Test previous navigation
15493 cx.update_editor(|editor, window, cx| {
15494 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
15495 });
15496
15497 cx.editor(|editor, _, _| {
15498 let popover = editor.signature_help_state.popover().cloned().unwrap();
15499 assert_eq!(popover.current_signature, 2);
15500 });
15501}
15502
15503#[gpui::test]
15504async fn test_completion_mode(cx: &mut TestAppContext) {
15505 init_test(cx, |_| {});
15506 let mut cx = EditorLspTestContext::new_rust(
15507 lsp::ServerCapabilities {
15508 completion_provider: Some(lsp::CompletionOptions {
15509 resolve_provider: Some(true),
15510 ..Default::default()
15511 }),
15512 ..Default::default()
15513 },
15514 cx,
15515 )
15516 .await;
15517
15518 struct Run {
15519 run_description: &'static str,
15520 initial_state: String,
15521 buffer_marked_text: String,
15522 completion_label: &'static str,
15523 completion_text: &'static str,
15524 expected_with_insert_mode: String,
15525 expected_with_replace_mode: String,
15526 expected_with_replace_subsequence_mode: String,
15527 expected_with_replace_suffix_mode: String,
15528 }
15529
15530 let runs = [
15531 Run {
15532 run_description: "Start of word matches completion text",
15533 initial_state: "before ediˇ after".into(),
15534 buffer_marked_text: "before <edi|> after".into(),
15535 completion_label: "editor",
15536 completion_text: "editor",
15537 expected_with_insert_mode: "before editorˇ after".into(),
15538 expected_with_replace_mode: "before editorˇ after".into(),
15539 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
15540 expected_with_replace_suffix_mode: "before editorˇ after".into(),
15541 },
15542 Run {
15543 run_description: "Accept same text at the middle of the word",
15544 initial_state: "before ediˇtor after".into(),
15545 buffer_marked_text: "before <edi|tor> after".into(),
15546 completion_label: "editor",
15547 completion_text: "editor",
15548 expected_with_insert_mode: "before editorˇtor after".into(),
15549 expected_with_replace_mode: "before editorˇ after".into(),
15550 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
15551 expected_with_replace_suffix_mode: "before editorˇ after".into(),
15552 },
15553 Run {
15554 run_description: "End of word matches completion text -- cursor at end",
15555 initial_state: "before torˇ after".into(),
15556 buffer_marked_text: "before <tor|> after".into(),
15557 completion_label: "editor",
15558 completion_text: "editor",
15559 expected_with_insert_mode: "before editorˇ after".into(),
15560 expected_with_replace_mode: "before editorˇ after".into(),
15561 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
15562 expected_with_replace_suffix_mode: "before editorˇ after".into(),
15563 },
15564 Run {
15565 run_description: "End of word matches completion text -- cursor at start",
15566 initial_state: "before ˇtor after".into(),
15567 buffer_marked_text: "before <|tor> after".into(),
15568 completion_label: "editor",
15569 completion_text: "editor",
15570 expected_with_insert_mode: "before editorˇtor after".into(),
15571 expected_with_replace_mode: "before editorˇ after".into(),
15572 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
15573 expected_with_replace_suffix_mode: "before editorˇ after".into(),
15574 },
15575 Run {
15576 run_description: "Prepend text containing whitespace",
15577 initial_state: "pˇfield: bool".into(),
15578 buffer_marked_text: "<p|field>: bool".into(),
15579 completion_label: "pub ",
15580 completion_text: "pub ",
15581 expected_with_insert_mode: "pub ˇfield: bool".into(),
15582 expected_with_replace_mode: "pub ˇ: bool".into(),
15583 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
15584 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
15585 },
15586 Run {
15587 run_description: "Add element to start of list",
15588 initial_state: "[element_ˇelement_2]".into(),
15589 buffer_marked_text: "[<element_|element_2>]".into(),
15590 completion_label: "element_1",
15591 completion_text: "element_1",
15592 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
15593 expected_with_replace_mode: "[element_1ˇ]".into(),
15594 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
15595 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
15596 },
15597 Run {
15598 run_description: "Add element to start of list -- first and second elements are equal",
15599 initial_state: "[elˇelement]".into(),
15600 buffer_marked_text: "[<el|element>]".into(),
15601 completion_label: "element",
15602 completion_text: "element",
15603 expected_with_insert_mode: "[elementˇelement]".into(),
15604 expected_with_replace_mode: "[elementˇ]".into(),
15605 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
15606 expected_with_replace_suffix_mode: "[elementˇ]".into(),
15607 },
15608 Run {
15609 run_description: "Ends with matching suffix",
15610 initial_state: "SubˇError".into(),
15611 buffer_marked_text: "<Sub|Error>".into(),
15612 completion_label: "SubscriptionError",
15613 completion_text: "SubscriptionError",
15614 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
15615 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
15616 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
15617 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
15618 },
15619 Run {
15620 run_description: "Suffix is a subsequence -- contiguous",
15621 initial_state: "SubˇErr".into(),
15622 buffer_marked_text: "<Sub|Err>".into(),
15623 completion_label: "SubscriptionError",
15624 completion_text: "SubscriptionError",
15625 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
15626 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
15627 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
15628 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
15629 },
15630 Run {
15631 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
15632 initial_state: "Suˇscrirr".into(),
15633 buffer_marked_text: "<Su|scrirr>".into(),
15634 completion_label: "SubscriptionError",
15635 completion_text: "SubscriptionError",
15636 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
15637 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
15638 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
15639 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
15640 },
15641 Run {
15642 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
15643 initial_state: "foo(indˇix)".into(),
15644 buffer_marked_text: "foo(<ind|ix>)".into(),
15645 completion_label: "node_index",
15646 completion_text: "node_index",
15647 expected_with_insert_mode: "foo(node_indexˇix)".into(),
15648 expected_with_replace_mode: "foo(node_indexˇ)".into(),
15649 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
15650 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
15651 },
15652 Run {
15653 run_description: "Replace range ends before cursor - should extend to cursor",
15654 initial_state: "before editˇo after".into(),
15655 buffer_marked_text: "before <{ed}>it|o after".into(),
15656 completion_label: "editor",
15657 completion_text: "editor",
15658 expected_with_insert_mode: "before editorˇo after".into(),
15659 expected_with_replace_mode: "before editorˇo after".into(),
15660 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
15661 expected_with_replace_suffix_mode: "before editorˇo after".into(),
15662 },
15663 Run {
15664 run_description: "Uses label for suffix matching",
15665 initial_state: "before ediˇtor after".into(),
15666 buffer_marked_text: "before <edi|tor> after".into(),
15667 completion_label: "editor",
15668 completion_text: "editor()",
15669 expected_with_insert_mode: "before editor()ˇtor after".into(),
15670 expected_with_replace_mode: "before editor()ˇ after".into(),
15671 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
15672 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
15673 },
15674 Run {
15675 run_description: "Case insensitive subsequence and suffix matching",
15676 initial_state: "before EDiˇtoR after".into(),
15677 buffer_marked_text: "before <EDi|toR> after".into(),
15678 completion_label: "editor",
15679 completion_text: "editor",
15680 expected_with_insert_mode: "before editorˇtoR after".into(),
15681 expected_with_replace_mode: "before editorˇ after".into(),
15682 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
15683 expected_with_replace_suffix_mode: "before editorˇ after".into(),
15684 },
15685 ];
15686
15687 for run in runs {
15688 let run_variations = [
15689 (LspInsertMode::Insert, run.expected_with_insert_mode),
15690 (LspInsertMode::Replace, run.expected_with_replace_mode),
15691 (
15692 LspInsertMode::ReplaceSubsequence,
15693 run.expected_with_replace_subsequence_mode,
15694 ),
15695 (
15696 LspInsertMode::ReplaceSuffix,
15697 run.expected_with_replace_suffix_mode,
15698 ),
15699 ];
15700
15701 for (lsp_insert_mode, expected_text) in run_variations {
15702 eprintln!(
15703 "run = {:?}, mode = {lsp_insert_mode:.?}",
15704 run.run_description,
15705 );
15706
15707 update_test_language_settings(&mut cx, &|settings| {
15708 settings.defaults.completions = Some(CompletionSettingsContent {
15709 lsp_insert_mode: Some(lsp_insert_mode),
15710 words: Some(WordsCompletionMode::Disabled),
15711 words_min_length: Some(0),
15712 ..Default::default()
15713 });
15714 });
15715
15716 cx.set_state(&run.initial_state);
15717
15718 // Set up resolve handler before showing completions, since resolve may be
15719 // triggered when menu becomes visible (for documentation), not just on confirm.
15720 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(
15721 move |_, _, _| async move {
15722 Ok(lsp::CompletionItem {
15723 additional_text_edits: None,
15724 ..Default::default()
15725 })
15726 },
15727 );
15728
15729 cx.update_editor(|editor, window, cx| {
15730 editor.show_completions(&ShowCompletions, window, cx);
15731 });
15732
15733 let counter = Arc::new(AtomicUsize::new(0));
15734 handle_completion_request_with_insert_and_replace(
15735 &mut cx,
15736 &run.buffer_marked_text,
15737 vec![(run.completion_label, run.completion_text)],
15738 counter.clone(),
15739 )
15740 .await;
15741 cx.condition(|editor, _| editor.context_menu_visible())
15742 .await;
15743 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15744
15745 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15746 editor
15747 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15748 .unwrap()
15749 });
15750 cx.assert_editor_state(&expected_text);
15751 apply_additional_edits.await.unwrap();
15752 }
15753 }
15754}
15755
15756#[gpui::test]
15757async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
15758 init_test(cx, |_| {});
15759 let mut cx = EditorLspTestContext::new_rust(
15760 lsp::ServerCapabilities {
15761 completion_provider: Some(lsp::CompletionOptions {
15762 resolve_provider: Some(true),
15763 ..Default::default()
15764 }),
15765 ..Default::default()
15766 },
15767 cx,
15768 )
15769 .await;
15770
15771 let initial_state = "SubˇError";
15772 let buffer_marked_text = "<Sub|Error>";
15773 let completion_text = "SubscriptionError";
15774 let expected_with_insert_mode = "SubscriptionErrorˇError";
15775 let expected_with_replace_mode = "SubscriptionErrorˇ";
15776
15777 update_test_language_settings(&mut cx, &|settings| {
15778 settings.defaults.completions = Some(CompletionSettingsContent {
15779 words: Some(WordsCompletionMode::Disabled),
15780 words_min_length: Some(0),
15781 // set the opposite here to ensure that the action is overriding the default behavior
15782 lsp_insert_mode: Some(LspInsertMode::Insert),
15783 ..Default::default()
15784 });
15785 });
15786
15787 cx.set_state(initial_state);
15788 cx.update_editor(|editor, window, cx| {
15789 editor.show_completions(&ShowCompletions, window, cx);
15790 });
15791
15792 let counter = Arc::new(AtomicUsize::new(0));
15793 handle_completion_request_with_insert_and_replace(
15794 &mut cx,
15795 buffer_marked_text,
15796 vec![(completion_text, completion_text)],
15797 counter.clone(),
15798 )
15799 .await;
15800 cx.condition(|editor, _| editor.context_menu_visible())
15801 .await;
15802 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15803
15804 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15805 editor
15806 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
15807 .unwrap()
15808 });
15809 cx.assert_editor_state(expected_with_replace_mode);
15810 handle_resolve_completion_request(&mut cx, None).await;
15811 apply_additional_edits.await.unwrap();
15812
15813 update_test_language_settings(&mut cx, &|settings| {
15814 settings.defaults.completions = Some(CompletionSettingsContent {
15815 words: Some(WordsCompletionMode::Disabled),
15816 words_min_length: Some(0),
15817 // set the opposite here to ensure that the action is overriding the default behavior
15818 lsp_insert_mode: Some(LspInsertMode::Replace),
15819 ..Default::default()
15820 });
15821 });
15822
15823 cx.set_state(initial_state);
15824 cx.update_editor(|editor, window, cx| {
15825 editor.show_completions(&ShowCompletions, window, cx);
15826 });
15827 handle_completion_request_with_insert_and_replace(
15828 &mut cx,
15829 buffer_marked_text,
15830 vec![(completion_text, completion_text)],
15831 counter.clone(),
15832 )
15833 .await;
15834 cx.condition(|editor, _| editor.context_menu_visible())
15835 .await;
15836 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15837
15838 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15839 editor
15840 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
15841 .unwrap()
15842 });
15843 cx.assert_editor_state(expected_with_insert_mode);
15844 handle_resolve_completion_request(&mut cx, None).await;
15845 apply_additional_edits.await.unwrap();
15846}
15847
15848#[gpui::test]
15849async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
15850 init_test(cx, |_| {});
15851 let mut cx = EditorLspTestContext::new_rust(
15852 lsp::ServerCapabilities {
15853 completion_provider: Some(lsp::CompletionOptions {
15854 resolve_provider: Some(true),
15855 ..Default::default()
15856 }),
15857 ..Default::default()
15858 },
15859 cx,
15860 )
15861 .await;
15862
15863 // scenario: surrounding text matches completion text
15864 let completion_text = "to_offset";
15865 let initial_state = indoc! {"
15866 1. buf.to_offˇsuffix
15867 2. buf.to_offˇsuf
15868 3. buf.to_offˇfix
15869 4. buf.to_offˇ
15870 5. into_offˇensive
15871 6. ˇsuffix
15872 7. let ˇ //
15873 8. aaˇzz
15874 9. buf.to_off«zzzzzˇ»suffix
15875 10. buf.«ˇzzzzz»suffix
15876 11. to_off«ˇzzzzz»
15877
15878 buf.to_offˇsuffix // newest cursor
15879 "};
15880 let completion_marked_buffer = indoc! {"
15881 1. buf.to_offsuffix
15882 2. buf.to_offsuf
15883 3. buf.to_offfix
15884 4. buf.to_off
15885 5. into_offensive
15886 6. suffix
15887 7. let //
15888 8. aazz
15889 9. buf.to_offzzzzzsuffix
15890 10. buf.zzzzzsuffix
15891 11. to_offzzzzz
15892
15893 buf.<to_off|suffix> // newest cursor
15894 "};
15895 let expected = indoc! {"
15896 1. buf.to_offsetˇ
15897 2. buf.to_offsetˇsuf
15898 3. buf.to_offsetˇfix
15899 4. buf.to_offsetˇ
15900 5. into_offsetˇensive
15901 6. to_offsetˇsuffix
15902 7. let to_offsetˇ //
15903 8. aato_offsetˇzz
15904 9. buf.to_offsetˇ
15905 10. buf.to_offsetˇsuffix
15906 11. to_offsetˇ
15907
15908 buf.to_offsetˇ // newest cursor
15909 "};
15910 cx.set_state(initial_state);
15911 cx.update_editor(|editor, window, cx| {
15912 editor.show_completions(&ShowCompletions, window, cx);
15913 });
15914 handle_completion_request_with_insert_and_replace(
15915 &mut cx,
15916 completion_marked_buffer,
15917 vec![(completion_text, completion_text)],
15918 Arc::new(AtomicUsize::new(0)),
15919 )
15920 .await;
15921 cx.condition(|editor, _| editor.context_menu_visible())
15922 .await;
15923 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15924 editor
15925 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
15926 .unwrap()
15927 });
15928 cx.assert_editor_state(expected);
15929 handle_resolve_completion_request(&mut cx, None).await;
15930 apply_additional_edits.await.unwrap();
15931
15932 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
15933 let completion_text = "foo_and_bar";
15934 let initial_state = indoc! {"
15935 1. ooanbˇ
15936 2. zooanbˇ
15937 3. ooanbˇz
15938 4. zooanbˇz
15939 5. ooanˇ
15940 6. oanbˇ
15941
15942 ooanbˇ
15943 "};
15944 let completion_marked_buffer = indoc! {"
15945 1. ooanb
15946 2. zooanb
15947 3. ooanbz
15948 4. zooanbz
15949 5. ooan
15950 6. oanb
15951
15952 <ooanb|>
15953 "};
15954 let expected = indoc! {"
15955 1. foo_and_barˇ
15956 2. zfoo_and_barˇ
15957 3. foo_and_barˇz
15958 4. zfoo_and_barˇz
15959 5. ooanfoo_and_barˇ
15960 6. oanbfoo_and_barˇ
15961
15962 foo_and_barˇ
15963 "};
15964 cx.set_state(initial_state);
15965 cx.update_editor(|editor, window, cx| {
15966 editor.show_completions(&ShowCompletions, window, cx);
15967 });
15968 handle_completion_request_with_insert_and_replace(
15969 &mut cx,
15970 completion_marked_buffer,
15971 vec![(completion_text, completion_text)],
15972 Arc::new(AtomicUsize::new(0)),
15973 )
15974 .await;
15975 cx.condition(|editor, _| editor.context_menu_visible())
15976 .await;
15977 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15978 editor
15979 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
15980 .unwrap()
15981 });
15982 cx.assert_editor_state(expected);
15983 handle_resolve_completion_request(&mut cx, None).await;
15984 apply_additional_edits.await.unwrap();
15985
15986 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
15987 // (expects the same as if it was inserted at the end)
15988 let completion_text = "foo_and_bar";
15989 let initial_state = indoc! {"
15990 1. ooˇanb
15991 2. zooˇanb
15992 3. ooˇanbz
15993 4. zooˇanbz
15994
15995 ooˇanb
15996 "};
15997 let completion_marked_buffer = indoc! {"
15998 1. ooanb
15999 2. zooanb
16000 3. ooanbz
16001 4. zooanbz
16002
16003 <oo|anb>
16004 "};
16005 let expected = indoc! {"
16006 1. foo_and_barˇ
16007 2. zfoo_and_barˇ
16008 3. foo_and_barˇz
16009 4. zfoo_and_barˇz
16010
16011 foo_and_barˇ
16012 "};
16013 cx.set_state(initial_state);
16014 cx.update_editor(|editor, window, cx| {
16015 editor.show_completions(&ShowCompletions, window, cx);
16016 });
16017 handle_completion_request_with_insert_and_replace(
16018 &mut cx,
16019 completion_marked_buffer,
16020 vec![(completion_text, completion_text)],
16021 Arc::new(AtomicUsize::new(0)),
16022 )
16023 .await;
16024 cx.condition(|editor, _| editor.context_menu_visible())
16025 .await;
16026 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16027 editor
16028 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
16029 .unwrap()
16030 });
16031 cx.assert_editor_state(expected);
16032 handle_resolve_completion_request(&mut cx, None).await;
16033 apply_additional_edits.await.unwrap();
16034}
16035
16036// This used to crash
16037#[gpui::test]
16038async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
16039 init_test(cx, |_| {});
16040
16041 let buffer_text = indoc! {"
16042 fn main() {
16043 10.satu;
16044
16045 //
16046 // separate1
16047 // separate2
16048 // separate3
16049 //
16050
16051 10.satu20;
16052 }
16053 "};
16054 let multibuffer_text_with_selections = indoc! {"
16055 fn main() {
16056 10.satuˇ;
16057
16058 //
16059
16060 10.satuˇ20;
16061 }
16062 "};
16063 let expected_multibuffer = indoc! {"
16064 fn main() {
16065 10.saturating_sub()ˇ;
16066
16067 //
16068
16069 10.saturating_sub()ˇ;
16070 }
16071 "};
16072
16073 let fs = FakeFs::new(cx.executor());
16074 fs.insert_tree(
16075 path!("/a"),
16076 json!({
16077 "main.rs": buffer_text,
16078 }),
16079 )
16080 .await;
16081
16082 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16083 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16084 language_registry.add(rust_lang());
16085 let mut fake_servers = language_registry.register_fake_lsp(
16086 "Rust",
16087 FakeLspAdapter {
16088 capabilities: lsp::ServerCapabilities {
16089 completion_provider: Some(lsp::CompletionOptions {
16090 resolve_provider: None,
16091 ..lsp::CompletionOptions::default()
16092 }),
16093 ..lsp::ServerCapabilities::default()
16094 },
16095 ..FakeLspAdapter::default()
16096 },
16097 );
16098 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
16099 let workspace = window
16100 .read_with(cx, |mw, _| mw.workspace().clone())
16101 .unwrap();
16102 let cx = &mut VisualTestContext::from_window(*window, cx);
16103 let buffer = project
16104 .update(cx, |project, cx| {
16105 project.open_local_buffer(path!("/a/main.rs"), cx)
16106 })
16107 .await
16108 .unwrap();
16109
16110 let multi_buffer = cx.new(|cx| {
16111 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
16112 multi_buffer.set_excerpts_for_path(
16113 PathKey::sorted(0),
16114 buffer.clone(),
16115 [
16116 Point::zero()..Point::new(2, 0),
16117 Point::new(7, 0)..buffer.read(cx).max_point(),
16118 ],
16119 0,
16120 cx,
16121 );
16122 multi_buffer
16123 });
16124
16125 let editor = workspace.update_in(cx, |_, window, cx| {
16126 cx.new(|cx| {
16127 Editor::new(
16128 EditorMode::Full {
16129 scale_ui_elements_with_buffer_font_size: false,
16130 show_active_line_background: false,
16131 sizing_behavior: SizingBehavior::Default,
16132 },
16133 multi_buffer.clone(),
16134 Some(project.clone()),
16135 window,
16136 cx,
16137 )
16138 })
16139 });
16140
16141 let pane = workspace.update_in(cx, |workspace, _, _| workspace.active_pane().clone());
16142 pane.update_in(cx, |pane, window, cx| {
16143 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
16144 });
16145
16146 let fake_server = fake_servers.next().await.unwrap();
16147 cx.run_until_parked();
16148
16149 editor.update_in(cx, |editor, window, cx| {
16150 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16151 s.select_ranges([
16152 Point::new(1, 11)..Point::new(1, 11),
16153 Point::new(5, 11)..Point::new(5, 11),
16154 ])
16155 });
16156
16157 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
16158 });
16159
16160 editor.update_in(cx, |editor, window, cx| {
16161 editor.show_completions(&ShowCompletions, window, cx);
16162 });
16163
16164 fake_server
16165 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16166 let completion_item = lsp::CompletionItem {
16167 label: "saturating_sub()".into(),
16168 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
16169 lsp::InsertReplaceEdit {
16170 new_text: "saturating_sub()".to_owned(),
16171 insert: lsp::Range::new(
16172 lsp::Position::new(9, 7),
16173 lsp::Position::new(9, 11),
16174 ),
16175 replace: lsp::Range::new(
16176 lsp::Position::new(9, 7),
16177 lsp::Position::new(9, 13),
16178 ),
16179 },
16180 )),
16181 ..lsp::CompletionItem::default()
16182 };
16183
16184 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
16185 })
16186 .next()
16187 .await
16188 .unwrap();
16189
16190 cx.condition(&editor, |editor, _| editor.context_menu_visible())
16191 .await;
16192
16193 editor
16194 .update_in(cx, |editor, window, cx| {
16195 editor
16196 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
16197 .unwrap()
16198 })
16199 .await
16200 .unwrap();
16201
16202 editor.update(cx, |editor, cx| {
16203 assert_text_with_selections(editor, expected_multibuffer, cx);
16204 })
16205}
16206
16207#[gpui::test]
16208async fn test_completion(cx: &mut TestAppContext) {
16209 init_test(cx, |_| {});
16210
16211 let mut cx = EditorLspTestContext::new_rust(
16212 lsp::ServerCapabilities {
16213 completion_provider: Some(lsp::CompletionOptions {
16214 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16215 resolve_provider: Some(true),
16216 ..Default::default()
16217 }),
16218 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
16219 ..Default::default()
16220 },
16221 cx,
16222 )
16223 .await;
16224 let counter = Arc::new(AtomicUsize::new(0));
16225
16226 cx.set_state(indoc! {"
16227 oneˇ
16228 two
16229 three
16230 "});
16231 cx.simulate_keystroke(".");
16232 handle_completion_request(
16233 indoc! {"
16234 one.|<>
16235 two
16236 three
16237 "},
16238 vec!["first_completion", "second_completion"],
16239 true,
16240 counter.clone(),
16241 &mut cx,
16242 )
16243 .await;
16244 cx.condition(|editor, _| editor.context_menu_visible())
16245 .await;
16246 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16247
16248 let _handler = handle_signature_help_request(
16249 &mut cx,
16250 lsp::SignatureHelp {
16251 signatures: vec![lsp::SignatureInformation {
16252 label: "test signature".to_string(),
16253 documentation: None,
16254 parameters: Some(vec![lsp::ParameterInformation {
16255 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
16256 documentation: None,
16257 }]),
16258 active_parameter: None,
16259 }],
16260 active_signature: None,
16261 active_parameter: None,
16262 },
16263 );
16264 cx.update_editor(|editor, window, cx| {
16265 assert!(
16266 !editor.signature_help_state.is_shown(),
16267 "No signature help was called for"
16268 );
16269 editor.show_signature_help(&ShowSignatureHelp, window, cx);
16270 });
16271 cx.run_until_parked();
16272 cx.update_editor(|editor, _, _| {
16273 assert!(
16274 !editor.signature_help_state.is_shown(),
16275 "No signature help should be shown when completions menu is open"
16276 );
16277 });
16278
16279 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16280 editor.context_menu_next(&Default::default(), window, cx);
16281 editor
16282 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16283 .unwrap()
16284 });
16285 cx.assert_editor_state(indoc! {"
16286 one.second_completionˇ
16287 two
16288 three
16289 "});
16290
16291 handle_resolve_completion_request(
16292 &mut cx,
16293 Some(vec![
16294 (
16295 //This overlaps with the primary completion edit which is
16296 //misbehavior from the LSP spec, test that we filter it out
16297 indoc! {"
16298 one.second_ˇcompletion
16299 two
16300 threeˇ
16301 "},
16302 "overlapping additional edit",
16303 ),
16304 (
16305 indoc! {"
16306 one.second_completion
16307 two
16308 threeˇ
16309 "},
16310 "\nadditional edit",
16311 ),
16312 ]),
16313 )
16314 .await;
16315 apply_additional_edits.await.unwrap();
16316 cx.assert_editor_state(indoc! {"
16317 one.second_completionˇ
16318 two
16319 three
16320 additional edit
16321 "});
16322
16323 cx.set_state(indoc! {"
16324 one.second_completion
16325 twoˇ
16326 threeˇ
16327 additional edit
16328 "});
16329 cx.simulate_keystroke(" ");
16330 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
16331 cx.simulate_keystroke("s");
16332 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
16333
16334 cx.assert_editor_state(indoc! {"
16335 one.second_completion
16336 two sˇ
16337 three sˇ
16338 additional edit
16339 "});
16340 handle_completion_request(
16341 indoc! {"
16342 one.second_completion
16343 two s
16344 three <s|>
16345 additional edit
16346 "},
16347 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
16348 true,
16349 counter.clone(),
16350 &mut cx,
16351 )
16352 .await;
16353 cx.condition(|editor, _| editor.context_menu_visible())
16354 .await;
16355 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
16356
16357 cx.simulate_keystroke("i");
16358
16359 handle_completion_request(
16360 indoc! {"
16361 one.second_completion
16362 two si
16363 three <si|>
16364 additional edit
16365 "},
16366 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
16367 true,
16368 counter.clone(),
16369 &mut cx,
16370 )
16371 .await;
16372 cx.condition(|editor, _| editor.context_menu_visible())
16373 .await;
16374 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
16375
16376 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16377 editor
16378 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16379 .unwrap()
16380 });
16381 cx.assert_editor_state(indoc! {"
16382 one.second_completion
16383 two sixth_completionˇ
16384 three sixth_completionˇ
16385 additional edit
16386 "});
16387
16388 apply_additional_edits.await.unwrap();
16389
16390 update_test_language_settings(&mut cx, &|settings| {
16391 settings.defaults.show_completions_on_input = Some(false);
16392 });
16393 cx.set_state("editorˇ");
16394 cx.simulate_keystroke(".");
16395 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
16396 cx.simulate_keystrokes("c l o");
16397 cx.assert_editor_state("editor.cloˇ");
16398 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
16399 cx.update_editor(|editor, window, cx| {
16400 editor.show_completions(&ShowCompletions, window, cx);
16401 });
16402 handle_completion_request(
16403 "editor.<clo|>",
16404 vec!["close", "clobber"],
16405 true,
16406 counter.clone(),
16407 &mut cx,
16408 )
16409 .await;
16410 cx.condition(|editor, _| editor.context_menu_visible())
16411 .await;
16412 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
16413
16414 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16415 editor
16416 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16417 .unwrap()
16418 });
16419 cx.assert_editor_state("editor.clobberˇ");
16420 handle_resolve_completion_request(&mut cx, None).await;
16421 apply_additional_edits.await.unwrap();
16422}
16423
16424#[gpui::test]
16425async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
16426 init_test(cx, |_| {});
16427
16428 let fs = FakeFs::new(cx.executor());
16429 fs.insert_tree(
16430 path!("/a"),
16431 json!({
16432 "main.rs": "",
16433 }),
16434 )
16435 .await;
16436
16437 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16438 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16439 language_registry.add(rust_lang());
16440 let command_calls = Arc::new(AtomicUsize::new(0));
16441 let registered_command = "_the/command";
16442
16443 let closure_command_calls = command_calls.clone();
16444 let mut fake_servers = language_registry.register_fake_lsp(
16445 "Rust",
16446 FakeLspAdapter {
16447 capabilities: lsp::ServerCapabilities {
16448 completion_provider: Some(lsp::CompletionOptions {
16449 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16450 ..lsp::CompletionOptions::default()
16451 }),
16452 execute_command_provider: Some(lsp::ExecuteCommandOptions {
16453 commands: vec![registered_command.to_owned()],
16454 ..lsp::ExecuteCommandOptions::default()
16455 }),
16456 ..lsp::ServerCapabilities::default()
16457 },
16458 initializer: Some(Box::new(move |fake_server| {
16459 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
16460 move |params, _| async move {
16461 Ok(Some(lsp::CompletionResponse::Array(vec![
16462 lsp::CompletionItem {
16463 label: "registered_command".to_owned(),
16464 text_edit: gen_text_edit(¶ms, ""),
16465 command: Some(lsp::Command {
16466 title: registered_command.to_owned(),
16467 command: "_the/command".to_owned(),
16468 arguments: Some(vec![serde_json::Value::Bool(true)]),
16469 }),
16470 ..lsp::CompletionItem::default()
16471 },
16472 lsp::CompletionItem {
16473 label: "unregistered_command".to_owned(),
16474 text_edit: gen_text_edit(¶ms, ""),
16475 command: Some(lsp::Command {
16476 title: "????????????".to_owned(),
16477 command: "????????????".to_owned(),
16478 arguments: Some(vec![serde_json::Value::Null]),
16479 }),
16480 ..lsp::CompletionItem::default()
16481 },
16482 ])))
16483 },
16484 );
16485 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
16486 let command_calls = closure_command_calls.clone();
16487 move |params, _| {
16488 assert_eq!(params.command, registered_command);
16489 let command_calls = command_calls.clone();
16490 async move {
16491 command_calls.fetch_add(1, atomic::Ordering::Release);
16492 Ok(Some(json!(null)))
16493 }
16494 }
16495 });
16496 })),
16497 ..FakeLspAdapter::default()
16498 },
16499 );
16500 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
16501 let workspace = window
16502 .read_with(cx, |mw, _| mw.workspace().clone())
16503 .unwrap();
16504 let cx = &mut VisualTestContext::from_window(*window, cx);
16505 let editor = workspace
16506 .update_in(cx, |workspace, window, cx| {
16507 workspace.open_abs_path(
16508 PathBuf::from(path!("/a/main.rs")),
16509 OpenOptions::default(),
16510 window,
16511 cx,
16512 )
16513 })
16514 .await
16515 .unwrap()
16516 .downcast::<Editor>()
16517 .unwrap();
16518 let _fake_server = fake_servers.next().await.unwrap();
16519 cx.run_until_parked();
16520
16521 editor.update_in(cx, |editor, window, cx| {
16522 cx.focus_self(window);
16523 editor.move_to_end(&MoveToEnd, window, cx);
16524 editor.handle_input(".", window, cx);
16525 });
16526 cx.run_until_parked();
16527 editor.update(cx, |editor, _| {
16528 assert!(editor.context_menu_visible());
16529 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16530 {
16531 let completion_labels = menu
16532 .completions
16533 .borrow()
16534 .iter()
16535 .map(|c| c.label.text.clone())
16536 .collect::<Vec<_>>();
16537 assert_eq!(
16538 completion_labels,
16539 &["registered_command", "unregistered_command",],
16540 );
16541 } else {
16542 panic!("expected completion menu to be open");
16543 }
16544 });
16545
16546 editor
16547 .update_in(cx, |editor, window, cx| {
16548 editor
16549 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16550 .unwrap()
16551 })
16552 .await
16553 .unwrap();
16554 cx.run_until_parked();
16555 assert_eq!(
16556 command_calls.load(atomic::Ordering::Acquire),
16557 1,
16558 "For completion with a registered command, Zed should send a command execution request",
16559 );
16560
16561 editor.update_in(cx, |editor, window, cx| {
16562 cx.focus_self(window);
16563 editor.handle_input(".", window, cx);
16564 });
16565 cx.run_until_parked();
16566 editor.update(cx, |editor, _| {
16567 assert!(editor.context_menu_visible());
16568 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16569 {
16570 let completion_labels = menu
16571 .completions
16572 .borrow()
16573 .iter()
16574 .map(|c| c.label.text.clone())
16575 .collect::<Vec<_>>();
16576 assert_eq!(
16577 completion_labels,
16578 &["registered_command", "unregistered_command",],
16579 );
16580 } else {
16581 panic!("expected completion menu to be open");
16582 }
16583 });
16584 editor
16585 .update_in(cx, |editor, window, cx| {
16586 editor.context_menu_next(&Default::default(), window, cx);
16587 editor
16588 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16589 .unwrap()
16590 })
16591 .await
16592 .unwrap();
16593 cx.run_until_parked();
16594 assert_eq!(
16595 command_calls.load(atomic::Ordering::Acquire),
16596 1,
16597 "For completion with an unregistered command, Zed should not send a command execution request",
16598 );
16599}
16600
16601#[gpui::test]
16602async fn test_completion_reuse(cx: &mut TestAppContext) {
16603 init_test(cx, |_| {});
16604
16605 let mut cx = EditorLspTestContext::new_rust(
16606 lsp::ServerCapabilities {
16607 completion_provider: Some(lsp::CompletionOptions {
16608 trigger_characters: Some(vec![".".to_string()]),
16609 ..Default::default()
16610 }),
16611 ..Default::default()
16612 },
16613 cx,
16614 )
16615 .await;
16616
16617 let counter = Arc::new(AtomicUsize::new(0));
16618 cx.set_state("objˇ");
16619 cx.simulate_keystroke(".");
16620
16621 // Initial completion request returns complete results
16622 let is_incomplete = false;
16623 handle_completion_request(
16624 "obj.|<>",
16625 vec!["a", "ab", "abc"],
16626 is_incomplete,
16627 counter.clone(),
16628 &mut cx,
16629 )
16630 .await;
16631 cx.run_until_parked();
16632 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16633 cx.assert_editor_state("obj.ˇ");
16634 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
16635
16636 // Type "a" - filters existing completions
16637 cx.simulate_keystroke("a");
16638 cx.run_until_parked();
16639 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16640 cx.assert_editor_state("obj.aˇ");
16641 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
16642
16643 // Type "b" - filters existing completions
16644 cx.simulate_keystroke("b");
16645 cx.run_until_parked();
16646 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16647 cx.assert_editor_state("obj.abˇ");
16648 check_displayed_completions(vec!["ab", "abc"], &mut cx);
16649
16650 // Type "c" - filters existing completions
16651 cx.simulate_keystroke("c");
16652 cx.run_until_parked();
16653 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16654 cx.assert_editor_state("obj.abcˇ");
16655 check_displayed_completions(vec!["abc"], &mut cx);
16656
16657 // Backspace to delete "c" - filters existing completions
16658 cx.update_editor(|editor, window, cx| {
16659 editor.backspace(&Backspace, window, cx);
16660 });
16661 cx.run_until_parked();
16662 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16663 cx.assert_editor_state("obj.abˇ");
16664 check_displayed_completions(vec!["ab", "abc"], &mut cx);
16665
16666 // Moving cursor to the left dismisses menu.
16667 cx.update_editor(|editor, window, cx| {
16668 editor.move_left(&MoveLeft, window, cx);
16669 });
16670 cx.run_until_parked();
16671 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16672 cx.assert_editor_state("obj.aˇb");
16673 cx.update_editor(|editor, _, _| {
16674 assert_eq!(editor.context_menu_visible(), false);
16675 });
16676
16677 // Type "b" - new request
16678 cx.simulate_keystroke("b");
16679 let is_incomplete = false;
16680 handle_completion_request(
16681 "obj.<ab|>a",
16682 vec!["ab", "abc"],
16683 is_incomplete,
16684 counter.clone(),
16685 &mut cx,
16686 )
16687 .await;
16688 cx.run_until_parked();
16689 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
16690 cx.assert_editor_state("obj.abˇb");
16691 check_displayed_completions(vec!["ab", "abc"], &mut cx);
16692
16693 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
16694 cx.update_editor(|editor, window, cx| {
16695 editor.backspace(&Backspace, window, cx);
16696 });
16697 let is_incomplete = false;
16698 handle_completion_request(
16699 "obj.<a|>b",
16700 vec!["a", "ab", "abc"],
16701 is_incomplete,
16702 counter.clone(),
16703 &mut cx,
16704 )
16705 .await;
16706 cx.run_until_parked();
16707 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
16708 cx.assert_editor_state("obj.aˇb");
16709 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
16710
16711 // Backspace to delete "a" - dismisses menu.
16712 cx.update_editor(|editor, window, cx| {
16713 editor.backspace(&Backspace, window, cx);
16714 });
16715 cx.run_until_parked();
16716 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
16717 cx.assert_editor_state("obj.ˇb");
16718 cx.update_editor(|editor, _, _| {
16719 assert_eq!(editor.context_menu_visible(), false);
16720 });
16721}
16722
16723#[gpui::test]
16724async fn test_word_completion(cx: &mut TestAppContext) {
16725 let lsp_fetch_timeout_ms = 10;
16726 init_test(cx, |language_settings| {
16727 language_settings.defaults.completions = Some(CompletionSettingsContent {
16728 words_min_length: Some(0),
16729 lsp_fetch_timeout_ms: Some(10),
16730 lsp_insert_mode: Some(LspInsertMode::Insert),
16731 ..Default::default()
16732 });
16733 });
16734
16735 let mut cx = EditorLspTestContext::new_rust(
16736 lsp::ServerCapabilities {
16737 completion_provider: Some(lsp::CompletionOptions {
16738 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16739 ..lsp::CompletionOptions::default()
16740 }),
16741 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
16742 ..lsp::ServerCapabilities::default()
16743 },
16744 cx,
16745 )
16746 .await;
16747
16748 let throttle_completions = Arc::new(AtomicBool::new(false));
16749
16750 let lsp_throttle_completions = throttle_completions.clone();
16751 let _completion_requests_handler =
16752 cx.lsp
16753 .server
16754 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
16755 let lsp_throttle_completions = lsp_throttle_completions.clone();
16756 let cx = cx.clone();
16757 async move {
16758 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
16759 cx.background_executor()
16760 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
16761 .await;
16762 }
16763 Ok(Some(lsp::CompletionResponse::Array(vec![
16764 lsp::CompletionItem {
16765 label: "first".into(),
16766 ..lsp::CompletionItem::default()
16767 },
16768 lsp::CompletionItem {
16769 label: "last".into(),
16770 ..lsp::CompletionItem::default()
16771 },
16772 ])))
16773 }
16774 });
16775
16776 cx.set_state(indoc! {"
16777 oneˇ
16778 two
16779 three
16780 "});
16781 cx.simulate_keystroke(".");
16782 cx.executor().run_until_parked();
16783 cx.condition(|editor, _| editor.context_menu_visible())
16784 .await;
16785 cx.update_editor(|editor, window, cx| {
16786 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16787 {
16788 assert_eq!(
16789 completion_menu_entries(menu),
16790 &["first", "last"],
16791 "When LSP server is fast to reply, no fallback word completions are used"
16792 );
16793 } else {
16794 panic!("expected completion menu to be open");
16795 }
16796 editor.cancel(&Cancel, window, cx);
16797 });
16798 cx.executor().run_until_parked();
16799 cx.condition(|editor, _| !editor.context_menu_visible())
16800 .await;
16801
16802 throttle_completions.store(true, atomic::Ordering::Release);
16803 cx.simulate_keystroke(".");
16804 cx.executor()
16805 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
16806 cx.executor().run_until_parked();
16807 cx.condition(|editor, _| editor.context_menu_visible())
16808 .await;
16809 cx.update_editor(|editor, _, _| {
16810 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16811 {
16812 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
16813 "When LSP server is slow, document words can be shown instead, if configured accordingly");
16814 } else {
16815 panic!("expected completion menu to be open");
16816 }
16817 });
16818}
16819
16820#[gpui::test]
16821async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
16822 init_test(cx, |language_settings| {
16823 language_settings.defaults.completions = Some(CompletionSettingsContent {
16824 words: Some(WordsCompletionMode::Enabled),
16825 words_min_length: Some(0),
16826 lsp_insert_mode: Some(LspInsertMode::Insert),
16827 ..Default::default()
16828 });
16829 });
16830
16831 let mut cx = EditorLspTestContext::new_rust(
16832 lsp::ServerCapabilities {
16833 completion_provider: Some(lsp::CompletionOptions {
16834 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16835 ..lsp::CompletionOptions::default()
16836 }),
16837 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
16838 ..lsp::ServerCapabilities::default()
16839 },
16840 cx,
16841 )
16842 .await;
16843
16844 let _completion_requests_handler =
16845 cx.lsp
16846 .server
16847 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
16848 Ok(Some(lsp::CompletionResponse::Array(vec![
16849 lsp::CompletionItem {
16850 label: "first".into(),
16851 ..lsp::CompletionItem::default()
16852 },
16853 lsp::CompletionItem {
16854 label: "last".into(),
16855 ..lsp::CompletionItem::default()
16856 },
16857 ])))
16858 });
16859
16860 cx.set_state(indoc! {"ˇ
16861 first
16862 last
16863 second
16864 "});
16865 cx.simulate_keystroke(".");
16866 cx.executor().run_until_parked();
16867 cx.condition(|editor, _| editor.context_menu_visible())
16868 .await;
16869 cx.update_editor(|editor, _, _| {
16870 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16871 {
16872 assert_eq!(
16873 completion_menu_entries(menu),
16874 &["first", "last", "second"],
16875 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
16876 );
16877 } else {
16878 panic!("expected completion menu to be open");
16879 }
16880 });
16881}
16882
16883#[gpui::test]
16884async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
16885 init_test(cx, |language_settings| {
16886 language_settings.defaults.completions = Some(CompletionSettingsContent {
16887 words: Some(WordsCompletionMode::Disabled),
16888 words_min_length: Some(0),
16889 lsp_insert_mode: Some(LspInsertMode::Insert),
16890 ..Default::default()
16891 });
16892 });
16893
16894 let mut cx = EditorLspTestContext::new_rust(
16895 lsp::ServerCapabilities {
16896 completion_provider: Some(lsp::CompletionOptions {
16897 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16898 ..lsp::CompletionOptions::default()
16899 }),
16900 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
16901 ..lsp::ServerCapabilities::default()
16902 },
16903 cx,
16904 )
16905 .await;
16906
16907 let _completion_requests_handler =
16908 cx.lsp
16909 .server
16910 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
16911 panic!("LSP completions should not be queried when dealing with word completions")
16912 });
16913
16914 cx.set_state(indoc! {"ˇ
16915 first
16916 last
16917 second
16918 "});
16919 cx.update_editor(|editor, window, cx| {
16920 editor.show_word_completions(&ShowWordCompletions, window, cx);
16921 });
16922 cx.executor().run_until_parked();
16923 cx.condition(|editor, _| editor.context_menu_visible())
16924 .await;
16925 cx.update_editor(|editor, _, _| {
16926 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16927 {
16928 assert_eq!(
16929 completion_menu_entries(menu),
16930 &["first", "last", "second"],
16931 "`ShowWordCompletions` action should show word completions"
16932 );
16933 } else {
16934 panic!("expected completion menu to be open");
16935 }
16936 });
16937
16938 cx.simulate_keystroke("l");
16939 cx.executor().run_until_parked();
16940 cx.condition(|editor, _| editor.context_menu_visible())
16941 .await;
16942 cx.update_editor(|editor, _, _| {
16943 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16944 {
16945 assert_eq!(
16946 completion_menu_entries(menu),
16947 &["last"],
16948 "After showing word completions, further editing should filter them and not query the LSP"
16949 );
16950 } else {
16951 panic!("expected completion menu to be open");
16952 }
16953 });
16954}
16955
16956#[gpui::test]
16957async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
16958 init_test(cx, |language_settings| {
16959 language_settings.defaults.completions = Some(CompletionSettingsContent {
16960 words_min_length: Some(0),
16961 lsp: Some(false),
16962 lsp_insert_mode: Some(LspInsertMode::Insert),
16963 ..Default::default()
16964 });
16965 });
16966
16967 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16968
16969 cx.set_state(indoc! {"ˇ
16970 0_usize
16971 let
16972 33
16973 4.5f32
16974 "});
16975 cx.update_editor(|editor, window, cx| {
16976 editor.show_completions(&ShowCompletions, window, cx);
16977 });
16978 cx.executor().run_until_parked();
16979 cx.condition(|editor, _| editor.context_menu_visible())
16980 .await;
16981 cx.update_editor(|editor, window, cx| {
16982 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16983 {
16984 assert_eq!(
16985 completion_menu_entries(menu),
16986 &["let"],
16987 "With no digits in the completion query, no digits should be in the word completions"
16988 );
16989 } else {
16990 panic!("expected completion menu to be open");
16991 }
16992 editor.cancel(&Cancel, window, cx);
16993 });
16994
16995 cx.set_state(indoc! {"3ˇ
16996 0_usize
16997 let
16998 3
16999 33.35f32
17000 "});
17001 cx.update_editor(|editor, window, cx| {
17002 editor.show_completions(&ShowCompletions, window, cx);
17003 });
17004 cx.executor().run_until_parked();
17005 cx.condition(|editor, _| editor.context_menu_visible())
17006 .await;
17007 cx.update_editor(|editor, _, _| {
17008 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17009 {
17010 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
17011 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
17012 } else {
17013 panic!("expected completion menu to be open");
17014 }
17015 });
17016}
17017
17018#[gpui::test]
17019async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
17020 init_test(cx, |language_settings| {
17021 language_settings.defaults.completions = Some(CompletionSettingsContent {
17022 words: Some(WordsCompletionMode::Enabled),
17023 words_min_length: Some(3),
17024 lsp_insert_mode: Some(LspInsertMode::Insert),
17025 ..Default::default()
17026 });
17027 });
17028
17029 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17030 cx.set_state(indoc! {"ˇ
17031 wow
17032 wowen
17033 wowser
17034 "});
17035 cx.simulate_keystroke("w");
17036 cx.executor().run_until_parked();
17037 cx.update_editor(|editor, _, _| {
17038 if editor.context_menu.borrow_mut().is_some() {
17039 panic!(
17040 "expected completion menu to be hidden, as words completion threshold is not met"
17041 );
17042 }
17043 });
17044
17045 cx.update_editor(|editor, window, cx| {
17046 editor.show_word_completions(&ShowWordCompletions, window, cx);
17047 });
17048 cx.executor().run_until_parked();
17049 cx.update_editor(|editor, window, cx| {
17050 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17051 {
17052 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");
17053 } else {
17054 panic!("expected completion menu to be open after the word completions are called with an action");
17055 }
17056
17057 editor.cancel(&Cancel, window, cx);
17058 });
17059 cx.update_editor(|editor, _, _| {
17060 if editor.context_menu.borrow_mut().is_some() {
17061 panic!("expected completion menu to be hidden after canceling");
17062 }
17063 });
17064
17065 cx.simulate_keystroke("o");
17066 cx.executor().run_until_parked();
17067 cx.update_editor(|editor, _, _| {
17068 if editor.context_menu.borrow_mut().is_some() {
17069 panic!(
17070 "expected completion menu to be hidden, as words completion threshold is not met still"
17071 );
17072 }
17073 });
17074
17075 cx.simulate_keystroke("w");
17076 cx.executor().run_until_parked();
17077 cx.update_editor(|editor, _, _| {
17078 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17079 {
17080 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
17081 } else {
17082 panic!("expected completion menu to be open after the word completions threshold is met");
17083 }
17084 });
17085}
17086
17087#[gpui::test]
17088async fn test_word_completions_disabled(cx: &mut TestAppContext) {
17089 init_test(cx, |language_settings| {
17090 language_settings.defaults.completions = Some(CompletionSettingsContent {
17091 words: Some(WordsCompletionMode::Enabled),
17092 words_min_length: Some(0),
17093 lsp_insert_mode: Some(LspInsertMode::Insert),
17094 ..Default::default()
17095 });
17096 });
17097
17098 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17099 cx.update_editor(|editor, _, _| {
17100 editor.disable_word_completions();
17101 });
17102 cx.set_state(indoc! {"ˇ
17103 wow
17104 wowen
17105 wowser
17106 "});
17107 cx.simulate_keystroke("w");
17108 cx.executor().run_until_parked();
17109 cx.update_editor(|editor, _, _| {
17110 if editor.context_menu.borrow_mut().is_some() {
17111 panic!(
17112 "expected completion menu to be hidden, as words completion are disabled for this editor"
17113 );
17114 }
17115 });
17116
17117 cx.update_editor(|editor, window, cx| {
17118 editor.show_word_completions(&ShowWordCompletions, window, cx);
17119 });
17120 cx.executor().run_until_parked();
17121 cx.update_editor(|editor, _, _| {
17122 if editor.context_menu.borrow_mut().is_some() {
17123 panic!(
17124 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
17125 );
17126 }
17127 });
17128}
17129
17130#[gpui::test]
17131async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
17132 init_test(cx, |language_settings| {
17133 language_settings.defaults.completions = Some(CompletionSettingsContent {
17134 words: Some(WordsCompletionMode::Disabled),
17135 words_min_length: Some(0),
17136 lsp_insert_mode: Some(LspInsertMode::Insert),
17137 ..Default::default()
17138 });
17139 });
17140
17141 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17142 cx.update_editor(|editor, _, _| {
17143 editor.set_completion_provider(None);
17144 });
17145 cx.set_state(indoc! {"ˇ
17146 wow
17147 wowen
17148 wowser
17149 "});
17150 cx.simulate_keystroke("w");
17151 cx.executor().run_until_parked();
17152 cx.update_editor(|editor, _, _| {
17153 if editor.context_menu.borrow_mut().is_some() {
17154 panic!("expected completion menu to be hidden, as disabled in settings");
17155 }
17156 });
17157}
17158
17159fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
17160 let position = || lsp::Position {
17161 line: params.text_document_position.position.line,
17162 character: params.text_document_position.position.character,
17163 };
17164 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17165 range: lsp::Range {
17166 start: position(),
17167 end: position(),
17168 },
17169 new_text: text.to_string(),
17170 }))
17171}
17172
17173#[gpui::test]
17174async fn test_multiline_completion(cx: &mut TestAppContext) {
17175 init_test(cx, |_| {});
17176
17177 let fs = FakeFs::new(cx.executor());
17178 fs.insert_tree(
17179 path!("/a"),
17180 json!({
17181 "main.ts": "a",
17182 }),
17183 )
17184 .await;
17185
17186 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17187 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17188 let typescript_language = Arc::new(Language::new(
17189 LanguageConfig {
17190 name: "TypeScript".into(),
17191 matcher: LanguageMatcher {
17192 path_suffixes: vec!["ts".to_string()],
17193 ..LanguageMatcher::default()
17194 },
17195 line_comments: vec!["// ".into()],
17196 ..LanguageConfig::default()
17197 },
17198 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17199 ));
17200 language_registry.add(typescript_language.clone());
17201 let mut fake_servers = language_registry.register_fake_lsp(
17202 "TypeScript",
17203 FakeLspAdapter {
17204 capabilities: lsp::ServerCapabilities {
17205 completion_provider: Some(lsp::CompletionOptions {
17206 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
17207 ..lsp::CompletionOptions::default()
17208 }),
17209 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
17210 ..lsp::ServerCapabilities::default()
17211 },
17212 // Emulate vtsls label generation
17213 label_for_completion: Some(Box::new(|item, _| {
17214 let text = if let Some(description) = item
17215 .label_details
17216 .as_ref()
17217 .and_then(|label_details| label_details.description.as_ref())
17218 {
17219 format!("{} {}", item.label, description)
17220 } else if let Some(detail) = &item.detail {
17221 format!("{} {}", item.label, detail)
17222 } else {
17223 item.label.clone()
17224 };
17225 Some(language::CodeLabel::plain(text, None))
17226 })),
17227 ..FakeLspAdapter::default()
17228 },
17229 );
17230 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
17231 let workspace = window
17232 .read_with(cx, |mw, _| mw.workspace().clone())
17233 .unwrap();
17234 let cx = &mut VisualTestContext::from_window(*window, cx);
17235 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
17236 workspace.project().update(cx, |project, cx| {
17237 project.worktrees(cx).next().unwrap().read(cx).id()
17238 })
17239 });
17240
17241 let _buffer = project
17242 .update(cx, |project, cx| {
17243 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
17244 })
17245 .await
17246 .unwrap();
17247 let editor = workspace
17248 .update_in(cx, |workspace, window, cx| {
17249 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
17250 })
17251 .await
17252 .unwrap()
17253 .downcast::<Editor>()
17254 .unwrap();
17255 let fake_server = fake_servers.next().await.unwrap();
17256 cx.run_until_parked();
17257
17258 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
17259 let multiline_label_2 = "a\nb\nc\n";
17260 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
17261 let multiline_description = "d\ne\nf\n";
17262 let multiline_detail_2 = "g\nh\ni\n";
17263
17264 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
17265 move |params, _| async move {
17266 Ok(Some(lsp::CompletionResponse::Array(vec![
17267 lsp::CompletionItem {
17268 label: multiline_label.to_string(),
17269 text_edit: gen_text_edit(¶ms, "new_text_1"),
17270 ..lsp::CompletionItem::default()
17271 },
17272 lsp::CompletionItem {
17273 label: "single line label 1".to_string(),
17274 detail: Some(multiline_detail.to_string()),
17275 text_edit: gen_text_edit(¶ms, "new_text_2"),
17276 ..lsp::CompletionItem::default()
17277 },
17278 lsp::CompletionItem {
17279 label: "single line label 2".to_string(),
17280 label_details: Some(lsp::CompletionItemLabelDetails {
17281 description: Some(multiline_description.to_string()),
17282 detail: None,
17283 }),
17284 text_edit: gen_text_edit(¶ms, "new_text_2"),
17285 ..lsp::CompletionItem::default()
17286 },
17287 lsp::CompletionItem {
17288 label: multiline_label_2.to_string(),
17289 detail: Some(multiline_detail_2.to_string()),
17290 text_edit: gen_text_edit(¶ms, "new_text_3"),
17291 ..lsp::CompletionItem::default()
17292 },
17293 lsp::CompletionItem {
17294 label: "Label with many spaces and \t but without newlines".to_string(),
17295 detail: Some(
17296 "Details with many spaces and \t but without newlines".to_string(),
17297 ),
17298 text_edit: gen_text_edit(¶ms, "new_text_4"),
17299 ..lsp::CompletionItem::default()
17300 },
17301 ])))
17302 },
17303 );
17304
17305 editor.update_in(cx, |editor, window, cx| {
17306 cx.focus_self(window);
17307 editor.move_to_end(&MoveToEnd, window, cx);
17308 editor.handle_input(".", window, cx);
17309 });
17310 cx.run_until_parked();
17311 completion_handle.next().await.unwrap();
17312
17313 editor.update(cx, |editor, _| {
17314 assert!(editor.context_menu_visible());
17315 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17316 {
17317 let completion_labels = menu
17318 .completions
17319 .borrow()
17320 .iter()
17321 .map(|c| c.label.text.clone())
17322 .collect::<Vec<_>>();
17323 assert_eq!(
17324 completion_labels,
17325 &[
17326 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
17327 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
17328 "single line label 2 d e f ",
17329 "a b c g h i ",
17330 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
17331 ],
17332 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
17333 );
17334
17335 for completion in menu
17336 .completions
17337 .borrow()
17338 .iter() {
17339 assert_eq!(
17340 completion.label.filter_range,
17341 0..completion.label.text.len(),
17342 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
17343 );
17344 }
17345 } else {
17346 panic!("expected completion menu to be open");
17347 }
17348 });
17349}
17350
17351#[gpui::test]
17352async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
17353 init_test(cx, |_| {});
17354 let mut cx = EditorLspTestContext::new_rust(
17355 lsp::ServerCapabilities {
17356 completion_provider: Some(lsp::CompletionOptions {
17357 trigger_characters: Some(vec![".".to_string()]),
17358 ..Default::default()
17359 }),
17360 ..Default::default()
17361 },
17362 cx,
17363 )
17364 .await;
17365 cx.lsp
17366 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17367 Ok(Some(lsp::CompletionResponse::Array(vec![
17368 lsp::CompletionItem {
17369 label: "first".into(),
17370 ..Default::default()
17371 },
17372 lsp::CompletionItem {
17373 label: "last".into(),
17374 ..Default::default()
17375 },
17376 ])))
17377 });
17378 cx.set_state("variableˇ");
17379 cx.simulate_keystroke(".");
17380 cx.executor().run_until_parked();
17381
17382 cx.update_editor(|editor, _, _| {
17383 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17384 {
17385 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
17386 } else {
17387 panic!("expected completion menu to be open");
17388 }
17389 });
17390
17391 cx.update_editor(|editor, window, cx| {
17392 editor.move_page_down(&MovePageDown::default(), window, cx);
17393 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17394 {
17395 assert!(
17396 menu.selected_item == 1,
17397 "expected PageDown to select the last item from the context menu"
17398 );
17399 } else {
17400 panic!("expected completion menu to stay open after PageDown");
17401 }
17402 });
17403
17404 cx.update_editor(|editor, window, cx| {
17405 editor.move_page_up(&MovePageUp::default(), window, cx);
17406 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17407 {
17408 assert!(
17409 menu.selected_item == 0,
17410 "expected PageUp to select the first item from the context menu"
17411 );
17412 } else {
17413 panic!("expected completion menu to stay open after PageUp");
17414 }
17415 });
17416}
17417
17418#[gpui::test]
17419async fn test_as_is_completions(cx: &mut TestAppContext) {
17420 init_test(cx, |_| {});
17421 let mut cx = EditorLspTestContext::new_rust(
17422 lsp::ServerCapabilities {
17423 completion_provider: Some(lsp::CompletionOptions {
17424 ..Default::default()
17425 }),
17426 ..Default::default()
17427 },
17428 cx,
17429 )
17430 .await;
17431 cx.lsp
17432 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17433 Ok(Some(lsp::CompletionResponse::Array(vec![
17434 lsp::CompletionItem {
17435 label: "unsafe".into(),
17436 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17437 range: lsp::Range {
17438 start: lsp::Position {
17439 line: 1,
17440 character: 2,
17441 },
17442 end: lsp::Position {
17443 line: 1,
17444 character: 3,
17445 },
17446 },
17447 new_text: "unsafe".to_string(),
17448 })),
17449 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
17450 ..Default::default()
17451 },
17452 ])))
17453 });
17454 cx.set_state("fn a() {}\n nˇ");
17455 cx.executor().run_until_parked();
17456 cx.update_editor(|editor, window, cx| {
17457 editor.trigger_completion_on_input("n", true, window, cx)
17458 });
17459 cx.executor().run_until_parked();
17460
17461 cx.update_editor(|editor, window, cx| {
17462 editor.confirm_completion(&Default::default(), window, cx)
17463 });
17464 cx.executor().run_until_parked();
17465 cx.assert_editor_state("fn a() {}\n unsafeˇ");
17466}
17467
17468#[gpui::test]
17469async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
17470 init_test(cx, |_| {});
17471 let language =
17472 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
17473 let mut cx = EditorLspTestContext::new(
17474 language,
17475 lsp::ServerCapabilities {
17476 completion_provider: Some(lsp::CompletionOptions {
17477 ..lsp::CompletionOptions::default()
17478 }),
17479 ..lsp::ServerCapabilities::default()
17480 },
17481 cx,
17482 )
17483 .await;
17484
17485 cx.set_state(
17486 "#ifndef BAR_H
17487#define BAR_H
17488
17489#include <stdbool.h>
17490
17491int fn_branch(bool do_branch1, bool do_branch2);
17492
17493#endif // BAR_H
17494ˇ",
17495 );
17496 cx.executor().run_until_parked();
17497 cx.update_editor(|editor, window, cx| {
17498 editor.handle_input("#", window, cx);
17499 });
17500 cx.executor().run_until_parked();
17501 cx.update_editor(|editor, window, cx| {
17502 editor.handle_input("i", window, cx);
17503 });
17504 cx.executor().run_until_parked();
17505 cx.update_editor(|editor, window, cx| {
17506 editor.handle_input("n", window, cx);
17507 });
17508 cx.executor().run_until_parked();
17509 cx.assert_editor_state(
17510 "#ifndef BAR_H
17511#define BAR_H
17512
17513#include <stdbool.h>
17514
17515int fn_branch(bool do_branch1, bool do_branch2);
17516
17517#endif // BAR_H
17518#inˇ",
17519 );
17520
17521 cx.lsp
17522 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17523 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17524 is_incomplete: false,
17525 item_defaults: None,
17526 items: vec![lsp::CompletionItem {
17527 kind: Some(lsp::CompletionItemKind::SNIPPET),
17528 label_details: Some(lsp::CompletionItemLabelDetails {
17529 detail: Some("header".to_string()),
17530 description: None,
17531 }),
17532 label: " include".to_string(),
17533 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17534 range: lsp::Range {
17535 start: lsp::Position {
17536 line: 8,
17537 character: 1,
17538 },
17539 end: lsp::Position {
17540 line: 8,
17541 character: 1,
17542 },
17543 },
17544 new_text: "include \"$0\"".to_string(),
17545 })),
17546 sort_text: Some("40b67681include".to_string()),
17547 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17548 filter_text: Some("include".to_string()),
17549 insert_text: Some("include \"$0\"".to_string()),
17550 ..lsp::CompletionItem::default()
17551 }],
17552 })))
17553 });
17554 cx.update_editor(|editor, window, cx| {
17555 editor.show_completions(&ShowCompletions, window, cx);
17556 });
17557 cx.executor().run_until_parked();
17558 cx.update_editor(|editor, window, cx| {
17559 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
17560 });
17561 cx.executor().run_until_parked();
17562 cx.assert_editor_state(
17563 "#ifndef BAR_H
17564#define BAR_H
17565
17566#include <stdbool.h>
17567
17568int fn_branch(bool do_branch1, bool do_branch2);
17569
17570#endif // BAR_H
17571#include \"ˇ\"",
17572 );
17573
17574 cx.lsp
17575 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17576 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17577 is_incomplete: true,
17578 item_defaults: None,
17579 items: vec![lsp::CompletionItem {
17580 kind: Some(lsp::CompletionItemKind::FILE),
17581 label: "AGL/".to_string(),
17582 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17583 range: lsp::Range {
17584 start: lsp::Position {
17585 line: 8,
17586 character: 10,
17587 },
17588 end: lsp::Position {
17589 line: 8,
17590 character: 11,
17591 },
17592 },
17593 new_text: "AGL/".to_string(),
17594 })),
17595 sort_text: Some("40b67681AGL/".to_string()),
17596 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17597 filter_text: Some("AGL/".to_string()),
17598 insert_text: Some("AGL/".to_string()),
17599 ..lsp::CompletionItem::default()
17600 }],
17601 })))
17602 });
17603 cx.update_editor(|editor, window, cx| {
17604 editor.show_completions(&ShowCompletions, window, cx);
17605 });
17606 cx.executor().run_until_parked();
17607 cx.update_editor(|editor, window, cx| {
17608 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
17609 });
17610 cx.executor().run_until_parked();
17611 cx.assert_editor_state(
17612 r##"#ifndef BAR_H
17613#define BAR_H
17614
17615#include <stdbool.h>
17616
17617int fn_branch(bool do_branch1, bool do_branch2);
17618
17619#endif // BAR_H
17620#include "AGL/ˇ"##,
17621 );
17622
17623 cx.update_editor(|editor, window, cx| {
17624 editor.handle_input("\"", window, cx);
17625 });
17626 cx.executor().run_until_parked();
17627 cx.assert_editor_state(
17628 r##"#ifndef BAR_H
17629#define BAR_H
17630
17631#include <stdbool.h>
17632
17633int fn_branch(bool do_branch1, bool do_branch2);
17634
17635#endif // BAR_H
17636#include "AGL/"ˇ"##,
17637 );
17638}
17639
17640#[gpui::test]
17641async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
17642 init_test(cx, |_| {});
17643
17644 let mut cx = EditorLspTestContext::new_rust(
17645 lsp::ServerCapabilities {
17646 completion_provider: Some(lsp::CompletionOptions {
17647 trigger_characters: Some(vec![".".to_string()]),
17648 resolve_provider: Some(false),
17649 ..lsp::CompletionOptions::default()
17650 }),
17651 ..lsp::ServerCapabilities::default()
17652 },
17653 cx,
17654 )
17655 .await;
17656
17657 cx.set_state("fn main() { let a = 2ˇ; }");
17658 cx.simulate_keystroke(".");
17659 let completion_item = lsp::CompletionItem {
17660 label: "Some".into(),
17661 kind: Some(lsp::CompletionItemKind::SNIPPET),
17662 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17663 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17664 kind: lsp::MarkupKind::Markdown,
17665 value: "```rust\nSome(2)\n```".to_string(),
17666 })),
17667 deprecated: Some(false),
17668 sort_text: Some("Some".to_string()),
17669 filter_text: Some("Some".to_string()),
17670 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17671 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17672 range: lsp::Range {
17673 start: lsp::Position {
17674 line: 0,
17675 character: 22,
17676 },
17677 end: lsp::Position {
17678 line: 0,
17679 character: 22,
17680 },
17681 },
17682 new_text: "Some(2)".to_string(),
17683 })),
17684 additional_text_edits: Some(vec![lsp::TextEdit {
17685 range: lsp::Range {
17686 start: lsp::Position {
17687 line: 0,
17688 character: 20,
17689 },
17690 end: lsp::Position {
17691 line: 0,
17692 character: 22,
17693 },
17694 },
17695 new_text: "".to_string(),
17696 }]),
17697 ..Default::default()
17698 };
17699
17700 let closure_completion_item = completion_item.clone();
17701 let counter = Arc::new(AtomicUsize::new(0));
17702 let counter_clone = counter.clone();
17703 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17704 let task_completion_item = closure_completion_item.clone();
17705 counter_clone.fetch_add(1, atomic::Ordering::Release);
17706 async move {
17707 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17708 is_incomplete: true,
17709 item_defaults: None,
17710 items: vec![task_completion_item],
17711 })))
17712 }
17713 });
17714
17715 cx.executor().run_until_parked();
17716 cx.condition(|editor, _| editor.context_menu_visible())
17717 .await;
17718 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
17719 assert!(request.next().await.is_some());
17720 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
17721
17722 cx.simulate_keystrokes("S o m");
17723 cx.condition(|editor, _| editor.context_menu_visible())
17724 .await;
17725 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
17726 assert!(request.next().await.is_some());
17727 assert!(request.next().await.is_some());
17728 assert!(request.next().await.is_some());
17729 request.close();
17730 assert!(request.next().await.is_none());
17731 assert_eq!(
17732 counter.load(atomic::Ordering::Acquire),
17733 4,
17734 "With the completions menu open, only one LSP request should happen per input"
17735 );
17736}
17737
17738#[gpui::test]
17739async fn test_toggle_comment(cx: &mut TestAppContext) {
17740 init_test(cx, |_| {});
17741 let mut cx = EditorTestContext::new(cx).await;
17742 let language = Arc::new(Language::new(
17743 LanguageConfig {
17744 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
17745 ..Default::default()
17746 },
17747 Some(tree_sitter_rust::LANGUAGE.into()),
17748 ));
17749 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
17750
17751 // If multiple selections intersect a line, the line is only toggled once.
17752 cx.set_state(indoc! {"
17753 fn a() {
17754 «//b();
17755 ˇ»// «c();
17756 //ˇ» d();
17757 }
17758 "});
17759
17760 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17761
17762 cx.assert_editor_state(indoc! {"
17763 fn a() {
17764 «b();
17765 ˇ»«c();
17766 ˇ» d();
17767 }
17768 "});
17769
17770 // The comment prefix is inserted at the same column for every line in a
17771 // selection.
17772 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17773
17774 cx.assert_editor_state(indoc! {"
17775 fn a() {
17776 // «b();
17777 ˇ»// «c();
17778 ˇ» // d();
17779 }
17780 "});
17781
17782 // If a selection ends at the beginning of a line, that line is not toggled.
17783 cx.set_selections_state(indoc! {"
17784 fn a() {
17785 // b();
17786 «// c();
17787 ˇ» // d();
17788 }
17789 "});
17790
17791 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17792
17793 cx.assert_editor_state(indoc! {"
17794 fn a() {
17795 // b();
17796 «c();
17797 ˇ» // d();
17798 }
17799 "});
17800
17801 // If a selection span a single line and is empty, the line is toggled.
17802 cx.set_state(indoc! {"
17803 fn a() {
17804 a();
17805 b();
17806 ˇ
17807 }
17808 "});
17809
17810 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17811
17812 cx.assert_editor_state(indoc! {"
17813 fn a() {
17814 a();
17815 b();
17816 //•ˇ
17817 }
17818 "});
17819
17820 // If a selection span multiple lines, empty lines are not toggled.
17821 cx.set_state(indoc! {"
17822 fn a() {
17823 «a();
17824
17825 c();ˇ»
17826 }
17827 "});
17828
17829 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17830
17831 cx.assert_editor_state(indoc! {"
17832 fn a() {
17833 // «a();
17834
17835 // c();ˇ»
17836 }
17837 "});
17838
17839 // If a selection includes multiple comment prefixes, all lines are uncommented.
17840 cx.set_state(indoc! {"
17841 fn a() {
17842 «// a();
17843 /// b();
17844 //! c();ˇ»
17845 }
17846 "});
17847
17848 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17849
17850 cx.assert_editor_state(indoc! {"
17851 fn a() {
17852 «a();
17853 b();
17854 c();ˇ»
17855 }
17856 "});
17857}
17858
17859#[gpui::test]
17860async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
17861 init_test(cx, |_| {});
17862 let mut cx = EditorTestContext::new(cx).await;
17863 let language = Arc::new(Language::new(
17864 LanguageConfig {
17865 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
17866 ..Default::default()
17867 },
17868 Some(tree_sitter_rust::LANGUAGE.into()),
17869 ));
17870 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
17871
17872 let toggle_comments = &ToggleComments {
17873 advance_downwards: false,
17874 ignore_indent: true,
17875 };
17876
17877 // If multiple selections intersect a line, the line is only toggled once.
17878 cx.set_state(indoc! {"
17879 fn a() {
17880 // «b();
17881 // c();
17882 // ˇ» d();
17883 }
17884 "});
17885
17886 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17887
17888 cx.assert_editor_state(indoc! {"
17889 fn a() {
17890 «b();
17891 c();
17892 ˇ» d();
17893 }
17894 "});
17895
17896 // The comment prefix is inserted at the beginning of each line
17897 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17898
17899 cx.assert_editor_state(indoc! {"
17900 fn a() {
17901 // «b();
17902 // c();
17903 // ˇ» d();
17904 }
17905 "});
17906
17907 // If a selection ends at the beginning of a line, that line is not toggled.
17908 cx.set_selections_state(indoc! {"
17909 fn a() {
17910 // b();
17911 // «c();
17912 ˇ»// d();
17913 }
17914 "});
17915
17916 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17917
17918 cx.assert_editor_state(indoc! {"
17919 fn a() {
17920 // b();
17921 «c();
17922 ˇ»// d();
17923 }
17924 "});
17925
17926 // If a selection span a single line and is empty, the line is toggled.
17927 cx.set_state(indoc! {"
17928 fn a() {
17929 a();
17930 b();
17931 ˇ
17932 }
17933 "});
17934
17935 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17936
17937 cx.assert_editor_state(indoc! {"
17938 fn a() {
17939 a();
17940 b();
17941 //ˇ
17942 }
17943 "});
17944
17945 // If a selection span multiple lines, empty lines are not toggled.
17946 cx.set_state(indoc! {"
17947 fn a() {
17948 «a();
17949
17950 c();ˇ»
17951 }
17952 "});
17953
17954 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17955
17956 cx.assert_editor_state(indoc! {"
17957 fn a() {
17958 // «a();
17959
17960 // c();ˇ»
17961 }
17962 "});
17963
17964 // If a selection includes multiple comment prefixes, all lines are uncommented.
17965 cx.set_state(indoc! {"
17966 fn a() {
17967 // «a();
17968 /// b();
17969 //! c();ˇ»
17970 }
17971 "});
17972
17973 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17974
17975 cx.assert_editor_state(indoc! {"
17976 fn a() {
17977 «a();
17978 b();
17979 c();ˇ»
17980 }
17981 "});
17982}
17983
17984#[gpui::test]
17985async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
17986 init_test(cx, |_| {});
17987
17988 let language = Arc::new(Language::new(
17989 LanguageConfig {
17990 line_comments: vec!["// ".into()],
17991 ..Default::default()
17992 },
17993 Some(tree_sitter_rust::LANGUAGE.into()),
17994 ));
17995
17996 let mut cx = EditorTestContext::new(cx).await;
17997
17998 cx.language_registry().add(language.clone());
17999 cx.update_buffer(|buffer, cx| {
18000 buffer.set_language(Some(language), cx);
18001 });
18002
18003 let toggle_comments = &ToggleComments {
18004 advance_downwards: true,
18005 ignore_indent: false,
18006 };
18007
18008 // Single cursor on one line -> advance
18009 // Cursor moves horizontally 3 characters as well on non-blank line
18010 cx.set_state(indoc!(
18011 "fn a() {
18012 ˇdog();
18013 cat();
18014 }"
18015 ));
18016 cx.update_editor(|editor, window, cx| {
18017 editor.toggle_comments(toggle_comments, window, cx);
18018 });
18019 cx.assert_editor_state(indoc!(
18020 "fn a() {
18021 // dog();
18022 catˇ();
18023 }"
18024 ));
18025
18026 // Single selection on one line -> don't advance
18027 cx.set_state(indoc!(
18028 "fn a() {
18029 «dog()ˇ»;
18030 cat();
18031 }"
18032 ));
18033 cx.update_editor(|editor, window, cx| {
18034 editor.toggle_comments(toggle_comments, window, cx);
18035 });
18036 cx.assert_editor_state(indoc!(
18037 "fn a() {
18038 // «dog()ˇ»;
18039 cat();
18040 }"
18041 ));
18042
18043 // Multiple cursors on one line -> advance
18044 cx.set_state(indoc!(
18045 "fn a() {
18046 ˇdˇog();
18047 cat();
18048 }"
18049 ));
18050 cx.update_editor(|editor, window, cx| {
18051 editor.toggle_comments(toggle_comments, window, cx);
18052 });
18053 cx.assert_editor_state(indoc!(
18054 "fn a() {
18055 // dog();
18056 catˇ(ˇ);
18057 }"
18058 ));
18059
18060 // Multiple cursors on one line, with selection -> don't advance
18061 cx.set_state(indoc!(
18062 "fn a() {
18063 ˇdˇog«()ˇ»;
18064 cat();
18065 }"
18066 ));
18067 cx.update_editor(|editor, window, cx| {
18068 editor.toggle_comments(toggle_comments, window, cx);
18069 });
18070 cx.assert_editor_state(indoc!(
18071 "fn a() {
18072 // ˇdˇog«()ˇ»;
18073 cat();
18074 }"
18075 ));
18076
18077 // Single cursor on one line -> advance
18078 // Cursor moves to column 0 on blank line
18079 cx.set_state(indoc!(
18080 "fn a() {
18081 ˇdog();
18082
18083 cat();
18084 }"
18085 ));
18086 cx.update_editor(|editor, window, cx| {
18087 editor.toggle_comments(toggle_comments, window, cx);
18088 });
18089 cx.assert_editor_state(indoc!(
18090 "fn a() {
18091 // dog();
18092 ˇ
18093 cat();
18094 }"
18095 ));
18096
18097 // Single cursor on one line -> advance
18098 // Cursor starts and ends at column 0
18099 cx.set_state(indoc!(
18100 "fn a() {
18101 ˇ dog();
18102 cat();
18103 }"
18104 ));
18105 cx.update_editor(|editor, window, cx| {
18106 editor.toggle_comments(toggle_comments, window, cx);
18107 });
18108 cx.assert_editor_state(indoc!(
18109 "fn a() {
18110 // dog();
18111 ˇ cat();
18112 }"
18113 ));
18114}
18115
18116#[gpui::test]
18117async fn test_toggle_block_comment(cx: &mut TestAppContext) {
18118 init_test(cx, |_| {});
18119
18120 let mut cx = EditorTestContext::new(cx).await;
18121
18122 let html_language = Arc::new(
18123 Language::new(
18124 LanguageConfig {
18125 name: "HTML".into(),
18126 block_comment: Some(BlockCommentConfig {
18127 start: "<!-- ".into(),
18128 prefix: "".into(),
18129 end: " -->".into(),
18130 tab_size: 0,
18131 }),
18132 ..Default::default()
18133 },
18134 Some(tree_sitter_html::LANGUAGE.into()),
18135 )
18136 .with_injection_query(
18137 r#"
18138 (script_element
18139 (raw_text) @injection.content
18140 (#set! injection.language "javascript"))
18141 "#,
18142 )
18143 .unwrap(),
18144 );
18145
18146 let javascript_language = Arc::new(Language::new(
18147 LanguageConfig {
18148 name: "JavaScript".into(),
18149 line_comments: vec!["// ".into()],
18150 ..Default::default()
18151 },
18152 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18153 ));
18154
18155 cx.language_registry().add(html_language.clone());
18156 cx.language_registry().add(javascript_language);
18157 cx.update_buffer(|buffer, cx| {
18158 buffer.set_language(Some(html_language), cx);
18159 });
18160
18161 // Toggle comments for empty selections
18162 cx.set_state(
18163 &r#"
18164 <p>A</p>ˇ
18165 <p>B</p>ˇ
18166 <p>C</p>ˇ
18167 "#
18168 .unindent(),
18169 );
18170 cx.update_editor(|editor, window, cx| {
18171 editor.toggle_comments(&ToggleComments::default(), window, cx)
18172 });
18173 cx.assert_editor_state(
18174 &r#"
18175 <!-- <p>A</p>ˇ -->
18176 <!-- <p>B</p>ˇ -->
18177 <!-- <p>C</p>ˇ -->
18178 "#
18179 .unindent(),
18180 );
18181 cx.update_editor(|editor, window, cx| {
18182 editor.toggle_comments(&ToggleComments::default(), window, cx)
18183 });
18184 cx.assert_editor_state(
18185 &r#"
18186 <p>A</p>ˇ
18187 <p>B</p>ˇ
18188 <p>C</p>ˇ
18189 "#
18190 .unindent(),
18191 );
18192
18193 // Toggle comments for mixture of empty and non-empty selections, where
18194 // multiple selections occupy a given line.
18195 cx.set_state(
18196 &r#"
18197 <p>A«</p>
18198 <p>ˇ»B</p>ˇ
18199 <p>C«</p>
18200 <p>ˇ»D</p>ˇ
18201 "#
18202 .unindent(),
18203 );
18204
18205 cx.update_editor(|editor, window, cx| {
18206 editor.toggle_comments(&ToggleComments::default(), window, cx)
18207 });
18208 cx.assert_editor_state(
18209 &r#"
18210 <!-- <p>A«</p>
18211 <p>ˇ»B</p>ˇ -->
18212 <!-- <p>C«</p>
18213 <p>ˇ»D</p>ˇ -->
18214 "#
18215 .unindent(),
18216 );
18217 cx.update_editor(|editor, window, cx| {
18218 editor.toggle_comments(&ToggleComments::default(), window, cx)
18219 });
18220 cx.assert_editor_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 // Toggle comments when different languages are active for different
18231 // selections.
18232 cx.set_state(
18233 &r#"
18234 ˇ<script>
18235 ˇvar x = new Y();
18236 ˇ</script>
18237 "#
18238 .unindent(),
18239 );
18240 cx.executor().run_until_parked();
18241 cx.update_editor(|editor, window, cx| {
18242 editor.toggle_comments(&ToggleComments::default(), window, cx)
18243 });
18244 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
18245 // Uncommenting and commenting from this position brings in even more wrong artifacts.
18246 cx.assert_editor_state(
18247 &r#"
18248 <!-- ˇ<script> -->
18249 // ˇvar x = new Y();
18250 <!-- ˇ</script> -->
18251 "#
18252 .unindent(),
18253 );
18254}
18255
18256#[gpui::test]
18257fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
18258 init_test(cx, |_| {});
18259
18260 let buffer = cx.new(|cx| Buffer::local(sample_text(6, 4, 'a'), cx));
18261 let multibuffer = cx.new(|cx| {
18262 let mut multibuffer = MultiBuffer::new(ReadWrite);
18263 multibuffer.set_excerpts_for_path(
18264 PathKey::sorted(0),
18265 buffer.clone(),
18266 [
18267 Point::new(0, 0)..Point::new(0, 4),
18268 Point::new(5, 0)..Point::new(5, 4),
18269 ],
18270 0,
18271 cx,
18272 );
18273 assert_eq!(multibuffer.read(cx).text(), "aaaa\nffff");
18274 multibuffer
18275 });
18276
18277 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
18278 editor.update_in(cx, |editor, window, cx| {
18279 assert_eq!(editor.text(cx), "aaaa\nffff");
18280 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18281 s.select_ranges([
18282 Point::new(0, 0)..Point::new(0, 0),
18283 Point::new(1, 0)..Point::new(1, 0),
18284 ])
18285 });
18286
18287 editor.handle_input("X", window, cx);
18288 assert_eq!(editor.text(cx), "Xaaaa\nXffff");
18289 assert_eq!(
18290 editor.selections.ranges(&editor.display_snapshot(cx)),
18291 [
18292 Point::new(0, 1)..Point::new(0, 1),
18293 Point::new(1, 1)..Point::new(1, 1),
18294 ]
18295 );
18296
18297 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
18298 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18299 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
18300 });
18301 editor.backspace(&Default::default(), window, cx);
18302 assert_eq!(editor.text(cx), "Xa\nfff");
18303 assert_eq!(
18304 editor.selections.ranges(&editor.display_snapshot(cx)),
18305 [Point::new(1, 0)..Point::new(1, 0)]
18306 );
18307
18308 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18309 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
18310 });
18311 editor.backspace(&Default::default(), window, cx);
18312 assert_eq!(editor.text(cx), "X\nff");
18313 assert_eq!(
18314 editor.selections.ranges(&editor.display_snapshot(cx)),
18315 [Point::new(0, 1)..Point::new(0, 1)]
18316 );
18317 });
18318}
18319
18320#[gpui::test]
18321fn test_refresh_selections(cx: &mut TestAppContext) {
18322 init_test(cx, |_| {});
18323
18324 let buffer = cx.new(|cx| Buffer::local(sample_text(5, 4, 'a'), cx));
18325 let multibuffer = cx.new(|cx| {
18326 let mut multibuffer = MultiBuffer::new(ReadWrite);
18327 multibuffer.set_excerpts_for_path(
18328 PathKey::sorted(0),
18329 buffer.clone(),
18330 [
18331 Point::new(0, 0)..Point::new(1, 4),
18332 Point::new(3, 0)..Point::new(4, 4),
18333 ],
18334 0,
18335 cx,
18336 );
18337 multibuffer
18338 });
18339
18340 let editor = cx.add_window(|window, cx| {
18341 let mut editor = build_editor(multibuffer.clone(), window, cx);
18342 let snapshot = editor.snapshot(window, cx);
18343 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18344 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
18345 });
18346 editor.begin_selection(
18347 Point::new(2, 1).to_display_point(&snapshot),
18348 true,
18349 1,
18350 window,
18351 cx,
18352 );
18353 assert_eq!(
18354 editor.selections.ranges(&editor.display_snapshot(cx)),
18355 [
18356 Point::new(1, 3)..Point::new(1, 3),
18357 Point::new(2, 1)..Point::new(2, 1),
18358 ]
18359 );
18360 editor
18361 });
18362
18363 // Refreshing selections is a no-op when excerpts haven't changed.
18364 _ = editor.update(cx, |editor, window, cx| {
18365 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
18366 assert_eq!(
18367 editor.selections.ranges(&editor.display_snapshot(cx)),
18368 [
18369 Point::new(1, 3)..Point::new(1, 3),
18370 Point::new(2, 1)..Point::new(2, 1),
18371 ]
18372 );
18373 });
18374
18375 multibuffer.update(cx, |multibuffer, cx| {
18376 multibuffer.set_excerpts_for_path(
18377 PathKey::sorted(0),
18378 buffer.clone(),
18379 [Point::new(3, 0)..Point::new(4, 4)],
18380 0,
18381 cx,
18382 );
18383 });
18384 _ = editor.update(cx, |editor, window, cx| {
18385 // Removing an excerpt causes the first selection to become degenerate.
18386 assert_eq!(
18387 editor.selections.ranges(&editor.display_snapshot(cx)),
18388 [
18389 Point::new(0, 0)..Point::new(0, 0),
18390 Point::new(0, 1)..Point::new(0, 1)
18391 ]
18392 );
18393
18394 // Refreshing selections will relocate the first selection to the original buffer
18395 // location.
18396 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
18397 assert_eq!(
18398 editor.selections.ranges(&editor.display_snapshot(cx)),
18399 [
18400 Point::new(0, 0)..Point::new(0, 0),
18401 Point::new(0, 1)..Point::new(0, 1),
18402 ]
18403 );
18404 assert!(editor.selections.pending_anchor().is_some());
18405 });
18406}
18407
18408#[gpui::test]
18409fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
18410 init_test(cx, |_| {});
18411
18412 let buffer = cx.new(|cx| Buffer::local(sample_text(5, 4, 'a'), cx));
18413 let multibuffer = cx.new(|cx| {
18414 let mut multibuffer = MultiBuffer::new(ReadWrite);
18415 multibuffer.set_excerpts_for_path(
18416 PathKey::sorted(0),
18417 buffer.clone(),
18418 [
18419 Point::new(0, 0)..Point::new(1, 4),
18420 Point::new(3, 0)..Point::new(4, 4),
18421 ],
18422 0,
18423 cx,
18424 );
18425 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\ndddd\neeee");
18426 multibuffer
18427 });
18428
18429 let editor = cx.add_window(|window, cx| {
18430 let mut editor = build_editor(multibuffer.clone(), window, cx);
18431 let snapshot = editor.snapshot(window, cx);
18432 editor.begin_selection(
18433 Point::new(1, 3).to_display_point(&snapshot),
18434 false,
18435 1,
18436 window,
18437 cx,
18438 );
18439 assert_eq!(
18440 editor.selections.ranges(&editor.display_snapshot(cx)),
18441 [Point::new(1, 3)..Point::new(1, 3)]
18442 );
18443 editor
18444 });
18445
18446 multibuffer.update(cx, |multibuffer, cx| {
18447 multibuffer.set_excerpts_for_path(
18448 PathKey::sorted(0),
18449 buffer.clone(),
18450 [Point::new(3, 0)..Point::new(4, 4)],
18451 0,
18452 cx,
18453 );
18454 });
18455 _ = editor.update(cx, |editor, window, cx| {
18456 assert_eq!(
18457 editor.selections.ranges(&editor.display_snapshot(cx)),
18458 [Point::new(0, 0)..Point::new(0, 0)]
18459 );
18460
18461 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
18462 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
18463 assert_eq!(
18464 editor.selections.ranges(&editor.display_snapshot(cx)),
18465 [Point::new(0, 0)..Point::new(0, 0)]
18466 );
18467 assert!(editor.selections.pending_anchor().is_some());
18468 });
18469}
18470
18471#[gpui::test]
18472async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
18473 init_test(cx, |_| {});
18474
18475 let language = Arc::new(
18476 Language::new(
18477 LanguageConfig {
18478 brackets: BracketPairConfig {
18479 pairs: vec![
18480 BracketPair {
18481 start: "{".to_string(),
18482 end: "}".to_string(),
18483 close: true,
18484 surround: true,
18485 newline: true,
18486 },
18487 BracketPair {
18488 start: "/* ".to_string(),
18489 end: " */".to_string(),
18490 close: true,
18491 surround: true,
18492 newline: true,
18493 },
18494 ],
18495 ..Default::default()
18496 },
18497 ..Default::default()
18498 },
18499 Some(tree_sitter_rust::LANGUAGE.into()),
18500 )
18501 .with_indents_query("")
18502 .unwrap(),
18503 );
18504
18505 let text = concat!(
18506 "{ }\n", //
18507 " x\n", //
18508 " /* */\n", //
18509 "x\n", //
18510 "{{} }\n", //
18511 );
18512
18513 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18514 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18515 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18516 editor
18517 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
18518 .await;
18519
18520 editor.update_in(cx, |editor, window, cx| {
18521 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18522 s.select_display_ranges([
18523 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
18524 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
18525 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
18526 ])
18527 });
18528 editor.newline(&Newline, window, cx);
18529
18530 assert_eq!(
18531 editor.buffer().read(cx).read(cx).text(),
18532 concat!(
18533 "{ \n", // Suppress rustfmt
18534 "\n", //
18535 "}\n", //
18536 " x\n", //
18537 " /* \n", //
18538 " \n", //
18539 " */\n", //
18540 "x\n", //
18541 "{{} \n", //
18542 "}\n", //
18543 )
18544 );
18545 });
18546}
18547
18548#[gpui::test]
18549fn test_highlighted_ranges(cx: &mut TestAppContext) {
18550 init_test(cx, |_| {});
18551
18552 let editor = cx.add_window(|window, cx| {
18553 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
18554 build_editor(buffer, window, cx)
18555 });
18556
18557 _ = editor.update(cx, |editor, window, cx| {
18558 let buffer = editor.buffer.read(cx).snapshot(cx);
18559
18560 let anchor_range =
18561 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
18562
18563 editor.highlight_background(
18564 HighlightKey::ColorizeBracket(0),
18565 &[
18566 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
18567 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
18568 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
18569 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
18570 ],
18571 |_, _| Hsla::red(),
18572 cx,
18573 );
18574 editor.highlight_background(
18575 HighlightKey::ColorizeBracket(1),
18576 &[
18577 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
18578 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
18579 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
18580 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
18581 ],
18582 |_, _| Hsla::green(),
18583 cx,
18584 );
18585
18586 let snapshot = editor.snapshot(window, cx);
18587 let highlighted_ranges = editor.sorted_background_highlights_in_range(
18588 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
18589 &snapshot,
18590 cx.theme(),
18591 );
18592 assert_eq!(
18593 highlighted_ranges,
18594 &[
18595 (
18596 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
18597 Hsla::green(),
18598 ),
18599 (
18600 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
18601 Hsla::red(),
18602 ),
18603 (
18604 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
18605 Hsla::green(),
18606 ),
18607 (
18608 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
18609 Hsla::red(),
18610 ),
18611 ]
18612 );
18613 assert_eq!(
18614 editor.sorted_background_highlights_in_range(
18615 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
18616 &snapshot,
18617 cx.theme(),
18618 ),
18619 &[(
18620 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
18621 Hsla::red(),
18622 )]
18623 );
18624 });
18625}
18626
18627#[gpui::test]
18628async fn test_following(cx: &mut TestAppContext) {
18629 init_test(cx, |_| {});
18630
18631 let fs = FakeFs::new(cx.executor());
18632 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
18633
18634 let buffer = project.update(cx, |project, cx| {
18635 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
18636 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
18637 });
18638 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
18639 let follower = cx.update(|cx| {
18640 cx.open_window(
18641 WindowOptions {
18642 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
18643 gpui::Point::new(px(0.), px(0.)),
18644 gpui::Point::new(px(10.), px(80.)),
18645 ))),
18646 ..Default::default()
18647 },
18648 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
18649 )
18650 .unwrap()
18651 });
18652
18653 let is_still_following = Rc::new(RefCell::new(true));
18654 let follower_edit_event_count = Rc::new(RefCell::new(0));
18655 let pending_update = Rc::new(RefCell::new(None));
18656 let leader_entity = leader.root(cx).unwrap();
18657 let follower_entity = follower.root(cx).unwrap();
18658 _ = follower.update(cx, {
18659 let update = pending_update.clone();
18660 let is_still_following = is_still_following.clone();
18661 let follower_edit_event_count = follower_edit_event_count.clone();
18662 |_, window, cx| {
18663 cx.subscribe_in(
18664 &leader_entity,
18665 window,
18666 move |_, leader, event, window, cx| {
18667 leader.update(cx, |leader, cx| {
18668 leader.add_event_to_update_proto(
18669 event,
18670 &mut update.borrow_mut(),
18671 window,
18672 cx,
18673 );
18674 });
18675 },
18676 )
18677 .detach();
18678
18679 cx.subscribe_in(
18680 &follower_entity,
18681 window,
18682 move |_, _, event: &EditorEvent, _window, _cx| {
18683 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
18684 *is_still_following.borrow_mut() = false;
18685 }
18686
18687 if let EditorEvent::BufferEdited = event {
18688 *follower_edit_event_count.borrow_mut() += 1;
18689 }
18690 },
18691 )
18692 .detach();
18693 }
18694 });
18695
18696 // Update the selections only
18697 _ = leader.update(cx, |leader, window, cx| {
18698 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18699 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
18700 });
18701 });
18702 follower
18703 .update(cx, |follower, window, cx| {
18704 follower.apply_update_proto(
18705 &project,
18706 pending_update.borrow_mut().take().unwrap(),
18707 window,
18708 cx,
18709 )
18710 })
18711 .unwrap()
18712 .await
18713 .unwrap();
18714 _ = follower.update(cx, |follower, _, cx| {
18715 assert_eq!(
18716 follower.selections.ranges(&follower.display_snapshot(cx)),
18717 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
18718 );
18719 });
18720 assert!(*is_still_following.borrow());
18721 assert_eq!(*follower_edit_event_count.borrow(), 0);
18722
18723 // Update the scroll position only
18724 _ = leader.update(cx, |leader, window, cx| {
18725 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
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 assert_eq!(
18740 follower
18741 .update(cx, |follower, _, cx| follower.scroll_position(cx))
18742 .unwrap(),
18743 gpui::Point::new(1.5, 3.5)
18744 );
18745 assert!(*is_still_following.borrow());
18746 assert_eq!(*follower_edit_event_count.borrow(), 0);
18747
18748 // Update the selections and scroll position. The follower's scroll position is updated
18749 // via autoscroll, not via the leader's exact scroll position.
18750 _ = leader.update(cx, |leader, window, cx| {
18751 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18752 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
18753 });
18754 leader.request_autoscroll(Autoscroll::newest(), cx);
18755 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
18756 });
18757 follower
18758 .update(cx, |follower, window, cx| {
18759 follower.apply_update_proto(
18760 &project,
18761 pending_update.borrow_mut().take().unwrap(),
18762 window,
18763 cx,
18764 )
18765 })
18766 .unwrap()
18767 .await
18768 .unwrap();
18769 _ = follower.update(cx, |follower, _, cx| {
18770 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
18771 assert_eq!(
18772 follower.selections.ranges(&follower.display_snapshot(cx)),
18773 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
18774 );
18775 });
18776 assert!(*is_still_following.borrow());
18777
18778 // Creating a pending selection that precedes another selection
18779 _ = leader.update(cx, |leader, window, cx| {
18780 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18781 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
18782 });
18783 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
18784 });
18785 follower
18786 .update(cx, |follower, window, cx| {
18787 follower.apply_update_proto(
18788 &project,
18789 pending_update.borrow_mut().take().unwrap(),
18790 window,
18791 cx,
18792 )
18793 })
18794 .unwrap()
18795 .await
18796 .unwrap();
18797 _ = follower.update(cx, |follower, _, cx| {
18798 assert_eq!(
18799 follower.selections.ranges(&follower.display_snapshot(cx)),
18800 vec![
18801 MultiBufferOffset(0)..MultiBufferOffset(0),
18802 MultiBufferOffset(1)..MultiBufferOffset(1)
18803 ]
18804 );
18805 });
18806 assert!(*is_still_following.borrow());
18807
18808 // Extend the pending selection so that it surrounds another selection
18809 _ = leader.update(cx, |leader, window, cx| {
18810 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
18811 });
18812 follower
18813 .update(cx, |follower, window, cx| {
18814 follower.apply_update_proto(
18815 &project,
18816 pending_update.borrow_mut().take().unwrap(),
18817 window,
18818 cx,
18819 )
18820 })
18821 .unwrap()
18822 .await
18823 .unwrap();
18824 _ = follower.update(cx, |follower, _, cx| {
18825 assert_eq!(
18826 follower.selections.ranges(&follower.display_snapshot(cx)),
18827 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
18828 );
18829 });
18830
18831 // Scrolling locally breaks the follow
18832 _ = follower.update(cx, |follower, window, cx| {
18833 let top_anchor = follower
18834 .buffer()
18835 .read(cx)
18836 .read(cx)
18837 .anchor_after(MultiBufferOffset(0));
18838 follower.set_scroll_anchor(
18839 ScrollAnchor {
18840 anchor: top_anchor,
18841 offset: gpui::Point::new(0.0, 0.5),
18842 },
18843 window,
18844 cx,
18845 );
18846 });
18847 assert!(!(*is_still_following.borrow()));
18848}
18849
18850#[gpui::test]
18851async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
18852 init_test(cx, |_| {});
18853
18854 let fs = FakeFs::new(cx.executor());
18855 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
18856 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
18857 let workspace = window
18858 .read_with(cx, |mw, _| mw.workspace().clone())
18859 .unwrap();
18860 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
18861
18862 let cx = &mut VisualTestContext::from_window(*window, cx);
18863
18864 let leader = pane.update_in(cx, |_, window, cx| {
18865 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
18866 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
18867 });
18868
18869 // Start following the editor when it has no excerpts.
18870 let mut state_message =
18871 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
18872 let workspace_entity = workspace.clone();
18873 let follower_1 = cx
18874 .update_window(*window, |_, window, cx| {
18875 Editor::from_state_proto(
18876 workspace_entity,
18877 ViewId {
18878 creator: CollaboratorId::PeerId(PeerId::default()),
18879 id: 0,
18880 },
18881 &mut state_message,
18882 window,
18883 cx,
18884 )
18885 })
18886 .unwrap()
18887 .unwrap()
18888 .await
18889 .unwrap();
18890
18891 let update_message = Rc::new(RefCell::new(None));
18892 follower_1.update_in(cx, {
18893 let update = update_message.clone();
18894 |_, window, cx| {
18895 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
18896 leader.update(cx, |leader, cx| {
18897 leader.add_event_to_update_proto(event, &mut update.borrow_mut(), window, cx);
18898 });
18899 })
18900 .detach();
18901 }
18902 });
18903
18904 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
18905 (
18906 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
18907 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
18908 )
18909 });
18910
18911 // Insert some excerpts.
18912 leader.update(cx, |leader, cx| {
18913 leader.buffer.update(cx, |multibuffer, cx| {
18914 multibuffer.set_excerpts_for_path(
18915 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
18916 buffer_1.clone(),
18917 vec![
18918 Point::row_range(0..3),
18919 Point::row_range(1..6),
18920 Point::row_range(12..15),
18921 ],
18922 0,
18923 cx,
18924 );
18925 multibuffer.set_excerpts_for_path(
18926 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
18927 buffer_2.clone(),
18928 vec![Point::row_range(0..6), Point::row_range(8..12)],
18929 0,
18930 cx,
18931 );
18932 });
18933 });
18934
18935 // Apply the update of adding the excerpts.
18936 follower_1
18937 .update_in(cx, |follower, window, cx| {
18938 follower.apply_update_proto(
18939 &project,
18940 update_message.borrow().clone().unwrap(),
18941 window,
18942 cx,
18943 )
18944 })
18945 .await
18946 .unwrap();
18947 assert_eq!(
18948 follower_1.update(cx, |editor, cx| editor.text(cx)),
18949 leader.update(cx, |editor, cx| editor.text(cx))
18950 );
18951 update_message.borrow_mut().take();
18952
18953 // Start following separately after it already has excerpts.
18954 let mut state_message =
18955 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
18956 let workspace_entity = workspace.clone();
18957 let follower_2 = cx
18958 .update_window(*window, |_, window, cx| {
18959 Editor::from_state_proto(
18960 workspace_entity,
18961 ViewId {
18962 creator: CollaboratorId::PeerId(PeerId::default()),
18963 id: 0,
18964 },
18965 &mut state_message,
18966 window,
18967 cx,
18968 )
18969 })
18970 .unwrap()
18971 .unwrap()
18972 .await
18973 .unwrap();
18974 assert_eq!(
18975 follower_2.update(cx, |editor, cx| editor.text(cx)),
18976 leader.update(cx, |editor, cx| editor.text(cx))
18977 );
18978
18979 // Remove some excerpts.
18980 leader.update(cx, |leader, cx| {
18981 leader.buffer.update(cx, |multibuffer, cx| {
18982 multibuffer.remove_excerpts_for_path(
18983 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
18984 cx,
18985 );
18986 });
18987 });
18988
18989 // Apply the update of removing the excerpts.
18990 follower_1
18991 .update_in(cx, |follower, window, cx| {
18992 follower.apply_update_proto(
18993 &project,
18994 update_message.borrow().clone().unwrap(),
18995 window,
18996 cx,
18997 )
18998 })
18999 .await
19000 .unwrap();
19001 follower_2
19002 .update_in(cx, |follower, window, cx| {
19003 follower.apply_update_proto(
19004 &project,
19005 update_message.borrow().clone().unwrap(),
19006 window,
19007 cx,
19008 )
19009 })
19010 .await
19011 .unwrap();
19012 update_message.borrow_mut().take();
19013 assert_eq!(
19014 follower_1.update(cx, |editor, cx| editor.text(cx)),
19015 leader.update(cx, |editor, cx| editor.text(cx))
19016 );
19017}
19018
19019#[gpui::test]
19020async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19021 init_test(cx, |_| {});
19022
19023 let mut cx = EditorTestContext::new(cx).await;
19024 let lsp_store =
19025 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
19026
19027 cx.set_state(indoc! {"
19028 ˇfn func(abc def: i32) -> u32 {
19029 }
19030 "});
19031
19032 cx.update(|_, cx| {
19033 lsp_store.update(cx, |lsp_store, cx| {
19034 lsp_store
19035 .update_diagnostics(
19036 LanguageServerId(0),
19037 lsp::PublishDiagnosticsParams {
19038 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
19039 version: None,
19040 diagnostics: vec![
19041 lsp::Diagnostic {
19042 range: lsp::Range::new(
19043 lsp::Position::new(0, 11),
19044 lsp::Position::new(0, 12),
19045 ),
19046 severity: Some(lsp::DiagnosticSeverity::ERROR),
19047 ..Default::default()
19048 },
19049 lsp::Diagnostic {
19050 range: lsp::Range::new(
19051 lsp::Position::new(0, 12),
19052 lsp::Position::new(0, 15),
19053 ),
19054 severity: Some(lsp::DiagnosticSeverity::ERROR),
19055 ..Default::default()
19056 },
19057 lsp::Diagnostic {
19058 range: lsp::Range::new(
19059 lsp::Position::new(0, 25),
19060 lsp::Position::new(0, 28),
19061 ),
19062 severity: Some(lsp::DiagnosticSeverity::ERROR),
19063 ..Default::default()
19064 },
19065 ],
19066 },
19067 None,
19068 DiagnosticSourceKind::Pushed,
19069 &[],
19070 cx,
19071 )
19072 .unwrap()
19073 });
19074 });
19075
19076 executor.run_until_parked();
19077
19078 cx.update_editor(|editor, window, cx| {
19079 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19080 });
19081
19082 cx.assert_editor_state(indoc! {"
19083 fn func(abc def: i32) -> ˇu32 {
19084 }
19085 "});
19086
19087 cx.update_editor(|editor, window, cx| {
19088 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19089 });
19090
19091 cx.assert_editor_state(indoc! {"
19092 fn func(abc ˇdef: i32) -> u32 {
19093 }
19094 "});
19095
19096 cx.update_editor(|editor, window, cx| {
19097 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19098 });
19099
19100 cx.assert_editor_state(indoc! {"
19101 fn func(abcˇ def: i32) -> u32 {
19102 }
19103 "});
19104
19105 cx.update_editor(|editor, window, cx| {
19106 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
19107 });
19108
19109 cx.assert_editor_state(indoc! {"
19110 fn func(abc def: i32) -> ˇu32 {
19111 }
19112 "});
19113}
19114
19115#[gpui::test]
19116async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19117 init_test(cx, |_| {});
19118
19119 let mut cx = EditorTestContext::new(cx).await;
19120
19121 let diff_base = r#"
19122 use some::mod;
19123
19124 const A: u32 = 42;
19125
19126 fn main() {
19127 println!("hello");
19128
19129 println!("world");
19130 }
19131 "#
19132 .unindent();
19133
19134 // Edits are modified, removed, modified, added
19135 cx.set_state(
19136 &r#"
19137 use some::modified;
19138
19139 ˇ
19140 fn main() {
19141 println!("hello there");
19142
19143 println!("around the");
19144 println!("world");
19145 }
19146 "#
19147 .unindent(),
19148 );
19149
19150 cx.set_head_text(&diff_base);
19151 executor.run_until_parked();
19152
19153 cx.update_editor(|editor, window, cx| {
19154 //Wrap around the bottom of the buffer
19155 for _ in 0..3 {
19156 editor.go_to_next_hunk(&GoToHunk, window, cx);
19157 }
19158 });
19159
19160 cx.assert_editor_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.update_editor(|editor, window, cx| {
19176 //Wrap around the top of the buffer
19177 for _ in 0..2 {
19178 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19179 }
19180 });
19181
19182 cx.assert_editor_state(
19183 &r#"
19184 use some::modified;
19185
19186
19187 fn main() {
19188 ˇ println!("hello there");
19189
19190 println!("around the");
19191 println!("world");
19192 }
19193 "#
19194 .unindent(),
19195 );
19196
19197 cx.update_editor(|editor, window, cx| {
19198 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19199 });
19200
19201 cx.assert_editor_state(
19202 &r#"
19203 use some::modified;
19204
19205 ˇ
19206 fn main() {
19207 println!("hello there");
19208
19209 println!("around the");
19210 println!("world");
19211 }
19212 "#
19213 .unindent(),
19214 );
19215
19216 cx.update_editor(|editor, window, cx| {
19217 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19218 });
19219
19220 cx.assert_editor_state(
19221 &r#"
19222 ˇuse some::modified;
19223
19224
19225 fn main() {
19226 println!("hello there");
19227
19228 println!("around the");
19229 println!("world");
19230 }
19231 "#
19232 .unindent(),
19233 );
19234
19235 cx.update_editor(|editor, window, cx| {
19236 for _ in 0..2 {
19237 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
19238 }
19239 });
19240
19241 cx.assert_editor_state(
19242 &r#"
19243 use some::modified;
19244
19245
19246 fn main() {
19247 ˇ println!("hello there");
19248
19249 println!("around the");
19250 println!("world");
19251 }
19252 "#
19253 .unindent(),
19254 );
19255
19256 cx.update_editor(|editor, window, cx| {
19257 editor.fold(&Fold, window, cx);
19258 });
19259
19260 cx.update_editor(|editor, window, cx| {
19261 editor.go_to_next_hunk(&GoToHunk, window, cx);
19262 });
19263
19264 cx.assert_editor_state(
19265 &r#"
19266 ˇuse some::modified;
19267
19268
19269 fn main() {
19270 println!("hello there");
19271
19272 println!("around the");
19273 println!("world");
19274 }
19275 "#
19276 .unindent(),
19277 );
19278}
19279
19280#[test]
19281fn test_split_words() {
19282 fn split(text: &str) -> Vec<&str> {
19283 split_words(text).collect()
19284 }
19285
19286 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
19287 assert_eq!(split("hello_world"), &["hello_", "world"]);
19288 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
19289 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
19290 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
19291 assert_eq!(split("helloworld"), &["helloworld"]);
19292
19293 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
19294}
19295
19296#[test]
19297fn test_split_words_for_snippet_prefix() {
19298 fn split(text: &str) -> Vec<&str> {
19299 snippet_candidate_suffixes(text, &|c| c.is_alphanumeric() || c == '_').collect()
19300 }
19301
19302 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
19303 assert_eq!(split("hello_world"), &["hello_world"]);
19304 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
19305 assert_eq!(split("Hello_World"), &["Hello_World"]);
19306 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
19307 assert_eq!(split("helloworld"), &["helloworld"]);
19308 assert_eq!(
19309 split("this@is!@#$^many . symbols"),
19310 &[
19311 "symbols",
19312 " symbols",
19313 ". symbols",
19314 " . symbols",
19315 " . symbols",
19316 " . symbols",
19317 "many . symbols",
19318 "^many . symbols",
19319 "$^many . symbols",
19320 "#$^many . symbols",
19321 "@#$^many . symbols",
19322 "!@#$^many . symbols",
19323 "is!@#$^many . symbols",
19324 "@is!@#$^many . symbols",
19325 "this@is!@#$^many . symbols",
19326 ],
19327 );
19328 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
19329}
19330
19331#[gpui::test]
19332async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
19333 init_test(cx, |_| {});
19334
19335 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
19336
19337 #[track_caller]
19338 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
19339 let _state_context = cx.set_state(before);
19340 cx.run_until_parked();
19341 cx.update_editor(|editor, window, cx| {
19342 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
19343 });
19344 cx.run_until_parked();
19345 cx.assert_editor_state(after);
19346 }
19347
19348 // Outside bracket jumps to outside of matching bracket
19349 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
19350 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
19351
19352 // Inside bracket jumps to inside of matching bracket
19353 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
19354 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
19355
19356 // When outside a bracket and inside, favor jumping to the inside bracket
19357 assert(
19358 "console.log('foo', [1, 2, 3]ˇ);",
19359 "console.log('foo', ˇ[1, 2, 3]);",
19360 &mut cx,
19361 );
19362 assert(
19363 "console.log(ˇ'foo', [1, 2, 3]);",
19364 "console.log('foo'ˇ, [1, 2, 3]);",
19365 &mut cx,
19366 );
19367
19368 // Bias forward if two options are equally likely
19369 assert(
19370 "let result = curried_fun()ˇ();",
19371 "let result = curried_fun()()ˇ;",
19372 &mut cx,
19373 );
19374
19375 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
19376 assert(
19377 indoc! {"
19378 function test() {
19379 console.log('test')ˇ
19380 }"},
19381 indoc! {"
19382 function test() {
19383 console.logˇ('test')
19384 }"},
19385 &mut cx,
19386 );
19387}
19388
19389#[gpui::test]
19390async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
19391 init_test(cx, |_| {});
19392 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
19393 language_registry.add(markdown_lang());
19394 language_registry.add(rust_lang());
19395 let buffer = cx.new(|cx| {
19396 let mut buffer = language::Buffer::local(
19397 indoc! {"
19398 ```rs
19399 impl Worktree {
19400 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
19401 }
19402 }
19403 ```
19404 "},
19405 cx,
19406 );
19407 buffer.set_language_registry(language_registry.clone());
19408 buffer.set_language(Some(markdown_lang()), cx);
19409 buffer
19410 });
19411 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19412 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
19413 cx.executor().run_until_parked();
19414 _ = editor.update(cx, |editor, window, cx| {
19415 // Case 1: Test outer enclosing brackets
19416 select_ranges(
19417 editor,
19418 &indoc! {"
19419 ```rs
19420 impl Worktree {
19421 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
19422 }
19423 }ˇ
19424 ```
19425 "},
19426 window,
19427 cx,
19428 );
19429 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
19430 assert_text_with_selections(
19431 editor,
19432 &indoc! {"
19433 ```rs
19434 impl Worktree ˇ{
19435 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
19436 }
19437 }
19438 ```
19439 "},
19440 cx,
19441 );
19442 // Case 2: Test inner enclosing brackets
19443 select_ranges(
19444 editor,
19445 &indoc! {"
19446 ```rs
19447 impl Worktree {
19448 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
19449 }ˇ
19450 }
19451 ```
19452 "},
19453 window,
19454 cx,
19455 );
19456 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
19457 assert_text_with_selections(
19458 editor,
19459 &indoc! {"
19460 ```rs
19461 impl Worktree {
19462 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
19463 }
19464 }
19465 ```
19466 "},
19467 cx,
19468 );
19469 });
19470}
19471
19472#[gpui::test]
19473async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
19474 init_test(cx, |_| {});
19475
19476 let fs = FakeFs::new(cx.executor());
19477 fs.insert_tree(
19478 path!("/a"),
19479 json!({
19480 "main.rs": "fn main() { let a = 5; }",
19481 "other.rs": "// Test file",
19482 }),
19483 )
19484 .await;
19485 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19486
19487 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19488 language_registry.add(Arc::new(Language::new(
19489 LanguageConfig {
19490 name: "Rust".into(),
19491 matcher: LanguageMatcher {
19492 path_suffixes: vec!["rs".to_string()],
19493 ..Default::default()
19494 },
19495 brackets: BracketPairConfig {
19496 pairs: vec![BracketPair {
19497 start: "{".to_string(),
19498 end: "}".to_string(),
19499 close: true,
19500 surround: true,
19501 newline: true,
19502 }],
19503 disabled_scopes_by_bracket_ix: Vec::new(),
19504 },
19505 ..Default::default()
19506 },
19507 Some(tree_sitter_rust::LANGUAGE.into()),
19508 )));
19509 let mut fake_servers = language_registry.register_fake_lsp(
19510 "Rust",
19511 FakeLspAdapter {
19512 capabilities: lsp::ServerCapabilities {
19513 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
19514 first_trigger_character: "{".to_string(),
19515 more_trigger_character: None,
19516 }),
19517 ..Default::default()
19518 },
19519 ..Default::default()
19520 },
19521 );
19522
19523 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
19524 let workspace = window
19525 .read_with(cx, |mw, _| mw.workspace().clone())
19526 .unwrap();
19527
19528 let cx = &mut VisualTestContext::from_window(*window, cx);
19529
19530 let worktree_id = workspace.update_in(cx, |workspace, _, cx| {
19531 workspace.project().update(cx, |project, cx| {
19532 project.worktrees(cx).next().unwrap().read(cx).id()
19533 })
19534 });
19535
19536 let buffer = project
19537 .update(cx, |project, cx| {
19538 project.open_local_buffer(path!("/a/main.rs"), cx)
19539 })
19540 .await
19541 .unwrap();
19542 let editor_handle = workspace
19543 .update_in(cx, |workspace, window, cx| {
19544 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
19545 })
19546 .await
19547 .unwrap()
19548 .downcast::<Editor>()
19549 .unwrap();
19550
19551 let fake_server = fake_servers.next().await.unwrap();
19552
19553 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
19554 |params, _| async move {
19555 assert_eq!(
19556 params.text_document_position.text_document.uri,
19557 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
19558 );
19559 assert_eq!(
19560 params.text_document_position.position,
19561 lsp::Position::new(0, 21),
19562 );
19563
19564 Ok(Some(vec![lsp::TextEdit {
19565 new_text: "]".to_string(),
19566 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19567 }]))
19568 },
19569 );
19570
19571 editor_handle.update_in(cx, |editor, window, cx| {
19572 window.focus(&editor.focus_handle(cx), cx);
19573 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19574 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
19575 });
19576 editor.handle_input("{", window, cx);
19577 });
19578
19579 cx.executor().run_until_parked();
19580
19581 buffer.update(cx, |buffer, _| {
19582 assert_eq!(
19583 buffer.text(),
19584 "fn main() { let a = {5}; }",
19585 "No extra braces from on type formatting should appear in the buffer"
19586 )
19587 });
19588}
19589
19590#[gpui::test(iterations = 20, seeds(31))]
19591async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
19592 init_test(cx, |_| {});
19593
19594 let mut cx = EditorLspTestContext::new_rust(
19595 lsp::ServerCapabilities {
19596 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
19597 first_trigger_character: ".".to_string(),
19598 more_trigger_character: None,
19599 }),
19600 ..Default::default()
19601 },
19602 cx,
19603 )
19604 .await;
19605
19606 cx.update_buffer(|buffer, _| {
19607 // This causes autoindent to be async.
19608 buffer.set_sync_parse_timeout(None)
19609 });
19610
19611 cx.set_state("fn c() {\n d()ˇ\n}\n");
19612 cx.simulate_keystroke("\n");
19613 cx.run_until_parked();
19614
19615 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
19616 let mut request =
19617 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
19618 let buffer_cloned = buffer_cloned.clone();
19619 async move {
19620 buffer_cloned.update(&mut cx, |buffer, _| {
19621 assert_eq!(
19622 buffer.text(),
19623 "fn c() {\n d()\n .\n}\n",
19624 "OnTypeFormatting should triggered after autoindent applied"
19625 )
19626 });
19627
19628 Ok(Some(vec![]))
19629 }
19630 });
19631
19632 cx.simulate_keystroke(".");
19633 cx.run_until_parked();
19634
19635 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
19636 assert!(request.next().await.is_some());
19637 request.close();
19638 assert!(request.next().await.is_none());
19639}
19640
19641#[gpui::test]
19642async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
19643 init_test(cx, |_| {});
19644
19645 let fs = FakeFs::new(cx.executor());
19646 fs.insert_tree(
19647 path!("/a"),
19648 json!({
19649 "main.rs": "fn main() { let a = 5; }",
19650 "other.rs": "// Test file",
19651 }),
19652 )
19653 .await;
19654
19655 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19656
19657 let server_restarts = Arc::new(AtomicUsize::new(0));
19658 let closure_restarts = Arc::clone(&server_restarts);
19659 let language_server_name = "test language server";
19660 let language_name: LanguageName = "Rust".into();
19661
19662 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19663 language_registry.add(Arc::new(Language::new(
19664 LanguageConfig {
19665 name: language_name.clone(),
19666 matcher: LanguageMatcher {
19667 path_suffixes: vec!["rs".to_string()],
19668 ..Default::default()
19669 },
19670 ..Default::default()
19671 },
19672 Some(tree_sitter_rust::LANGUAGE.into()),
19673 )));
19674 let mut fake_servers = language_registry.register_fake_lsp(
19675 "Rust",
19676 FakeLspAdapter {
19677 name: language_server_name,
19678 initialization_options: Some(json!({
19679 "testOptionValue": true
19680 })),
19681 initializer: Some(Box::new(move |fake_server| {
19682 let task_restarts = Arc::clone(&closure_restarts);
19683 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
19684 task_restarts.fetch_add(1, atomic::Ordering::Release);
19685 futures::future::ready(Ok(()))
19686 });
19687 })),
19688 ..Default::default()
19689 },
19690 );
19691
19692 let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
19693 let _buffer = project
19694 .update(cx, |project, cx| {
19695 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
19696 })
19697 .await
19698 .unwrap();
19699 let _fake_server = fake_servers.next().await.unwrap();
19700 update_test_language_settings(cx, &|language_settings| {
19701 language_settings.languages.0.insert(
19702 language_name.clone().0.to_string(),
19703 LanguageSettingsContent {
19704 tab_size: NonZeroU32::new(8),
19705 ..Default::default()
19706 },
19707 );
19708 });
19709 cx.executor().run_until_parked();
19710 assert_eq!(
19711 server_restarts.load(atomic::Ordering::Acquire),
19712 0,
19713 "Should not restart LSP server on an unrelated change"
19714 );
19715
19716 update_test_project_settings(cx, &|project_settings| {
19717 project_settings.lsp.0.insert(
19718 "Some other server name".into(),
19719 LspSettings {
19720 binary: None,
19721 settings: None,
19722 initialization_options: Some(json!({
19723 "some other init value": false
19724 })),
19725 enable_lsp_tasks: false,
19726 fetch: None,
19727 },
19728 );
19729 });
19730 cx.executor().run_until_parked();
19731 assert_eq!(
19732 server_restarts.load(atomic::Ordering::Acquire),
19733 0,
19734 "Should not restart LSP server on an unrelated LSP settings change"
19735 );
19736
19737 update_test_project_settings(cx, &|project_settings| {
19738 project_settings.lsp.0.insert(
19739 language_server_name.into(),
19740 LspSettings {
19741 binary: None,
19742 settings: None,
19743 initialization_options: Some(json!({
19744 "anotherInitValue": false
19745 })),
19746 enable_lsp_tasks: false,
19747 fetch: None,
19748 },
19749 );
19750 });
19751 cx.executor().run_until_parked();
19752 assert_eq!(
19753 server_restarts.load(atomic::Ordering::Acquire),
19754 1,
19755 "Should restart LSP server on a related LSP settings change"
19756 );
19757
19758 update_test_project_settings(cx, &|project_settings| {
19759 project_settings.lsp.0.insert(
19760 language_server_name.into(),
19761 LspSettings {
19762 binary: None,
19763 settings: None,
19764 initialization_options: Some(json!({
19765 "anotherInitValue": false
19766 })),
19767 enable_lsp_tasks: false,
19768 fetch: None,
19769 },
19770 );
19771 });
19772 cx.executor().run_until_parked();
19773 assert_eq!(
19774 server_restarts.load(atomic::Ordering::Acquire),
19775 1,
19776 "Should not restart LSP server on a related LSP settings change that is the same"
19777 );
19778
19779 update_test_project_settings(cx, &|project_settings| {
19780 project_settings.lsp.0.insert(
19781 language_server_name.into(),
19782 LspSettings {
19783 binary: None,
19784 settings: None,
19785 initialization_options: None,
19786 enable_lsp_tasks: false,
19787 fetch: None,
19788 },
19789 );
19790 });
19791 cx.executor().run_until_parked();
19792 assert_eq!(
19793 server_restarts.load(atomic::Ordering::Acquire),
19794 2,
19795 "Should restart LSP server on another related LSP settings change"
19796 );
19797}
19798
19799#[gpui::test]
19800async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
19801 init_test(cx, |_| {});
19802
19803 let mut cx = EditorLspTestContext::new_rust(
19804 lsp::ServerCapabilities {
19805 completion_provider: Some(lsp::CompletionOptions {
19806 trigger_characters: Some(vec![".".to_string()]),
19807 resolve_provider: Some(true),
19808 ..Default::default()
19809 }),
19810 ..Default::default()
19811 },
19812 cx,
19813 )
19814 .await;
19815
19816 cx.set_state("fn main() { let a = 2ˇ; }");
19817 cx.simulate_keystroke(".");
19818 let completion_item = lsp::CompletionItem {
19819 label: "some".into(),
19820 kind: Some(lsp::CompletionItemKind::SNIPPET),
19821 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
19822 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
19823 kind: lsp::MarkupKind::Markdown,
19824 value: "```rust\nSome(2)\n```".to_string(),
19825 })),
19826 deprecated: Some(false),
19827 sort_text: Some("fffffff2".to_string()),
19828 filter_text: Some("some".to_string()),
19829 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
19830 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19831 range: lsp::Range {
19832 start: lsp::Position {
19833 line: 0,
19834 character: 22,
19835 },
19836 end: lsp::Position {
19837 line: 0,
19838 character: 22,
19839 },
19840 },
19841 new_text: "Some(2)".to_string(),
19842 })),
19843 additional_text_edits: Some(vec![lsp::TextEdit {
19844 range: lsp::Range {
19845 start: lsp::Position {
19846 line: 0,
19847 character: 20,
19848 },
19849 end: lsp::Position {
19850 line: 0,
19851 character: 22,
19852 },
19853 },
19854 new_text: "".to_string(),
19855 }]),
19856 ..Default::default()
19857 };
19858
19859 let closure_completion_item = completion_item.clone();
19860 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19861 let task_completion_item = closure_completion_item.clone();
19862 async move {
19863 Ok(Some(lsp::CompletionResponse::Array(vec![
19864 task_completion_item,
19865 ])))
19866 }
19867 });
19868
19869 request.next().await;
19870
19871 cx.condition(|editor, _| editor.context_menu_visible())
19872 .await;
19873 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
19874 editor
19875 .confirm_completion(&ConfirmCompletion::default(), window, cx)
19876 .unwrap()
19877 });
19878 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
19879
19880 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
19881 let task_completion_item = completion_item.clone();
19882 async move { Ok(task_completion_item) }
19883 })
19884 .next()
19885 .await
19886 .unwrap();
19887 apply_additional_edits.await.unwrap();
19888 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
19889}
19890
19891#[gpui::test]
19892async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
19893 init_test(cx, |_| {});
19894
19895 let mut cx = EditorLspTestContext::new_rust(
19896 lsp::ServerCapabilities {
19897 completion_provider: Some(lsp::CompletionOptions {
19898 trigger_characters: Some(vec![".".to_string()]),
19899 resolve_provider: Some(true),
19900 ..Default::default()
19901 }),
19902 ..Default::default()
19903 },
19904 cx,
19905 )
19906 .await;
19907
19908 cx.set_state("fn main() { let a = 2ˇ; }");
19909 cx.simulate_keystroke(".");
19910
19911 let item1 = lsp::CompletionItem {
19912 label: "method id()".to_string(),
19913 filter_text: Some("id".to_string()),
19914 detail: None,
19915 documentation: None,
19916 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19917 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19918 new_text: ".id".to_string(),
19919 })),
19920 ..lsp::CompletionItem::default()
19921 };
19922
19923 let item2 = lsp::CompletionItem {
19924 label: "other".to_string(),
19925 filter_text: Some("other".to_string()),
19926 detail: None,
19927 documentation: None,
19928 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19929 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19930 new_text: ".other".to_string(),
19931 })),
19932 ..lsp::CompletionItem::default()
19933 };
19934
19935 let item1 = item1.clone();
19936 cx.set_request_handler::<lsp::request::Completion, _, _>({
19937 let item1 = item1.clone();
19938 move |_, _, _| {
19939 let item1 = item1.clone();
19940 let item2 = item2.clone();
19941 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
19942 }
19943 })
19944 .next()
19945 .await;
19946
19947 cx.condition(|editor, _| editor.context_menu_visible())
19948 .await;
19949 cx.update_editor(|editor, _, _| {
19950 let context_menu = editor.context_menu.borrow_mut();
19951 let context_menu = context_menu
19952 .as_ref()
19953 .expect("Should have the context menu deployed");
19954 match context_menu {
19955 CodeContextMenu::Completions(completions_menu) => {
19956 let completions = completions_menu.completions.borrow_mut();
19957 assert_eq!(
19958 completions
19959 .iter()
19960 .map(|completion| &completion.label.text)
19961 .collect::<Vec<_>>(),
19962 vec!["method id()", "other"]
19963 )
19964 }
19965 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
19966 }
19967 });
19968
19969 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
19970 let item1 = item1.clone();
19971 move |_, item_to_resolve, _| {
19972 let item1 = item1.clone();
19973 async move {
19974 if item1 == item_to_resolve {
19975 Ok(lsp::CompletionItem {
19976 label: "method id()".to_string(),
19977 filter_text: Some("id".to_string()),
19978 detail: Some("Now resolved!".to_string()),
19979 documentation: Some(lsp::Documentation::String("Docs".to_string())),
19980 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19981 range: lsp::Range::new(
19982 lsp::Position::new(0, 22),
19983 lsp::Position::new(0, 22),
19984 ),
19985 new_text: ".id".to_string(),
19986 })),
19987 ..lsp::CompletionItem::default()
19988 })
19989 } else {
19990 Ok(item_to_resolve)
19991 }
19992 }
19993 }
19994 })
19995 .next()
19996 .await
19997 .unwrap();
19998 cx.run_until_parked();
19999
20000 cx.update_editor(|editor, window, cx| {
20001 editor.context_menu_next(&Default::default(), window, cx);
20002 });
20003 cx.run_until_parked();
20004
20005 cx.update_editor(|editor, _, _| {
20006 let context_menu = editor.context_menu.borrow_mut();
20007 let context_menu = context_menu
20008 .as_ref()
20009 .expect("Should have the context menu deployed");
20010 match context_menu {
20011 CodeContextMenu::Completions(completions_menu) => {
20012 let completions = completions_menu.completions.borrow_mut();
20013 assert_eq!(
20014 completions
20015 .iter()
20016 .map(|completion| &completion.label.text)
20017 .collect::<Vec<_>>(),
20018 vec!["method id() Now resolved!", "other"],
20019 "Should update first completion label, but not second as the filter text did not match."
20020 );
20021 }
20022 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
20023 }
20024 });
20025}
20026
20027#[gpui::test]
20028async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
20029 init_test(cx, |_| {});
20030 let mut cx = EditorLspTestContext::new_rust(
20031 lsp::ServerCapabilities {
20032 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
20033 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
20034 completion_provider: Some(lsp::CompletionOptions {
20035 resolve_provider: Some(true),
20036 ..Default::default()
20037 }),
20038 ..Default::default()
20039 },
20040 cx,
20041 )
20042 .await;
20043 cx.set_state(indoc! {"
20044 struct TestStruct {
20045 field: i32
20046 }
20047
20048 fn mainˇ() {
20049 let unused_var = 42;
20050 let test_struct = TestStruct { field: 42 };
20051 }
20052 "});
20053 let symbol_range = cx.lsp_range(indoc! {"
20054 struct TestStruct {
20055 field: i32
20056 }
20057
20058 «fn main»() {
20059 let unused_var = 42;
20060 let test_struct = TestStruct { field: 42 };
20061 }
20062 "});
20063 let mut hover_requests =
20064 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
20065 Ok(Some(lsp::Hover {
20066 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
20067 kind: lsp::MarkupKind::Markdown,
20068 value: "Function documentation".to_string(),
20069 }),
20070 range: Some(symbol_range),
20071 }))
20072 });
20073
20074 // Case 1: Test that code action menu hide hover popover
20075 cx.dispatch_action(Hover);
20076 hover_requests.next().await;
20077 cx.condition(|editor, _| editor.hover_state.visible()).await;
20078 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
20079 move |_, _, _| async move {
20080 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
20081 lsp::CodeAction {
20082 title: "Remove unused variable".to_string(),
20083 kind: Some(CodeActionKind::QUICKFIX),
20084 edit: Some(lsp::WorkspaceEdit {
20085 changes: Some(
20086 [(
20087 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
20088 vec![lsp::TextEdit {
20089 range: lsp::Range::new(
20090 lsp::Position::new(5, 4),
20091 lsp::Position::new(5, 27),
20092 ),
20093 new_text: "".to_string(),
20094 }],
20095 )]
20096 .into_iter()
20097 .collect(),
20098 ),
20099 ..Default::default()
20100 }),
20101 ..Default::default()
20102 },
20103 )]))
20104 },
20105 );
20106 cx.update_editor(|editor, window, cx| {
20107 editor.toggle_code_actions(
20108 &ToggleCodeActions {
20109 deployed_from: None,
20110 quick_launch: false,
20111 },
20112 window,
20113 cx,
20114 );
20115 });
20116 code_action_requests.next().await;
20117 cx.run_until_parked();
20118 cx.condition(|editor, _| editor.context_menu_visible())
20119 .await;
20120 cx.update_editor(|editor, _, _| {
20121 assert!(
20122 !editor.hover_state.visible(),
20123 "Hover popover should be hidden when code action menu is shown"
20124 );
20125 // Hide code actions
20126 editor.context_menu.take();
20127 });
20128
20129 // Case 2: Test that code completions hide hover popover
20130 cx.dispatch_action(Hover);
20131 hover_requests.next().await;
20132 cx.condition(|editor, _| editor.hover_state.visible()).await;
20133 let counter = Arc::new(AtomicUsize::new(0));
20134 let mut completion_requests =
20135 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
20136 let counter = counter.clone();
20137 async move {
20138 counter.fetch_add(1, atomic::Ordering::Release);
20139 Ok(Some(lsp::CompletionResponse::Array(vec![
20140 lsp::CompletionItem {
20141 label: "main".into(),
20142 kind: Some(lsp::CompletionItemKind::FUNCTION),
20143 detail: Some("() -> ()".to_string()),
20144 ..Default::default()
20145 },
20146 lsp::CompletionItem {
20147 label: "TestStruct".into(),
20148 kind: Some(lsp::CompletionItemKind::STRUCT),
20149 detail: Some("struct TestStruct".to_string()),
20150 ..Default::default()
20151 },
20152 ])))
20153 }
20154 });
20155 cx.update_editor(|editor, window, cx| {
20156 editor.show_completions(&ShowCompletions, window, cx);
20157 });
20158 completion_requests.next().await;
20159 cx.condition(|editor, _| editor.context_menu_visible())
20160 .await;
20161 cx.update_editor(|editor, _, _| {
20162 assert!(
20163 !editor.hover_state.visible(),
20164 "Hover popover should be hidden when completion menu is shown"
20165 );
20166 });
20167}
20168
20169#[gpui::test]
20170async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
20171 init_test(cx, |_| {});
20172
20173 let mut cx = EditorLspTestContext::new_rust(
20174 lsp::ServerCapabilities {
20175 completion_provider: Some(lsp::CompletionOptions {
20176 trigger_characters: Some(vec![".".to_string()]),
20177 resolve_provider: Some(true),
20178 ..Default::default()
20179 }),
20180 ..Default::default()
20181 },
20182 cx,
20183 )
20184 .await;
20185
20186 cx.set_state("fn main() { let a = 2ˇ; }");
20187 cx.simulate_keystroke(".");
20188
20189 let unresolved_item_1 = lsp::CompletionItem {
20190 label: "id".to_string(),
20191 filter_text: Some("id".to_string()),
20192 detail: None,
20193 documentation: None,
20194 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20195 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
20196 new_text: ".id".to_string(),
20197 })),
20198 ..lsp::CompletionItem::default()
20199 };
20200 let resolved_item_1 = lsp::CompletionItem {
20201 additional_text_edits: Some(vec![lsp::TextEdit {
20202 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
20203 new_text: "!!".to_string(),
20204 }]),
20205 ..unresolved_item_1.clone()
20206 };
20207 let unresolved_item_2 = lsp::CompletionItem {
20208 label: "other".to_string(),
20209 filter_text: Some("other".to_string()),
20210 detail: None,
20211 documentation: None,
20212 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20213 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
20214 new_text: ".other".to_string(),
20215 })),
20216 ..lsp::CompletionItem::default()
20217 };
20218 let resolved_item_2 = lsp::CompletionItem {
20219 additional_text_edits: Some(vec![lsp::TextEdit {
20220 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
20221 new_text: "??".to_string(),
20222 }]),
20223 ..unresolved_item_2.clone()
20224 };
20225
20226 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
20227 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
20228 cx.lsp
20229 .server
20230 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
20231 let unresolved_item_1 = unresolved_item_1.clone();
20232 let resolved_item_1 = resolved_item_1.clone();
20233 let unresolved_item_2 = unresolved_item_2.clone();
20234 let resolved_item_2 = resolved_item_2.clone();
20235 let resolve_requests_1 = resolve_requests_1.clone();
20236 let resolve_requests_2 = resolve_requests_2.clone();
20237 move |unresolved_request, _| {
20238 let unresolved_item_1 = unresolved_item_1.clone();
20239 let resolved_item_1 = resolved_item_1.clone();
20240 let unresolved_item_2 = unresolved_item_2.clone();
20241 let resolved_item_2 = resolved_item_2.clone();
20242 let resolve_requests_1 = resolve_requests_1.clone();
20243 let resolve_requests_2 = resolve_requests_2.clone();
20244 async move {
20245 if unresolved_request == unresolved_item_1 {
20246 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
20247 Ok(resolved_item_1.clone())
20248 } else if unresolved_request == unresolved_item_2 {
20249 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
20250 Ok(resolved_item_2.clone())
20251 } else {
20252 panic!("Unexpected completion item {unresolved_request:?}")
20253 }
20254 }
20255 }
20256 })
20257 .detach();
20258
20259 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
20260 let unresolved_item_1 = unresolved_item_1.clone();
20261 let unresolved_item_2 = unresolved_item_2.clone();
20262 async move {
20263 Ok(Some(lsp::CompletionResponse::Array(vec![
20264 unresolved_item_1,
20265 unresolved_item_2,
20266 ])))
20267 }
20268 })
20269 .next()
20270 .await;
20271
20272 cx.condition(|editor, _| editor.context_menu_visible())
20273 .await;
20274 cx.update_editor(|editor, _, _| {
20275 let context_menu = editor.context_menu.borrow_mut();
20276 let context_menu = context_menu
20277 .as_ref()
20278 .expect("Should have the context menu deployed");
20279 match context_menu {
20280 CodeContextMenu::Completions(completions_menu) => {
20281 let completions = completions_menu.completions.borrow_mut();
20282 assert_eq!(
20283 completions
20284 .iter()
20285 .map(|completion| &completion.label.text)
20286 .collect::<Vec<_>>(),
20287 vec!["id", "other"]
20288 )
20289 }
20290 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
20291 }
20292 });
20293 cx.run_until_parked();
20294
20295 cx.update_editor(|editor, window, cx| {
20296 editor.context_menu_next(&ContextMenuNext, window, cx);
20297 });
20298 cx.run_until_parked();
20299 cx.update_editor(|editor, window, cx| {
20300 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
20301 });
20302 cx.run_until_parked();
20303 cx.update_editor(|editor, window, cx| {
20304 editor.context_menu_next(&ContextMenuNext, window, cx);
20305 });
20306 cx.run_until_parked();
20307 cx.update_editor(|editor, window, cx| {
20308 editor
20309 .compose_completion(&ComposeCompletion::default(), window, cx)
20310 .expect("No task returned")
20311 })
20312 .await
20313 .expect("Completion failed");
20314 cx.run_until_parked();
20315
20316 cx.update_editor(|editor, _, cx| {
20317 assert_eq!(
20318 resolve_requests_1.load(atomic::Ordering::Acquire),
20319 1,
20320 "Should always resolve once despite multiple selections"
20321 );
20322 assert_eq!(
20323 resolve_requests_2.load(atomic::Ordering::Acquire),
20324 1,
20325 "Should always resolve once after multiple selections and applying the completion"
20326 );
20327 assert_eq!(
20328 editor.text(cx),
20329 "fn main() { let a = ??.other; }",
20330 "Should use resolved data when applying the completion"
20331 );
20332 });
20333}
20334
20335#[gpui::test]
20336async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
20337 init_test(cx, |_| {});
20338
20339 let item_0 = lsp::CompletionItem {
20340 label: "abs".into(),
20341 insert_text: Some("abs".into()),
20342 data: Some(json!({ "very": "special"})),
20343 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
20344 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20345 lsp::InsertReplaceEdit {
20346 new_text: "abs".to_string(),
20347 insert: lsp::Range::default(),
20348 replace: lsp::Range::default(),
20349 },
20350 )),
20351 ..lsp::CompletionItem::default()
20352 };
20353 let items = iter::once(item_0.clone())
20354 .chain((11..51).map(|i| lsp::CompletionItem {
20355 label: format!("item_{}", i),
20356 insert_text: Some(format!("item_{}", i)),
20357 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
20358 ..lsp::CompletionItem::default()
20359 }))
20360 .collect::<Vec<_>>();
20361
20362 let default_commit_characters = vec!["?".to_string()];
20363 let default_data = json!({ "default": "data"});
20364 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
20365 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
20366 let default_edit_range = lsp::Range {
20367 start: lsp::Position {
20368 line: 0,
20369 character: 5,
20370 },
20371 end: lsp::Position {
20372 line: 0,
20373 character: 5,
20374 },
20375 };
20376
20377 let mut cx = EditorLspTestContext::new_rust(
20378 lsp::ServerCapabilities {
20379 completion_provider: Some(lsp::CompletionOptions {
20380 trigger_characters: Some(vec![".".to_string()]),
20381 resolve_provider: Some(true),
20382 ..Default::default()
20383 }),
20384 ..Default::default()
20385 },
20386 cx,
20387 )
20388 .await;
20389
20390 cx.set_state("fn main() { let a = 2ˇ; }");
20391 cx.simulate_keystroke(".");
20392
20393 let completion_data = default_data.clone();
20394 let completion_characters = default_commit_characters.clone();
20395 let completion_items = items.clone();
20396 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
20397 let default_data = completion_data.clone();
20398 let default_commit_characters = completion_characters.clone();
20399 let items = completion_items.clone();
20400 async move {
20401 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
20402 items,
20403 item_defaults: Some(lsp::CompletionListItemDefaults {
20404 data: Some(default_data.clone()),
20405 commit_characters: Some(default_commit_characters.clone()),
20406 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
20407 default_edit_range,
20408 )),
20409 insert_text_format: Some(default_insert_text_format),
20410 insert_text_mode: Some(default_insert_text_mode),
20411 }),
20412 ..lsp::CompletionList::default()
20413 })))
20414 }
20415 })
20416 .next()
20417 .await;
20418
20419 let resolved_items = Arc::new(Mutex::new(Vec::new()));
20420 cx.lsp
20421 .server
20422 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
20423 let closure_resolved_items = resolved_items.clone();
20424 move |item_to_resolve, _| {
20425 let closure_resolved_items = closure_resolved_items.clone();
20426 async move {
20427 closure_resolved_items.lock().push(item_to_resolve.clone());
20428 Ok(item_to_resolve)
20429 }
20430 }
20431 })
20432 .detach();
20433
20434 cx.condition(|editor, _| editor.context_menu_visible())
20435 .await;
20436 cx.run_until_parked();
20437 cx.update_editor(|editor, _, _| {
20438 let menu = editor.context_menu.borrow_mut();
20439 match menu.as_ref().expect("should have the completions menu") {
20440 CodeContextMenu::Completions(completions_menu) => {
20441 assert_eq!(
20442 completions_menu
20443 .entries
20444 .borrow()
20445 .iter()
20446 .map(|mat| mat.string.clone())
20447 .collect::<Vec<String>>(),
20448 items
20449 .iter()
20450 .map(|completion| completion.label.clone())
20451 .collect::<Vec<String>>()
20452 );
20453 }
20454 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
20455 }
20456 });
20457 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
20458 // with 4 from the end.
20459 assert_eq!(
20460 *resolved_items.lock(),
20461 [&items[0..16], &items[items.len() - 4..items.len()]]
20462 .concat()
20463 .iter()
20464 .cloned()
20465 .map(|mut item| {
20466 if item.data.is_none() {
20467 item.data = Some(default_data.clone());
20468 }
20469 item
20470 })
20471 .collect::<Vec<lsp::CompletionItem>>(),
20472 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
20473 );
20474 resolved_items.lock().clear();
20475
20476 cx.update_editor(|editor, window, cx| {
20477 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
20478 });
20479 cx.run_until_parked();
20480 // Completions that have already been resolved are skipped.
20481 assert_eq!(
20482 *resolved_items.lock(),
20483 items[items.len() - 17..items.len() - 4]
20484 .iter()
20485 .cloned()
20486 .map(|mut item| {
20487 if item.data.is_none() {
20488 item.data = Some(default_data.clone());
20489 }
20490 item
20491 })
20492 .collect::<Vec<lsp::CompletionItem>>()
20493 );
20494 resolved_items.lock().clear();
20495}
20496
20497#[gpui::test]
20498async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
20499 init_test(cx, |_| {});
20500
20501 let mut cx = EditorLspTestContext::new(
20502 Language::new(
20503 LanguageConfig {
20504 matcher: LanguageMatcher {
20505 path_suffixes: vec!["jsx".into()],
20506 ..Default::default()
20507 },
20508 overrides: [(
20509 "element".into(),
20510 LanguageConfigOverride {
20511 completion_query_characters: Override::Set(['-'].into_iter().collect()),
20512 ..Default::default()
20513 },
20514 )]
20515 .into_iter()
20516 .collect(),
20517 ..Default::default()
20518 },
20519 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
20520 )
20521 .with_override_query("(jsx_self_closing_element) @element")
20522 .unwrap(),
20523 lsp::ServerCapabilities {
20524 completion_provider: Some(lsp::CompletionOptions {
20525 trigger_characters: Some(vec![":".to_string()]),
20526 ..Default::default()
20527 }),
20528 ..Default::default()
20529 },
20530 cx,
20531 )
20532 .await;
20533
20534 cx.lsp
20535 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20536 Ok(Some(lsp::CompletionResponse::Array(vec![
20537 lsp::CompletionItem {
20538 label: "bg-blue".into(),
20539 ..Default::default()
20540 },
20541 lsp::CompletionItem {
20542 label: "bg-red".into(),
20543 ..Default::default()
20544 },
20545 lsp::CompletionItem {
20546 label: "bg-yellow".into(),
20547 ..Default::default()
20548 },
20549 ])))
20550 });
20551
20552 cx.set_state(r#"<p class="bgˇ" />"#);
20553
20554 // Trigger completion when typing a dash, because the dash is an extra
20555 // word character in the 'element' scope, which contains the cursor.
20556 cx.simulate_keystroke("-");
20557 cx.executor().run_until_parked();
20558 cx.update_editor(|editor, _, _| {
20559 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
20560 {
20561 assert_eq!(
20562 completion_menu_entries(menu),
20563 &["bg-blue", "bg-red", "bg-yellow"]
20564 );
20565 } else {
20566 panic!("expected completion menu to be open");
20567 }
20568 });
20569
20570 cx.simulate_keystroke("l");
20571 cx.executor().run_until_parked();
20572 cx.update_editor(|editor, _, _| {
20573 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
20574 {
20575 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
20576 } else {
20577 panic!("expected completion menu to be open");
20578 }
20579 });
20580
20581 // When filtering completions, consider the character after the '-' to
20582 // be the start of a subword.
20583 cx.set_state(r#"<p class="yelˇ" />"#);
20584 cx.simulate_keystroke("l");
20585 cx.executor().run_until_parked();
20586 cx.update_editor(|editor, _, _| {
20587 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
20588 {
20589 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
20590 } else {
20591 panic!("expected completion menu to be open");
20592 }
20593 });
20594}
20595
20596fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
20597 let entries = menu.entries.borrow();
20598 entries.iter().map(|mat| mat.string.clone()).collect()
20599}
20600
20601#[gpui::test]
20602async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
20603 init_test(cx, |settings| {
20604 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
20605 });
20606
20607 let fs = FakeFs::new(cx.executor());
20608 fs.insert_file(path!("/file.ts"), Default::default()).await;
20609
20610 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
20611 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20612
20613 language_registry.add(Arc::new(Language::new(
20614 LanguageConfig {
20615 name: "TypeScript".into(),
20616 matcher: LanguageMatcher {
20617 path_suffixes: vec!["ts".to_string()],
20618 ..Default::default()
20619 },
20620 ..Default::default()
20621 },
20622 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20623 )));
20624 update_test_language_settings(cx, &|settings| {
20625 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
20626 });
20627
20628 let test_plugin = "test_plugin";
20629 let _ = language_registry.register_fake_lsp(
20630 "TypeScript",
20631 FakeLspAdapter {
20632 prettier_plugins: vec![test_plugin],
20633 ..Default::default()
20634 },
20635 );
20636
20637 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
20638 let buffer = project
20639 .update(cx, |project, cx| {
20640 project.open_local_buffer(path!("/file.ts"), cx)
20641 })
20642 .await
20643 .unwrap();
20644
20645 let buffer_text = "one\ntwo\nthree\n";
20646 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
20647 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
20648 editor.update_in(cx, |editor, window, cx| {
20649 editor.set_text(buffer_text, window, cx)
20650 });
20651
20652 editor
20653 .update_in(cx, |editor, window, cx| {
20654 editor.perform_format(
20655 project.clone(),
20656 FormatTrigger::Manual,
20657 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
20658 window,
20659 cx,
20660 )
20661 })
20662 .unwrap()
20663 .await;
20664 assert_eq!(
20665 editor.update(cx, |editor, cx| editor.text(cx)),
20666 buffer_text.to_string() + prettier_format_suffix,
20667 "Test prettier formatting was not applied to the original buffer text",
20668 );
20669
20670 update_test_language_settings(cx, &|settings| {
20671 settings.defaults.formatter = Some(FormatterList::default())
20672 });
20673 let format = editor.update_in(cx, |editor, window, cx| {
20674 editor.perform_format(
20675 project.clone(),
20676 FormatTrigger::Manual,
20677 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
20678 window,
20679 cx,
20680 )
20681 });
20682 format.await.unwrap();
20683 assert_eq!(
20684 editor.update(cx, |editor, cx| editor.text(cx)),
20685 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
20686 "Autoformatting (via test prettier) was not applied to the original buffer text",
20687 );
20688}
20689
20690#[gpui::test]
20691async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
20692 init_test(cx, |settings| {
20693 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
20694 });
20695
20696 let fs = FakeFs::new(cx.executor());
20697 fs.insert_file(path!("/file.settings"), Default::default())
20698 .await;
20699
20700 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
20701 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20702
20703 let ts_lang = Arc::new(Language::new(
20704 LanguageConfig {
20705 name: "TypeScript".into(),
20706 matcher: LanguageMatcher {
20707 path_suffixes: vec!["ts".to_string()],
20708 ..LanguageMatcher::default()
20709 },
20710 prettier_parser_name: Some("typescript".to_string()),
20711 ..LanguageConfig::default()
20712 },
20713 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20714 ));
20715
20716 language_registry.add(ts_lang.clone());
20717
20718 update_test_language_settings(cx, &|settings| {
20719 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
20720 });
20721
20722 let test_plugin = "test_plugin";
20723 let _ = language_registry.register_fake_lsp(
20724 "TypeScript",
20725 FakeLspAdapter {
20726 prettier_plugins: vec![test_plugin],
20727 ..Default::default()
20728 },
20729 );
20730
20731 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
20732 let buffer = project
20733 .update(cx, |project, cx| {
20734 project.open_local_buffer(path!("/file.settings"), cx)
20735 })
20736 .await
20737 .unwrap();
20738
20739 project.update(cx, |project, cx| {
20740 project.set_language_for_buffer(&buffer, ts_lang, cx)
20741 });
20742
20743 let buffer_text = "one\ntwo\nthree\n";
20744 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
20745 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
20746 editor.update_in(cx, |editor, window, cx| {
20747 editor.set_text(buffer_text, window, cx)
20748 });
20749
20750 editor
20751 .update_in(cx, |editor, window, cx| {
20752 editor.perform_format(
20753 project.clone(),
20754 FormatTrigger::Manual,
20755 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
20756 window,
20757 cx,
20758 )
20759 })
20760 .unwrap()
20761 .await;
20762 assert_eq!(
20763 editor.update(cx, |editor, cx| editor.text(cx)),
20764 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
20765 "Test prettier formatting was not applied to the original buffer text",
20766 );
20767
20768 update_test_language_settings(cx, &|settings| {
20769 settings.defaults.formatter = Some(FormatterList::default())
20770 });
20771 let format = editor.update_in(cx, |editor, window, cx| {
20772 editor.perform_format(
20773 project.clone(),
20774 FormatTrigger::Manual,
20775 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
20776 window,
20777 cx,
20778 )
20779 });
20780 format.await.unwrap();
20781
20782 assert_eq!(
20783 editor.update(cx, |editor, cx| editor.text(cx)),
20784 buffer_text.to_string()
20785 + prettier_format_suffix
20786 + "\ntypescript\n"
20787 + prettier_format_suffix
20788 + "\ntypescript",
20789 "Autoformatting (via test prettier) was not applied to the original buffer text",
20790 );
20791}
20792
20793#[gpui::test]
20794async fn test_addition_reverts(cx: &mut TestAppContext) {
20795 init_test(cx, |_| {});
20796 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
20797 let base_text = indoc! {r#"
20798 struct Row;
20799 struct Row1;
20800 struct Row2;
20801
20802 struct Row4;
20803 struct Row5;
20804 struct Row6;
20805
20806 struct Row8;
20807 struct Row9;
20808 struct Row10;"#};
20809
20810 // When addition hunks are not adjacent to carets, no hunk revert is performed
20811 assert_hunk_revert(
20812 indoc! {r#"struct Row;
20813 struct Row1;
20814 struct Row1.1;
20815 struct Row1.2;
20816 struct Row2;ˇ
20817
20818 struct Row4;
20819 struct Row5;
20820 struct Row6;
20821
20822 struct Row8;
20823 ˇstruct Row9;
20824 struct Row9.1;
20825 struct Row9.2;
20826 struct Row9.3;
20827 struct Row10;"#},
20828 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
20829 indoc! {r#"struct Row;
20830 struct Row1;
20831 struct Row1.1;
20832 struct Row1.2;
20833 struct Row2;ˇ
20834
20835 struct Row4;
20836 struct Row5;
20837 struct Row6;
20838
20839 struct Row8;
20840 ˇstruct Row9;
20841 struct Row9.1;
20842 struct Row9.2;
20843 struct Row9.3;
20844 struct Row10;"#},
20845 base_text,
20846 &mut cx,
20847 );
20848 // Same for selections
20849 assert_hunk_revert(
20850 indoc! {r#"struct Row;
20851 struct Row1;
20852 struct Row2;
20853 struct Row2.1;
20854 struct Row2.2;
20855 «ˇ
20856 struct Row4;
20857 struct» Row5;
20858 «struct Row6;
20859 ˇ»
20860 struct Row9.1;
20861 struct Row9.2;
20862 struct Row9.3;
20863 struct Row8;
20864 struct Row9;
20865 struct Row10;"#},
20866 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
20867 indoc! {r#"struct Row;
20868 struct Row1;
20869 struct Row2;
20870 struct Row2.1;
20871 struct Row2.2;
20872 «ˇ
20873 struct Row4;
20874 struct» Row5;
20875 «struct Row6;
20876 ˇ»
20877 struct Row9.1;
20878 struct Row9.2;
20879 struct Row9.3;
20880 struct Row8;
20881 struct Row9;
20882 struct Row10;"#},
20883 base_text,
20884 &mut cx,
20885 );
20886
20887 // When carets and selections intersect the addition hunks, those are reverted.
20888 // Adjacent carets got merged.
20889 assert_hunk_revert(
20890 indoc! {r#"struct Row;
20891 ˇ// something on the top
20892 struct Row1;
20893 struct Row2;
20894 struct Roˇw3.1;
20895 struct Row2.2;
20896 struct Row2.3;ˇ
20897
20898 struct Row4;
20899 struct ˇRow5.1;
20900 struct Row5.2;
20901 struct «Rowˇ»5.3;
20902 struct Row5;
20903 struct Row6;
20904 ˇ
20905 struct Row9.1;
20906 struct «Rowˇ»9.2;
20907 struct «ˇRow»9.3;
20908 struct Row8;
20909 struct Row9;
20910 «ˇ// something on bottom»
20911 struct Row10;"#},
20912 vec![
20913 DiffHunkStatusKind::Added,
20914 DiffHunkStatusKind::Added,
20915 DiffHunkStatusKind::Added,
20916 DiffHunkStatusKind::Added,
20917 DiffHunkStatusKind::Added,
20918 ],
20919 indoc! {r#"struct Row;
20920 ˇstruct Row1;
20921 struct Row2;
20922 ˇ
20923 struct Row4;
20924 ˇstruct Row5;
20925 struct Row6;
20926 ˇ
20927 ˇstruct Row8;
20928 struct Row9;
20929 ˇstruct Row10;"#},
20930 base_text,
20931 &mut cx,
20932 );
20933}
20934
20935#[gpui::test]
20936async fn test_modification_reverts(cx: &mut TestAppContext) {
20937 init_test(cx, |_| {});
20938 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
20939 let base_text = indoc! {r#"
20940 struct Row;
20941 struct Row1;
20942 struct Row2;
20943
20944 struct Row4;
20945 struct Row5;
20946 struct Row6;
20947
20948 struct Row8;
20949 struct Row9;
20950 struct Row10;"#};
20951
20952 // Modification hunks behave the same as the addition ones.
20953 assert_hunk_revert(
20954 indoc! {r#"struct Row;
20955 struct Row1;
20956 struct Row33;
20957 ˇ
20958 struct Row4;
20959 struct Row5;
20960 struct Row6;
20961 ˇ
20962 struct Row99;
20963 struct Row9;
20964 struct Row10;"#},
20965 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
20966 indoc! {r#"struct Row;
20967 struct Row1;
20968 struct Row33;
20969 ˇ
20970 struct Row4;
20971 struct Row5;
20972 struct Row6;
20973 ˇ
20974 struct Row99;
20975 struct Row9;
20976 struct Row10;"#},
20977 base_text,
20978 &mut cx,
20979 );
20980 assert_hunk_revert(
20981 indoc! {r#"struct Row;
20982 struct Row1;
20983 struct Row33;
20984 «ˇ
20985 struct Row4;
20986 struct» Row5;
20987 «struct Row6;
20988 ˇ»
20989 struct Row99;
20990 struct Row9;
20991 struct Row10;"#},
20992 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
20993 indoc! {r#"struct Row;
20994 struct Row1;
20995 struct Row33;
20996 «ˇ
20997 struct Row4;
20998 struct» Row5;
20999 «struct Row6;
21000 ˇ»
21001 struct Row99;
21002 struct Row9;
21003 struct Row10;"#},
21004 base_text,
21005 &mut cx,
21006 );
21007
21008 assert_hunk_revert(
21009 indoc! {r#"ˇstruct Row1.1;
21010 struct Row1;
21011 «ˇstr»uct Row22;
21012
21013 struct ˇRow44;
21014 struct Row5;
21015 struct «Rˇ»ow66;ˇ
21016
21017 «struˇ»ct Row88;
21018 struct Row9;
21019 struct Row1011;ˇ"#},
21020 vec![
21021 DiffHunkStatusKind::Modified,
21022 DiffHunkStatusKind::Modified,
21023 DiffHunkStatusKind::Modified,
21024 DiffHunkStatusKind::Modified,
21025 DiffHunkStatusKind::Modified,
21026 DiffHunkStatusKind::Modified,
21027 ],
21028 indoc! {r#"struct Row;
21029 ˇstruct Row1;
21030 struct Row2;
21031 ˇ
21032 struct Row4;
21033 ˇstruct Row5;
21034 struct Row6;
21035 ˇ
21036 struct Row8;
21037 ˇstruct Row9;
21038 struct Row10;ˇ"#},
21039 base_text,
21040 &mut cx,
21041 );
21042}
21043
21044#[gpui::test]
21045async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
21046 init_test(cx, |_| {});
21047 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
21048 let base_text = indoc! {r#"
21049 one
21050
21051 two
21052 three
21053 "#};
21054
21055 cx.set_head_text(base_text);
21056 cx.set_state("\nˇ\n");
21057 cx.executor().run_until_parked();
21058 cx.update_editor(|editor, _window, cx| {
21059 editor.expand_selected_diff_hunks(cx);
21060 });
21061 cx.executor().run_until_parked();
21062 cx.update_editor(|editor, window, cx| {
21063 editor.backspace(&Default::default(), window, cx);
21064 });
21065 cx.run_until_parked();
21066 cx.assert_state_with_diff(
21067 indoc! {r#"
21068
21069 - two
21070 - threeˇ
21071 +
21072 "#}
21073 .to_string(),
21074 );
21075}
21076
21077#[gpui::test]
21078async fn test_deletion_reverts(cx: &mut TestAppContext) {
21079 init_test(cx, |_| {});
21080 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
21081 let base_text = indoc! {r#"struct Row;
21082struct Row1;
21083struct Row2;
21084
21085struct Row4;
21086struct Row5;
21087struct Row6;
21088
21089struct Row8;
21090struct Row9;
21091struct Row10;"#};
21092
21093 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
21094 assert_hunk_revert(
21095 indoc! {r#"struct Row;
21096 struct Row2;
21097
21098 ˇstruct Row4;
21099 struct Row5;
21100 struct Row6;
21101 ˇ
21102 struct Row8;
21103 struct Row10;"#},
21104 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
21105 indoc! {r#"struct Row;
21106 struct Row2;
21107
21108 ˇstruct Row4;
21109 struct Row5;
21110 struct Row6;
21111 ˇ
21112 struct Row8;
21113 struct Row10;"#},
21114 base_text,
21115 &mut cx,
21116 );
21117 assert_hunk_revert(
21118 indoc! {r#"struct Row;
21119 struct Row2;
21120
21121 «ˇstruct Row4;
21122 struct» Row5;
21123 «struct Row6;
21124 ˇ»
21125 struct Row8;
21126 struct Row10;"#},
21127 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
21128 indoc! {r#"struct Row;
21129 struct Row2;
21130
21131 «ˇstruct Row4;
21132 struct» Row5;
21133 «struct Row6;
21134 ˇ»
21135 struct Row8;
21136 struct Row10;"#},
21137 base_text,
21138 &mut cx,
21139 );
21140
21141 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
21142 assert_hunk_revert(
21143 indoc! {r#"struct Row;
21144 ˇstruct Row2;
21145
21146 struct Row4;
21147 struct Row5;
21148 struct Row6;
21149
21150 struct Row8;ˇ
21151 struct Row10;"#},
21152 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
21153 indoc! {r#"struct Row;
21154 struct Row1;
21155 ˇstruct Row2;
21156
21157 struct Row4;
21158 struct Row5;
21159 struct Row6;
21160
21161 struct Row8;ˇ
21162 struct Row9;
21163 struct Row10;"#},
21164 base_text,
21165 &mut cx,
21166 );
21167 assert_hunk_revert(
21168 indoc! {r#"struct Row;
21169 struct Row2«ˇ;
21170 struct Row4;
21171 struct» Row5;
21172 «struct Row6;
21173
21174 struct Row8;ˇ»
21175 struct Row10;"#},
21176 vec![
21177 DiffHunkStatusKind::Deleted,
21178 DiffHunkStatusKind::Deleted,
21179 DiffHunkStatusKind::Deleted,
21180 ],
21181 indoc! {r#"struct Row;
21182 struct Row1;
21183 struct Row2«ˇ;
21184
21185 struct Row4;
21186 struct» Row5;
21187 «struct Row6;
21188
21189 struct Row8;ˇ»
21190 struct Row9;
21191 struct Row10;"#},
21192 base_text,
21193 &mut cx,
21194 );
21195}
21196
21197#[gpui::test]
21198async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
21199 init_test(cx, |_| {});
21200
21201 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
21202 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
21203 let base_text_3 =
21204 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
21205
21206 let text_1 = edit_first_char_of_every_line(base_text_1);
21207 let text_2 = edit_first_char_of_every_line(base_text_2);
21208 let text_3 = edit_first_char_of_every_line(base_text_3);
21209
21210 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
21211 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
21212 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
21213
21214 let multibuffer = cx.new(|cx| {
21215 let mut multibuffer = MultiBuffer::new(ReadWrite);
21216 multibuffer.set_excerpts_for_path(
21217 PathKey::sorted(0),
21218 buffer_1.clone(),
21219 [
21220 Point::new(0, 0)..Point::new(2, 0),
21221 Point::new(5, 0)..Point::new(6, 0),
21222 Point::new(9, 0)..Point::new(9, 4),
21223 ],
21224 0,
21225 cx,
21226 );
21227 multibuffer.set_excerpts_for_path(
21228 PathKey::sorted(1),
21229 buffer_2.clone(),
21230 [
21231 Point::new(0, 0)..Point::new(2, 0),
21232 Point::new(5, 0)..Point::new(6, 0),
21233 Point::new(9, 0)..Point::new(9, 4),
21234 ],
21235 0,
21236 cx,
21237 );
21238 multibuffer.set_excerpts_for_path(
21239 PathKey::sorted(2),
21240 buffer_3.clone(),
21241 [
21242 Point::new(0, 0)..Point::new(2, 0),
21243 Point::new(5, 0)..Point::new(6, 0),
21244 Point::new(9, 0)..Point::new(9, 4),
21245 ],
21246 0,
21247 cx,
21248 );
21249 multibuffer
21250 });
21251
21252 let fs = FakeFs::new(cx.executor());
21253 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21254 let (editor, cx) = cx
21255 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
21256 editor.update_in(cx, |editor, _window, cx| {
21257 for (buffer, diff_base) in [
21258 (buffer_1.clone(), base_text_1),
21259 (buffer_2.clone(), base_text_2),
21260 (buffer_3.clone(), base_text_3),
21261 ] {
21262 let diff = cx.new(|cx| {
21263 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
21264 });
21265 editor
21266 .buffer
21267 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
21268 }
21269 });
21270 cx.executor().run_until_parked();
21271
21272 editor.update_in(cx, |editor, window, cx| {
21273 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}");
21274 editor.select_all(&SelectAll, window, cx);
21275 editor.git_restore(&Default::default(), window, cx);
21276 });
21277 cx.executor().run_until_parked();
21278
21279 // When all ranges are selected, all buffer hunks are reverted.
21280 editor.update(cx, |editor, cx| {
21281 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");
21282 });
21283 buffer_1.update(cx, |buffer, _| {
21284 assert_eq!(buffer.text(), base_text_1);
21285 });
21286 buffer_2.update(cx, |buffer, _| {
21287 assert_eq!(buffer.text(), base_text_2);
21288 });
21289 buffer_3.update(cx, |buffer, _| {
21290 assert_eq!(buffer.text(), base_text_3);
21291 });
21292
21293 editor.update_in(cx, |editor, window, cx| {
21294 editor.undo(&Default::default(), window, cx);
21295 });
21296
21297 editor.update_in(cx, |editor, window, cx| {
21298 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21299 s.select_ranges(Some(Point::new(0, 0)..Point::new(5, 0)));
21300 });
21301 editor.git_restore(&Default::default(), window, cx);
21302 });
21303
21304 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
21305 // but not affect buffer_2 and its related excerpts.
21306 editor.update(cx, |editor, cx| {
21307 assert_eq!(
21308 editor.display_text(cx),
21309 "\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}"
21310 );
21311 });
21312 buffer_1.update(cx, |buffer, _| {
21313 assert_eq!(buffer.text(), base_text_1);
21314 });
21315 buffer_2.update(cx, |buffer, _| {
21316 assert_eq!(
21317 buffer.text(),
21318 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
21319 );
21320 });
21321 buffer_3.update(cx, |buffer, _| {
21322 assert_eq!(
21323 buffer.text(),
21324 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
21325 );
21326 });
21327
21328 fn edit_first_char_of_every_line(text: &str) -> String {
21329 text.split('\n')
21330 .map(|line| format!("X{}", &line[1..]))
21331 .collect::<Vec<_>>()
21332 .join("\n")
21333 }
21334}
21335
21336#[gpui::test]
21337async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
21338 init_test(cx, |_| {});
21339
21340 let cols = 4;
21341 let rows = 10;
21342 let sample_text_1 = sample_text(rows, cols, 'a');
21343 assert_eq!(
21344 sample_text_1,
21345 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
21346 );
21347 let sample_text_2 = sample_text(rows, cols, 'l');
21348 assert_eq!(
21349 sample_text_2,
21350 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
21351 );
21352 let sample_text_3 = sample_text(rows, cols, 'v');
21353 assert_eq!(
21354 sample_text_3,
21355 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
21356 );
21357
21358 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
21359 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
21360 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
21361
21362 let multi_buffer = cx.new(|cx| {
21363 let mut multibuffer = MultiBuffer::new(ReadWrite);
21364 multibuffer.set_excerpts_for_path(
21365 PathKey::sorted(0),
21366 buffer_1.clone(),
21367 [
21368 Point::new(0, 0)..Point::new(2, 0),
21369 Point::new(5, 0)..Point::new(6, 0),
21370 Point::new(9, 0)..Point::new(9, 4),
21371 ],
21372 0,
21373 cx,
21374 );
21375 multibuffer.set_excerpts_for_path(
21376 PathKey::sorted(1),
21377 buffer_2.clone(),
21378 [
21379 Point::new(0, 0)..Point::new(2, 0),
21380 Point::new(5, 0)..Point::new(6, 0),
21381 Point::new(9, 0)..Point::new(9, 4),
21382 ],
21383 0,
21384 cx,
21385 );
21386 multibuffer.set_excerpts_for_path(
21387 PathKey::sorted(2),
21388 buffer_3.clone(),
21389 [
21390 Point::new(0, 0)..Point::new(2, 0),
21391 Point::new(5, 0)..Point::new(6, 0),
21392 Point::new(9, 0)..Point::new(9, 4),
21393 ],
21394 0,
21395 cx,
21396 );
21397 multibuffer
21398 });
21399
21400 let fs = FakeFs::new(cx.executor());
21401 fs.insert_tree(
21402 "/a",
21403 json!({
21404 "main.rs": sample_text_1,
21405 "other.rs": sample_text_2,
21406 "lib.rs": sample_text_3,
21407 }),
21408 )
21409 .await;
21410 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21411 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
21412 let workspace = window
21413 .read_with(cx, |mw, _| mw.workspace().clone())
21414 .unwrap();
21415 let cx = &mut VisualTestContext::from_window(*window, cx);
21416 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21417 Editor::new(
21418 EditorMode::full(),
21419 multi_buffer,
21420 Some(project.clone()),
21421 window,
21422 cx,
21423 )
21424 });
21425 let multibuffer_item_id = workspace.update_in(cx, |workspace, window, cx| {
21426 assert!(
21427 workspace.active_item(cx).is_none(),
21428 "active item should be None before the first item is added"
21429 );
21430 workspace.add_item_to_active_pane(
21431 Box::new(multi_buffer_editor.clone()),
21432 None,
21433 true,
21434 window,
21435 cx,
21436 );
21437 let active_item = workspace
21438 .active_item(cx)
21439 .expect("should have an active item after adding the multi buffer");
21440 assert_eq!(
21441 active_item.buffer_kind(cx),
21442 ItemBufferKind::Multibuffer,
21443 "A multi buffer was expected to active after adding"
21444 );
21445 active_item.item_id()
21446 });
21447
21448 cx.executor().run_until_parked();
21449
21450 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21451 editor.change_selections(
21452 SelectionEffects::scroll(Autoscroll::Next),
21453 window,
21454 cx,
21455 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
21456 );
21457 editor.open_excerpts(&OpenExcerpts, window, cx);
21458 });
21459 cx.executor().run_until_parked();
21460 let first_item_id = workspace.update_in(cx, |workspace, window, cx| {
21461 let active_item = workspace
21462 .active_item(cx)
21463 .expect("should have an active item after navigating into the 1st buffer");
21464 let first_item_id = active_item.item_id();
21465 assert_ne!(
21466 first_item_id, multibuffer_item_id,
21467 "Should navigate into the 1st buffer and activate it"
21468 );
21469 assert_eq!(
21470 active_item.buffer_kind(cx),
21471 ItemBufferKind::Singleton,
21472 "New active item should be a singleton buffer"
21473 );
21474 assert_eq!(
21475 active_item
21476 .act_as::<Editor>(cx)
21477 .expect("should have navigated into an editor for the 1st buffer")
21478 .read(cx)
21479 .text(cx),
21480 sample_text_1
21481 );
21482
21483 workspace
21484 .go_back(workspace.active_pane().downgrade(), window, cx)
21485 .detach_and_log_err(cx);
21486
21487 first_item_id
21488 });
21489
21490 cx.executor().run_until_parked();
21491 workspace.update_in(cx, |workspace, _, cx| {
21492 let active_item = workspace
21493 .active_item(cx)
21494 .expect("should have an active item after navigating back");
21495 assert_eq!(
21496 active_item.item_id(),
21497 multibuffer_item_id,
21498 "Should navigate back to the multi buffer"
21499 );
21500 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
21501 });
21502
21503 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21504 editor.change_selections(
21505 SelectionEffects::scroll(Autoscroll::Next),
21506 window,
21507 cx,
21508 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
21509 );
21510 editor.open_excerpts(&OpenExcerpts, window, cx);
21511 });
21512 cx.executor().run_until_parked();
21513 let second_item_id = workspace.update_in(cx, |workspace, window, cx| {
21514 let active_item = workspace
21515 .active_item(cx)
21516 .expect("should have an active item after navigating into the 2nd buffer");
21517 let second_item_id = active_item.item_id();
21518 assert_ne!(
21519 second_item_id, multibuffer_item_id,
21520 "Should navigate away from the multibuffer"
21521 );
21522 assert_ne!(
21523 second_item_id, first_item_id,
21524 "Should navigate into the 2nd buffer and activate it"
21525 );
21526 assert_eq!(
21527 active_item.buffer_kind(cx),
21528 ItemBufferKind::Singleton,
21529 "New active item should be a singleton buffer"
21530 );
21531 assert_eq!(
21532 active_item
21533 .act_as::<Editor>(cx)
21534 .expect("should have navigated into an editor")
21535 .read(cx)
21536 .text(cx),
21537 sample_text_2
21538 );
21539
21540 workspace
21541 .go_back(workspace.active_pane().downgrade(), window, cx)
21542 .detach_and_log_err(cx);
21543
21544 second_item_id
21545 });
21546
21547 cx.executor().run_until_parked();
21548 workspace.update_in(cx, |workspace, _, cx| {
21549 let active_item = workspace
21550 .active_item(cx)
21551 .expect("should have an active item after navigating back from the 2nd buffer");
21552 assert_eq!(
21553 active_item.item_id(),
21554 multibuffer_item_id,
21555 "Should navigate back from the 2nd buffer to the multi buffer"
21556 );
21557 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
21558 });
21559
21560 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21561 editor.change_selections(
21562 SelectionEffects::scroll(Autoscroll::Next),
21563 window,
21564 cx,
21565 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
21566 );
21567 editor.open_excerpts(&OpenExcerpts, window, cx);
21568 });
21569 cx.executor().run_until_parked();
21570 workspace.update_in(cx, |workspace, window, cx| {
21571 let active_item = workspace
21572 .active_item(cx)
21573 .expect("should have an active item after navigating into the 3rd buffer");
21574 let third_item_id = active_item.item_id();
21575 assert_ne!(
21576 third_item_id, multibuffer_item_id,
21577 "Should navigate into the 3rd buffer and activate it"
21578 );
21579 assert_ne!(third_item_id, first_item_id);
21580 assert_ne!(third_item_id, second_item_id);
21581 assert_eq!(
21582 active_item.buffer_kind(cx),
21583 ItemBufferKind::Singleton,
21584 "New active item should be a singleton buffer"
21585 );
21586 assert_eq!(
21587 active_item
21588 .act_as::<Editor>(cx)
21589 .expect("should have navigated into an editor")
21590 .read(cx)
21591 .text(cx),
21592 sample_text_3
21593 );
21594
21595 workspace
21596 .go_back(workspace.active_pane().downgrade(), window, cx)
21597 .detach_and_log_err(cx);
21598 });
21599
21600 cx.executor().run_until_parked();
21601 workspace.update_in(cx, |workspace, _, cx| {
21602 let active_item = workspace
21603 .active_item(cx)
21604 .expect("should have an active item after navigating back from the 3rd buffer");
21605 assert_eq!(
21606 active_item.item_id(),
21607 multibuffer_item_id,
21608 "Should navigate back from the 3rd buffer to the multi buffer"
21609 );
21610 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
21611 });
21612}
21613
21614#[gpui::test]
21615async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21616 init_test(cx, |_| {});
21617
21618 let mut cx = EditorTestContext::new(cx).await;
21619
21620 let diff_base = r#"
21621 use some::mod;
21622
21623 const A: u32 = 42;
21624
21625 fn main() {
21626 println!("hello");
21627
21628 println!("world");
21629 }
21630 "#
21631 .unindent();
21632
21633 cx.set_state(
21634 &r#"
21635 use some::modified;
21636
21637 ˇ
21638 fn main() {
21639 println!("hello there");
21640
21641 println!("around the");
21642 println!("world");
21643 }
21644 "#
21645 .unindent(),
21646 );
21647
21648 cx.set_head_text(&diff_base);
21649 executor.run_until_parked();
21650
21651 cx.update_editor(|editor, window, cx| {
21652 editor.go_to_next_hunk(&GoToHunk, window, cx);
21653 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21654 });
21655 executor.run_until_parked();
21656 cx.assert_state_with_diff(
21657 r#"
21658 use some::modified;
21659
21660
21661 fn main() {
21662 - println!("hello");
21663 + ˇ println!("hello there");
21664
21665 println!("around the");
21666 println!("world");
21667 }
21668 "#
21669 .unindent(),
21670 );
21671
21672 cx.update_editor(|editor, window, cx| {
21673 for _ in 0..2 {
21674 editor.go_to_next_hunk(&GoToHunk, window, cx);
21675 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21676 }
21677 });
21678 executor.run_until_parked();
21679 cx.assert_state_with_diff(
21680 r#"
21681 - use some::mod;
21682 + ˇuse some::modified;
21683
21684
21685 fn main() {
21686 - println!("hello");
21687 + println!("hello there");
21688
21689 + println!("around the");
21690 println!("world");
21691 }
21692 "#
21693 .unindent(),
21694 );
21695
21696 cx.update_editor(|editor, window, cx| {
21697 editor.go_to_next_hunk(&GoToHunk, window, cx);
21698 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21699 });
21700 executor.run_until_parked();
21701 cx.assert_state_with_diff(
21702 r#"
21703 - use some::mod;
21704 + use some::modified;
21705
21706 - const A: u32 = 42;
21707 ˇ
21708 fn main() {
21709 - println!("hello");
21710 + println!("hello there");
21711
21712 + println!("around the");
21713 println!("world");
21714 }
21715 "#
21716 .unindent(),
21717 );
21718
21719 cx.update_editor(|editor, window, cx| {
21720 editor.cancel(&Cancel, window, cx);
21721 });
21722
21723 cx.assert_state_with_diff(
21724 r#"
21725 use some::modified;
21726
21727 ˇ
21728 fn main() {
21729 println!("hello there");
21730
21731 println!("around the");
21732 println!("world");
21733 }
21734 "#
21735 .unindent(),
21736 );
21737}
21738
21739#[gpui::test]
21740async fn test_diff_base_change_with_expanded_diff_hunks(
21741 executor: BackgroundExecutor,
21742 cx: &mut TestAppContext,
21743) {
21744 init_test(cx, |_| {});
21745
21746 let mut cx = EditorTestContext::new(cx).await;
21747
21748 let diff_base = r#"
21749 use some::mod1;
21750 use some::mod2;
21751
21752 const A: u32 = 42;
21753 const B: u32 = 42;
21754 const C: u32 = 42;
21755
21756 fn main() {
21757 println!("hello");
21758
21759 println!("world");
21760 }
21761 "#
21762 .unindent();
21763
21764 cx.set_state(
21765 &r#"
21766 use some::mod2;
21767
21768 const A: u32 = 42;
21769 const C: u32 = 42;
21770
21771 fn main(ˇ) {
21772 //println!("hello");
21773
21774 println!("world");
21775 //
21776 //
21777 }
21778 "#
21779 .unindent(),
21780 );
21781
21782 cx.set_head_text(&diff_base);
21783 executor.run_until_parked();
21784
21785 cx.update_editor(|editor, window, cx| {
21786 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21787 });
21788 executor.run_until_parked();
21789 cx.assert_state_with_diff(
21790 r#"
21791 - use some::mod1;
21792 use some::mod2;
21793
21794 const A: u32 = 42;
21795 - const B: u32 = 42;
21796 const C: u32 = 42;
21797
21798 fn main(ˇ) {
21799 - println!("hello");
21800 + //println!("hello");
21801
21802 println!("world");
21803 + //
21804 + //
21805 }
21806 "#
21807 .unindent(),
21808 );
21809
21810 cx.set_head_text("new diff base!");
21811 executor.run_until_parked();
21812 cx.assert_state_with_diff(
21813 r#"
21814 - new diff base!
21815 + use some::mod2;
21816 +
21817 + const A: u32 = 42;
21818 + const C: u32 = 42;
21819 +
21820 + fn main(ˇ) {
21821 + //println!("hello");
21822 +
21823 + println!("world");
21824 + //
21825 + //
21826 + }
21827 "#
21828 .unindent(),
21829 );
21830}
21831
21832#[gpui::test]
21833async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
21834 init_test(cx, |_| {});
21835
21836 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
21837 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
21838 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
21839 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
21840 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
21841 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
21842
21843 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
21844 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
21845 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
21846
21847 let multi_buffer = cx.new(|cx| {
21848 let mut multibuffer = MultiBuffer::new(ReadWrite);
21849 multibuffer.set_excerpts_for_path(
21850 PathKey::sorted(0),
21851 buffer_1.clone(),
21852 [
21853 Point::new(0, 0)..Point::new(2, 3),
21854 Point::new(5, 0)..Point::new(6, 3),
21855 Point::new(9, 0)..Point::new(10, 3),
21856 ],
21857 0,
21858 cx,
21859 );
21860 multibuffer.set_excerpts_for_path(
21861 PathKey::sorted(1),
21862 buffer_2.clone(),
21863 [
21864 Point::new(0, 0)..Point::new(2, 3),
21865 Point::new(5, 0)..Point::new(6, 3),
21866 Point::new(9, 0)..Point::new(10, 3),
21867 ],
21868 0,
21869 cx,
21870 );
21871 multibuffer.set_excerpts_for_path(
21872 PathKey::sorted(2),
21873 buffer_3.clone(),
21874 [
21875 Point::new(0, 0)..Point::new(2, 3),
21876 Point::new(5, 0)..Point::new(6, 3),
21877 Point::new(9, 0)..Point::new(10, 3),
21878 ],
21879 0,
21880 cx,
21881 );
21882 assert_eq!(multibuffer.excerpt_ids().len(), 9);
21883 multibuffer
21884 });
21885
21886 let editor =
21887 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
21888 editor
21889 .update(cx, |editor, _window, cx| {
21890 for (buffer, diff_base) in [
21891 (buffer_1.clone(), file_1_old),
21892 (buffer_2.clone(), file_2_old),
21893 (buffer_3.clone(), file_3_old),
21894 ] {
21895 let diff = cx.new(|cx| {
21896 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
21897 });
21898 editor
21899 .buffer
21900 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
21901 }
21902 })
21903 .unwrap();
21904
21905 let mut cx = EditorTestContext::for_editor(editor, cx).await;
21906 cx.run_until_parked();
21907
21908 cx.assert_editor_state(
21909 &"
21910 ˇaaa
21911 ccc
21912 ddd
21913 ggg
21914 hhh
21915
21916 lll
21917 mmm
21918 NNN
21919 qqq
21920 rrr
21921 uuu
21922 111
21923 222
21924 333
21925 666
21926 777
21927 000
21928 !!!"
21929 .unindent(),
21930 );
21931
21932 cx.update_editor(|editor, window, cx| {
21933 editor.select_all(&SelectAll, window, cx);
21934 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21935 });
21936 cx.executor().run_until_parked();
21937
21938 cx.assert_state_with_diff(
21939 "
21940 «aaa
21941 - bbb
21942 ccc
21943 ddd
21944 ggg
21945 hhh
21946
21947 lll
21948 mmm
21949 - nnn
21950 + NNN
21951 qqq
21952 rrr
21953 uuu
21954 111
21955 222
21956 333
21957 + 666
21958 777
21959 000
21960 !!!ˇ»"
21961 .unindent(),
21962 );
21963}
21964
21965#[gpui::test]
21966async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
21967 init_test(cx, |_| {});
21968
21969 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
21970 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
21971
21972 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
21973 let multi_buffer = cx.new(|cx| {
21974 let mut multibuffer = MultiBuffer::new(ReadWrite);
21975 multibuffer.set_excerpts_for_path(
21976 PathKey::sorted(0),
21977 buffer.clone(),
21978 [
21979 Point::new(0, 0)..Point::new(1, 3),
21980 Point::new(4, 0)..Point::new(6, 3),
21981 Point::new(9, 0)..Point::new(9, 3),
21982 ],
21983 0,
21984 cx,
21985 );
21986 assert_eq!(multibuffer.excerpt_ids().len(), 3);
21987 multibuffer
21988 });
21989
21990 let editor =
21991 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
21992 editor
21993 .update(cx, |editor, _window, cx| {
21994 let diff = cx.new(|cx| {
21995 BufferDiff::new_with_base_text(base, &buffer.read(cx).text_snapshot(), cx)
21996 });
21997 editor
21998 .buffer
21999 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
22000 })
22001 .unwrap();
22002
22003 let mut cx = EditorTestContext::for_editor(editor, cx).await;
22004 cx.run_until_parked();
22005
22006 cx.update_editor(|editor, window, cx| {
22007 editor.expand_all_diff_hunks(&Default::default(), window, cx)
22008 });
22009 cx.executor().run_until_parked();
22010
22011 // When the start of a hunk coincides with the start of its excerpt,
22012 // the hunk is expanded. When the start of a hunk is earlier than
22013 // the start of its excerpt, the hunk is not expanded.
22014 cx.assert_state_with_diff(
22015 "
22016 ˇaaa
22017 - bbb
22018 + BBB
22019 - ddd
22020 - eee
22021 + DDD
22022 + EEE
22023 fff
22024 iii"
22025 .unindent(),
22026 );
22027}
22028
22029#[gpui::test]
22030async fn test_edits_around_expanded_insertion_hunks(
22031 executor: BackgroundExecutor,
22032 cx: &mut TestAppContext,
22033) {
22034 init_test(cx, |_| {});
22035
22036 let mut cx = EditorTestContext::new(cx).await;
22037
22038 let diff_base = r#"
22039 use some::mod1;
22040 use some::mod2;
22041
22042 const A: u32 = 42;
22043
22044 fn main() {
22045 println!("hello");
22046
22047 println!("world");
22048 }
22049 "#
22050 .unindent();
22051 executor.run_until_parked();
22052 cx.set_state(
22053 &r#"
22054 use some::mod1;
22055 use some::mod2;
22056
22057 const A: u32 = 42;
22058 const B: u32 = 42;
22059 const C: u32 = 42;
22060 ˇ
22061
22062 fn main() {
22063 println!("hello");
22064
22065 println!("world");
22066 }
22067 "#
22068 .unindent(),
22069 );
22070
22071 cx.set_head_text(&diff_base);
22072 executor.run_until_parked();
22073
22074 cx.update_editor(|editor, window, cx| {
22075 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22076 });
22077 executor.run_until_parked();
22078
22079 cx.assert_state_with_diff(
22080 r#"
22081 use some::mod1;
22082 use some::mod2;
22083
22084 const A: u32 = 42;
22085 + const B: u32 = 42;
22086 + const C: u32 = 42;
22087 + ˇ
22088
22089 fn main() {
22090 println!("hello");
22091
22092 println!("world");
22093 }
22094 "#
22095 .unindent(),
22096 );
22097
22098 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
22099 executor.run_until_parked();
22100
22101 cx.assert_state_with_diff(
22102 r#"
22103 use some::mod1;
22104 use some::mod2;
22105
22106 const A: u32 = 42;
22107 + const B: u32 = 42;
22108 + const C: u32 = 42;
22109 + const D: u32 = 42;
22110 + ˇ
22111
22112 fn main() {
22113 println!("hello");
22114
22115 println!("world");
22116 }
22117 "#
22118 .unindent(),
22119 );
22120
22121 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
22122 executor.run_until_parked();
22123
22124 cx.assert_state_with_diff(
22125 r#"
22126 use some::mod1;
22127 use some::mod2;
22128
22129 const A: u32 = 42;
22130 + const B: u32 = 42;
22131 + const C: u32 = 42;
22132 + const D: u32 = 42;
22133 + const E: u32 = 42;
22134 + ˇ
22135
22136 fn main() {
22137 println!("hello");
22138
22139 println!("world");
22140 }
22141 "#
22142 .unindent(),
22143 );
22144
22145 cx.update_editor(|editor, window, cx| {
22146 editor.delete_line(&DeleteLine, window, cx);
22147 });
22148 executor.run_until_parked();
22149
22150 cx.assert_state_with_diff(
22151 r#"
22152 use some::mod1;
22153 use some::mod2;
22154
22155 const A: u32 = 42;
22156 + const B: u32 = 42;
22157 + const C: u32 = 42;
22158 + const D: u32 = 42;
22159 + const E: u32 = 42;
22160 ˇ
22161 fn main() {
22162 println!("hello");
22163
22164 println!("world");
22165 }
22166 "#
22167 .unindent(),
22168 );
22169
22170 cx.update_editor(|editor, window, cx| {
22171 editor.move_up(&MoveUp, window, cx);
22172 editor.delete_line(&DeleteLine, window, cx);
22173 editor.move_up(&MoveUp, window, cx);
22174 editor.delete_line(&DeleteLine, window, cx);
22175 editor.move_up(&MoveUp, window, cx);
22176 editor.delete_line(&DeleteLine, window, cx);
22177 });
22178 executor.run_until_parked();
22179 cx.assert_state_with_diff(
22180 r#"
22181 use some::mod1;
22182 use some::mod2;
22183
22184 const A: u32 = 42;
22185 + const B: u32 = 42;
22186 ˇ
22187 fn main() {
22188 println!("hello");
22189
22190 println!("world");
22191 }
22192 "#
22193 .unindent(),
22194 );
22195
22196 cx.update_editor(|editor, window, cx| {
22197 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
22198 editor.delete_line(&DeleteLine, window, cx);
22199 });
22200 executor.run_until_parked();
22201 cx.assert_state_with_diff(
22202 r#"
22203 ˇ
22204 fn main() {
22205 println!("hello");
22206
22207 println!("world");
22208 }
22209 "#
22210 .unindent(),
22211 );
22212}
22213
22214#[gpui::test]
22215async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
22216 init_test(cx, |_| {});
22217
22218 let mut cx = EditorTestContext::new(cx).await;
22219 cx.set_head_text(indoc! { "
22220 one
22221 two
22222 three
22223 four
22224 five
22225 "
22226 });
22227 cx.set_state(indoc! { "
22228 one
22229 ˇthree
22230 five
22231 "});
22232 cx.run_until_parked();
22233 cx.update_editor(|editor, window, cx| {
22234 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
22235 });
22236 cx.assert_state_with_diff(
22237 indoc! { "
22238 one
22239 - two
22240 ˇthree
22241 - four
22242 five
22243 "}
22244 .to_string(),
22245 );
22246 cx.update_editor(|editor, window, cx| {
22247 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
22248 });
22249
22250 cx.assert_state_with_diff(
22251 indoc! { "
22252 one
22253 ˇthree
22254 five
22255 "}
22256 .to_string(),
22257 );
22258
22259 cx.update_editor(|editor, window, cx| {
22260 editor.move_up(&MoveUp, window, cx);
22261 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
22262 });
22263 cx.assert_state_with_diff(
22264 indoc! { "
22265 ˇone
22266 - two
22267 three
22268 five
22269 "}
22270 .to_string(),
22271 );
22272
22273 cx.update_editor(|editor, window, cx| {
22274 editor.move_down(&MoveDown, window, cx);
22275 editor.move_down(&MoveDown, window, cx);
22276 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
22277 });
22278 cx.assert_state_with_diff(
22279 indoc! { "
22280 one
22281 - two
22282 ˇthree
22283 - four
22284 five
22285 "}
22286 .to_string(),
22287 );
22288
22289 cx.set_state(indoc! { "
22290 one
22291 ˇTWO
22292 three
22293 four
22294 five
22295 "});
22296 cx.run_until_parked();
22297 cx.update_editor(|editor, window, cx| {
22298 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
22299 });
22300
22301 cx.assert_state_with_diff(
22302 indoc! { "
22303 one
22304 - two
22305 + ˇTWO
22306 three
22307 four
22308 five
22309 "}
22310 .to_string(),
22311 );
22312 cx.update_editor(|editor, window, cx| {
22313 editor.move_up(&Default::default(), window, cx);
22314 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
22315 });
22316 cx.assert_state_with_diff(
22317 indoc! { "
22318 one
22319 ˇTWO
22320 three
22321 four
22322 five
22323 "}
22324 .to_string(),
22325 );
22326}
22327
22328#[gpui::test]
22329async fn test_toggling_adjacent_diff_hunks_2(
22330 executor: BackgroundExecutor,
22331 cx: &mut TestAppContext,
22332) {
22333 init_test(cx, |_| {});
22334
22335 let mut cx = EditorTestContext::new(cx).await;
22336
22337 let diff_base = r#"
22338 lineA
22339 lineB
22340 lineC
22341 lineD
22342 "#
22343 .unindent();
22344
22345 cx.set_state(
22346 &r#"
22347 ˇlineA1
22348 lineB
22349 lineD
22350 "#
22351 .unindent(),
22352 );
22353 cx.set_head_text(&diff_base);
22354 executor.run_until_parked();
22355
22356 cx.update_editor(|editor, window, cx| {
22357 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
22358 });
22359 executor.run_until_parked();
22360 cx.assert_state_with_diff(
22361 r#"
22362 - lineA
22363 + ˇlineA1
22364 lineB
22365 lineD
22366 "#
22367 .unindent(),
22368 );
22369
22370 cx.update_editor(|editor, window, cx| {
22371 editor.move_down(&MoveDown, window, cx);
22372 editor.move_right(&MoveRight, window, cx);
22373 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
22374 });
22375 executor.run_until_parked();
22376 cx.assert_state_with_diff(
22377 r#"
22378 - lineA
22379 + lineA1
22380 lˇineB
22381 - lineC
22382 lineD
22383 "#
22384 .unindent(),
22385 );
22386}
22387
22388#[gpui::test]
22389async fn test_edits_around_expanded_deletion_hunks(
22390 executor: BackgroundExecutor,
22391 cx: &mut TestAppContext,
22392) {
22393 init_test(cx, |_| {});
22394
22395 let mut cx = EditorTestContext::new(cx).await;
22396
22397 let diff_base = r#"
22398 use some::mod1;
22399 use some::mod2;
22400
22401 const A: u32 = 42;
22402 const B: u32 = 42;
22403 const C: u32 = 42;
22404
22405
22406 fn main() {
22407 println!("hello");
22408
22409 println!("world");
22410 }
22411 "#
22412 .unindent();
22413 executor.run_until_parked();
22414 cx.set_state(
22415 &r#"
22416 use some::mod1;
22417 use some::mod2;
22418
22419 ˇconst B: u32 = 42;
22420 const C: u32 = 42;
22421
22422
22423 fn main() {
22424 println!("hello");
22425
22426 println!("world");
22427 }
22428 "#
22429 .unindent(),
22430 );
22431
22432 cx.set_head_text(&diff_base);
22433 executor.run_until_parked();
22434
22435 cx.update_editor(|editor, window, cx| {
22436 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22437 });
22438 executor.run_until_parked();
22439
22440 cx.assert_state_with_diff(
22441 r#"
22442 use some::mod1;
22443 use some::mod2;
22444
22445 - const A: u32 = 42;
22446 ˇconst B: u32 = 42;
22447 const C: u32 = 42;
22448
22449
22450 fn main() {
22451 println!("hello");
22452
22453 println!("world");
22454 }
22455 "#
22456 .unindent(),
22457 );
22458
22459 cx.update_editor(|editor, window, cx| {
22460 editor.delete_line(&DeleteLine, window, cx);
22461 });
22462 executor.run_until_parked();
22463 cx.assert_state_with_diff(
22464 r#"
22465 use some::mod1;
22466 use some::mod2;
22467
22468 - const A: u32 = 42;
22469 - const B: u32 = 42;
22470 ˇconst C: u32 = 42;
22471
22472
22473 fn main() {
22474 println!("hello");
22475
22476 println!("world");
22477 }
22478 "#
22479 .unindent(),
22480 );
22481
22482 cx.update_editor(|editor, window, cx| {
22483 editor.delete_line(&DeleteLine, window, cx);
22484 });
22485 executor.run_until_parked();
22486 cx.assert_state_with_diff(
22487 r#"
22488 use some::mod1;
22489 use some::mod2;
22490
22491 - const A: u32 = 42;
22492 - const B: u32 = 42;
22493 - const C: u32 = 42;
22494 ˇ
22495
22496 fn main() {
22497 println!("hello");
22498
22499 println!("world");
22500 }
22501 "#
22502 .unindent(),
22503 );
22504
22505 cx.update_editor(|editor, window, cx| {
22506 editor.handle_input("replacement", window, cx);
22507 });
22508 executor.run_until_parked();
22509 cx.assert_state_with_diff(
22510 r#"
22511 use some::mod1;
22512 use some::mod2;
22513
22514 - const A: u32 = 42;
22515 - const B: u32 = 42;
22516 - const C: u32 = 42;
22517 -
22518 + replacementˇ
22519
22520 fn main() {
22521 println!("hello");
22522
22523 println!("world");
22524 }
22525 "#
22526 .unindent(),
22527 );
22528}
22529
22530#[gpui::test]
22531async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22532 init_test(cx, |_| {});
22533
22534 let mut cx = EditorTestContext::new(cx).await;
22535
22536 let base_text = r#"
22537 one
22538 two
22539 three
22540 four
22541 five
22542 "#
22543 .unindent();
22544 executor.run_until_parked();
22545 cx.set_state(
22546 &r#"
22547 one
22548 two
22549 fˇour
22550 five
22551 "#
22552 .unindent(),
22553 );
22554
22555 cx.set_head_text(&base_text);
22556 executor.run_until_parked();
22557
22558 cx.update_editor(|editor, window, cx| {
22559 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22560 });
22561 executor.run_until_parked();
22562
22563 cx.assert_state_with_diff(
22564 r#"
22565 one
22566 two
22567 - three
22568 fˇour
22569 five
22570 "#
22571 .unindent(),
22572 );
22573
22574 cx.update_editor(|editor, window, cx| {
22575 editor.backspace(&Backspace, window, cx);
22576 editor.backspace(&Backspace, window, cx);
22577 });
22578 executor.run_until_parked();
22579 cx.assert_state_with_diff(
22580 r#"
22581 one
22582 two
22583 - threeˇ
22584 - four
22585 + our
22586 five
22587 "#
22588 .unindent(),
22589 );
22590}
22591
22592#[gpui::test]
22593async fn test_edit_after_expanded_modification_hunk(
22594 executor: BackgroundExecutor,
22595 cx: &mut TestAppContext,
22596) {
22597 init_test(cx, |_| {});
22598
22599 let mut cx = EditorTestContext::new(cx).await;
22600
22601 let diff_base = r#"
22602 use some::mod1;
22603 use some::mod2;
22604
22605 const A: u32 = 42;
22606 const B: u32 = 42;
22607 const C: u32 = 42;
22608 const D: u32 = 42;
22609
22610
22611 fn main() {
22612 println!("hello");
22613
22614 println!("world");
22615 }"#
22616 .unindent();
22617
22618 cx.set_state(
22619 &r#"
22620 use some::mod1;
22621 use some::mod2;
22622
22623 const A: u32 = 42;
22624 const B: u32 = 42;
22625 const C: u32 = 43ˇ
22626 const D: u32 = 42;
22627
22628
22629 fn main() {
22630 println!("hello");
22631
22632 println!("world");
22633 }"#
22634 .unindent(),
22635 );
22636
22637 cx.set_head_text(&diff_base);
22638 executor.run_until_parked();
22639 cx.update_editor(|editor, window, cx| {
22640 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22641 });
22642 executor.run_until_parked();
22643
22644 cx.assert_state_with_diff(
22645 r#"
22646 use some::mod1;
22647 use some::mod2;
22648
22649 const A: u32 = 42;
22650 const B: u32 = 42;
22651 - const C: u32 = 42;
22652 + const C: u32 = 43ˇ
22653 const D: u32 = 42;
22654
22655
22656 fn main() {
22657 println!("hello");
22658
22659 println!("world");
22660 }"#
22661 .unindent(),
22662 );
22663
22664 cx.update_editor(|editor, window, cx| {
22665 editor.handle_input("\nnew_line\n", window, cx);
22666 });
22667 executor.run_until_parked();
22668
22669 cx.assert_state_with_diff(
22670 r#"
22671 use some::mod1;
22672 use some::mod2;
22673
22674 const A: u32 = 42;
22675 const B: u32 = 42;
22676 - const C: u32 = 42;
22677 + const C: u32 = 43
22678 + new_line
22679 + ˇ
22680 const D: u32 = 42;
22681
22682
22683 fn main() {
22684 println!("hello");
22685
22686 println!("world");
22687 }"#
22688 .unindent(),
22689 );
22690}
22691
22692#[gpui::test]
22693async fn test_stage_and_unstage_added_file_hunk(
22694 executor: BackgroundExecutor,
22695 cx: &mut TestAppContext,
22696) {
22697 init_test(cx, |_| {});
22698
22699 let mut cx = EditorTestContext::new(cx).await;
22700 cx.update_editor(|editor, _, cx| {
22701 editor.set_expand_all_diff_hunks(cx);
22702 });
22703
22704 let working_copy = r#"
22705 ˇfn main() {
22706 println!("hello, world!");
22707 }
22708 "#
22709 .unindent();
22710
22711 cx.set_state(&working_copy);
22712 executor.run_until_parked();
22713
22714 cx.assert_state_with_diff(
22715 r#"
22716 + ˇfn main() {
22717 + println!("hello, world!");
22718 + }
22719 "#
22720 .unindent(),
22721 );
22722 cx.assert_index_text(None);
22723
22724 cx.update_editor(|editor, window, cx| {
22725 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22726 });
22727 executor.run_until_parked();
22728 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
22729 cx.assert_state_with_diff(
22730 r#"
22731 + ˇfn main() {
22732 + println!("hello, world!");
22733 + }
22734 "#
22735 .unindent(),
22736 );
22737
22738 cx.update_editor(|editor, window, cx| {
22739 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22740 });
22741 executor.run_until_parked();
22742 cx.assert_index_text(None);
22743}
22744
22745async fn setup_indent_guides_editor(
22746 text: &str,
22747 cx: &mut TestAppContext,
22748) -> (BufferId, EditorTestContext) {
22749 init_test(cx, |_| {});
22750
22751 let mut cx = EditorTestContext::new(cx).await;
22752
22753 let buffer_id = cx.update_editor(|editor, window, cx| {
22754 editor.set_text(text, window, cx);
22755 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
22756
22757 buffer_ids[0]
22758 });
22759
22760 (buffer_id, cx)
22761}
22762
22763fn assert_indent_guides(
22764 range: Range<u32>,
22765 expected: Vec<IndentGuide>,
22766 active_indices: Option<Vec<usize>>,
22767 cx: &mut EditorTestContext,
22768) {
22769 let indent_guides = cx.update_editor(|editor, window, cx| {
22770 let snapshot = editor.snapshot(window, cx).display_snapshot;
22771 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
22772 editor,
22773 MultiBufferRow(range.start)..MultiBufferRow(range.end),
22774 true,
22775 &snapshot,
22776 cx,
22777 );
22778
22779 indent_guides.sort_by(|a, b| {
22780 a.depth.cmp(&b.depth).then(
22781 a.start_row
22782 .cmp(&b.start_row)
22783 .then(a.end_row.cmp(&b.end_row)),
22784 )
22785 });
22786 indent_guides
22787 });
22788
22789 if let Some(expected) = active_indices {
22790 let active_indices = cx.update_editor(|editor, window, cx| {
22791 let snapshot = editor.snapshot(window, cx).display_snapshot;
22792 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
22793 });
22794
22795 assert_eq!(
22796 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
22797 expected,
22798 "Active indent guide indices do not match"
22799 );
22800 }
22801
22802 assert_eq!(indent_guides, expected, "Indent guides do not match");
22803}
22804
22805fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
22806 IndentGuide {
22807 buffer_id,
22808 start_row: MultiBufferRow(start_row),
22809 end_row: MultiBufferRow(end_row),
22810 depth,
22811 tab_size: 4,
22812 settings: IndentGuideSettings {
22813 enabled: true,
22814 line_width: 1,
22815 active_line_width: 1,
22816 coloring: IndentGuideColoring::default(),
22817 background_coloring: IndentGuideBackgroundColoring::default(),
22818 },
22819 }
22820}
22821
22822#[gpui::test]
22823async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
22824 let (buffer_id, mut cx) = setup_indent_guides_editor(
22825 &"
22826 fn main() {
22827 let a = 1;
22828 }"
22829 .unindent(),
22830 cx,
22831 )
22832 .await;
22833
22834 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
22835}
22836
22837#[gpui::test]
22838async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
22839 let (buffer_id, mut cx) = setup_indent_guides_editor(
22840 &"
22841 fn main() {
22842 let a = 1;
22843 let b = 2;
22844 }"
22845 .unindent(),
22846 cx,
22847 )
22848 .await;
22849
22850 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
22851}
22852
22853#[gpui::test]
22854async fn test_indent_guide_nested(cx: &mut TestAppContext) {
22855 let (buffer_id, mut cx) = setup_indent_guides_editor(
22856 &"
22857 fn main() {
22858 let a = 1;
22859 if a == 3 {
22860 let b = 2;
22861 } else {
22862 let c = 3;
22863 }
22864 }"
22865 .unindent(),
22866 cx,
22867 )
22868 .await;
22869
22870 assert_indent_guides(
22871 0..8,
22872 vec![
22873 indent_guide(buffer_id, 1, 6, 0),
22874 indent_guide(buffer_id, 3, 3, 1),
22875 indent_guide(buffer_id, 5, 5, 1),
22876 ],
22877 None,
22878 &mut cx,
22879 );
22880}
22881
22882#[gpui::test]
22883async fn test_indent_guide_tab(cx: &mut TestAppContext) {
22884 let (buffer_id, mut cx) = setup_indent_guides_editor(
22885 &"
22886 fn main() {
22887 let a = 1;
22888 let b = 2;
22889 let c = 3;
22890 }"
22891 .unindent(),
22892 cx,
22893 )
22894 .await;
22895
22896 assert_indent_guides(
22897 0..5,
22898 vec![
22899 indent_guide(buffer_id, 1, 3, 0),
22900 indent_guide(buffer_id, 2, 2, 1),
22901 ],
22902 None,
22903 &mut cx,
22904 );
22905}
22906
22907#[gpui::test]
22908async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
22909 let (buffer_id, mut cx) = setup_indent_guides_editor(
22910 &"
22911 fn main() {
22912 let a = 1;
22913
22914 let c = 3;
22915 }"
22916 .unindent(),
22917 cx,
22918 )
22919 .await;
22920
22921 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
22922}
22923
22924#[gpui::test]
22925async fn test_indent_guide_complex(cx: &mut TestAppContext) {
22926 let (buffer_id, mut cx) = setup_indent_guides_editor(
22927 &"
22928 fn main() {
22929 let a = 1;
22930
22931 let c = 3;
22932
22933 if a == 3 {
22934 let b = 2;
22935 } else {
22936 let c = 3;
22937 }
22938 }"
22939 .unindent(),
22940 cx,
22941 )
22942 .await;
22943
22944 assert_indent_guides(
22945 0..11,
22946 vec![
22947 indent_guide(buffer_id, 1, 9, 0),
22948 indent_guide(buffer_id, 6, 6, 1),
22949 indent_guide(buffer_id, 8, 8, 1),
22950 ],
22951 None,
22952 &mut cx,
22953 );
22954}
22955
22956#[gpui::test]
22957async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
22958 let (buffer_id, mut cx) = setup_indent_guides_editor(
22959 &"
22960 fn main() {
22961 let a = 1;
22962
22963 let c = 3;
22964
22965 if a == 3 {
22966 let b = 2;
22967 } else {
22968 let c = 3;
22969 }
22970 }"
22971 .unindent(),
22972 cx,
22973 )
22974 .await;
22975
22976 assert_indent_guides(
22977 1..11,
22978 vec![
22979 indent_guide(buffer_id, 1, 9, 0),
22980 indent_guide(buffer_id, 6, 6, 1),
22981 indent_guide(buffer_id, 8, 8, 1),
22982 ],
22983 None,
22984 &mut cx,
22985 );
22986}
22987
22988#[gpui::test]
22989async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
22990 let (buffer_id, mut cx) = setup_indent_guides_editor(
22991 &"
22992 fn main() {
22993 let a = 1;
22994
22995 let c = 3;
22996
22997 if a == 3 {
22998 let b = 2;
22999 } else {
23000 let c = 3;
23001 }
23002 }"
23003 .unindent(),
23004 cx,
23005 )
23006 .await;
23007
23008 assert_indent_guides(
23009 1..10,
23010 vec![
23011 indent_guide(buffer_id, 1, 9, 0),
23012 indent_guide(buffer_id, 6, 6, 1),
23013 indent_guide(buffer_id, 8, 8, 1),
23014 ],
23015 None,
23016 &mut cx,
23017 );
23018}
23019
23020#[gpui::test]
23021async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
23022 let (buffer_id, mut cx) = setup_indent_guides_editor(
23023 &"
23024 fn main() {
23025 if a {
23026 b(
23027 c,
23028 d,
23029 )
23030 } else {
23031 e(
23032 f
23033 )
23034 }
23035 }"
23036 .unindent(),
23037 cx,
23038 )
23039 .await;
23040
23041 assert_indent_guides(
23042 0..11,
23043 vec![
23044 indent_guide(buffer_id, 1, 10, 0),
23045 indent_guide(buffer_id, 2, 5, 1),
23046 indent_guide(buffer_id, 7, 9, 1),
23047 indent_guide(buffer_id, 3, 4, 2),
23048 indent_guide(buffer_id, 8, 8, 2),
23049 ],
23050 None,
23051 &mut cx,
23052 );
23053
23054 cx.update_editor(|editor, window, cx| {
23055 editor.fold_at(MultiBufferRow(2), window, cx);
23056 assert_eq!(
23057 editor.display_text(cx),
23058 "
23059 fn main() {
23060 if a {
23061 b(⋯
23062 )
23063 } else {
23064 e(
23065 f
23066 )
23067 }
23068 }"
23069 .unindent()
23070 );
23071 });
23072
23073 assert_indent_guides(
23074 0..11,
23075 vec![
23076 indent_guide(buffer_id, 1, 10, 0),
23077 indent_guide(buffer_id, 2, 5, 1),
23078 indent_guide(buffer_id, 7, 9, 1),
23079 indent_guide(buffer_id, 8, 8, 2),
23080 ],
23081 None,
23082 &mut cx,
23083 );
23084}
23085
23086#[gpui::test]
23087async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
23088 let (buffer_id, mut cx) = setup_indent_guides_editor(
23089 &"
23090 block1
23091 block2
23092 block3
23093 block4
23094 block2
23095 block1
23096 block1"
23097 .unindent(),
23098 cx,
23099 )
23100 .await;
23101
23102 assert_indent_guides(
23103 1..10,
23104 vec![
23105 indent_guide(buffer_id, 1, 4, 0),
23106 indent_guide(buffer_id, 2, 3, 1),
23107 indent_guide(buffer_id, 3, 3, 2),
23108 ],
23109 None,
23110 &mut cx,
23111 );
23112}
23113
23114#[gpui::test]
23115async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
23116 let (buffer_id, mut cx) = setup_indent_guides_editor(
23117 &"
23118 block1
23119 block2
23120 block3
23121
23122 block1
23123 block1"
23124 .unindent(),
23125 cx,
23126 )
23127 .await;
23128
23129 assert_indent_guides(
23130 0..6,
23131 vec![
23132 indent_guide(buffer_id, 1, 2, 0),
23133 indent_guide(buffer_id, 2, 2, 1),
23134 ],
23135 None,
23136 &mut cx,
23137 );
23138}
23139
23140#[gpui::test]
23141async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
23142 let (buffer_id, mut cx) = setup_indent_guides_editor(
23143 &"
23144 function component() {
23145 \treturn (
23146 \t\t\t
23147 \t\t<div>
23148 \t\t\t<abc></abc>
23149 \t\t</div>
23150 \t)
23151 }"
23152 .unindent(),
23153 cx,
23154 )
23155 .await;
23156
23157 assert_indent_guides(
23158 0..8,
23159 vec![
23160 indent_guide(buffer_id, 1, 6, 0),
23161 indent_guide(buffer_id, 2, 5, 1),
23162 indent_guide(buffer_id, 4, 4, 2),
23163 ],
23164 None,
23165 &mut cx,
23166 );
23167}
23168
23169#[gpui::test]
23170async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
23171 let (buffer_id, mut cx) = setup_indent_guides_editor(
23172 &"
23173 function component() {
23174 \treturn (
23175 \t
23176 \t\t<div>
23177 \t\t\t<abc></abc>
23178 \t\t</div>
23179 \t)
23180 }"
23181 .unindent(),
23182 cx,
23183 )
23184 .await;
23185
23186 assert_indent_guides(
23187 0..8,
23188 vec![
23189 indent_guide(buffer_id, 1, 6, 0),
23190 indent_guide(buffer_id, 2, 5, 1),
23191 indent_guide(buffer_id, 4, 4, 2),
23192 ],
23193 None,
23194 &mut cx,
23195 );
23196}
23197
23198#[gpui::test]
23199async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
23200 let (buffer_id, mut cx) = setup_indent_guides_editor(
23201 &"
23202 block1
23203
23204
23205
23206 block2
23207 "
23208 .unindent(),
23209 cx,
23210 )
23211 .await;
23212
23213 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
23214}
23215
23216#[gpui::test]
23217async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
23218 let (buffer_id, mut cx) = setup_indent_guides_editor(
23219 &"
23220 def a:
23221 \tb = 3
23222 \tif True:
23223 \t\tc = 4
23224 \t\td = 5
23225 \tprint(b)
23226 "
23227 .unindent(),
23228 cx,
23229 )
23230 .await;
23231
23232 assert_indent_guides(
23233 0..6,
23234 vec![
23235 indent_guide(buffer_id, 1, 5, 0),
23236 indent_guide(buffer_id, 3, 4, 1),
23237 ],
23238 None,
23239 &mut cx,
23240 );
23241}
23242
23243#[gpui::test]
23244async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
23245 let (buffer_id, mut cx) = setup_indent_guides_editor(
23246 &"
23247 fn main() {
23248 let a = 1;
23249 }"
23250 .unindent(),
23251 cx,
23252 )
23253 .await;
23254
23255 cx.update_editor(|editor, window, cx| {
23256 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23257 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
23258 });
23259 });
23260
23261 assert_indent_guides(
23262 0..3,
23263 vec![indent_guide(buffer_id, 1, 1, 0)],
23264 Some(vec![0]),
23265 &mut cx,
23266 );
23267}
23268
23269#[gpui::test]
23270async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
23271 let (buffer_id, mut cx) = setup_indent_guides_editor(
23272 &"
23273 fn main() {
23274 if 1 == 2 {
23275 let a = 1;
23276 }
23277 }"
23278 .unindent(),
23279 cx,
23280 )
23281 .await;
23282
23283 cx.update_editor(|editor, window, cx| {
23284 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23285 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
23286 });
23287 });
23288 cx.run_until_parked();
23289
23290 assert_indent_guides(
23291 0..4,
23292 vec![
23293 indent_guide(buffer_id, 1, 3, 0),
23294 indent_guide(buffer_id, 2, 2, 1),
23295 ],
23296 Some(vec![1]),
23297 &mut cx,
23298 );
23299
23300 cx.update_editor(|editor, window, cx| {
23301 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23302 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
23303 });
23304 });
23305 cx.run_until_parked();
23306
23307 assert_indent_guides(
23308 0..4,
23309 vec![
23310 indent_guide(buffer_id, 1, 3, 0),
23311 indent_guide(buffer_id, 2, 2, 1),
23312 ],
23313 Some(vec![1]),
23314 &mut cx,
23315 );
23316
23317 cx.update_editor(|editor, window, cx| {
23318 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23319 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
23320 });
23321 });
23322 cx.run_until_parked();
23323
23324 assert_indent_guides(
23325 0..4,
23326 vec![
23327 indent_guide(buffer_id, 1, 3, 0),
23328 indent_guide(buffer_id, 2, 2, 1),
23329 ],
23330 Some(vec![0]),
23331 &mut cx,
23332 );
23333}
23334
23335#[gpui::test]
23336async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
23337 let (buffer_id, mut cx) = setup_indent_guides_editor(
23338 &"
23339 fn main() {
23340 let a = 1;
23341
23342 let b = 2;
23343 }"
23344 .unindent(),
23345 cx,
23346 )
23347 .await;
23348
23349 cx.update_editor(|editor, window, cx| {
23350 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23351 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
23352 });
23353 });
23354
23355 assert_indent_guides(
23356 0..5,
23357 vec![indent_guide(buffer_id, 1, 3, 0)],
23358 Some(vec![0]),
23359 &mut cx,
23360 );
23361}
23362
23363#[gpui::test]
23364async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
23365 let (buffer_id, mut cx) = setup_indent_guides_editor(
23366 &"
23367 def m:
23368 a = 1
23369 pass"
23370 .unindent(),
23371 cx,
23372 )
23373 .await;
23374
23375 cx.update_editor(|editor, window, cx| {
23376 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23377 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
23378 });
23379 });
23380
23381 assert_indent_guides(
23382 0..3,
23383 vec![indent_guide(buffer_id, 1, 2, 0)],
23384 Some(vec![0]),
23385 &mut cx,
23386 );
23387}
23388
23389#[gpui::test]
23390async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
23391 init_test(cx, |_| {});
23392 let mut cx = EditorTestContext::new(cx).await;
23393 let text = indoc! {
23394 "
23395 impl A {
23396 fn b() {
23397 0;
23398 3;
23399 5;
23400 6;
23401 7;
23402 }
23403 }
23404 "
23405 };
23406 let base_text = indoc! {
23407 "
23408 impl A {
23409 fn b() {
23410 0;
23411 1;
23412 2;
23413 3;
23414 4;
23415 }
23416 fn c() {
23417 5;
23418 6;
23419 7;
23420 }
23421 }
23422 "
23423 };
23424
23425 cx.update_editor(|editor, window, cx| {
23426 editor.set_text(text, window, cx);
23427
23428 editor.buffer().update(cx, |multibuffer, cx| {
23429 let buffer = multibuffer.as_singleton().unwrap();
23430 let diff = cx.new(|cx| {
23431 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
23432 });
23433
23434 multibuffer.set_all_diff_hunks_expanded(cx);
23435 multibuffer.add_diff(diff, cx);
23436
23437 buffer.read(cx).remote_id()
23438 })
23439 });
23440 cx.run_until_parked();
23441
23442 cx.assert_state_with_diff(
23443 indoc! { "
23444 impl A {
23445 fn b() {
23446 0;
23447 - 1;
23448 - 2;
23449 3;
23450 - 4;
23451 - }
23452 - fn c() {
23453 5;
23454 6;
23455 7;
23456 }
23457 }
23458 ˇ"
23459 }
23460 .to_string(),
23461 );
23462
23463 let mut actual_guides = cx.update_editor(|editor, window, cx| {
23464 editor
23465 .snapshot(window, cx)
23466 .buffer_snapshot()
23467 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
23468 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
23469 .collect::<Vec<_>>()
23470 });
23471 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
23472 assert_eq!(
23473 actual_guides,
23474 vec![
23475 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
23476 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
23477 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
23478 ]
23479 );
23480}
23481
23482#[gpui::test]
23483async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
23484 init_test(cx, |_| {});
23485 let mut cx = EditorTestContext::new(cx).await;
23486
23487 let diff_base = r#"
23488 a
23489 b
23490 c
23491 "#
23492 .unindent();
23493
23494 cx.set_state(
23495 &r#"
23496 ˇA
23497 b
23498 C
23499 "#
23500 .unindent(),
23501 );
23502 cx.set_head_text(&diff_base);
23503 cx.update_editor(|editor, window, cx| {
23504 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23505 });
23506 executor.run_until_parked();
23507
23508 let both_hunks_expanded = r#"
23509 - a
23510 + ˇA
23511 b
23512 - c
23513 + C
23514 "#
23515 .unindent();
23516
23517 cx.assert_state_with_diff(both_hunks_expanded.clone());
23518
23519 let hunk_ranges = cx.update_editor(|editor, window, cx| {
23520 let snapshot = editor.snapshot(window, cx);
23521 let hunks = editor
23522 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
23523 .collect::<Vec<_>>();
23524 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
23525 hunks
23526 .into_iter()
23527 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
23528 .collect::<Vec<_>>()
23529 });
23530 assert_eq!(hunk_ranges.len(), 2);
23531
23532 cx.update_editor(|editor, _, cx| {
23533 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
23534 });
23535 executor.run_until_parked();
23536
23537 let second_hunk_expanded = r#"
23538 ˇA
23539 b
23540 - c
23541 + C
23542 "#
23543 .unindent();
23544
23545 cx.assert_state_with_diff(second_hunk_expanded);
23546
23547 cx.update_editor(|editor, _, cx| {
23548 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
23549 });
23550 executor.run_until_parked();
23551
23552 cx.assert_state_with_diff(both_hunks_expanded.clone());
23553
23554 cx.update_editor(|editor, _, cx| {
23555 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
23556 });
23557 executor.run_until_parked();
23558
23559 let first_hunk_expanded = r#"
23560 - a
23561 + ˇA
23562 b
23563 C
23564 "#
23565 .unindent();
23566
23567 cx.assert_state_with_diff(first_hunk_expanded);
23568
23569 cx.update_editor(|editor, _, cx| {
23570 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
23571 });
23572 executor.run_until_parked();
23573
23574 cx.assert_state_with_diff(both_hunks_expanded);
23575
23576 cx.set_state(
23577 &r#"
23578 ˇA
23579 b
23580 "#
23581 .unindent(),
23582 );
23583 cx.run_until_parked();
23584
23585 // TODO this cursor position seems bad
23586 cx.assert_state_with_diff(
23587 r#"
23588 - ˇa
23589 + A
23590 b
23591 "#
23592 .unindent(),
23593 );
23594
23595 cx.update_editor(|editor, window, cx| {
23596 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23597 });
23598
23599 cx.assert_state_with_diff(
23600 r#"
23601 - ˇa
23602 + A
23603 b
23604 - c
23605 "#
23606 .unindent(),
23607 );
23608
23609 let hunk_ranges = cx.update_editor(|editor, window, cx| {
23610 let snapshot = editor.snapshot(window, cx);
23611 let hunks = editor
23612 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
23613 .collect::<Vec<_>>();
23614 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
23615 hunks
23616 .into_iter()
23617 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
23618 .collect::<Vec<_>>()
23619 });
23620 assert_eq!(hunk_ranges.len(), 2);
23621
23622 cx.update_editor(|editor, _, cx| {
23623 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
23624 });
23625 executor.run_until_parked();
23626
23627 cx.assert_state_with_diff(
23628 r#"
23629 - ˇa
23630 + A
23631 b
23632 "#
23633 .unindent(),
23634 );
23635}
23636
23637#[gpui::test]
23638async fn test_toggle_deletion_hunk_at_start_of_file(
23639 executor: BackgroundExecutor,
23640 cx: &mut TestAppContext,
23641) {
23642 init_test(cx, |_| {});
23643 let mut cx = EditorTestContext::new(cx).await;
23644
23645 let diff_base = r#"
23646 a
23647 b
23648 c
23649 "#
23650 .unindent();
23651
23652 cx.set_state(
23653 &r#"
23654 ˇb
23655 c
23656 "#
23657 .unindent(),
23658 );
23659 cx.set_head_text(&diff_base);
23660 cx.update_editor(|editor, window, cx| {
23661 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23662 });
23663 executor.run_until_parked();
23664
23665 let hunk_expanded = r#"
23666 - a
23667 ˇb
23668 c
23669 "#
23670 .unindent();
23671
23672 cx.assert_state_with_diff(hunk_expanded.clone());
23673
23674 let hunk_ranges = cx.update_editor(|editor, window, cx| {
23675 let snapshot = editor.snapshot(window, cx);
23676 let hunks = editor
23677 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
23678 .collect::<Vec<_>>();
23679 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
23680 hunks
23681 .into_iter()
23682 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
23683 .collect::<Vec<_>>()
23684 });
23685 assert_eq!(hunk_ranges.len(), 1);
23686
23687 cx.update_editor(|editor, _, cx| {
23688 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
23689 });
23690 executor.run_until_parked();
23691
23692 let hunk_collapsed = r#"
23693 ˇb
23694 c
23695 "#
23696 .unindent();
23697
23698 cx.assert_state_with_diff(hunk_collapsed);
23699
23700 cx.update_editor(|editor, _, cx| {
23701 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
23702 });
23703 executor.run_until_parked();
23704
23705 cx.assert_state_with_diff(hunk_expanded);
23706}
23707
23708#[gpui::test]
23709async fn test_select_smaller_syntax_node_after_diff_hunk_collapse(
23710 executor: BackgroundExecutor,
23711 cx: &mut TestAppContext,
23712) {
23713 init_test(cx, |_| {});
23714
23715 let mut cx = EditorTestContext::new(cx).await;
23716 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
23717
23718 cx.set_state(
23719 &r#"
23720 fn main() {
23721 let x = ˇ1;
23722 }
23723 "#
23724 .unindent(),
23725 );
23726
23727 let diff_base = r#"
23728 fn removed_one() {
23729 println!("this function was deleted");
23730 }
23731
23732 fn removed_two() {
23733 println!("this function was also deleted");
23734 }
23735
23736 fn main() {
23737 let x = 1;
23738 }
23739 "#
23740 .unindent();
23741 cx.set_head_text(&diff_base);
23742 executor.run_until_parked();
23743
23744 cx.update_editor(|editor, window, cx| {
23745 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23746 });
23747 executor.run_until_parked();
23748
23749 cx.update_editor(|editor, window, cx| {
23750 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
23751 });
23752
23753 cx.update_editor(|editor, window, cx| {
23754 editor.collapse_all_diff_hunks(&CollapseAllDiffHunks, window, cx);
23755 });
23756 executor.run_until_parked();
23757
23758 cx.update_editor(|editor, window, cx| {
23759 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
23760 });
23761}
23762
23763#[gpui::test]
23764async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
23765 executor: BackgroundExecutor,
23766 cx: &mut TestAppContext,
23767) {
23768 init_test(cx, |_| {});
23769 let mut cx = EditorTestContext::new(cx).await;
23770
23771 cx.set_state("ˇnew\nsecond\nthird\n");
23772 cx.set_head_text("old\nsecond\nthird\n");
23773 cx.update_editor(|editor, window, cx| {
23774 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
23775 });
23776 executor.run_until_parked();
23777 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
23778
23779 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
23780 cx.update_editor(|editor, window, cx| {
23781 let snapshot = editor.snapshot(window, cx);
23782 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
23783 let hunks = editor
23784 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
23785 .collect::<Vec<_>>();
23786 assert_eq!(hunks.len(), 1);
23787 let hunk_range = Anchor::range_in_buffer(excerpt_id, hunks[0].buffer_range.clone());
23788 editor.toggle_single_diff_hunk(hunk_range, cx)
23789 });
23790 executor.run_until_parked();
23791 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
23792
23793 // Keep the editor scrolled to the top so the full hunk remains visible.
23794 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
23795}
23796
23797#[gpui::test]
23798async fn test_display_diff_hunks(cx: &mut TestAppContext) {
23799 init_test(cx, |_| {});
23800
23801 let fs = FakeFs::new(cx.executor());
23802 fs.insert_tree(
23803 path!("/test"),
23804 json!({
23805 ".git": {},
23806 "file-1": "ONE\n",
23807 "file-2": "TWO\n",
23808 "file-3": "THREE\n",
23809 }),
23810 )
23811 .await;
23812
23813 fs.set_head_for_repo(
23814 path!("/test/.git").as_ref(),
23815 &[
23816 ("file-1", "one\n".into()),
23817 ("file-2", "two\n".into()),
23818 ("file-3", "three\n".into()),
23819 ],
23820 "deadbeef",
23821 );
23822
23823 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
23824 let mut buffers = vec![];
23825 for i in 1..=3 {
23826 let buffer = project
23827 .update(cx, |project, cx| {
23828 let path = format!(path!("/test/file-{}"), i);
23829 project.open_local_buffer(path, cx)
23830 })
23831 .await
23832 .unwrap();
23833 buffers.push(buffer);
23834 }
23835
23836 let multibuffer = cx.new(|cx| {
23837 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
23838 multibuffer.set_all_diff_hunks_expanded(cx);
23839 for buffer in &buffers {
23840 let snapshot = buffer.read(cx).snapshot();
23841 multibuffer.set_excerpts_for_path(
23842 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
23843 buffer.clone(),
23844 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
23845 2,
23846 cx,
23847 );
23848 }
23849 multibuffer
23850 });
23851
23852 let editor = cx.add_window(|window, cx| {
23853 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
23854 });
23855 cx.run_until_parked();
23856
23857 let snapshot = editor
23858 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
23859 .unwrap();
23860 let hunks = snapshot
23861 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
23862 .map(|hunk| match hunk {
23863 DisplayDiffHunk::Unfolded {
23864 display_row_range, ..
23865 } => display_row_range,
23866 DisplayDiffHunk::Folded { .. } => unreachable!(),
23867 })
23868 .collect::<Vec<_>>();
23869 assert_eq!(
23870 hunks,
23871 [
23872 DisplayRow(2)..DisplayRow(4),
23873 DisplayRow(7)..DisplayRow(9),
23874 DisplayRow(12)..DisplayRow(14),
23875 ]
23876 );
23877}
23878
23879#[gpui::test]
23880async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
23881 init_test(cx, |_| {});
23882
23883 let mut cx = EditorTestContext::new(cx).await;
23884 cx.set_head_text(indoc! { "
23885 one
23886 two
23887 three
23888 four
23889 five
23890 "
23891 });
23892 cx.set_index_text(indoc! { "
23893 one
23894 two
23895 three
23896 four
23897 five
23898 "
23899 });
23900 cx.set_state(indoc! {"
23901 one
23902 TWO
23903 ˇTHREE
23904 FOUR
23905 five
23906 "});
23907 cx.run_until_parked();
23908 cx.update_editor(|editor, window, cx| {
23909 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
23910 });
23911 cx.run_until_parked();
23912 cx.assert_index_text(Some(indoc! {"
23913 one
23914 TWO
23915 THREE
23916 FOUR
23917 five
23918 "}));
23919 cx.set_state(indoc! { "
23920 one
23921 TWO
23922 ˇTHREE-HUNDRED
23923 FOUR
23924 five
23925 "});
23926 cx.run_until_parked();
23927 cx.update_editor(|editor, window, cx| {
23928 let snapshot = editor.snapshot(window, cx);
23929 let hunks = editor
23930 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
23931 .collect::<Vec<_>>();
23932 assert_eq!(hunks.len(), 1);
23933 assert_eq!(
23934 hunks[0].status(),
23935 DiffHunkStatus {
23936 kind: DiffHunkStatusKind::Modified,
23937 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
23938 }
23939 );
23940
23941 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
23942 });
23943 cx.run_until_parked();
23944 cx.assert_index_text(Some(indoc! {"
23945 one
23946 TWO
23947 THREE-HUNDRED
23948 FOUR
23949 five
23950 "}));
23951}
23952
23953#[gpui::test]
23954fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
23955 init_test(cx, |_| {});
23956
23957 let editor = cx.add_window(|window, cx| {
23958 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
23959 build_editor(buffer, window, cx)
23960 });
23961
23962 let render_args = Arc::new(Mutex::new(None));
23963 let snapshot = editor
23964 .update(cx, |editor, window, cx| {
23965 let snapshot = editor.buffer().read(cx).snapshot(cx);
23966 let range =
23967 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
23968
23969 struct RenderArgs {
23970 row: MultiBufferRow,
23971 folded: bool,
23972 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
23973 }
23974
23975 let crease = Crease::inline(
23976 range,
23977 FoldPlaceholder::test(),
23978 {
23979 let toggle_callback = render_args.clone();
23980 move |row, folded, callback, _window, _cx| {
23981 *toggle_callback.lock() = Some(RenderArgs {
23982 row,
23983 folded,
23984 callback,
23985 });
23986 div()
23987 }
23988 },
23989 |_row, _folded, _window, _cx| div(),
23990 );
23991
23992 editor.insert_creases(Some(crease), cx);
23993 let snapshot = editor.snapshot(window, cx);
23994 let _div =
23995 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
23996 snapshot
23997 })
23998 .unwrap();
23999
24000 let render_args = render_args.lock().take().unwrap();
24001 assert_eq!(render_args.row, MultiBufferRow(1));
24002 assert!(!render_args.folded);
24003 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
24004
24005 cx.update_window(*editor, |_, window, cx| {
24006 (render_args.callback)(true, window, cx)
24007 })
24008 .unwrap();
24009 let snapshot = editor
24010 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
24011 .unwrap();
24012 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
24013
24014 cx.update_window(*editor, |_, window, cx| {
24015 (render_args.callback)(false, window, cx)
24016 })
24017 .unwrap();
24018 let snapshot = editor
24019 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
24020 .unwrap();
24021 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
24022}
24023
24024#[gpui::test]
24025async fn test_input_text(cx: &mut TestAppContext) {
24026 init_test(cx, |_| {});
24027 let mut cx = EditorTestContext::new(cx).await;
24028
24029 cx.set_state(
24030 &r#"ˇone
24031 two
24032
24033 three
24034 fourˇ
24035 five
24036
24037 siˇx"#
24038 .unindent(),
24039 );
24040
24041 cx.dispatch_action(HandleInput(String::new()));
24042 cx.assert_editor_state(
24043 &r#"ˇone
24044 two
24045
24046 three
24047 fourˇ
24048 five
24049
24050 siˇx"#
24051 .unindent(),
24052 );
24053
24054 cx.dispatch_action(HandleInput("AAAA".to_string()));
24055 cx.assert_editor_state(
24056 &r#"AAAAˇone
24057 two
24058
24059 three
24060 fourAAAAˇ
24061 five
24062
24063 siAAAAˇx"#
24064 .unindent(),
24065 );
24066}
24067
24068#[gpui::test]
24069async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
24070 init_test(cx, |_| {});
24071
24072 let mut cx = EditorTestContext::new(cx).await;
24073 cx.set_state(
24074 r#"let foo = 1;
24075let foo = 2;
24076let foo = 3;
24077let fooˇ = 4;
24078let foo = 5;
24079let foo = 6;
24080let foo = 7;
24081let foo = 8;
24082let foo = 9;
24083let foo = 10;
24084let foo = 11;
24085let foo = 12;
24086let foo = 13;
24087let foo = 14;
24088let foo = 15;"#,
24089 );
24090
24091 cx.update_editor(|e, window, cx| {
24092 assert_eq!(
24093 e.next_scroll_position,
24094 NextScrollCursorCenterTopBottom::Center,
24095 "Default next scroll direction is center",
24096 );
24097
24098 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
24099 assert_eq!(
24100 e.next_scroll_position,
24101 NextScrollCursorCenterTopBottom::Top,
24102 "After center, next scroll direction should be top",
24103 );
24104
24105 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
24106 assert_eq!(
24107 e.next_scroll_position,
24108 NextScrollCursorCenterTopBottom::Bottom,
24109 "After top, next scroll direction should be bottom",
24110 );
24111
24112 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
24113 assert_eq!(
24114 e.next_scroll_position,
24115 NextScrollCursorCenterTopBottom::Center,
24116 "After bottom, scrolling should start over",
24117 );
24118
24119 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
24120 assert_eq!(
24121 e.next_scroll_position,
24122 NextScrollCursorCenterTopBottom::Top,
24123 "Scrolling continues if retriggered fast enough"
24124 );
24125 });
24126
24127 cx.executor()
24128 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
24129 cx.executor().run_until_parked();
24130 cx.update_editor(|e, _, _| {
24131 assert_eq!(
24132 e.next_scroll_position,
24133 NextScrollCursorCenterTopBottom::Center,
24134 "If scrolling is not triggered fast enough, it should reset"
24135 );
24136 });
24137}
24138
24139#[gpui::test]
24140async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
24141 init_test(cx, |_| {});
24142 let mut cx = EditorLspTestContext::new_rust(
24143 lsp::ServerCapabilities {
24144 definition_provider: Some(lsp::OneOf::Left(true)),
24145 references_provider: Some(lsp::OneOf::Left(true)),
24146 ..lsp::ServerCapabilities::default()
24147 },
24148 cx,
24149 )
24150 .await;
24151
24152 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
24153 let go_to_definition = cx
24154 .lsp
24155 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
24156 move |params, _| async move {
24157 if empty_go_to_definition {
24158 Ok(None)
24159 } else {
24160 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
24161 uri: params.text_document_position_params.text_document.uri,
24162 range: lsp::Range::new(
24163 lsp::Position::new(4, 3),
24164 lsp::Position::new(4, 6),
24165 ),
24166 })))
24167 }
24168 },
24169 );
24170 let references = cx
24171 .lsp
24172 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
24173 Ok(Some(vec![lsp::Location {
24174 uri: params.text_document_position.text_document.uri,
24175 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
24176 }]))
24177 });
24178 (go_to_definition, references)
24179 };
24180
24181 cx.set_state(
24182 &r#"fn one() {
24183 let mut a = ˇtwo();
24184 }
24185
24186 fn two() {}"#
24187 .unindent(),
24188 );
24189 set_up_lsp_handlers(false, &mut cx);
24190 let navigated = cx
24191 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
24192 .await
24193 .expect("Failed to navigate to definition");
24194 assert_eq!(
24195 navigated,
24196 Navigated::Yes,
24197 "Should have navigated to definition from the GetDefinition response"
24198 );
24199 cx.assert_editor_state(
24200 &r#"fn one() {
24201 let mut a = two();
24202 }
24203
24204 fn «twoˇ»() {}"#
24205 .unindent(),
24206 );
24207
24208 let editors = cx.update_workspace(|workspace, _, cx| {
24209 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
24210 });
24211 cx.update_editor(|_, _, test_editor_cx| {
24212 assert_eq!(
24213 editors.len(),
24214 1,
24215 "Initially, only one, test, editor should be open in the workspace"
24216 );
24217 assert_eq!(
24218 test_editor_cx.entity(),
24219 editors.last().expect("Asserted len is 1").clone()
24220 );
24221 });
24222
24223 set_up_lsp_handlers(true, &mut cx);
24224 let navigated = cx
24225 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
24226 .await
24227 .expect("Failed to navigate to lookup references");
24228 assert_eq!(
24229 navigated,
24230 Navigated::Yes,
24231 "Should have navigated to references as a fallback after empty GoToDefinition response"
24232 );
24233 // We should not change the selections in the existing file,
24234 // if opening another milti buffer with the references
24235 cx.assert_editor_state(
24236 &r#"fn one() {
24237 let mut a = two();
24238 }
24239
24240 fn «twoˇ»() {}"#
24241 .unindent(),
24242 );
24243 let editors = cx.update_workspace(|workspace, _, cx| {
24244 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
24245 });
24246 cx.update_editor(|_, _, test_editor_cx| {
24247 assert_eq!(
24248 editors.len(),
24249 2,
24250 "After falling back to references search, we open a new editor with the results"
24251 );
24252 let references_fallback_text = editors
24253 .into_iter()
24254 .find(|new_editor| *new_editor != test_editor_cx.entity())
24255 .expect("Should have one non-test editor now")
24256 .read(test_editor_cx)
24257 .text(test_editor_cx);
24258 assert_eq!(
24259 references_fallback_text, "fn one() {\n let mut a = two();\n}",
24260 "Should use the range from the references response and not the GoToDefinition one"
24261 );
24262 });
24263}
24264
24265#[gpui::test]
24266async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
24267 init_test(cx, |_| {});
24268 cx.update(|cx| {
24269 let mut editor_settings = EditorSettings::get_global(cx).clone();
24270 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
24271 EditorSettings::override_global(editor_settings, cx);
24272 });
24273 let mut cx = EditorLspTestContext::new_rust(
24274 lsp::ServerCapabilities {
24275 definition_provider: Some(lsp::OneOf::Left(true)),
24276 references_provider: Some(lsp::OneOf::Left(true)),
24277 ..lsp::ServerCapabilities::default()
24278 },
24279 cx,
24280 )
24281 .await;
24282 let original_state = r#"fn one() {
24283 let mut a = ˇtwo();
24284 }
24285
24286 fn two() {}"#
24287 .unindent();
24288 cx.set_state(&original_state);
24289
24290 let mut go_to_definition = cx
24291 .lsp
24292 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
24293 move |_, _| async move { Ok(None) },
24294 );
24295 let _references = cx
24296 .lsp
24297 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
24298 panic!("Should not call for references with no go to definition fallback")
24299 });
24300
24301 let navigated = cx
24302 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
24303 .await
24304 .expect("Failed to navigate to lookup references");
24305 go_to_definition
24306 .next()
24307 .await
24308 .expect("Should have called the go_to_definition handler");
24309
24310 assert_eq!(
24311 navigated,
24312 Navigated::No,
24313 "Should have navigated to references as a fallback after empty GoToDefinition response"
24314 );
24315 cx.assert_editor_state(&original_state);
24316 let editors = cx.update_workspace(|workspace, _, cx| {
24317 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
24318 });
24319 cx.update_editor(|_, _, _| {
24320 assert_eq!(
24321 editors.len(),
24322 1,
24323 "After unsuccessful fallback, no other editor should have been opened"
24324 );
24325 });
24326}
24327
24328#[gpui::test]
24329async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
24330 init_test(cx, |_| {});
24331 let mut cx = EditorLspTestContext::new_rust(
24332 lsp::ServerCapabilities {
24333 references_provider: Some(lsp::OneOf::Left(true)),
24334 ..lsp::ServerCapabilities::default()
24335 },
24336 cx,
24337 )
24338 .await;
24339
24340 cx.set_state(
24341 &r#"
24342 fn one() {
24343 let mut a = two();
24344 }
24345
24346 fn ˇtwo() {}"#
24347 .unindent(),
24348 );
24349 cx.lsp
24350 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
24351 Ok(Some(vec![
24352 lsp::Location {
24353 uri: params.text_document_position.text_document.uri.clone(),
24354 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
24355 },
24356 lsp::Location {
24357 uri: params.text_document_position.text_document.uri,
24358 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
24359 },
24360 ]))
24361 });
24362 let navigated = cx
24363 .update_editor(|editor, window, cx| {
24364 editor.find_all_references(&FindAllReferences::default(), window, cx)
24365 })
24366 .unwrap()
24367 .await
24368 .expect("Failed to navigate to references");
24369 assert_eq!(
24370 navigated,
24371 Navigated::Yes,
24372 "Should have navigated to references from the FindAllReferences response"
24373 );
24374 cx.assert_editor_state(
24375 &r#"fn one() {
24376 let mut a = two();
24377 }
24378
24379 fn ˇtwo() {}"#
24380 .unindent(),
24381 );
24382
24383 let editors = cx.update_workspace(|workspace, _, cx| {
24384 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
24385 });
24386 cx.update_editor(|_, _, _| {
24387 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
24388 });
24389
24390 cx.set_state(
24391 &r#"fn one() {
24392 let mut a = ˇtwo();
24393 }
24394
24395 fn two() {}"#
24396 .unindent(),
24397 );
24398 let navigated = cx
24399 .update_editor(|editor, window, cx| {
24400 editor.find_all_references(&FindAllReferences::default(), window, cx)
24401 })
24402 .unwrap()
24403 .await
24404 .expect("Failed to navigate to references");
24405 assert_eq!(
24406 navigated,
24407 Navigated::Yes,
24408 "Should have navigated to references from the FindAllReferences response"
24409 );
24410 cx.assert_editor_state(
24411 &r#"fn one() {
24412 let mut a = ˇtwo();
24413 }
24414
24415 fn two() {}"#
24416 .unindent(),
24417 );
24418 let editors = cx.update_workspace(|workspace, _, cx| {
24419 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
24420 });
24421 cx.update_editor(|_, _, _| {
24422 assert_eq!(
24423 editors.len(),
24424 2,
24425 "should have re-used the previous multibuffer"
24426 );
24427 });
24428
24429 cx.set_state(
24430 &r#"fn one() {
24431 let mut a = ˇtwo();
24432 }
24433 fn three() {}
24434 fn two() {}"#
24435 .unindent(),
24436 );
24437 cx.lsp
24438 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
24439 Ok(Some(vec![
24440 lsp::Location {
24441 uri: params.text_document_position.text_document.uri.clone(),
24442 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
24443 },
24444 lsp::Location {
24445 uri: params.text_document_position.text_document.uri,
24446 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
24447 },
24448 ]))
24449 });
24450 let navigated = cx
24451 .update_editor(|editor, window, cx| {
24452 editor.find_all_references(&FindAllReferences::default(), window, cx)
24453 })
24454 .unwrap()
24455 .await
24456 .expect("Failed to navigate to references");
24457 assert_eq!(
24458 navigated,
24459 Navigated::Yes,
24460 "Should have navigated to references from the FindAllReferences response"
24461 );
24462 cx.assert_editor_state(
24463 &r#"fn one() {
24464 let mut a = ˇtwo();
24465 }
24466 fn three() {}
24467 fn two() {}"#
24468 .unindent(),
24469 );
24470 let editors = cx.update_workspace(|workspace, _, cx| {
24471 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
24472 });
24473 cx.update_editor(|_, _, _| {
24474 assert_eq!(
24475 editors.len(),
24476 3,
24477 "should have used a new multibuffer as offsets changed"
24478 );
24479 });
24480}
24481#[gpui::test]
24482async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
24483 init_test(cx, |_| {});
24484
24485 let language = Arc::new(Language::new(
24486 LanguageConfig::default(),
24487 Some(tree_sitter_rust::LANGUAGE.into()),
24488 ));
24489
24490 let text = r#"
24491 #[cfg(test)]
24492 mod tests() {
24493 #[test]
24494 fn runnable_1() {
24495 let a = 1;
24496 }
24497
24498 #[test]
24499 fn runnable_2() {
24500 let a = 1;
24501 let b = 2;
24502 }
24503 }
24504 "#
24505 .unindent();
24506
24507 let fs = FakeFs::new(cx.executor());
24508 fs.insert_file("/file.rs", Default::default()).await;
24509
24510 let project = Project::test(fs, ["/a".as_ref()], cx).await;
24511 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
24512 let cx = &mut VisualTestContext::from_window(*window, cx);
24513 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
24514 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
24515
24516 let editor = cx.new_window_entity(|window, cx| {
24517 Editor::new(
24518 EditorMode::full(),
24519 multi_buffer,
24520 Some(project.clone()),
24521 window,
24522 cx,
24523 )
24524 });
24525
24526 editor.update_in(cx, |editor, window, cx| {
24527 let snapshot = editor.buffer().read(cx).snapshot(cx);
24528 editor.runnables.insert(
24529 buffer.read(cx).remote_id(),
24530 3,
24531 buffer.read(cx).version(),
24532 RunnableTasks {
24533 templates: Vec::new(),
24534 offset: snapshot.anchor_before(MultiBufferOffset(43)),
24535 column: 0,
24536 extra_variables: HashMap::default(),
24537 context_range: BufferOffset(43)..BufferOffset(85),
24538 },
24539 );
24540 editor.runnables.insert(
24541 buffer.read(cx).remote_id(),
24542 8,
24543 buffer.read(cx).version(),
24544 RunnableTasks {
24545 templates: Vec::new(),
24546 offset: snapshot.anchor_before(MultiBufferOffset(86)),
24547 column: 0,
24548 extra_variables: HashMap::default(),
24549 context_range: BufferOffset(86)..BufferOffset(191),
24550 },
24551 );
24552
24553 // Test finding task when cursor is inside function body
24554 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24555 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
24556 });
24557 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
24558 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
24559
24560 // Test finding task when cursor is on function name
24561 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24562 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
24563 });
24564 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
24565 assert_eq!(row, 8, "Should find task when cursor is on function name");
24566 });
24567}
24568
24569#[gpui::test]
24570async fn test_folding_buffers(cx: &mut TestAppContext) {
24571 init_test(cx, |_| {});
24572
24573 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
24574 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
24575 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
24576
24577 let fs = FakeFs::new(cx.executor());
24578 fs.insert_tree(
24579 path!("/a"),
24580 json!({
24581 "first.rs": sample_text_1,
24582 "second.rs": sample_text_2,
24583 "third.rs": sample_text_3,
24584 }),
24585 )
24586 .await;
24587 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24588 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
24589 let cx = &mut VisualTestContext::from_window(*window, cx);
24590 let worktree = project.update(cx, |project, cx| {
24591 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
24592 assert_eq!(worktrees.len(), 1);
24593 worktrees.pop().unwrap()
24594 });
24595 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
24596
24597 let buffer_1 = project
24598 .update(cx, |project, cx| {
24599 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
24600 })
24601 .await
24602 .unwrap();
24603 let buffer_2 = project
24604 .update(cx, |project, cx| {
24605 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
24606 })
24607 .await
24608 .unwrap();
24609 let buffer_3 = project
24610 .update(cx, |project, cx| {
24611 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
24612 })
24613 .await
24614 .unwrap();
24615
24616 let multi_buffer = cx.new(|cx| {
24617 let mut multi_buffer = MultiBuffer::new(ReadWrite);
24618 multi_buffer.set_excerpts_for_path(
24619 PathKey::sorted(0),
24620 buffer_1.clone(),
24621 [
24622 Point::new(0, 0)..Point::new(2, 0),
24623 Point::new(5, 0)..Point::new(6, 0),
24624 Point::new(9, 0)..Point::new(10, 4),
24625 ],
24626 0,
24627 cx,
24628 );
24629 multi_buffer.set_excerpts_for_path(
24630 PathKey::sorted(1),
24631 buffer_2.clone(),
24632 [
24633 Point::new(0, 0)..Point::new(2, 0),
24634 Point::new(5, 0)..Point::new(6, 0),
24635 Point::new(9, 0)..Point::new(10, 4),
24636 ],
24637 0,
24638 cx,
24639 );
24640 multi_buffer.set_excerpts_for_path(
24641 PathKey::sorted(2),
24642 buffer_3.clone(),
24643 [
24644 Point::new(0, 0)..Point::new(2, 0),
24645 Point::new(5, 0)..Point::new(6, 0),
24646 Point::new(9, 0)..Point::new(10, 4),
24647 ],
24648 0,
24649 cx,
24650 );
24651 multi_buffer
24652 });
24653 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
24654 Editor::new(
24655 EditorMode::full(),
24656 multi_buffer.clone(),
24657 Some(project.clone()),
24658 window,
24659 cx,
24660 )
24661 });
24662
24663 assert_eq!(
24664 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24665 "\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",
24666 );
24667
24668 multi_buffer_editor.update(cx, |editor, cx| {
24669 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
24670 });
24671 assert_eq!(
24672 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24673 "\n\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n1111\n2222\n\n5555",
24674 "After folding the first buffer, its text should not be displayed"
24675 );
24676
24677 multi_buffer_editor.update(cx, |editor, cx| {
24678 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
24679 });
24680 assert_eq!(
24681 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24682 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n1111\n2222\n\n5555",
24683 "After folding the second buffer, its text should not be displayed"
24684 );
24685
24686 multi_buffer_editor.update(cx, |editor, cx| {
24687 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
24688 });
24689 assert_eq!(
24690 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24691 "\n\n\n\n\n",
24692 "After folding the third buffer, its text should not be displayed"
24693 );
24694
24695 // Emulate selection inside the fold logic, that should work
24696 multi_buffer_editor.update_in(cx, |editor, window, cx| {
24697 editor
24698 .snapshot(window, cx)
24699 .next_line_boundary(Point::new(0, 4));
24700 });
24701
24702 multi_buffer_editor.update(cx, |editor, cx| {
24703 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
24704 });
24705 assert_eq!(
24706 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24707 "\n\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n",
24708 "After unfolding the second buffer, its text should be displayed"
24709 );
24710
24711 // Typing inside of buffer 1 causes that buffer to be unfolded.
24712 multi_buffer_editor.update_in(cx, |editor, window, cx| {
24713 assert_eq!(
24714 multi_buffer
24715 .read(cx)
24716 .snapshot(cx)
24717 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
24718 .collect::<String>(),
24719 "bbbb"
24720 );
24721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24722 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
24723 });
24724 editor.handle_input("B", window, cx);
24725 });
24726
24727 assert_eq!(
24728 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24729 "\n\naaaa\nBbbbb\ncccc\n\nffff\ngggg\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\nqqqq\nrrrr\n\nuuuu\n\n",
24730 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
24731 );
24732
24733 multi_buffer_editor.update(cx, |editor, cx| {
24734 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
24735 });
24736 assert_eq!(
24737 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24738 "\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",
24739 "After unfolding the all buffers, all original text should be displayed"
24740 );
24741}
24742
24743#[gpui::test]
24744async fn test_folded_buffers_cleared_on_excerpts_removed(cx: &mut TestAppContext) {
24745 init_test(cx, |_| {});
24746
24747 let fs = FakeFs::new(cx.executor());
24748 fs.insert_tree(
24749 path!("/root"),
24750 json!({
24751 "file_a.txt": "File A\nFile A\nFile A",
24752 "file_b.txt": "File B\nFile B\nFile B",
24753 }),
24754 )
24755 .await;
24756
24757 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
24758 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
24759 let cx = &mut VisualTestContext::from_window(*window, cx);
24760 let worktree = project.update(cx, |project, cx| {
24761 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
24762 assert_eq!(worktrees.len(), 1);
24763 worktrees.pop().unwrap()
24764 });
24765 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
24766
24767 let buffer_a = project
24768 .update(cx, |project, cx| {
24769 project.open_buffer((worktree_id, rel_path("file_a.txt")), cx)
24770 })
24771 .await
24772 .unwrap();
24773 let buffer_b = project
24774 .update(cx, |project, cx| {
24775 project.open_buffer((worktree_id, rel_path("file_b.txt")), cx)
24776 })
24777 .await
24778 .unwrap();
24779
24780 let multi_buffer = cx.new(|cx| {
24781 let mut multi_buffer = MultiBuffer::new(ReadWrite);
24782 let range_a = Point::new(0, 0)..Point::new(2, 4);
24783 let range_b = Point::new(0, 0)..Point::new(2, 4);
24784
24785 multi_buffer.set_excerpts_for_path(PathKey::sorted(0), buffer_a.clone(), [range_a], 0, cx);
24786 multi_buffer.set_excerpts_for_path(PathKey::sorted(1), buffer_b.clone(), [range_b], 0, cx);
24787 multi_buffer
24788 });
24789
24790 let editor = cx.new_window_entity(|window, cx| {
24791 Editor::new(
24792 EditorMode::full(),
24793 multi_buffer.clone(),
24794 Some(project.clone()),
24795 window,
24796 cx,
24797 )
24798 });
24799
24800 editor.update(cx, |editor, cx| {
24801 editor.fold_buffer(buffer_a.read(cx).remote_id(), cx);
24802 });
24803 assert!(editor.update(cx, |editor, cx| editor.has_any_buffer_folded(cx)));
24804
24805 // When the excerpts for `buffer_a` are removed, a
24806 // `multi_buffer::Event::ExcerptsRemoved` event is emitted, which should be
24807 // picked up by the editor and update the display map accordingly.
24808 multi_buffer.update(cx, |multi_buffer, cx| {
24809 multi_buffer.remove_excerpts_for_path(PathKey::sorted(0), cx)
24810 });
24811 assert!(!editor.update(cx, |editor, cx| editor.has_any_buffer_folded(cx)));
24812}
24813
24814#[gpui::test]
24815async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
24816 init_test(cx, |_| {});
24817
24818 let sample_text_1 = "1111\n2222\n3333".to_string();
24819 let sample_text_2 = "4444\n5555\n6666".to_string();
24820 let sample_text_3 = "7777\n8888\n9999".to_string();
24821
24822 let fs = FakeFs::new(cx.executor());
24823 fs.insert_tree(
24824 path!("/a"),
24825 json!({
24826 "first.rs": sample_text_1,
24827 "second.rs": sample_text_2,
24828 "third.rs": sample_text_3,
24829 }),
24830 )
24831 .await;
24832 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24833 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
24834 let cx = &mut VisualTestContext::from_window(*window, cx);
24835 let worktree = project.update(cx, |project, cx| {
24836 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
24837 assert_eq!(worktrees.len(), 1);
24838 worktrees.pop().unwrap()
24839 });
24840 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
24841
24842 let buffer_1 = project
24843 .update(cx, |project, cx| {
24844 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
24845 })
24846 .await
24847 .unwrap();
24848 let buffer_2 = project
24849 .update(cx, |project, cx| {
24850 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
24851 })
24852 .await
24853 .unwrap();
24854 let buffer_3 = project
24855 .update(cx, |project, cx| {
24856 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
24857 })
24858 .await
24859 .unwrap();
24860
24861 let multi_buffer = cx.new(|cx| {
24862 let mut multi_buffer = MultiBuffer::new(ReadWrite);
24863 multi_buffer.set_excerpts_for_path(
24864 PathKey::sorted(0),
24865 buffer_1.clone(),
24866 [Point::new(0, 0)..Point::new(3, 0)],
24867 0,
24868 cx,
24869 );
24870 multi_buffer.set_excerpts_for_path(
24871 PathKey::sorted(1),
24872 buffer_2.clone(),
24873 [Point::new(0, 0)..Point::new(3, 0)],
24874 0,
24875 cx,
24876 );
24877 multi_buffer.set_excerpts_for_path(
24878 PathKey::sorted(2),
24879 buffer_3.clone(),
24880 [Point::new(0, 0)..Point::new(3, 0)],
24881 0,
24882 cx,
24883 );
24884 multi_buffer
24885 });
24886
24887 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
24888 Editor::new(
24889 EditorMode::full(),
24890 multi_buffer,
24891 Some(project.clone()),
24892 window,
24893 cx,
24894 )
24895 });
24896
24897 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
24898 assert_eq!(
24899 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24900 full_text,
24901 );
24902
24903 multi_buffer_editor.update(cx, |editor, cx| {
24904 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
24905 });
24906 assert_eq!(
24907 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24908 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
24909 "After folding the first buffer, its text should not be displayed"
24910 );
24911
24912 multi_buffer_editor.update(cx, |editor, cx| {
24913 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
24914 });
24915
24916 assert_eq!(
24917 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24918 "\n\n\n\n\n\n7777\n8888\n9999",
24919 "After folding the second buffer, its text should not be displayed"
24920 );
24921
24922 multi_buffer_editor.update(cx, |editor, cx| {
24923 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
24924 });
24925 assert_eq!(
24926 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24927 "\n\n\n\n\n",
24928 "After folding the third buffer, its text should not be displayed"
24929 );
24930
24931 multi_buffer_editor.update(cx, |editor, cx| {
24932 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
24933 });
24934 assert_eq!(
24935 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24936 "\n\n\n\n4444\n5555\n6666\n\n",
24937 "After unfolding the second buffer, its text should be displayed"
24938 );
24939
24940 multi_buffer_editor.update(cx, |editor, cx| {
24941 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
24942 });
24943 assert_eq!(
24944 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24945 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
24946 "After unfolding the first buffer, its text should be displayed"
24947 );
24948
24949 multi_buffer_editor.update(cx, |editor, cx| {
24950 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
24951 });
24952 assert_eq!(
24953 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24954 full_text,
24955 "After unfolding all buffers, all original text should be displayed"
24956 );
24957}
24958
24959#[gpui::test]
24960async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
24961 init_test(cx, |_| {});
24962
24963 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
24964
24965 let fs = FakeFs::new(cx.executor());
24966 fs.insert_tree(
24967 path!("/a"),
24968 json!({
24969 "main.rs": sample_text,
24970 }),
24971 )
24972 .await;
24973 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24974 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
24975 let cx = &mut VisualTestContext::from_window(*window, cx);
24976 let worktree = project.update(cx, |project, cx| {
24977 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
24978 assert_eq!(worktrees.len(), 1);
24979 worktrees.pop().unwrap()
24980 });
24981 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
24982
24983 let buffer_1 = project
24984 .update(cx, |project, cx| {
24985 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24986 })
24987 .await
24988 .unwrap();
24989
24990 let multi_buffer = cx.new(|cx| {
24991 let mut multi_buffer = MultiBuffer::new(ReadWrite);
24992 multi_buffer.set_excerpts_for_path(
24993 PathKey::sorted(0),
24994 buffer_1.clone(),
24995 [Point::new(0, 0)
24996 ..Point::new(
24997 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
24998 0,
24999 )],
25000 0,
25001 cx,
25002 );
25003 multi_buffer
25004 });
25005 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
25006 Editor::new(
25007 EditorMode::full(),
25008 multi_buffer,
25009 Some(project.clone()),
25010 window,
25011 cx,
25012 )
25013 });
25014
25015 let selection_range = Point::new(1, 0)..Point::new(2, 0);
25016 multi_buffer_editor.update_in(cx, |editor, window, cx| {
25017 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
25018 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
25019 editor.highlight_text(
25020 HighlightKey::Editor,
25021 vec![highlight_range.clone()],
25022 HighlightStyle::color(Hsla::green()),
25023 cx,
25024 );
25025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25026 s.select_ranges(Some(highlight_range))
25027 });
25028 });
25029
25030 let full_text = format!("\n\n{sample_text}");
25031 assert_eq!(
25032 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
25033 full_text,
25034 );
25035}
25036
25037#[gpui::test]
25038async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
25039 init_test(cx, |_| {});
25040 cx.update(|cx| {
25041 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
25042 "keymaps/default-linux.json",
25043 cx,
25044 )
25045 .unwrap();
25046 cx.bind_keys(default_key_bindings);
25047 });
25048
25049 let (editor, cx) = cx.add_window_view(|window, cx| {
25050 let multi_buffer = MultiBuffer::build_multi(
25051 [
25052 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
25053 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
25054 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
25055 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
25056 ],
25057 cx,
25058 );
25059 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
25060
25061 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
25062 // fold all but the second buffer, so that we test navigating between two
25063 // adjacent folded buffers, as well as folded buffers at the start and
25064 // end the multibuffer
25065 editor.fold_buffer(buffer_ids[0], cx);
25066 editor.fold_buffer(buffer_ids[2], cx);
25067 editor.fold_buffer(buffer_ids[3], cx);
25068
25069 editor
25070 });
25071 cx.simulate_resize(size(px(1000.), px(1000.)));
25072
25073 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
25074 cx.assert_excerpts_with_selections(indoc! {"
25075 [EXCERPT]
25076 ˇ[FOLDED]
25077 [EXCERPT]
25078 a1
25079 b1
25080 [EXCERPT]
25081 [FOLDED]
25082 [EXCERPT]
25083 [FOLDED]
25084 "
25085 });
25086 cx.simulate_keystroke("down");
25087 cx.assert_excerpts_with_selections(indoc! {"
25088 [EXCERPT]
25089 [FOLDED]
25090 [EXCERPT]
25091 ˇa1
25092 b1
25093 [EXCERPT]
25094 [FOLDED]
25095 [EXCERPT]
25096 [FOLDED]
25097 "
25098 });
25099 cx.simulate_keystroke("down");
25100 cx.assert_excerpts_with_selections(indoc! {"
25101 [EXCERPT]
25102 [FOLDED]
25103 [EXCERPT]
25104 a1
25105 ˇb1
25106 [EXCERPT]
25107 [FOLDED]
25108 [EXCERPT]
25109 [FOLDED]
25110 "
25111 });
25112 cx.simulate_keystroke("down");
25113 cx.assert_excerpts_with_selections(indoc! {"
25114 [EXCERPT]
25115 [FOLDED]
25116 [EXCERPT]
25117 a1
25118 b1
25119 ˇ[EXCERPT]
25120 [FOLDED]
25121 [EXCERPT]
25122 [FOLDED]
25123 "
25124 });
25125 cx.simulate_keystroke("down");
25126 cx.assert_excerpts_with_selections(indoc! {"
25127 [EXCERPT]
25128 [FOLDED]
25129 [EXCERPT]
25130 a1
25131 b1
25132 [EXCERPT]
25133 ˇ[FOLDED]
25134 [EXCERPT]
25135 [FOLDED]
25136 "
25137 });
25138 for _ in 0..5 {
25139 cx.simulate_keystroke("down");
25140 cx.assert_excerpts_with_selections(indoc! {"
25141 [EXCERPT]
25142 [FOLDED]
25143 [EXCERPT]
25144 a1
25145 b1
25146 [EXCERPT]
25147 [FOLDED]
25148 [EXCERPT]
25149 ˇ[FOLDED]
25150 "
25151 });
25152 }
25153
25154 cx.simulate_keystroke("up");
25155 cx.assert_excerpts_with_selections(indoc! {"
25156 [EXCERPT]
25157 [FOLDED]
25158 [EXCERPT]
25159 a1
25160 b1
25161 [EXCERPT]
25162 ˇ[FOLDED]
25163 [EXCERPT]
25164 [FOLDED]
25165 "
25166 });
25167 cx.simulate_keystroke("up");
25168 cx.assert_excerpts_with_selections(indoc! {"
25169 [EXCERPT]
25170 [FOLDED]
25171 [EXCERPT]
25172 a1
25173 b1
25174 ˇ[EXCERPT]
25175 [FOLDED]
25176 [EXCERPT]
25177 [FOLDED]
25178 "
25179 });
25180 cx.simulate_keystroke("up");
25181 cx.assert_excerpts_with_selections(indoc! {"
25182 [EXCERPT]
25183 [FOLDED]
25184 [EXCERPT]
25185 a1
25186 ˇb1
25187 [EXCERPT]
25188 [FOLDED]
25189 [EXCERPT]
25190 [FOLDED]
25191 "
25192 });
25193 cx.simulate_keystroke("up");
25194 cx.assert_excerpts_with_selections(indoc! {"
25195 [EXCERPT]
25196 [FOLDED]
25197 [EXCERPT]
25198 ˇa1
25199 b1
25200 [EXCERPT]
25201 [FOLDED]
25202 [EXCERPT]
25203 [FOLDED]
25204 "
25205 });
25206 for _ in 0..5 {
25207 cx.simulate_keystroke("up");
25208 cx.assert_excerpts_with_selections(indoc! {"
25209 [EXCERPT]
25210 ˇ[FOLDED]
25211 [EXCERPT]
25212 a1
25213 b1
25214 [EXCERPT]
25215 [FOLDED]
25216 [EXCERPT]
25217 [FOLDED]
25218 "
25219 });
25220 }
25221}
25222
25223#[gpui::test]
25224async fn test_edit_prediction_text(cx: &mut TestAppContext) {
25225 init_test(cx, |_| {});
25226
25227 // Simple insertion
25228 assert_highlighted_edits(
25229 "Hello, world!",
25230 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
25231 true,
25232 cx,
25233 &|highlighted_edits, cx| {
25234 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
25235 assert_eq!(highlighted_edits.highlights.len(), 1);
25236 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
25237 assert_eq!(
25238 highlighted_edits.highlights[0].1.background_color,
25239 Some(cx.theme().status().created_background)
25240 );
25241 },
25242 )
25243 .await;
25244
25245 // Replacement
25246 assert_highlighted_edits(
25247 "This is a test.",
25248 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
25249 false,
25250 cx,
25251 &|highlighted_edits, cx| {
25252 assert_eq!(highlighted_edits.text, "That is a test.");
25253 assert_eq!(highlighted_edits.highlights.len(), 1);
25254 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
25255 assert_eq!(
25256 highlighted_edits.highlights[0].1.background_color,
25257 Some(cx.theme().status().created_background)
25258 );
25259 },
25260 )
25261 .await;
25262
25263 // Multiple edits
25264 assert_highlighted_edits(
25265 "Hello, world!",
25266 vec![
25267 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
25268 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
25269 ],
25270 false,
25271 cx,
25272 &|highlighted_edits, cx| {
25273 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
25274 assert_eq!(highlighted_edits.highlights.len(), 2);
25275 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
25276 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
25277 assert_eq!(
25278 highlighted_edits.highlights[0].1.background_color,
25279 Some(cx.theme().status().created_background)
25280 );
25281 assert_eq!(
25282 highlighted_edits.highlights[1].1.background_color,
25283 Some(cx.theme().status().created_background)
25284 );
25285 },
25286 )
25287 .await;
25288
25289 // Multiple lines with edits
25290 assert_highlighted_edits(
25291 "First line\nSecond line\nThird line\nFourth line",
25292 vec![
25293 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
25294 (
25295 Point::new(2, 0)..Point::new(2, 10),
25296 "New third line".to_string(),
25297 ),
25298 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
25299 ],
25300 false,
25301 cx,
25302 &|highlighted_edits, cx| {
25303 assert_eq!(
25304 highlighted_edits.text,
25305 "Second modified\nNew third line\nFourth updated line"
25306 );
25307 assert_eq!(highlighted_edits.highlights.len(), 3);
25308 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
25309 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
25310 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
25311 for highlight in &highlighted_edits.highlights {
25312 assert_eq!(
25313 highlight.1.background_color,
25314 Some(cx.theme().status().created_background)
25315 );
25316 }
25317 },
25318 )
25319 .await;
25320}
25321
25322#[gpui::test]
25323async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
25324 init_test(cx, |_| {});
25325
25326 // Deletion
25327 assert_highlighted_edits(
25328 "Hello, world!",
25329 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
25330 true,
25331 cx,
25332 &|highlighted_edits, cx| {
25333 assert_eq!(highlighted_edits.text, "Hello, world!");
25334 assert_eq!(highlighted_edits.highlights.len(), 1);
25335 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
25336 assert_eq!(
25337 highlighted_edits.highlights[0].1.background_color,
25338 Some(cx.theme().status().deleted_background)
25339 );
25340 },
25341 )
25342 .await;
25343
25344 // Insertion
25345 assert_highlighted_edits(
25346 "Hello, world!",
25347 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
25348 true,
25349 cx,
25350 &|highlighted_edits, cx| {
25351 assert_eq!(highlighted_edits.highlights.len(), 1);
25352 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
25353 assert_eq!(
25354 highlighted_edits.highlights[0].1.background_color,
25355 Some(cx.theme().status().created_background)
25356 );
25357 },
25358 )
25359 .await;
25360}
25361
25362async fn assert_highlighted_edits(
25363 text: &str,
25364 edits: Vec<(Range<Point>, String)>,
25365 include_deletions: bool,
25366 cx: &mut TestAppContext,
25367 assertion_fn: &dyn Fn(HighlightedText, &App),
25368) {
25369 let window = cx.add_window(|window, cx| {
25370 let buffer = MultiBuffer::build_simple(text, cx);
25371 Editor::new(EditorMode::full(), buffer, None, window, cx)
25372 });
25373 let cx = &mut VisualTestContext::from_window(*window, cx);
25374
25375 let (buffer, snapshot) = window
25376 .update(cx, |editor, _window, cx| {
25377 (
25378 editor.buffer().clone(),
25379 editor.buffer().read(cx).snapshot(cx),
25380 )
25381 })
25382 .unwrap();
25383
25384 let edits = edits
25385 .into_iter()
25386 .map(|(range, edit)| {
25387 (
25388 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
25389 edit,
25390 )
25391 })
25392 .collect::<Vec<_>>();
25393
25394 let text_anchor_edits = edits
25395 .clone()
25396 .into_iter()
25397 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
25398 .collect::<Vec<_>>();
25399
25400 let edit_preview = window
25401 .update(cx, |_, _window, cx| {
25402 buffer
25403 .read(cx)
25404 .as_singleton()
25405 .unwrap()
25406 .read(cx)
25407 .preview_edits(text_anchor_edits.into(), cx)
25408 })
25409 .unwrap()
25410 .await;
25411
25412 cx.update(|_window, cx| {
25413 let highlighted_edits = edit_prediction_edit_text(
25414 snapshot.as_singleton().unwrap().2,
25415 &edits,
25416 &edit_preview,
25417 include_deletions,
25418 cx,
25419 );
25420 assertion_fn(highlighted_edits, cx)
25421 });
25422}
25423
25424#[track_caller]
25425fn assert_breakpoint(
25426 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
25427 path: &Arc<Path>,
25428 expected: Vec<(u32, Breakpoint)>,
25429) {
25430 if expected.is_empty() {
25431 assert!(!breakpoints.contains_key(path), "{}", path.display());
25432 } else {
25433 let mut breakpoint = breakpoints
25434 .get(path)
25435 .unwrap()
25436 .iter()
25437 .map(|breakpoint| {
25438 (
25439 breakpoint.row,
25440 Breakpoint {
25441 message: breakpoint.message.clone(),
25442 state: breakpoint.state,
25443 condition: breakpoint.condition.clone(),
25444 hit_condition: breakpoint.hit_condition.clone(),
25445 },
25446 )
25447 })
25448 .collect::<Vec<_>>();
25449
25450 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
25451
25452 assert_eq!(expected, breakpoint);
25453 }
25454}
25455
25456fn add_log_breakpoint_at_cursor(
25457 editor: &mut Editor,
25458 log_message: &str,
25459 window: &mut Window,
25460 cx: &mut Context<Editor>,
25461) {
25462 let (anchor, bp) = editor
25463 .breakpoints_at_cursors(window, cx)
25464 .first()
25465 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
25466 .unwrap_or_else(|| {
25467 let snapshot = editor.snapshot(window, cx);
25468 let cursor_position: Point =
25469 editor.selections.newest(&snapshot.display_snapshot).head();
25470
25471 let breakpoint_position = snapshot
25472 .buffer_snapshot()
25473 .anchor_before(Point::new(cursor_position.row, 0));
25474
25475 (breakpoint_position, Breakpoint::new_log(log_message))
25476 });
25477
25478 editor.edit_breakpoint_at_anchor(
25479 anchor,
25480 bp,
25481 BreakpointEditAction::EditLogMessage(log_message.into()),
25482 cx,
25483 );
25484}
25485
25486#[gpui::test]
25487async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
25488 init_test(cx, |_| {});
25489
25490 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
25491 let fs = FakeFs::new(cx.executor());
25492 fs.insert_tree(
25493 path!("/a"),
25494 json!({
25495 "main.rs": sample_text,
25496 }),
25497 )
25498 .await;
25499 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25500 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25501 let cx = &mut VisualTestContext::from_window(*window, cx);
25502
25503 let fs = FakeFs::new(cx.executor());
25504 fs.insert_tree(
25505 path!("/a"),
25506 json!({
25507 "main.rs": sample_text,
25508 }),
25509 )
25510 .await;
25511 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25512 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25513 let workspace = window
25514 .read_with(cx, |mw, _| mw.workspace().clone())
25515 .unwrap();
25516 let cx = &mut VisualTestContext::from_window(*window, cx);
25517 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
25518 workspace.project().update(cx, |project, cx| {
25519 project.worktrees(cx).next().unwrap().read(cx).id()
25520 })
25521 });
25522
25523 let buffer = project
25524 .update(cx, |project, cx| {
25525 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
25526 })
25527 .await
25528 .unwrap();
25529
25530 let (editor, cx) = cx.add_window_view(|window, cx| {
25531 Editor::new(
25532 EditorMode::full(),
25533 MultiBuffer::build_from_buffer(buffer, cx),
25534 Some(project.clone()),
25535 window,
25536 cx,
25537 )
25538 });
25539
25540 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
25541 let abs_path = project.read_with(cx, |project, cx| {
25542 project
25543 .absolute_path(&project_path, cx)
25544 .map(Arc::from)
25545 .unwrap()
25546 });
25547
25548 // assert we can add breakpoint on the first line
25549 editor.update_in(cx, |editor, window, cx| {
25550 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25551 editor.move_to_end(&MoveToEnd, window, cx);
25552 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25553 });
25554
25555 let breakpoints = editor.update(cx, |editor, cx| {
25556 editor
25557 .breakpoint_store()
25558 .as_ref()
25559 .unwrap()
25560 .read(cx)
25561 .all_source_breakpoints(cx)
25562 });
25563
25564 assert_eq!(1, breakpoints.len());
25565 assert_breakpoint(
25566 &breakpoints,
25567 &abs_path,
25568 vec![
25569 (0, Breakpoint::new_standard()),
25570 (3, Breakpoint::new_standard()),
25571 ],
25572 );
25573
25574 editor.update_in(cx, |editor, window, cx| {
25575 editor.move_to_beginning(&MoveToBeginning, window, cx);
25576 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25577 });
25578
25579 let breakpoints = editor.update(cx, |editor, cx| {
25580 editor
25581 .breakpoint_store()
25582 .as_ref()
25583 .unwrap()
25584 .read(cx)
25585 .all_source_breakpoints(cx)
25586 });
25587
25588 assert_eq!(1, breakpoints.len());
25589 assert_breakpoint(
25590 &breakpoints,
25591 &abs_path,
25592 vec![(3, Breakpoint::new_standard())],
25593 );
25594
25595 editor.update_in(cx, |editor, window, cx| {
25596 editor.move_to_end(&MoveToEnd, window, cx);
25597 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25598 });
25599
25600 let breakpoints = editor.update(cx, |editor, cx| {
25601 editor
25602 .breakpoint_store()
25603 .as_ref()
25604 .unwrap()
25605 .read(cx)
25606 .all_source_breakpoints(cx)
25607 });
25608
25609 assert_eq!(0, breakpoints.len());
25610 assert_breakpoint(&breakpoints, &abs_path, vec![]);
25611}
25612
25613#[gpui::test]
25614async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
25615 init_test(cx, |_| {});
25616
25617 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
25618
25619 let fs = FakeFs::new(cx.executor());
25620 fs.insert_tree(
25621 path!("/a"),
25622 json!({
25623 "main.rs": sample_text,
25624 }),
25625 )
25626 .await;
25627 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25628 let (multi_workspace, cx) =
25629 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25630 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
25631
25632 let worktree_id = workspace.update(cx, |workspace, cx| {
25633 workspace.project().update(cx, |project, cx| {
25634 project.worktrees(cx).next().unwrap().read(cx).id()
25635 })
25636 });
25637
25638 let buffer = project
25639 .update(cx, |project, cx| {
25640 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
25641 })
25642 .await
25643 .unwrap();
25644
25645 let (editor, cx) = cx.add_window_view(|window, cx| {
25646 Editor::new(
25647 EditorMode::full(),
25648 MultiBuffer::build_from_buffer(buffer, cx),
25649 Some(project.clone()),
25650 window,
25651 cx,
25652 )
25653 });
25654
25655 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
25656 let abs_path = project.read_with(cx, |project, cx| {
25657 project
25658 .absolute_path(&project_path, cx)
25659 .map(Arc::from)
25660 .unwrap()
25661 });
25662
25663 editor.update_in(cx, |editor, window, cx| {
25664 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
25665 });
25666
25667 let breakpoints = editor.update(cx, |editor, cx| {
25668 editor
25669 .breakpoint_store()
25670 .as_ref()
25671 .unwrap()
25672 .read(cx)
25673 .all_source_breakpoints(cx)
25674 });
25675
25676 assert_breakpoint(
25677 &breakpoints,
25678 &abs_path,
25679 vec![(0, Breakpoint::new_log("hello world"))],
25680 );
25681
25682 // Removing a log message from a log breakpoint should remove it
25683 editor.update_in(cx, |editor, window, cx| {
25684 add_log_breakpoint_at_cursor(editor, "", window, cx);
25685 });
25686
25687 let breakpoints = editor.update(cx, |editor, cx| {
25688 editor
25689 .breakpoint_store()
25690 .as_ref()
25691 .unwrap()
25692 .read(cx)
25693 .all_source_breakpoints(cx)
25694 });
25695
25696 assert_breakpoint(&breakpoints, &abs_path, vec![]);
25697
25698 editor.update_in(cx, |editor, window, cx| {
25699 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25700 editor.move_to_end(&MoveToEnd, window, cx);
25701 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25702 // Not adding a log message to a standard breakpoint shouldn't remove it
25703 add_log_breakpoint_at_cursor(editor, "", window, cx);
25704 });
25705
25706 let breakpoints = editor.update(cx, |editor, cx| {
25707 editor
25708 .breakpoint_store()
25709 .as_ref()
25710 .unwrap()
25711 .read(cx)
25712 .all_source_breakpoints(cx)
25713 });
25714
25715 assert_breakpoint(
25716 &breakpoints,
25717 &abs_path,
25718 vec![
25719 (0, Breakpoint::new_standard()),
25720 (3, Breakpoint::new_standard()),
25721 ],
25722 );
25723
25724 editor.update_in(cx, |editor, window, cx| {
25725 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
25726 });
25727
25728 let breakpoints = editor.update(cx, |editor, cx| {
25729 editor
25730 .breakpoint_store()
25731 .as_ref()
25732 .unwrap()
25733 .read(cx)
25734 .all_source_breakpoints(cx)
25735 });
25736
25737 assert_breakpoint(
25738 &breakpoints,
25739 &abs_path,
25740 vec![
25741 (0, Breakpoint::new_standard()),
25742 (3, Breakpoint::new_log("hello world")),
25743 ],
25744 );
25745
25746 editor.update_in(cx, |editor, window, cx| {
25747 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
25748 });
25749
25750 let breakpoints = editor.update(cx, |editor, cx| {
25751 editor
25752 .breakpoint_store()
25753 .as_ref()
25754 .unwrap()
25755 .read(cx)
25756 .all_source_breakpoints(cx)
25757 });
25758
25759 assert_breakpoint(
25760 &breakpoints,
25761 &abs_path,
25762 vec![
25763 (0, Breakpoint::new_standard()),
25764 (3, Breakpoint::new_log("hello Earth!!")),
25765 ],
25766 );
25767}
25768
25769/// This also tests that Editor::breakpoint_at_cursor_head is working properly
25770/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
25771/// or when breakpoints were placed out of order. This tests for a regression too
25772#[gpui::test]
25773async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
25774 init_test(cx, |_| {});
25775
25776 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
25777 let fs = FakeFs::new(cx.executor());
25778 fs.insert_tree(
25779 path!("/a"),
25780 json!({
25781 "main.rs": sample_text,
25782 }),
25783 )
25784 .await;
25785 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25786 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25787 let cx = &mut VisualTestContext::from_window(*window, cx);
25788
25789 let fs = FakeFs::new(cx.executor());
25790 fs.insert_tree(
25791 path!("/a"),
25792 json!({
25793 "main.rs": sample_text,
25794 }),
25795 )
25796 .await;
25797 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25798 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25799 let workspace = window
25800 .read_with(cx, |mw, _| mw.workspace().clone())
25801 .unwrap();
25802 let cx = &mut VisualTestContext::from_window(*window, cx);
25803 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
25804 workspace.project().update(cx, |project, cx| {
25805 project.worktrees(cx).next().unwrap().read(cx).id()
25806 })
25807 });
25808
25809 let buffer = project
25810 .update(cx, |project, cx| {
25811 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
25812 })
25813 .await
25814 .unwrap();
25815
25816 let (editor, cx) = cx.add_window_view(|window, cx| {
25817 Editor::new(
25818 EditorMode::full(),
25819 MultiBuffer::build_from_buffer(buffer, cx),
25820 Some(project.clone()),
25821 window,
25822 cx,
25823 )
25824 });
25825
25826 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
25827 let abs_path = project.read_with(cx, |project, cx| {
25828 project
25829 .absolute_path(&project_path, cx)
25830 .map(Arc::from)
25831 .unwrap()
25832 });
25833
25834 // assert we can add breakpoint on the first line
25835 editor.update_in(cx, |editor, window, cx| {
25836 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25837 editor.move_to_end(&MoveToEnd, window, cx);
25838 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25839 editor.move_up(&MoveUp, window, cx);
25840 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25841 });
25842
25843 let breakpoints = editor.update(cx, |editor, cx| {
25844 editor
25845 .breakpoint_store()
25846 .as_ref()
25847 .unwrap()
25848 .read(cx)
25849 .all_source_breakpoints(cx)
25850 });
25851
25852 assert_eq!(1, breakpoints.len());
25853 assert_breakpoint(
25854 &breakpoints,
25855 &abs_path,
25856 vec![
25857 (0, Breakpoint::new_standard()),
25858 (2, Breakpoint::new_standard()),
25859 (3, Breakpoint::new_standard()),
25860 ],
25861 );
25862
25863 editor.update_in(cx, |editor, window, cx| {
25864 editor.move_to_beginning(&MoveToBeginning, window, cx);
25865 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
25866 editor.move_to_end(&MoveToEnd, window, cx);
25867 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
25868 // Disabling a breakpoint that doesn't exist should do nothing
25869 editor.move_up(&MoveUp, window, cx);
25870 editor.move_up(&MoveUp, window, cx);
25871 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
25872 });
25873
25874 let breakpoints = editor.update(cx, |editor, cx| {
25875 editor
25876 .breakpoint_store()
25877 .as_ref()
25878 .unwrap()
25879 .read(cx)
25880 .all_source_breakpoints(cx)
25881 });
25882
25883 let disable_breakpoint = {
25884 let mut bp = Breakpoint::new_standard();
25885 bp.state = BreakpointState::Disabled;
25886 bp
25887 };
25888
25889 assert_eq!(1, breakpoints.len());
25890 assert_breakpoint(
25891 &breakpoints,
25892 &abs_path,
25893 vec![
25894 (0, disable_breakpoint.clone()),
25895 (2, Breakpoint::new_standard()),
25896 (3, disable_breakpoint.clone()),
25897 ],
25898 );
25899
25900 editor.update_in(cx, |editor, window, cx| {
25901 editor.move_to_beginning(&MoveToBeginning, window, cx);
25902 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
25903 editor.move_to_end(&MoveToEnd, window, cx);
25904 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
25905 editor.move_up(&MoveUp, window, cx);
25906 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
25907 });
25908
25909 let breakpoints = editor.update(cx, |editor, cx| {
25910 editor
25911 .breakpoint_store()
25912 .as_ref()
25913 .unwrap()
25914 .read(cx)
25915 .all_source_breakpoints(cx)
25916 });
25917
25918 assert_eq!(1, breakpoints.len());
25919 assert_breakpoint(
25920 &breakpoints,
25921 &abs_path,
25922 vec![
25923 (0, Breakpoint::new_standard()),
25924 (2, disable_breakpoint),
25925 (3, Breakpoint::new_standard()),
25926 ],
25927 );
25928}
25929
25930#[gpui::test]
25931async fn test_breakpoint_phantom_indicator_collision_on_toggle(cx: &mut TestAppContext) {
25932 init_test(cx, |_| {});
25933
25934 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
25935 let fs = FakeFs::new(cx.executor());
25936 fs.insert_tree(
25937 path!("/a"),
25938 json!({
25939 "main.rs": sample_text,
25940 }),
25941 )
25942 .await;
25943 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25944 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25945 let workspace = window
25946 .read_with(cx, |mw, _| mw.workspace().clone())
25947 .unwrap();
25948 let cx = &mut VisualTestContext::from_window(*window, cx);
25949 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
25950 workspace.project().update(cx, |project, cx| {
25951 project.worktrees(cx).next().unwrap().read(cx).id()
25952 })
25953 });
25954
25955 let buffer = project
25956 .update(cx, |project, cx| {
25957 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
25958 })
25959 .await
25960 .unwrap();
25961
25962 let (editor, cx) = cx.add_window_view(|window, cx| {
25963 Editor::new(
25964 EditorMode::full(),
25965 MultiBuffer::build_from_buffer(buffer, cx),
25966 Some(project.clone()),
25967 window,
25968 cx,
25969 )
25970 });
25971
25972 // Simulate hovering over row 0 with no existing breakpoint.
25973 editor.update(cx, |editor, _cx| {
25974 editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
25975 display_row: DisplayRow(0),
25976 is_active: true,
25977 collides_with_existing_breakpoint: false,
25978 });
25979 });
25980
25981 // Toggle breakpoint on the same row (row 0) — collision should flip to true.
25982 editor.update_in(cx, |editor, window, cx| {
25983 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25984 });
25985 editor.update(cx, |editor, _cx| {
25986 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
25987 assert!(
25988 indicator.collides_with_existing_breakpoint,
25989 "Adding a breakpoint on the hovered row should set collision to true"
25990 );
25991 });
25992
25993 // Toggle again on the same row — breakpoint is removed, collision should flip back to false.
25994 editor.update_in(cx, |editor, window, cx| {
25995 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25996 });
25997 editor.update(cx, |editor, _cx| {
25998 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
25999 assert!(
26000 !indicator.collides_with_existing_breakpoint,
26001 "Removing a breakpoint on the hovered row should set collision to false"
26002 );
26003 });
26004
26005 // Now move cursor to row 2 while phantom indicator stays on row 0.
26006 editor.update_in(cx, |editor, window, cx| {
26007 editor.move_down(&MoveDown, window, cx);
26008 editor.move_down(&MoveDown, window, cx);
26009 });
26010
26011 // Ensure phantom indicator is still on row 0, not colliding.
26012 editor.update(cx, |editor, _cx| {
26013 editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
26014 display_row: DisplayRow(0),
26015 is_active: true,
26016 collides_with_existing_breakpoint: false,
26017 });
26018 });
26019
26020 // Toggle breakpoint on row 2 (cursor row) — phantom on row 0 should NOT be affected.
26021 editor.update_in(cx, |editor, window, cx| {
26022 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
26023 });
26024 editor.update(cx, |editor, _cx| {
26025 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
26026 assert!(
26027 !indicator.collides_with_existing_breakpoint,
26028 "Toggling a breakpoint on a different row should not affect the phantom indicator"
26029 );
26030 });
26031}
26032
26033#[gpui::test]
26034async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
26035 init_test(cx, |_| {});
26036 let capabilities = lsp::ServerCapabilities {
26037 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
26038 prepare_provider: Some(true),
26039 work_done_progress_options: Default::default(),
26040 })),
26041 ..Default::default()
26042 };
26043 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
26044
26045 cx.set_state(indoc! {"
26046 struct Fˇoo {}
26047 "});
26048
26049 cx.update_editor(|editor, _, cx| {
26050 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
26051 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
26052 editor.highlight_background(
26053 HighlightKey::DocumentHighlightRead,
26054 &[highlight_range],
26055 |_, theme| theme.colors().editor_document_highlight_read_background,
26056 cx,
26057 );
26058 });
26059
26060 let mut prepare_rename_handler = cx
26061 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
26062 move |_, _, _| async move {
26063 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
26064 start: lsp::Position {
26065 line: 0,
26066 character: 7,
26067 },
26068 end: lsp::Position {
26069 line: 0,
26070 character: 10,
26071 },
26072 })))
26073 },
26074 );
26075 let prepare_rename_task = cx
26076 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
26077 .expect("Prepare rename was not started");
26078 prepare_rename_handler.next().await.unwrap();
26079 prepare_rename_task.await.expect("Prepare rename failed");
26080
26081 let mut rename_handler =
26082 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
26083 let edit = lsp::TextEdit {
26084 range: lsp::Range {
26085 start: lsp::Position {
26086 line: 0,
26087 character: 7,
26088 },
26089 end: lsp::Position {
26090 line: 0,
26091 character: 10,
26092 },
26093 },
26094 new_text: "FooRenamed".to_string(),
26095 };
26096 Ok(Some(lsp::WorkspaceEdit::new(
26097 // Specify the same edit twice
26098 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
26099 )))
26100 });
26101 let rename_task = cx
26102 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
26103 .expect("Confirm rename was not started");
26104 rename_handler.next().await.unwrap();
26105 rename_task.await.expect("Confirm rename failed");
26106 cx.run_until_parked();
26107
26108 // Despite two edits, only one is actually applied as those are identical
26109 cx.assert_editor_state(indoc! {"
26110 struct FooRenamedˇ {}
26111 "});
26112}
26113
26114#[gpui::test]
26115async fn test_rename_without_prepare(cx: &mut TestAppContext) {
26116 init_test(cx, |_| {});
26117 // These capabilities indicate that the server does not support prepare rename.
26118 let capabilities = lsp::ServerCapabilities {
26119 rename_provider: Some(lsp::OneOf::Left(true)),
26120 ..Default::default()
26121 };
26122 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
26123
26124 cx.set_state(indoc! {"
26125 struct Fˇoo {}
26126 "});
26127
26128 cx.update_editor(|editor, _window, cx| {
26129 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
26130 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
26131 editor.highlight_background(
26132 HighlightKey::DocumentHighlightRead,
26133 &[highlight_range],
26134 |_, theme| theme.colors().editor_document_highlight_read_background,
26135 cx,
26136 );
26137 });
26138
26139 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
26140 .expect("Prepare rename was not started")
26141 .await
26142 .expect("Prepare rename failed");
26143
26144 let mut rename_handler =
26145 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
26146 let edit = lsp::TextEdit {
26147 range: lsp::Range {
26148 start: lsp::Position {
26149 line: 0,
26150 character: 7,
26151 },
26152 end: lsp::Position {
26153 line: 0,
26154 character: 10,
26155 },
26156 },
26157 new_text: "FooRenamed".to_string(),
26158 };
26159 Ok(Some(lsp::WorkspaceEdit::new(
26160 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
26161 )))
26162 });
26163 let rename_task = cx
26164 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
26165 .expect("Confirm rename was not started");
26166 rename_handler.next().await.unwrap();
26167 rename_task.await.expect("Confirm rename failed");
26168 cx.run_until_parked();
26169
26170 // Correct range is renamed, as `surrounding_word` is used to find it.
26171 cx.assert_editor_state(indoc! {"
26172 struct FooRenamedˇ {}
26173 "});
26174}
26175
26176#[gpui::test]
26177async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
26178 init_test(cx, |_| {});
26179 let mut cx = EditorTestContext::new(cx).await;
26180
26181 let language = Arc::new(
26182 Language::new(
26183 LanguageConfig::default(),
26184 Some(tree_sitter_html::LANGUAGE.into()),
26185 )
26186 .with_brackets_query(
26187 r#"
26188 ("<" @open "/>" @close)
26189 ("</" @open ">" @close)
26190 ("<" @open ">" @close)
26191 ("\"" @open "\"" @close)
26192 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
26193 "#,
26194 )
26195 .unwrap(),
26196 );
26197 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26198
26199 cx.set_state(indoc! {"
26200 <span>ˇ</span>
26201 "});
26202 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
26203 cx.assert_editor_state(indoc! {"
26204 <span>
26205 ˇ
26206 </span>
26207 "});
26208
26209 cx.set_state(indoc! {"
26210 <span><span></span>ˇ</span>
26211 "});
26212 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
26213 cx.assert_editor_state(indoc! {"
26214 <span><span></span>
26215 ˇ</span>
26216 "});
26217
26218 cx.set_state(indoc! {"
26219 <span>ˇ
26220 </span>
26221 "});
26222 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
26223 cx.assert_editor_state(indoc! {"
26224 <span>
26225 ˇ
26226 </span>
26227 "});
26228}
26229
26230#[gpui::test(iterations = 10)]
26231async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
26232 init_test(cx, |_| {});
26233
26234 let fs = FakeFs::new(cx.executor());
26235 fs.insert_tree(
26236 path!("/dir"),
26237 json!({
26238 "a.ts": "a",
26239 }),
26240 )
26241 .await;
26242
26243 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
26244 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26245 let workspace = window
26246 .read_with(cx, |mw, _| mw.workspace().clone())
26247 .unwrap();
26248 let cx = &mut VisualTestContext::from_window(*window, cx);
26249
26250 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26251 language_registry.add(Arc::new(Language::new(
26252 LanguageConfig {
26253 name: "TypeScript".into(),
26254 matcher: LanguageMatcher {
26255 path_suffixes: vec!["ts".to_string()],
26256 ..Default::default()
26257 },
26258 ..Default::default()
26259 },
26260 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
26261 )));
26262 let mut fake_language_servers = language_registry.register_fake_lsp(
26263 "TypeScript",
26264 FakeLspAdapter {
26265 capabilities: lsp::ServerCapabilities {
26266 code_lens_provider: Some(lsp::CodeLensOptions {
26267 resolve_provider: Some(true),
26268 }),
26269 execute_command_provider: Some(lsp::ExecuteCommandOptions {
26270 commands: vec!["_the/command".to_string()],
26271 ..lsp::ExecuteCommandOptions::default()
26272 }),
26273 ..lsp::ServerCapabilities::default()
26274 },
26275 ..FakeLspAdapter::default()
26276 },
26277 );
26278
26279 let editor = workspace
26280 .update_in(cx, |workspace, window, cx| {
26281 workspace.open_abs_path(
26282 PathBuf::from(path!("/dir/a.ts")),
26283 OpenOptions::default(),
26284 window,
26285 cx,
26286 )
26287 })
26288 .await
26289 .unwrap()
26290 .downcast::<Editor>()
26291 .unwrap();
26292 cx.executor().run_until_parked();
26293
26294 let fake_server = fake_language_servers.next().await.unwrap();
26295
26296 let buffer = editor.update(cx, |editor, cx| {
26297 editor
26298 .buffer()
26299 .read(cx)
26300 .as_singleton()
26301 .expect("have opened a single file by path")
26302 });
26303
26304 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
26305 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
26306 drop(buffer_snapshot);
26307 let actions = cx
26308 .update_window(*window, |_, window, cx| {
26309 project.code_actions(&buffer, anchor..anchor, window, cx)
26310 })
26311 .unwrap();
26312
26313 fake_server
26314 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
26315 Ok(Some(vec![
26316 lsp::CodeLens {
26317 range: lsp::Range::default(),
26318 command: Some(lsp::Command {
26319 title: "Code lens command".to_owned(),
26320 command: "_the/command".to_owned(),
26321 arguments: None,
26322 }),
26323 data: None,
26324 },
26325 lsp::CodeLens {
26326 range: lsp::Range::default(),
26327 command: Some(lsp::Command {
26328 title: "Command not in capabilities".to_owned(),
26329 command: "not in capabilities".to_owned(),
26330 arguments: None,
26331 }),
26332 data: None,
26333 },
26334 lsp::CodeLens {
26335 range: lsp::Range {
26336 start: lsp::Position {
26337 line: 1,
26338 character: 1,
26339 },
26340 end: lsp::Position {
26341 line: 1,
26342 character: 1,
26343 },
26344 },
26345 command: Some(lsp::Command {
26346 title: "Command not in range".to_owned(),
26347 command: "_the/command".to_owned(),
26348 arguments: None,
26349 }),
26350 data: None,
26351 },
26352 ]))
26353 })
26354 .next()
26355 .await;
26356
26357 let actions = actions.await.unwrap();
26358 assert_eq!(
26359 actions.len(),
26360 1,
26361 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
26362 );
26363 let action = actions[0].clone();
26364 let apply = project.update(cx, |project, cx| {
26365 project.apply_code_action(buffer.clone(), action, true, cx)
26366 });
26367
26368 // Resolving the code action does not populate its edits. In absence of
26369 // edits, we must execute the given command.
26370 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
26371 |mut lens, _| async move {
26372 let lens_command = lens.command.as_mut().expect("should have a command");
26373 assert_eq!(lens_command.title, "Code lens command");
26374 lens_command.arguments = Some(vec![json!("the-argument")]);
26375 Ok(lens)
26376 },
26377 );
26378
26379 // While executing the command, the language server sends the editor
26380 // a `workspaceEdit` request.
26381 fake_server
26382 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
26383 let fake = fake_server.clone();
26384 move |params, _| {
26385 assert_eq!(params.command, "_the/command");
26386 let fake = fake.clone();
26387 async move {
26388 fake.server
26389 .request::<lsp::request::ApplyWorkspaceEdit>(
26390 lsp::ApplyWorkspaceEditParams {
26391 label: None,
26392 edit: lsp::WorkspaceEdit {
26393 changes: Some(
26394 [(
26395 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
26396 vec![lsp::TextEdit {
26397 range: lsp::Range::new(
26398 lsp::Position::new(0, 0),
26399 lsp::Position::new(0, 0),
26400 ),
26401 new_text: "X".into(),
26402 }],
26403 )]
26404 .into_iter()
26405 .collect(),
26406 ),
26407 ..lsp::WorkspaceEdit::default()
26408 },
26409 },
26410 DEFAULT_LSP_REQUEST_TIMEOUT,
26411 )
26412 .await
26413 .into_response()
26414 .unwrap();
26415 Ok(Some(json!(null)))
26416 }
26417 }
26418 })
26419 .next()
26420 .await;
26421
26422 // Applying the code lens command returns a project transaction containing the edits
26423 // sent by the language server in its `workspaceEdit` request.
26424 let transaction = apply.await.unwrap();
26425 assert!(transaction.0.contains_key(&buffer));
26426 buffer.update(cx, |buffer, cx| {
26427 assert_eq!(buffer.text(), "Xa");
26428 buffer.undo(cx);
26429 assert_eq!(buffer.text(), "a");
26430 });
26431
26432 let actions_after_edits = cx
26433 .update(|window, cx| project.code_actions(&buffer, anchor..anchor, window, cx))
26434 .unwrap()
26435 .await;
26436 assert_eq!(
26437 actions, actions_after_edits,
26438 "For the same selection, same code lens actions should be returned"
26439 );
26440
26441 let _responses =
26442 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
26443 panic!("No more code lens requests are expected");
26444 });
26445 editor.update_in(cx, |editor, window, cx| {
26446 editor.select_all(&SelectAll, window, cx);
26447 });
26448 cx.executor().run_until_parked();
26449 let new_actions = cx
26450 .update(|window, cx| project.code_actions(&buffer, anchor..anchor, window, cx))
26451 .unwrap()
26452 .await;
26453 assert_eq!(
26454 actions, new_actions,
26455 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
26456 );
26457}
26458
26459#[gpui::test]
26460async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
26461 init_test(cx, |_| {});
26462
26463 let fs = FakeFs::new(cx.executor());
26464 let main_text = r#"fn main() {
26465println!("1");
26466println!("2");
26467println!("3");
26468println!("4");
26469println!("5");
26470}"#;
26471 let lib_text = "mod foo {}";
26472 fs.insert_tree(
26473 path!("/a"),
26474 json!({
26475 "lib.rs": lib_text,
26476 "main.rs": main_text,
26477 }),
26478 )
26479 .await;
26480
26481 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26482 let (multi_workspace, cx) =
26483 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26484 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
26485 let worktree_id = workspace.update(cx, |workspace, cx| {
26486 workspace.project().update(cx, |project, cx| {
26487 project.worktrees(cx).next().unwrap().read(cx).id()
26488 })
26489 });
26490
26491 let expected_ranges = vec![
26492 Point::new(0, 0)..Point::new(0, 0),
26493 Point::new(1, 0)..Point::new(1, 1),
26494 Point::new(2, 0)..Point::new(2, 2),
26495 Point::new(3, 0)..Point::new(3, 3),
26496 ];
26497
26498 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
26499 let editor_1 = workspace
26500 .update_in(cx, |workspace, window, cx| {
26501 workspace.open_path(
26502 (worktree_id, rel_path("main.rs")),
26503 Some(pane_1.downgrade()),
26504 true,
26505 window,
26506 cx,
26507 )
26508 })
26509 .unwrap()
26510 .await
26511 .downcast::<Editor>()
26512 .unwrap();
26513 pane_1.update(cx, |pane, cx| {
26514 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26515 open_editor.update(cx, |editor, cx| {
26516 assert_eq!(
26517 editor.display_text(cx),
26518 main_text,
26519 "Original main.rs text on initial open",
26520 );
26521 assert_eq!(
26522 editor
26523 .selections
26524 .all::<Point>(&editor.display_snapshot(cx))
26525 .into_iter()
26526 .map(|s| s.range())
26527 .collect::<Vec<_>>(),
26528 vec![Point::zero()..Point::zero()],
26529 "Default selections on initial open",
26530 );
26531 })
26532 });
26533 editor_1.update_in(cx, |editor, window, cx| {
26534 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26535 s.select_ranges(expected_ranges.clone());
26536 });
26537 });
26538
26539 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
26540 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
26541 });
26542 let editor_2 = workspace
26543 .update_in(cx, |workspace, window, cx| {
26544 workspace.open_path(
26545 (worktree_id, rel_path("main.rs")),
26546 Some(pane_2.downgrade()),
26547 true,
26548 window,
26549 cx,
26550 )
26551 })
26552 .unwrap()
26553 .await
26554 .downcast::<Editor>()
26555 .unwrap();
26556 pane_2.update(cx, |pane, cx| {
26557 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26558 open_editor.update(cx, |editor, cx| {
26559 assert_eq!(
26560 editor.display_text(cx),
26561 main_text,
26562 "Original main.rs text on initial open in another panel",
26563 );
26564 assert_eq!(
26565 editor
26566 .selections
26567 .all::<Point>(&editor.display_snapshot(cx))
26568 .into_iter()
26569 .map(|s| s.range())
26570 .collect::<Vec<_>>(),
26571 vec![Point::zero()..Point::zero()],
26572 "Default selections on initial open in another panel",
26573 );
26574 })
26575 });
26576
26577 editor_2.update_in(cx, |editor, window, cx| {
26578 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
26579 });
26580
26581 let _other_editor_1 = workspace
26582 .update_in(cx, |workspace, window, cx| {
26583 workspace.open_path(
26584 (worktree_id, rel_path("lib.rs")),
26585 Some(pane_1.downgrade()),
26586 true,
26587 window,
26588 cx,
26589 )
26590 })
26591 .unwrap()
26592 .await
26593 .downcast::<Editor>()
26594 .unwrap();
26595 pane_1
26596 .update_in(cx, |pane, window, cx| {
26597 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
26598 })
26599 .await
26600 .unwrap();
26601 drop(editor_1);
26602 pane_1.update(cx, |pane, cx| {
26603 pane.active_item()
26604 .unwrap()
26605 .downcast::<Editor>()
26606 .unwrap()
26607 .update(cx, |editor, cx| {
26608 assert_eq!(
26609 editor.display_text(cx),
26610 lib_text,
26611 "Other file should be open and active",
26612 );
26613 });
26614 assert_eq!(pane.items().count(), 1, "No other editors should be open");
26615 });
26616
26617 let _other_editor_2 = workspace
26618 .update_in(cx, |workspace, window, cx| {
26619 workspace.open_path(
26620 (worktree_id, rel_path("lib.rs")),
26621 Some(pane_2.downgrade()),
26622 true,
26623 window,
26624 cx,
26625 )
26626 })
26627 .unwrap()
26628 .await
26629 .downcast::<Editor>()
26630 .unwrap();
26631 pane_2
26632 .update_in(cx, |pane, window, cx| {
26633 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
26634 })
26635 .await
26636 .unwrap();
26637 drop(editor_2);
26638 pane_2.update(cx, |pane, cx| {
26639 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26640 open_editor.update(cx, |editor, cx| {
26641 assert_eq!(
26642 editor.display_text(cx),
26643 lib_text,
26644 "Other file should be open and active in another panel too",
26645 );
26646 });
26647 assert_eq!(
26648 pane.items().count(),
26649 1,
26650 "No other editors should be open in another pane",
26651 );
26652 });
26653
26654 let _editor_1_reopened = workspace
26655 .update_in(cx, |workspace, window, cx| {
26656 workspace.open_path(
26657 (worktree_id, rel_path("main.rs")),
26658 Some(pane_1.downgrade()),
26659 true,
26660 window,
26661 cx,
26662 )
26663 })
26664 .unwrap()
26665 .await
26666 .downcast::<Editor>()
26667 .unwrap();
26668 let _editor_2_reopened = workspace
26669 .update_in(cx, |workspace, window, cx| {
26670 workspace.open_path(
26671 (worktree_id, rel_path("main.rs")),
26672 Some(pane_2.downgrade()),
26673 true,
26674 window,
26675 cx,
26676 )
26677 })
26678 .unwrap()
26679 .await
26680 .downcast::<Editor>()
26681 .unwrap();
26682 pane_1.update(cx, |pane, cx| {
26683 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26684 open_editor.update(cx, |editor, cx| {
26685 assert_eq!(
26686 editor.display_text(cx),
26687 main_text,
26688 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
26689 );
26690 assert_eq!(
26691 editor
26692 .selections
26693 .all::<Point>(&editor.display_snapshot(cx))
26694 .into_iter()
26695 .map(|s| s.range())
26696 .collect::<Vec<_>>(),
26697 expected_ranges,
26698 "Previous editor in the 1st panel had selections and should get them restored on reopen",
26699 );
26700 })
26701 });
26702 pane_2.update(cx, |pane, cx| {
26703 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26704 open_editor.update(cx, |editor, cx| {
26705 assert_eq!(
26706 editor.display_text(cx),
26707 r#"fn main() {
26708⋯rintln!("1");
26709⋯intln!("2");
26710⋯ntln!("3");
26711println!("4");
26712println!("5");
26713}"#,
26714 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
26715 );
26716 assert_eq!(
26717 editor
26718 .selections
26719 .all::<Point>(&editor.display_snapshot(cx))
26720 .into_iter()
26721 .map(|s| s.range())
26722 .collect::<Vec<_>>(),
26723 vec![Point::zero()..Point::zero()],
26724 "Previous editor in the 2nd pane had no selections changed hence should restore none",
26725 );
26726 })
26727 });
26728}
26729
26730#[gpui::test]
26731async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
26732 init_test(cx, |_| {});
26733
26734 let fs = FakeFs::new(cx.executor());
26735 let main_text = r#"fn main() {
26736println!("1");
26737println!("2");
26738println!("3");
26739println!("4");
26740println!("5");
26741}"#;
26742 let lib_text = "mod foo {}";
26743 fs.insert_tree(
26744 path!("/a"),
26745 json!({
26746 "lib.rs": lib_text,
26747 "main.rs": main_text,
26748 }),
26749 )
26750 .await;
26751
26752 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26753 let (multi_workspace, cx) =
26754 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26755 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
26756 let worktree_id = workspace.update(cx, |workspace, cx| {
26757 workspace.project().update(cx, |project, cx| {
26758 project.worktrees(cx).next().unwrap().read(cx).id()
26759 })
26760 });
26761
26762 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
26763 let editor = workspace
26764 .update_in(cx, |workspace, window, cx| {
26765 workspace.open_path(
26766 (worktree_id, rel_path("main.rs")),
26767 Some(pane.downgrade()),
26768 true,
26769 window,
26770 cx,
26771 )
26772 })
26773 .unwrap()
26774 .await
26775 .downcast::<Editor>()
26776 .unwrap();
26777 pane.update(cx, |pane, cx| {
26778 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26779 open_editor.update(cx, |editor, cx| {
26780 assert_eq!(
26781 editor.display_text(cx),
26782 main_text,
26783 "Original main.rs text on initial open",
26784 );
26785 })
26786 });
26787 editor.update_in(cx, |editor, window, cx| {
26788 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
26789 });
26790
26791 cx.update_global(|store: &mut SettingsStore, cx| {
26792 store.update_user_settings(cx, |s| {
26793 s.workspace.restore_on_file_reopen = Some(false);
26794 });
26795 });
26796 editor.update_in(cx, |editor, window, cx| {
26797 editor.fold_ranges(
26798 vec![
26799 Point::new(1, 0)..Point::new(1, 1),
26800 Point::new(2, 0)..Point::new(2, 2),
26801 Point::new(3, 0)..Point::new(3, 3),
26802 ],
26803 false,
26804 window,
26805 cx,
26806 );
26807 });
26808 pane.update_in(cx, |pane, window, cx| {
26809 pane.close_all_items(&CloseAllItems::default(), window, cx)
26810 })
26811 .await
26812 .unwrap();
26813 pane.update(cx, |pane, _| {
26814 assert!(pane.active_item().is_none());
26815 });
26816 cx.update_global(|store: &mut SettingsStore, cx| {
26817 store.update_user_settings(cx, |s| {
26818 s.workspace.restore_on_file_reopen = Some(true);
26819 });
26820 });
26821
26822 let _editor_reopened = workspace
26823 .update_in(cx, |workspace, window, cx| {
26824 workspace.open_path(
26825 (worktree_id, rel_path("main.rs")),
26826 Some(pane.downgrade()),
26827 true,
26828 window,
26829 cx,
26830 )
26831 })
26832 .unwrap()
26833 .await
26834 .downcast::<Editor>()
26835 .unwrap();
26836 pane.update(cx, |pane, cx| {
26837 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26838 open_editor.update(cx, |editor, cx| {
26839 assert_eq!(
26840 editor.display_text(cx),
26841 main_text,
26842 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
26843 );
26844 })
26845 });
26846}
26847
26848#[gpui::test]
26849async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
26850 struct EmptyModalView {
26851 focus_handle: gpui::FocusHandle,
26852 }
26853 impl EventEmitter<DismissEvent> for EmptyModalView {}
26854 impl Render for EmptyModalView {
26855 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
26856 div()
26857 }
26858 }
26859 impl Focusable for EmptyModalView {
26860 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
26861 self.focus_handle.clone()
26862 }
26863 }
26864 impl workspace::ModalView for EmptyModalView {}
26865 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
26866 EmptyModalView {
26867 focus_handle: cx.focus_handle(),
26868 }
26869 }
26870
26871 init_test(cx, |_| {});
26872
26873 let fs = FakeFs::new(cx.executor());
26874 let project = Project::test(fs, [], cx).await;
26875 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26876 let workspace = window
26877 .read_with(cx, |mw, _| mw.workspace().clone())
26878 .unwrap();
26879 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
26880 let cx = &mut VisualTestContext::from_window(*window, cx);
26881 let editor = cx.new_window_entity(|window, cx| {
26882 Editor::new(
26883 EditorMode::full(),
26884 buffer,
26885 Some(project.clone()),
26886 window,
26887 cx,
26888 )
26889 });
26890 workspace.update_in(cx, |workspace, window, cx| {
26891 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
26892 });
26893
26894 editor.update_in(cx, |editor, window, cx| {
26895 editor.open_context_menu(&OpenContextMenu, window, cx);
26896 assert!(editor.mouse_context_menu.is_some());
26897 });
26898 workspace.update_in(cx, |workspace, window, cx| {
26899 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
26900 });
26901
26902 cx.read(|cx| {
26903 assert!(editor.read(cx).mouse_context_menu.is_none());
26904 });
26905}
26906
26907fn set_linked_edit_ranges(
26908 opening: (Point, Point),
26909 closing: (Point, Point),
26910 editor: &mut Editor,
26911 cx: &mut Context<Editor>,
26912) {
26913 let Some((buffer, _)) = editor
26914 .buffer
26915 .read(cx)
26916 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
26917 else {
26918 panic!("Failed to get buffer for selection position");
26919 };
26920 let buffer = buffer.read(cx);
26921 let buffer_id = buffer.remote_id();
26922 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
26923 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
26924 let mut linked_ranges = HashMap::default();
26925 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
26926 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
26927}
26928
26929#[gpui::test]
26930async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
26931 init_test(cx, |_| {});
26932
26933 let fs = FakeFs::new(cx.executor());
26934 fs.insert_file(path!("/file.html"), Default::default())
26935 .await;
26936
26937 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
26938
26939 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26940 let html_language = Arc::new(Language::new(
26941 LanguageConfig {
26942 name: "HTML".into(),
26943 matcher: LanguageMatcher {
26944 path_suffixes: vec!["html".to_string()],
26945 ..LanguageMatcher::default()
26946 },
26947 brackets: BracketPairConfig {
26948 pairs: vec![BracketPair {
26949 start: "<".into(),
26950 end: ">".into(),
26951 close: true,
26952 ..Default::default()
26953 }],
26954 ..Default::default()
26955 },
26956 ..Default::default()
26957 },
26958 Some(tree_sitter_html::LANGUAGE.into()),
26959 ));
26960 language_registry.add(html_language);
26961 let mut fake_servers = language_registry.register_fake_lsp(
26962 "HTML",
26963 FakeLspAdapter {
26964 capabilities: lsp::ServerCapabilities {
26965 completion_provider: Some(lsp::CompletionOptions {
26966 resolve_provider: Some(true),
26967 ..Default::default()
26968 }),
26969 ..Default::default()
26970 },
26971 ..Default::default()
26972 },
26973 );
26974
26975 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26976 let workspace = window
26977 .read_with(cx, |mw, _| mw.workspace().clone())
26978 .unwrap();
26979 let cx = &mut VisualTestContext::from_window(*window, cx);
26980
26981 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
26982 workspace.project().update(cx, |project, cx| {
26983 project.worktrees(cx).next().unwrap().read(cx).id()
26984 })
26985 });
26986
26987 project
26988 .update(cx, |project, cx| {
26989 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
26990 })
26991 .await
26992 .unwrap();
26993 let editor = workspace
26994 .update_in(cx, |workspace, window, cx| {
26995 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
26996 })
26997 .await
26998 .unwrap()
26999 .downcast::<Editor>()
27000 .unwrap();
27001
27002 let fake_server = fake_servers.next().await.unwrap();
27003 cx.run_until_parked();
27004 editor.update_in(cx, |editor, window, cx| {
27005 editor.set_text("<ad></ad>", window, cx);
27006 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
27007 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
27008 });
27009 set_linked_edit_ranges(
27010 (Point::new(0, 1), Point::new(0, 3)),
27011 (Point::new(0, 6), Point::new(0, 8)),
27012 editor,
27013 cx,
27014 );
27015 });
27016 let mut completion_handle =
27017 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
27018 Ok(Some(lsp::CompletionResponse::Array(vec![
27019 lsp::CompletionItem {
27020 label: "head".to_string(),
27021 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
27022 lsp::InsertReplaceEdit {
27023 new_text: "head".to_string(),
27024 insert: lsp::Range::new(
27025 lsp::Position::new(0, 1),
27026 lsp::Position::new(0, 3),
27027 ),
27028 replace: lsp::Range::new(
27029 lsp::Position::new(0, 1),
27030 lsp::Position::new(0, 3),
27031 ),
27032 },
27033 )),
27034 ..Default::default()
27035 },
27036 ])))
27037 });
27038 editor.update_in(cx, |editor, window, cx| {
27039 editor.show_completions(&ShowCompletions, window, cx);
27040 });
27041 cx.run_until_parked();
27042 completion_handle.next().await.unwrap();
27043 editor.update(cx, |editor, _| {
27044 assert!(
27045 editor.context_menu_visible(),
27046 "Completion menu should be visible"
27047 );
27048 });
27049 editor.update_in(cx, |editor, window, cx| {
27050 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
27051 });
27052 cx.executor().run_until_parked();
27053 editor.update(cx, |editor, cx| {
27054 assert_eq!(editor.text(cx), "<head></head>");
27055 });
27056}
27057
27058#[gpui::test]
27059async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
27060 init_test(cx, |_| {});
27061
27062 let mut cx = EditorTestContext::new(cx).await;
27063 let language = Arc::new(Language::new(
27064 LanguageConfig {
27065 name: "TSX".into(),
27066 matcher: LanguageMatcher {
27067 path_suffixes: vec!["tsx".to_string()],
27068 ..LanguageMatcher::default()
27069 },
27070 brackets: BracketPairConfig {
27071 pairs: vec![BracketPair {
27072 start: "<".into(),
27073 end: ">".into(),
27074 close: true,
27075 ..Default::default()
27076 }],
27077 ..Default::default()
27078 },
27079 linked_edit_characters: HashSet::from_iter(['.']),
27080 ..Default::default()
27081 },
27082 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
27083 ));
27084 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27085
27086 // Test typing > does not extend linked pair
27087 cx.set_state("<divˇ<div></div>");
27088 cx.update_editor(|editor, _, cx| {
27089 set_linked_edit_ranges(
27090 (Point::new(0, 1), Point::new(0, 4)),
27091 (Point::new(0, 11), Point::new(0, 14)),
27092 editor,
27093 cx,
27094 );
27095 });
27096 cx.update_editor(|editor, window, cx| {
27097 editor.handle_input(">", window, cx);
27098 });
27099 cx.assert_editor_state("<div>ˇ<div></div>");
27100
27101 // Test typing . do extend linked pair
27102 cx.set_state("<Animatedˇ></Animated>");
27103 cx.update_editor(|editor, _, cx| {
27104 set_linked_edit_ranges(
27105 (Point::new(0, 1), Point::new(0, 9)),
27106 (Point::new(0, 12), Point::new(0, 20)),
27107 editor,
27108 cx,
27109 );
27110 });
27111 cx.update_editor(|editor, window, cx| {
27112 editor.handle_input(".", window, cx);
27113 });
27114 cx.assert_editor_state("<Animated.ˇ></Animated.>");
27115 cx.update_editor(|editor, _, cx| {
27116 set_linked_edit_ranges(
27117 (Point::new(0, 1), Point::new(0, 10)),
27118 (Point::new(0, 13), Point::new(0, 21)),
27119 editor,
27120 cx,
27121 );
27122 });
27123 cx.update_editor(|editor, window, cx| {
27124 editor.handle_input("V", window, cx);
27125 });
27126 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
27127}
27128
27129#[gpui::test]
27130async fn test_linked_edits_on_typing_dot_without_language_override(cx: &mut TestAppContext) {
27131 init_test(cx, |_| {});
27132
27133 let mut cx = EditorTestContext::new(cx).await;
27134 let language = Arc::new(Language::new(
27135 LanguageConfig {
27136 name: "HTML".into(),
27137 matcher: LanguageMatcher {
27138 path_suffixes: vec!["html".to_string()],
27139 ..LanguageMatcher::default()
27140 },
27141 brackets: BracketPairConfig {
27142 pairs: vec![BracketPair {
27143 start: "<".into(),
27144 end: ">".into(),
27145 close: true,
27146 ..Default::default()
27147 }],
27148 ..Default::default()
27149 },
27150 ..Default::default()
27151 },
27152 Some(tree_sitter_html::LANGUAGE.into()),
27153 ));
27154 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27155
27156 cx.set_state("<Tableˇ></Table>");
27157 cx.update_editor(|editor, _, cx| {
27158 set_linked_edit_ranges(
27159 (Point::new(0, 1), Point::new(0, 6)),
27160 (Point::new(0, 9), Point::new(0, 14)),
27161 editor,
27162 cx,
27163 );
27164 });
27165 cx.update_editor(|editor, window, cx| {
27166 editor.handle_input(".", window, cx);
27167 });
27168 cx.assert_editor_state("<Table.ˇ></Table.>");
27169}
27170
27171#[gpui::test]
27172async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
27173 init_test(cx, |_| {});
27174
27175 let fs = FakeFs::new(cx.executor());
27176 fs.insert_tree(
27177 path!("/root"),
27178 json!({
27179 "a": {
27180 "main.rs": "fn main() {}",
27181 },
27182 "foo": {
27183 "bar": {
27184 "external_file.rs": "pub mod external {}",
27185 }
27186 }
27187 }),
27188 )
27189 .await;
27190
27191 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
27192 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27193 language_registry.add(rust_lang());
27194 let _fake_servers = language_registry.register_fake_lsp(
27195 "Rust",
27196 FakeLspAdapter {
27197 ..FakeLspAdapter::default()
27198 },
27199 );
27200 let (multi_workspace, cx) =
27201 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27202 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
27203 let worktree_id = workspace.update(cx, |workspace, cx| {
27204 workspace.project().update(cx, |project, cx| {
27205 project.worktrees(cx).next().unwrap().read(cx).id()
27206 })
27207 });
27208
27209 let assert_language_servers_count =
27210 |expected: usize, context: &str, cx: &mut VisualTestContext| {
27211 project.update(cx, |project, cx| {
27212 let current = project
27213 .lsp_store()
27214 .read(cx)
27215 .as_local()
27216 .unwrap()
27217 .language_servers
27218 .len();
27219 assert_eq!(expected, current, "{context}");
27220 });
27221 };
27222
27223 assert_language_servers_count(
27224 0,
27225 "No servers should be running before any file is open",
27226 cx,
27227 );
27228 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
27229 let main_editor = workspace
27230 .update_in(cx, |workspace, window, cx| {
27231 workspace.open_path(
27232 (worktree_id, rel_path("main.rs")),
27233 Some(pane.downgrade()),
27234 true,
27235 window,
27236 cx,
27237 )
27238 })
27239 .unwrap()
27240 .await
27241 .downcast::<Editor>()
27242 .unwrap();
27243 pane.update(cx, |pane, cx| {
27244 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
27245 open_editor.update(cx, |editor, cx| {
27246 assert_eq!(
27247 editor.display_text(cx),
27248 "fn main() {}",
27249 "Original main.rs text on initial open",
27250 );
27251 });
27252 assert_eq!(open_editor, main_editor);
27253 });
27254 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
27255
27256 let external_editor = workspace
27257 .update_in(cx, |workspace, window, cx| {
27258 workspace.open_abs_path(
27259 PathBuf::from("/root/foo/bar/external_file.rs"),
27260 OpenOptions::default(),
27261 window,
27262 cx,
27263 )
27264 })
27265 .await
27266 .expect("opening external file")
27267 .downcast::<Editor>()
27268 .expect("downcasted external file's open element to editor");
27269 pane.update(cx, |pane, cx| {
27270 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
27271 open_editor.update(cx, |editor, cx| {
27272 assert_eq!(
27273 editor.display_text(cx),
27274 "pub mod external {}",
27275 "External file is open now",
27276 );
27277 });
27278 assert_eq!(open_editor, external_editor);
27279 });
27280 assert_language_servers_count(
27281 1,
27282 "Second, external, *.rs file should join the existing server",
27283 cx,
27284 );
27285
27286 pane.update_in(cx, |pane, window, cx| {
27287 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27288 })
27289 .await
27290 .unwrap();
27291 pane.update_in(cx, |pane, window, cx| {
27292 pane.navigate_backward(&Default::default(), window, cx);
27293 });
27294 cx.run_until_parked();
27295 pane.update(cx, |pane, cx| {
27296 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
27297 open_editor.update(cx, |editor, cx| {
27298 assert_eq!(
27299 editor.display_text(cx),
27300 "pub mod external {}",
27301 "External file is open now",
27302 );
27303 });
27304 });
27305 assert_language_servers_count(
27306 1,
27307 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
27308 cx,
27309 );
27310
27311 cx.update(|_, cx| {
27312 workspace::reload(cx);
27313 });
27314 assert_language_servers_count(
27315 1,
27316 "After reloading the worktree with local and external files opened, only one project should be started",
27317 cx,
27318 );
27319}
27320
27321#[gpui::test]
27322async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
27323 init_test(cx, |_| {});
27324
27325 let mut cx = EditorTestContext::new(cx).await;
27326 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
27327 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27328
27329 // test cursor move to start of each line on tab
27330 // for `if`, `elif`, `else`, `while`, `with` and `for`
27331 cx.set_state(indoc! {"
27332 def main():
27333 ˇ for item in items:
27334 ˇ while item.active:
27335 ˇ if item.value > 10:
27336 ˇ continue
27337 ˇ elif item.value < 0:
27338 ˇ break
27339 ˇ else:
27340 ˇ with item.context() as ctx:
27341 ˇ yield count
27342 ˇ else:
27343 ˇ log('while else')
27344 ˇ else:
27345 ˇ log('for else')
27346 "});
27347 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
27348 cx.wait_for_autoindent_applied().await;
27349 cx.assert_editor_state(indoc! {"
27350 def main():
27351 ˇfor item in items:
27352 ˇwhile item.active:
27353 ˇif item.value > 10:
27354 ˇcontinue
27355 ˇelif item.value < 0:
27356 ˇbreak
27357 ˇelse:
27358 ˇwith item.context() as ctx:
27359 ˇyield count
27360 ˇelse:
27361 ˇlog('while else')
27362 ˇelse:
27363 ˇlog('for else')
27364 "});
27365 // test relative indent is preserved when tab
27366 // for `if`, `elif`, `else`, `while`, `with` and `for`
27367 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
27368 cx.wait_for_autoindent_applied().await;
27369 cx.assert_editor_state(indoc! {"
27370 def main():
27371 ˇfor item in items:
27372 ˇwhile item.active:
27373 ˇif item.value > 10:
27374 ˇcontinue
27375 ˇelif item.value < 0:
27376 ˇbreak
27377 ˇelse:
27378 ˇwith item.context() as ctx:
27379 ˇyield count
27380 ˇelse:
27381 ˇlog('while else')
27382 ˇelse:
27383 ˇlog('for else')
27384 "});
27385
27386 // test cursor move to start of each line on tab
27387 // for `try`, `except`, `else`, `finally`, `match` and `def`
27388 cx.set_state(indoc! {"
27389 def main():
27390 ˇ try:
27391 ˇ fetch()
27392 ˇ except ValueError:
27393 ˇ handle_error()
27394 ˇ else:
27395 ˇ match value:
27396 ˇ case _:
27397 ˇ finally:
27398 ˇ def status():
27399 ˇ return 0
27400 "});
27401 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
27402 cx.wait_for_autoindent_applied().await;
27403 cx.assert_editor_state(indoc! {"
27404 def main():
27405 ˇtry:
27406 ˇfetch()
27407 ˇexcept ValueError:
27408 ˇhandle_error()
27409 ˇelse:
27410 ˇmatch value:
27411 ˇcase _:
27412 ˇfinally:
27413 ˇdef status():
27414 ˇreturn 0
27415 "});
27416 // test relative indent is preserved when tab
27417 // for `try`, `except`, `else`, `finally`, `match` and `def`
27418 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
27419 cx.wait_for_autoindent_applied().await;
27420 cx.assert_editor_state(indoc! {"
27421 def main():
27422 ˇtry:
27423 ˇfetch()
27424 ˇexcept ValueError:
27425 ˇhandle_error()
27426 ˇelse:
27427 ˇmatch value:
27428 ˇcase _:
27429 ˇfinally:
27430 ˇdef status():
27431 ˇreturn 0
27432 "});
27433}
27434
27435#[gpui::test]
27436async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
27437 init_test(cx, |_| {});
27438
27439 let mut cx = EditorTestContext::new(cx).await;
27440 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
27441 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27442
27443 // test `else` auto outdents when typed inside `if` block
27444 cx.set_state(indoc! {"
27445 def main():
27446 if i == 2:
27447 return
27448 ˇ
27449 "});
27450 cx.update_editor(|editor, window, cx| {
27451 editor.handle_input("else:", window, cx);
27452 });
27453 cx.wait_for_autoindent_applied().await;
27454 cx.assert_editor_state(indoc! {"
27455 def main():
27456 if i == 2:
27457 return
27458 else:ˇ
27459 "});
27460
27461 // test `except` auto outdents when typed inside `try` block
27462 cx.set_state(indoc! {"
27463 def main():
27464 try:
27465 i = 2
27466 ˇ
27467 "});
27468 cx.update_editor(|editor, window, cx| {
27469 editor.handle_input("except:", window, cx);
27470 });
27471 cx.wait_for_autoindent_applied().await;
27472 cx.assert_editor_state(indoc! {"
27473 def main():
27474 try:
27475 i = 2
27476 except:ˇ
27477 "});
27478
27479 // test `else` auto outdents when typed inside `except` block
27480 cx.set_state(indoc! {"
27481 def main():
27482 try:
27483 i = 2
27484 except:
27485 j = 2
27486 ˇ
27487 "});
27488 cx.update_editor(|editor, window, cx| {
27489 editor.handle_input("else:", window, cx);
27490 });
27491 cx.wait_for_autoindent_applied().await;
27492 cx.assert_editor_state(indoc! {"
27493 def main():
27494 try:
27495 i = 2
27496 except:
27497 j = 2
27498 else:ˇ
27499 "});
27500
27501 // test `finally` auto outdents when typed inside `else` block
27502 cx.set_state(indoc! {"
27503 def main():
27504 try:
27505 i = 2
27506 except:
27507 j = 2
27508 else:
27509 k = 2
27510 ˇ
27511 "});
27512 cx.update_editor(|editor, window, cx| {
27513 editor.handle_input("finally:", window, cx);
27514 });
27515 cx.wait_for_autoindent_applied().await;
27516 cx.assert_editor_state(indoc! {"
27517 def main():
27518 try:
27519 i = 2
27520 except:
27521 j = 2
27522 else:
27523 k = 2
27524 finally:ˇ
27525 "});
27526
27527 // test `else` does not outdents when typed inside `except` block right after for block
27528 cx.set_state(indoc! {"
27529 def main():
27530 try:
27531 i = 2
27532 except:
27533 for i in range(n):
27534 pass
27535 ˇ
27536 "});
27537 cx.update_editor(|editor, window, cx| {
27538 editor.handle_input("else:", window, cx);
27539 });
27540 cx.wait_for_autoindent_applied().await;
27541 cx.assert_editor_state(indoc! {"
27542 def main():
27543 try:
27544 i = 2
27545 except:
27546 for i in range(n):
27547 pass
27548 else:ˇ
27549 "});
27550
27551 // test `finally` auto outdents when typed inside `else` block right after for block
27552 cx.set_state(indoc! {"
27553 def main():
27554 try:
27555 i = 2
27556 except:
27557 j = 2
27558 else:
27559 for i in range(n):
27560 pass
27561 ˇ
27562 "});
27563 cx.update_editor(|editor, window, cx| {
27564 editor.handle_input("finally:", window, cx);
27565 });
27566 cx.wait_for_autoindent_applied().await;
27567 cx.assert_editor_state(indoc! {"
27568 def main():
27569 try:
27570 i = 2
27571 except:
27572 j = 2
27573 else:
27574 for i in range(n):
27575 pass
27576 finally:ˇ
27577 "});
27578
27579 // test `except` outdents to inner "try" block
27580 cx.set_state(indoc! {"
27581 def main():
27582 try:
27583 i = 2
27584 if i == 2:
27585 try:
27586 i = 3
27587 ˇ
27588 "});
27589 cx.update_editor(|editor, window, cx| {
27590 editor.handle_input("except:", window, cx);
27591 });
27592 cx.wait_for_autoindent_applied().await;
27593 cx.assert_editor_state(indoc! {"
27594 def main():
27595 try:
27596 i = 2
27597 if i == 2:
27598 try:
27599 i = 3
27600 except:ˇ
27601 "});
27602
27603 // test `except` outdents to outer "try" block
27604 cx.set_state(indoc! {"
27605 def main():
27606 try:
27607 i = 2
27608 if i == 2:
27609 try:
27610 i = 3
27611 ˇ
27612 "});
27613 cx.update_editor(|editor, window, cx| {
27614 editor.handle_input("except:", window, cx);
27615 });
27616 cx.wait_for_autoindent_applied().await;
27617 cx.assert_editor_state(indoc! {"
27618 def main():
27619 try:
27620 i = 2
27621 if i == 2:
27622 try:
27623 i = 3
27624 except:ˇ
27625 "});
27626
27627 // test `else` stays at correct indent when typed after `for` block
27628 cx.set_state(indoc! {"
27629 def main():
27630 for i in range(10):
27631 if i == 3:
27632 break
27633 ˇ
27634 "});
27635 cx.update_editor(|editor, window, cx| {
27636 editor.handle_input("else:", window, cx);
27637 });
27638 cx.wait_for_autoindent_applied().await;
27639 cx.assert_editor_state(indoc! {"
27640 def main():
27641 for i in range(10):
27642 if i == 3:
27643 break
27644 else:ˇ
27645 "});
27646
27647 // test does not outdent on typing after line with square brackets
27648 cx.set_state(indoc! {"
27649 def f() -> list[str]:
27650 ˇ
27651 "});
27652 cx.update_editor(|editor, window, cx| {
27653 editor.handle_input("a", window, cx);
27654 });
27655 cx.wait_for_autoindent_applied().await;
27656 cx.assert_editor_state(indoc! {"
27657 def f() -> list[str]:
27658 aˇ
27659 "});
27660
27661 // test does not outdent on typing : after case keyword
27662 cx.set_state(indoc! {"
27663 match 1:
27664 caseˇ
27665 "});
27666 cx.update_editor(|editor, window, cx| {
27667 editor.handle_input(":", window, cx);
27668 });
27669 cx.wait_for_autoindent_applied().await;
27670 cx.assert_editor_state(indoc! {"
27671 match 1:
27672 case:ˇ
27673 "});
27674}
27675
27676#[gpui::test]
27677async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
27678 init_test(cx, |_| {});
27679 update_test_language_settings(cx, &|settings| {
27680 settings.defaults.extend_comment_on_newline = Some(false);
27681 });
27682 let mut cx = EditorTestContext::new(cx).await;
27683 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
27684 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27685
27686 // test correct indent after newline on comment
27687 cx.set_state(indoc! {"
27688 # COMMENT:ˇ
27689 "});
27690 cx.update_editor(|editor, window, cx| {
27691 editor.newline(&Newline, window, cx);
27692 });
27693 cx.wait_for_autoindent_applied().await;
27694 cx.assert_editor_state(indoc! {"
27695 # COMMENT:
27696 ˇ
27697 "});
27698
27699 // test correct indent after newline in brackets
27700 cx.set_state(indoc! {"
27701 {ˇ}
27702 "});
27703 cx.update_editor(|editor, window, cx| {
27704 editor.newline(&Newline, window, cx);
27705 });
27706 cx.wait_for_autoindent_applied().await;
27707 cx.assert_editor_state(indoc! {"
27708 {
27709 ˇ
27710 }
27711 "});
27712
27713 cx.set_state(indoc! {"
27714 (ˇ)
27715 "});
27716 cx.update_editor(|editor, window, cx| {
27717 editor.newline(&Newline, window, cx);
27718 });
27719 cx.run_until_parked();
27720 cx.assert_editor_state(indoc! {"
27721 (
27722 ˇ
27723 )
27724 "});
27725
27726 // do not indent after empty lists or dictionaries
27727 cx.set_state(indoc! {"
27728 a = []ˇ
27729 "});
27730 cx.update_editor(|editor, window, cx| {
27731 editor.newline(&Newline, window, cx);
27732 });
27733 cx.run_until_parked();
27734 cx.assert_editor_state(indoc! {"
27735 a = []
27736 ˇ
27737 "});
27738}
27739
27740#[gpui::test]
27741async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
27742 init_test(cx, |_| {});
27743
27744 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
27745 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
27746 language_registry.add(markdown_lang());
27747 language_registry.add(python_lang);
27748
27749 let mut cx = EditorTestContext::new(cx).await;
27750 cx.update_buffer(|buffer, cx| {
27751 buffer.set_language_registry(language_registry);
27752 buffer.set_language(Some(markdown_lang()), cx);
27753 });
27754
27755 // Test that `else:` correctly outdents to match `if:` inside the Python code block
27756 cx.set_state(indoc! {"
27757 # Heading
27758
27759 ```python
27760 def main():
27761 if condition:
27762 pass
27763 ˇ
27764 ```
27765 "});
27766 cx.update_editor(|editor, window, cx| {
27767 editor.handle_input("else:", window, cx);
27768 });
27769 cx.run_until_parked();
27770 cx.assert_editor_state(indoc! {"
27771 # Heading
27772
27773 ```python
27774 def main():
27775 if condition:
27776 pass
27777 else:ˇ
27778 ```
27779 "});
27780}
27781
27782#[gpui::test]
27783async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
27784 init_test(cx, |_| {});
27785
27786 let mut cx = EditorTestContext::new(cx).await;
27787 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
27788 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27789
27790 // test cursor move to start of each line on tab
27791 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
27792 cx.set_state(indoc! {"
27793 function main() {
27794 ˇ for item in $items; do
27795 ˇ while [ -n \"$item\" ]; do
27796 ˇ if [ \"$value\" -gt 10 ]; then
27797 ˇ continue
27798 ˇ elif [ \"$value\" -lt 0 ]; then
27799 ˇ break
27800 ˇ else
27801 ˇ echo \"$item\"
27802 ˇ fi
27803 ˇ done
27804 ˇ done
27805 ˇ}
27806 "});
27807 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
27808 cx.wait_for_autoindent_applied().await;
27809 cx.assert_editor_state(indoc! {"
27810 function main() {
27811 ˇfor item in $items; do
27812 ˇwhile [ -n \"$item\" ]; do
27813 ˇif [ \"$value\" -gt 10 ]; then
27814 ˇcontinue
27815 ˇelif [ \"$value\" -lt 0 ]; then
27816 ˇbreak
27817 ˇelse
27818 ˇecho \"$item\"
27819 ˇfi
27820 ˇdone
27821 ˇdone
27822 ˇ}
27823 "});
27824 // test relative indent is preserved when tab
27825 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
27826 cx.wait_for_autoindent_applied().await;
27827 cx.assert_editor_state(indoc! {"
27828 function main() {
27829 ˇfor item in $items; do
27830 ˇwhile [ -n \"$item\" ]; do
27831 ˇif [ \"$value\" -gt 10 ]; then
27832 ˇcontinue
27833 ˇelif [ \"$value\" -lt 0 ]; then
27834 ˇbreak
27835 ˇelse
27836 ˇecho \"$item\"
27837 ˇfi
27838 ˇdone
27839 ˇdone
27840 ˇ}
27841 "});
27842
27843 // test cursor move to start of each line on tab
27844 // for `case` statement with patterns
27845 cx.set_state(indoc! {"
27846 function handle() {
27847 ˇ case \"$1\" in
27848 ˇ start)
27849 ˇ echo \"a\"
27850 ˇ ;;
27851 ˇ stop)
27852 ˇ echo \"b\"
27853 ˇ ;;
27854 ˇ *)
27855 ˇ echo \"c\"
27856 ˇ ;;
27857 ˇ esac
27858 ˇ}
27859 "});
27860 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
27861 cx.wait_for_autoindent_applied().await;
27862 cx.assert_editor_state(indoc! {"
27863 function handle() {
27864 ˇcase \"$1\" in
27865 ˇstart)
27866 ˇecho \"a\"
27867 ˇ;;
27868 ˇstop)
27869 ˇecho \"b\"
27870 ˇ;;
27871 ˇ*)
27872 ˇecho \"c\"
27873 ˇ;;
27874 ˇesac
27875 ˇ}
27876 "});
27877}
27878
27879#[gpui::test]
27880async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
27881 init_test(cx, |_| {});
27882
27883 let mut cx = EditorTestContext::new(cx).await;
27884 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
27885 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27886
27887 // test indents on comment insert
27888 cx.set_state(indoc! {"
27889 function main() {
27890 ˇ for item in $items; do
27891 ˇ while [ -n \"$item\" ]; do
27892 ˇ if [ \"$value\" -gt 10 ]; then
27893 ˇ continue
27894 ˇ elif [ \"$value\" -lt 0 ]; then
27895 ˇ break
27896 ˇ else
27897 ˇ echo \"$item\"
27898 ˇ fi
27899 ˇ done
27900 ˇ done
27901 ˇ}
27902 "});
27903 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
27904 cx.wait_for_autoindent_applied().await;
27905 cx.assert_editor_state(indoc! {"
27906 function main() {
27907 #ˇ for item in $items; do
27908 #ˇ while [ -n \"$item\" ]; do
27909 #ˇ if [ \"$value\" -gt 10 ]; then
27910 #ˇ continue
27911 #ˇ elif [ \"$value\" -lt 0 ]; then
27912 #ˇ break
27913 #ˇ else
27914 #ˇ echo \"$item\"
27915 #ˇ fi
27916 #ˇ done
27917 #ˇ done
27918 #ˇ}
27919 "});
27920}
27921
27922#[gpui::test]
27923async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
27924 init_test(cx, |_| {});
27925
27926 let mut cx = EditorTestContext::new(cx).await;
27927 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
27928 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27929
27930 // test `else` auto outdents when typed inside `if` block
27931 cx.set_state(indoc! {"
27932 if [ \"$1\" = \"test\" ]; then
27933 echo \"foo bar\"
27934 ˇ
27935 "});
27936 cx.update_editor(|editor, window, cx| {
27937 editor.handle_input("else", window, cx);
27938 });
27939 cx.wait_for_autoindent_applied().await;
27940 cx.assert_editor_state(indoc! {"
27941 if [ \"$1\" = \"test\" ]; then
27942 echo \"foo bar\"
27943 elseˇ
27944 "});
27945
27946 // test `elif` auto outdents when typed inside `if` block
27947 cx.set_state(indoc! {"
27948 if [ \"$1\" = \"test\" ]; then
27949 echo \"foo bar\"
27950 ˇ
27951 "});
27952 cx.update_editor(|editor, window, cx| {
27953 editor.handle_input("elif", window, cx);
27954 });
27955 cx.wait_for_autoindent_applied().await;
27956 cx.assert_editor_state(indoc! {"
27957 if [ \"$1\" = \"test\" ]; then
27958 echo \"foo bar\"
27959 elifˇ
27960 "});
27961
27962 // test `fi` auto outdents when typed inside `else` block
27963 cx.set_state(indoc! {"
27964 if [ \"$1\" = \"test\" ]; then
27965 echo \"foo bar\"
27966 else
27967 echo \"bar baz\"
27968 ˇ
27969 "});
27970 cx.update_editor(|editor, window, cx| {
27971 editor.handle_input("fi", window, cx);
27972 });
27973 cx.wait_for_autoindent_applied().await;
27974 cx.assert_editor_state(indoc! {"
27975 if [ \"$1\" = \"test\" ]; then
27976 echo \"foo bar\"
27977 else
27978 echo \"bar baz\"
27979 fiˇ
27980 "});
27981
27982 // test `done` auto outdents when typed inside `while` block
27983 cx.set_state(indoc! {"
27984 while read line; do
27985 echo \"$line\"
27986 ˇ
27987 "});
27988 cx.update_editor(|editor, window, cx| {
27989 editor.handle_input("done", window, cx);
27990 });
27991 cx.wait_for_autoindent_applied().await;
27992 cx.assert_editor_state(indoc! {"
27993 while read line; do
27994 echo \"$line\"
27995 doneˇ
27996 "});
27997
27998 // test `done` auto outdents when typed inside `for` block
27999 cx.set_state(indoc! {"
28000 for file in *.txt; do
28001 cat \"$file\"
28002 ˇ
28003 "});
28004 cx.update_editor(|editor, window, cx| {
28005 editor.handle_input("done", window, cx);
28006 });
28007 cx.wait_for_autoindent_applied().await;
28008 cx.assert_editor_state(indoc! {"
28009 for file in *.txt; do
28010 cat \"$file\"
28011 doneˇ
28012 "});
28013
28014 // test `esac` auto outdents when typed inside `case` block
28015 cx.set_state(indoc! {"
28016 case \"$1\" in
28017 start)
28018 echo \"foo bar\"
28019 ;;
28020 stop)
28021 echo \"bar baz\"
28022 ;;
28023 ˇ
28024 "});
28025 cx.update_editor(|editor, window, cx| {
28026 editor.handle_input("esac", window, cx);
28027 });
28028 cx.wait_for_autoindent_applied().await;
28029 cx.assert_editor_state(indoc! {"
28030 case \"$1\" in
28031 start)
28032 echo \"foo bar\"
28033 ;;
28034 stop)
28035 echo \"bar baz\"
28036 ;;
28037 esacˇ
28038 "});
28039
28040 // test `*)` auto outdents when typed inside `case` block
28041 cx.set_state(indoc! {"
28042 case \"$1\" in
28043 start)
28044 echo \"foo bar\"
28045 ;;
28046 ˇ
28047 "});
28048 cx.update_editor(|editor, window, cx| {
28049 editor.handle_input("*)", window, cx);
28050 });
28051 cx.wait_for_autoindent_applied().await;
28052 cx.assert_editor_state(indoc! {"
28053 case \"$1\" in
28054 start)
28055 echo \"foo bar\"
28056 ;;
28057 *)ˇ
28058 "});
28059
28060 // test `fi` outdents to correct level with nested if blocks
28061 cx.set_state(indoc! {"
28062 if [ \"$1\" = \"test\" ]; then
28063 echo \"outer if\"
28064 if [ \"$2\" = \"debug\" ]; then
28065 echo \"inner if\"
28066 ˇ
28067 "});
28068 cx.update_editor(|editor, window, cx| {
28069 editor.handle_input("fi", window, cx);
28070 });
28071 cx.wait_for_autoindent_applied().await;
28072 cx.assert_editor_state(indoc! {"
28073 if [ \"$1\" = \"test\" ]; then
28074 echo \"outer if\"
28075 if [ \"$2\" = \"debug\" ]; then
28076 echo \"inner if\"
28077 fiˇ
28078 "});
28079}
28080
28081#[gpui::test]
28082async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
28083 init_test(cx, |_| {});
28084 update_test_language_settings(cx, &|settings| {
28085 settings.defaults.extend_comment_on_newline = Some(false);
28086 });
28087 let mut cx = EditorTestContext::new(cx).await;
28088 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
28089 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
28090
28091 // test correct indent after newline on comment
28092 cx.set_state(indoc! {"
28093 # COMMENT:ˇ
28094 "});
28095 cx.update_editor(|editor, window, cx| {
28096 editor.newline(&Newline, window, cx);
28097 });
28098 cx.wait_for_autoindent_applied().await;
28099 cx.assert_editor_state(indoc! {"
28100 # COMMENT:
28101 ˇ
28102 "});
28103
28104 // test correct indent after newline after `then`
28105 cx.set_state(indoc! {"
28106
28107 if [ \"$1\" = \"test\" ]; thenˇ
28108 "});
28109 cx.update_editor(|editor, window, cx| {
28110 editor.newline(&Newline, window, cx);
28111 });
28112 cx.wait_for_autoindent_applied().await;
28113 cx.assert_editor_state(indoc! {"
28114
28115 if [ \"$1\" = \"test\" ]; then
28116 ˇ
28117 "});
28118
28119 // test correct indent after newline after `else`
28120 cx.set_state(indoc! {"
28121 if [ \"$1\" = \"test\" ]; then
28122 elseˇ
28123 "});
28124 cx.update_editor(|editor, window, cx| {
28125 editor.newline(&Newline, window, cx);
28126 });
28127 cx.wait_for_autoindent_applied().await;
28128 cx.assert_editor_state(indoc! {"
28129 if [ \"$1\" = \"test\" ]; then
28130 else
28131 ˇ
28132 "});
28133
28134 // test correct indent after newline after `elif`
28135 cx.set_state(indoc! {"
28136 if [ \"$1\" = \"test\" ]; then
28137 elifˇ
28138 "});
28139 cx.update_editor(|editor, window, cx| {
28140 editor.newline(&Newline, window, cx);
28141 });
28142 cx.wait_for_autoindent_applied().await;
28143 cx.assert_editor_state(indoc! {"
28144 if [ \"$1\" = \"test\" ]; then
28145 elif
28146 ˇ
28147 "});
28148
28149 // test correct indent after newline after `do`
28150 cx.set_state(indoc! {"
28151 for file in *.txt; doˇ
28152 "});
28153 cx.update_editor(|editor, window, cx| {
28154 editor.newline(&Newline, window, cx);
28155 });
28156 cx.wait_for_autoindent_applied().await;
28157 cx.assert_editor_state(indoc! {"
28158 for file in *.txt; do
28159 ˇ
28160 "});
28161
28162 // test correct indent after newline after case pattern
28163 cx.set_state(indoc! {"
28164 case \"$1\" in
28165 start)ˇ
28166 "});
28167 cx.update_editor(|editor, window, cx| {
28168 editor.newline(&Newline, window, cx);
28169 });
28170 cx.wait_for_autoindent_applied().await;
28171 cx.assert_editor_state(indoc! {"
28172 case \"$1\" in
28173 start)
28174 ˇ
28175 "});
28176
28177 // test correct indent after newline after case pattern
28178 cx.set_state(indoc! {"
28179 case \"$1\" in
28180 start)
28181 ;;
28182 *)ˇ
28183 "});
28184 cx.update_editor(|editor, window, cx| {
28185 editor.newline(&Newline, window, cx);
28186 });
28187 cx.wait_for_autoindent_applied().await;
28188 cx.assert_editor_state(indoc! {"
28189 case \"$1\" in
28190 start)
28191 ;;
28192 *)
28193 ˇ
28194 "});
28195
28196 // test correct indent after newline after function opening brace
28197 cx.set_state(indoc! {"
28198 function test() {ˇ}
28199 "});
28200 cx.update_editor(|editor, window, cx| {
28201 editor.newline(&Newline, window, cx);
28202 });
28203 cx.wait_for_autoindent_applied().await;
28204 cx.assert_editor_state(indoc! {"
28205 function test() {
28206 ˇ
28207 }
28208 "});
28209
28210 // test no extra indent after semicolon on same line
28211 cx.set_state(indoc! {"
28212 echo \"test\";ˇ
28213 "});
28214 cx.update_editor(|editor, window, cx| {
28215 editor.newline(&Newline, window, cx);
28216 });
28217 cx.wait_for_autoindent_applied().await;
28218 cx.assert_editor_state(indoc! {"
28219 echo \"test\";
28220 ˇ
28221 "});
28222}
28223
28224fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
28225 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
28226 point..point
28227}
28228
28229#[track_caller]
28230fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
28231 let (text, ranges) = marked_text_ranges(marked_text, true);
28232 assert_eq!(editor.text(cx), text);
28233 assert_eq!(
28234 editor.selections.ranges(&editor.display_snapshot(cx)),
28235 ranges
28236 .iter()
28237 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
28238 .collect::<Vec<_>>(),
28239 "Assert selections are {}",
28240 marked_text
28241 );
28242}
28243
28244pub fn handle_signature_help_request(
28245 cx: &mut EditorLspTestContext,
28246 mocked_response: lsp::SignatureHelp,
28247) -> impl Future<Output = ()> + use<> {
28248 let mut request =
28249 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
28250 let mocked_response = mocked_response.clone();
28251 async move { Ok(Some(mocked_response)) }
28252 });
28253
28254 async move {
28255 request.next().await;
28256 }
28257}
28258
28259#[track_caller]
28260pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
28261 cx.update_editor(|editor, _, _| {
28262 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
28263 let entries = menu.entries.borrow();
28264 let entries = entries
28265 .iter()
28266 .map(|entry| entry.string.as_str())
28267 .collect::<Vec<_>>();
28268 assert_eq!(entries, expected);
28269 } else {
28270 panic!("Expected completions menu");
28271 }
28272 });
28273}
28274
28275#[gpui::test]
28276async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
28277 init_test(cx, |_| {});
28278 let mut cx = EditorLspTestContext::new_rust(
28279 lsp::ServerCapabilities {
28280 completion_provider: Some(lsp::CompletionOptions {
28281 ..Default::default()
28282 }),
28283 ..Default::default()
28284 },
28285 cx,
28286 )
28287 .await;
28288 cx.lsp
28289 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
28290 Ok(Some(lsp::CompletionResponse::Array(vec![
28291 lsp::CompletionItem {
28292 label: "unsafe".into(),
28293 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
28294 range: lsp::Range {
28295 start: lsp::Position {
28296 line: 0,
28297 character: 9,
28298 },
28299 end: lsp::Position {
28300 line: 0,
28301 character: 11,
28302 },
28303 },
28304 new_text: "unsafe".to_string(),
28305 })),
28306 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
28307 ..Default::default()
28308 },
28309 ])))
28310 });
28311
28312 cx.update_editor(|editor, _, cx| {
28313 editor.project().unwrap().update(cx, |project, cx| {
28314 project.snippets().update(cx, |snippets, _cx| {
28315 snippets.add_snippet_for_test(
28316 None,
28317 PathBuf::from("test_snippets.json"),
28318 vec![
28319 Arc::new(project::snippet_provider::Snippet {
28320 prefix: vec![
28321 "unlimited word count".to_string(),
28322 "unlimit word count".to_string(),
28323 "unlimited unknown".to_string(),
28324 ],
28325 body: "this is many words".to_string(),
28326 description: Some("description".to_string()),
28327 name: "multi-word snippet test".to_string(),
28328 }),
28329 Arc::new(project::snippet_provider::Snippet {
28330 prefix: vec!["unsnip".to_string(), "@few".to_string()],
28331 body: "fewer words".to_string(),
28332 description: Some("alt description".to_string()),
28333 name: "other name".to_string(),
28334 }),
28335 Arc::new(project::snippet_provider::Snippet {
28336 prefix: vec!["ab aa".to_string()],
28337 body: "abcd".to_string(),
28338 description: None,
28339 name: "alphabet".to_string(),
28340 }),
28341 ],
28342 );
28343 });
28344 })
28345 });
28346
28347 let get_completions = |cx: &mut EditorLspTestContext| {
28348 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
28349 Some(CodeContextMenu::Completions(context_menu)) => {
28350 let entries = context_menu.entries.borrow();
28351 entries
28352 .iter()
28353 .map(|entry| entry.string.clone())
28354 .collect_vec()
28355 }
28356 _ => vec![],
28357 })
28358 };
28359
28360 // snippets:
28361 // @foo
28362 // foo bar
28363 //
28364 // when typing:
28365 //
28366 // when typing:
28367 // - if I type a symbol "open the completions with snippets only"
28368 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
28369 //
28370 // stuff we need:
28371 // - filtering logic change?
28372 // - remember how far back the completion started.
28373
28374 let test_cases: &[(&str, &[&str])] = &[
28375 (
28376 "un",
28377 &[
28378 "unsafe",
28379 "unlimit word count",
28380 "unlimited unknown",
28381 "unlimited word count",
28382 "unsnip",
28383 ],
28384 ),
28385 (
28386 "u ",
28387 &[
28388 "unlimit word count",
28389 "unlimited unknown",
28390 "unlimited word count",
28391 ],
28392 ),
28393 ("u a", &["ab aa", "unsafe"]), // unsAfe
28394 (
28395 "u u",
28396 &[
28397 "unsafe",
28398 "unlimit word count",
28399 "unlimited unknown", // ranked highest among snippets
28400 "unlimited word count",
28401 "unsnip",
28402 ],
28403 ),
28404 ("uw c", &["unlimit word count", "unlimited word count"]),
28405 (
28406 "u w",
28407 &[
28408 "unlimit word count",
28409 "unlimited word count",
28410 "unlimited unknown",
28411 ],
28412 ),
28413 ("u w ", &["unlimit word count", "unlimited word count"]),
28414 (
28415 "u ",
28416 &[
28417 "unlimit word count",
28418 "unlimited unknown",
28419 "unlimited word count",
28420 ],
28421 ),
28422 ("wor", &[]),
28423 ("uf", &["unsafe"]),
28424 ("af", &["unsafe"]),
28425 ("afu", &[]),
28426 (
28427 "ue",
28428 &["unsafe", "unlimited unknown", "unlimited word count"],
28429 ),
28430 ("@", &["@few"]),
28431 ("@few", &["@few"]),
28432 ("@ ", &[]),
28433 ("a@", &["@few"]),
28434 ("a@f", &["@few", "unsafe"]),
28435 ("a@fw", &["@few"]),
28436 ("a", &["ab aa", "unsafe"]),
28437 ("aa", &["ab aa"]),
28438 ("aaa", &["ab aa"]),
28439 ("ab", &["ab aa"]),
28440 ("ab ", &["ab aa"]),
28441 ("ab a", &["ab aa", "unsafe"]),
28442 ("ab ab", &["ab aa"]),
28443 ("ab ab aa", &["ab aa"]),
28444 ];
28445
28446 for &(input_to_simulate, expected_completions) in test_cases {
28447 cx.set_state("fn a() { ˇ }\n");
28448 for c in input_to_simulate.split("") {
28449 cx.simulate_input(c);
28450 cx.run_until_parked();
28451 }
28452 let expected_completions = expected_completions
28453 .iter()
28454 .map(|s| s.to_string())
28455 .collect_vec();
28456 assert_eq!(
28457 get_completions(&mut cx),
28458 expected_completions,
28459 "< actual / expected >, input = {input_to_simulate:?}",
28460 );
28461 }
28462}
28463
28464/// Handle completion request passing a marked string specifying where the completion
28465/// should be triggered from using '|' character, what range should be replaced, and what completions
28466/// should be returned using '<' and '>' to delimit the range.
28467///
28468/// Also see `handle_completion_request_with_insert_and_replace`.
28469#[track_caller]
28470pub fn handle_completion_request(
28471 marked_string: &str,
28472 completions: Vec<&'static str>,
28473 is_incomplete: bool,
28474 counter: Arc<AtomicUsize>,
28475 cx: &mut EditorLspTestContext,
28476) -> impl Future<Output = ()> {
28477 let complete_from_marker: TextRangeMarker = '|'.into();
28478 let replace_range_marker: TextRangeMarker = ('<', '>').into();
28479 let (_, mut marked_ranges) = marked_text_ranges_by(
28480 marked_string,
28481 vec![complete_from_marker.clone(), replace_range_marker.clone()],
28482 );
28483
28484 let complete_from_position = cx.to_lsp(MultiBufferOffset(
28485 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
28486 ));
28487 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
28488 let replace_range =
28489 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
28490
28491 let mut request =
28492 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
28493 let completions = completions.clone();
28494 counter.fetch_add(1, atomic::Ordering::Release);
28495 async move {
28496 assert_eq!(params.text_document_position.text_document.uri, url.clone());
28497 assert_eq!(
28498 params.text_document_position.position,
28499 complete_from_position
28500 );
28501 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
28502 is_incomplete,
28503 item_defaults: None,
28504 items: completions
28505 .iter()
28506 .map(|completion_text| lsp::CompletionItem {
28507 label: completion_text.to_string(),
28508 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
28509 range: replace_range,
28510 new_text: completion_text.to_string(),
28511 })),
28512 ..Default::default()
28513 })
28514 .collect(),
28515 })))
28516 }
28517 });
28518
28519 async move {
28520 request.next().await;
28521 }
28522}
28523
28524/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
28525/// given instead, which also contains an `insert` range.
28526///
28527/// This function uses markers to define ranges:
28528/// - `|` marks the cursor position
28529/// - `<>` marks the replace range
28530/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
28531pub fn handle_completion_request_with_insert_and_replace(
28532 cx: &mut EditorLspTestContext,
28533 marked_string: &str,
28534 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
28535 counter: Arc<AtomicUsize>,
28536) -> impl Future<Output = ()> {
28537 let complete_from_marker: TextRangeMarker = '|'.into();
28538 let replace_range_marker: TextRangeMarker = ('<', '>').into();
28539 let insert_range_marker: TextRangeMarker = ('{', '}').into();
28540
28541 let (_, mut marked_ranges) = marked_text_ranges_by(
28542 marked_string,
28543 vec![
28544 complete_from_marker.clone(),
28545 replace_range_marker.clone(),
28546 insert_range_marker.clone(),
28547 ],
28548 );
28549
28550 let complete_from_position = cx.to_lsp(MultiBufferOffset(
28551 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
28552 ));
28553 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
28554 let replace_range =
28555 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
28556
28557 let insert_range = match marked_ranges.remove(&insert_range_marker) {
28558 Some(ranges) if !ranges.is_empty() => {
28559 let range1 = ranges[0].clone();
28560 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
28561 }
28562 _ => lsp::Range {
28563 start: replace_range.start,
28564 end: complete_from_position,
28565 },
28566 };
28567
28568 let mut request =
28569 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
28570 let completions = completions.clone();
28571 counter.fetch_add(1, atomic::Ordering::Release);
28572 async move {
28573 assert_eq!(params.text_document_position.text_document.uri, url.clone());
28574 assert_eq!(
28575 params.text_document_position.position, complete_from_position,
28576 "marker `|` position doesn't match",
28577 );
28578 Ok(Some(lsp::CompletionResponse::Array(
28579 completions
28580 .iter()
28581 .map(|(label, new_text)| lsp::CompletionItem {
28582 label: label.to_string(),
28583 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
28584 lsp::InsertReplaceEdit {
28585 insert: insert_range,
28586 replace: replace_range,
28587 new_text: new_text.to_string(),
28588 },
28589 )),
28590 ..Default::default()
28591 })
28592 .collect(),
28593 )))
28594 }
28595 });
28596
28597 async move {
28598 request.next().await;
28599 }
28600}
28601
28602fn handle_resolve_completion_request(
28603 cx: &mut EditorLspTestContext,
28604 edits: Option<Vec<(&'static str, &'static str)>>,
28605) -> impl Future<Output = ()> {
28606 let edits = edits.map(|edits| {
28607 edits
28608 .iter()
28609 .map(|(marked_string, new_text)| {
28610 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
28611 let replace_range = cx.to_lsp_range(
28612 MultiBufferOffset(marked_ranges[0].start)
28613 ..MultiBufferOffset(marked_ranges[0].end),
28614 );
28615 lsp::TextEdit::new(replace_range, new_text.to_string())
28616 })
28617 .collect::<Vec<_>>()
28618 });
28619
28620 let mut request =
28621 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
28622 let edits = edits.clone();
28623 async move {
28624 Ok(lsp::CompletionItem {
28625 additional_text_edits: edits,
28626 ..Default::default()
28627 })
28628 }
28629 });
28630
28631 async move {
28632 request.next().await;
28633 }
28634}
28635
28636pub(crate) fn update_test_language_settings(
28637 cx: &mut TestAppContext,
28638 f: &dyn Fn(&mut AllLanguageSettingsContent),
28639) {
28640 cx.update(|cx| {
28641 SettingsStore::update_global(cx, |store, cx| {
28642 store.update_user_settings(cx, &|settings: &mut SettingsContent| {
28643 f(&mut settings.project.all_languages)
28644 });
28645 });
28646 });
28647}
28648
28649pub(crate) fn update_test_project_settings(
28650 cx: &mut TestAppContext,
28651 f: &dyn Fn(&mut ProjectSettingsContent),
28652) {
28653 cx.update(|cx| {
28654 SettingsStore::update_global(cx, |store, cx| {
28655 store.update_user_settings(cx, |settings| f(&mut settings.project));
28656 });
28657 });
28658}
28659
28660pub(crate) fn update_test_editor_settings(
28661 cx: &mut TestAppContext,
28662 f: &dyn Fn(&mut EditorSettingsContent),
28663) {
28664 cx.update(|cx| {
28665 SettingsStore::update_global(cx, |store, cx| {
28666 store.update_user_settings(cx, |settings| f(&mut settings.editor));
28667 })
28668 })
28669}
28670
28671pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
28672 cx.update(|cx| {
28673 assets::Assets.load_test_fonts(cx);
28674 let store = SettingsStore::test(cx);
28675 cx.set_global(store);
28676 theme::init(theme::LoadThemes::JustBase, cx);
28677 release_channel::init(semver::Version::new(0, 0, 0), cx);
28678 crate::init(cx);
28679 });
28680 zlog::init_test();
28681 update_test_language_settings(cx, &f);
28682}
28683
28684#[track_caller]
28685fn assert_hunk_revert(
28686 not_reverted_text_with_selections: &str,
28687 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
28688 expected_reverted_text_with_selections: &str,
28689 base_text: &str,
28690 cx: &mut EditorLspTestContext,
28691) {
28692 cx.set_state(not_reverted_text_with_selections);
28693 cx.set_head_text(base_text);
28694 cx.executor().run_until_parked();
28695
28696 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
28697 let snapshot = editor.snapshot(window, cx);
28698 let reverted_hunk_statuses = snapshot
28699 .buffer_snapshot()
28700 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
28701 .map(|hunk| hunk.status().kind)
28702 .collect::<Vec<_>>();
28703
28704 editor.git_restore(&Default::default(), window, cx);
28705 reverted_hunk_statuses
28706 });
28707 cx.executor().run_until_parked();
28708 cx.assert_editor_state(expected_reverted_text_with_selections);
28709 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
28710}
28711
28712#[gpui::test(iterations = 10)]
28713async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
28714 init_test(cx, |_| {});
28715
28716 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
28717 let counter = diagnostic_requests.clone();
28718
28719 let fs = FakeFs::new(cx.executor());
28720 fs.insert_tree(
28721 path!("/a"),
28722 json!({
28723 "first.rs": "fn main() { let a = 5; }",
28724 "second.rs": "// Test file",
28725 }),
28726 )
28727 .await;
28728
28729 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
28730 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
28731 let workspace = window
28732 .read_with(cx, |mw, _| mw.workspace().clone())
28733 .unwrap();
28734 let cx = &mut VisualTestContext::from_window(*window, cx);
28735
28736 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28737 language_registry.add(rust_lang());
28738 let mut fake_servers = language_registry.register_fake_lsp(
28739 "Rust",
28740 FakeLspAdapter {
28741 capabilities: lsp::ServerCapabilities {
28742 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
28743 lsp::DiagnosticOptions {
28744 identifier: None,
28745 inter_file_dependencies: true,
28746 workspace_diagnostics: true,
28747 work_done_progress_options: Default::default(),
28748 },
28749 )),
28750 ..Default::default()
28751 },
28752 ..Default::default()
28753 },
28754 );
28755
28756 let editor = workspace
28757 .update_in(cx, |workspace, window, cx| {
28758 workspace.open_abs_path(
28759 PathBuf::from(path!("/a/first.rs")),
28760 OpenOptions::default(),
28761 window,
28762 cx,
28763 )
28764 })
28765 .await
28766 .unwrap()
28767 .downcast::<Editor>()
28768 .unwrap();
28769 let fake_server = fake_servers.next().await.unwrap();
28770 let server_id = fake_server.server.server_id();
28771 let mut first_request = fake_server
28772 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
28773 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
28774 let result_id = Some(new_result_id.to_string());
28775 assert_eq!(
28776 params.text_document.uri,
28777 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
28778 );
28779 async move {
28780 Ok(lsp::DocumentDiagnosticReportResult::Report(
28781 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
28782 related_documents: None,
28783 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
28784 items: Vec::new(),
28785 result_id,
28786 },
28787 }),
28788 ))
28789 }
28790 });
28791
28792 let ensure_result_id = |expected_result_id: Option<SharedString>, cx: &mut TestAppContext| {
28793 project.update(cx, |project, cx| {
28794 let buffer_id = editor
28795 .read(cx)
28796 .buffer()
28797 .read(cx)
28798 .as_singleton()
28799 .expect("created a singleton buffer")
28800 .read(cx)
28801 .remote_id();
28802 let buffer_result_id = project
28803 .lsp_store()
28804 .read(cx)
28805 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
28806 assert_eq!(expected_result_id, buffer_result_id);
28807 });
28808 };
28809
28810 ensure_result_id(None, cx);
28811 cx.executor().advance_clock(Duration::from_millis(60));
28812 cx.executor().run_until_parked();
28813 assert_eq!(
28814 diagnostic_requests.load(atomic::Ordering::Acquire),
28815 1,
28816 "Opening file should trigger diagnostic request"
28817 );
28818 first_request
28819 .next()
28820 .await
28821 .expect("should have sent the first diagnostics pull request");
28822 ensure_result_id(Some(SharedString::new_static("1")), cx);
28823
28824 // Editing should trigger diagnostics
28825 editor.update_in(cx, |editor, window, cx| {
28826 editor.handle_input("2", window, cx)
28827 });
28828 cx.executor().advance_clock(Duration::from_millis(60));
28829 cx.executor().run_until_parked();
28830 assert_eq!(
28831 diagnostic_requests.load(atomic::Ordering::Acquire),
28832 2,
28833 "Editing should trigger diagnostic request"
28834 );
28835 ensure_result_id(Some(SharedString::new_static("2")), cx);
28836
28837 // Moving cursor should not trigger diagnostic request
28838 editor.update_in(cx, |editor, window, cx| {
28839 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28840 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
28841 });
28842 });
28843 cx.executor().advance_clock(Duration::from_millis(60));
28844 cx.executor().run_until_parked();
28845 assert_eq!(
28846 diagnostic_requests.load(atomic::Ordering::Acquire),
28847 2,
28848 "Cursor movement should not trigger diagnostic request"
28849 );
28850 ensure_result_id(Some(SharedString::new_static("2")), cx);
28851 // Multiple rapid edits should be debounced
28852 for _ in 0..5 {
28853 editor.update_in(cx, |editor, window, cx| {
28854 editor.handle_input("x", window, cx)
28855 });
28856 }
28857 cx.executor().advance_clock(Duration::from_millis(60));
28858 cx.executor().run_until_parked();
28859
28860 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
28861 assert!(
28862 final_requests <= 4,
28863 "Multiple rapid edits should be debounced (got {final_requests} requests)",
28864 );
28865 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
28866}
28867
28868#[gpui::test]
28869async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
28870 // Regression test for issue #11671
28871 // Previously, adding a cursor after moving multiple cursors would reset
28872 // the cursor count instead of adding to the existing cursors.
28873 init_test(cx, |_| {});
28874 let mut cx = EditorTestContext::new(cx).await;
28875
28876 // Create a simple buffer with cursor at start
28877 cx.set_state(indoc! {"
28878 ˇaaaa
28879 bbbb
28880 cccc
28881 dddd
28882 eeee
28883 ffff
28884 gggg
28885 hhhh"});
28886
28887 // Add 2 cursors below (so we have 3 total)
28888 cx.update_editor(|editor, window, cx| {
28889 editor.add_selection_below(&Default::default(), window, cx);
28890 editor.add_selection_below(&Default::default(), window, cx);
28891 });
28892
28893 // Verify we have 3 cursors
28894 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
28895 assert_eq!(
28896 initial_count, 3,
28897 "Should have 3 cursors after adding 2 below"
28898 );
28899
28900 // Move down one line
28901 cx.update_editor(|editor, window, cx| {
28902 editor.move_down(&MoveDown, window, cx);
28903 });
28904
28905 // Add another cursor below
28906 cx.update_editor(|editor, window, cx| {
28907 editor.add_selection_below(&Default::default(), window, cx);
28908 });
28909
28910 // Should now have 4 cursors (3 original + 1 new)
28911 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
28912 assert_eq!(
28913 final_count, 4,
28914 "Should have 4 cursors after moving and adding another"
28915 );
28916}
28917
28918#[gpui::test]
28919async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
28920 init_test(cx, |_| {});
28921
28922 let mut cx = EditorTestContext::new(cx).await;
28923
28924 cx.set_state(indoc!(
28925 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
28926 Second line here"#
28927 ));
28928
28929 cx.update_editor(|editor, window, cx| {
28930 // Enable soft wrapping with a narrow width to force soft wrapping and
28931 // confirm that more than 2 rows are being displayed.
28932 editor.set_wrap_width(Some(100.0.into()), cx);
28933 assert!(editor.display_text(cx).lines().count() > 2);
28934
28935 editor.add_selection_below(
28936 &AddSelectionBelow {
28937 skip_soft_wrap: true,
28938 },
28939 window,
28940 cx,
28941 );
28942
28943 assert_eq!(
28944 display_ranges(editor, cx),
28945 &[
28946 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
28947 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
28948 ]
28949 );
28950
28951 editor.add_selection_above(
28952 &AddSelectionAbove {
28953 skip_soft_wrap: true,
28954 },
28955 window,
28956 cx,
28957 );
28958
28959 assert_eq!(
28960 display_ranges(editor, cx),
28961 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
28962 );
28963
28964 editor.add_selection_below(
28965 &AddSelectionBelow {
28966 skip_soft_wrap: false,
28967 },
28968 window,
28969 cx,
28970 );
28971
28972 assert_eq!(
28973 display_ranges(editor, cx),
28974 &[
28975 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
28976 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
28977 ]
28978 );
28979
28980 editor.add_selection_above(
28981 &AddSelectionAbove {
28982 skip_soft_wrap: false,
28983 },
28984 window,
28985 cx,
28986 );
28987
28988 assert_eq!(
28989 display_ranges(editor, cx),
28990 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
28991 );
28992 });
28993
28994 // Set up text where selections are in the middle of a soft-wrapped line.
28995 // When adding selection below with `skip_soft_wrap` set to `true`, the new
28996 // selection should be at the same buffer column, not the same pixel
28997 // position.
28998 cx.set_state(indoc!(
28999 r#"1. Very long line to show «howˇ» a wrapped line would look
29000 2. Very long line to show how a wrapped line would look"#
29001 ));
29002
29003 cx.update_editor(|editor, window, cx| {
29004 // Enable soft wrapping with a narrow width to force soft wrapping and
29005 // confirm that more than 2 rows are being displayed.
29006 editor.set_wrap_width(Some(100.0.into()), cx);
29007 assert!(editor.display_text(cx).lines().count() > 2);
29008
29009 editor.add_selection_below(
29010 &AddSelectionBelow {
29011 skip_soft_wrap: true,
29012 },
29013 window,
29014 cx,
29015 );
29016
29017 // Assert that there's now 2 selections, both selecting the same column
29018 // range in the buffer row.
29019 let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
29020 let selections = editor.selections.all::<Point>(&display_map);
29021 assert_eq!(selections.len(), 2);
29022 assert_eq!(selections[0].start.column, selections[1].start.column);
29023 assert_eq!(selections[0].end.column, selections[1].end.column);
29024 });
29025}
29026
29027#[gpui::test]
29028async fn test_insert_snippet(cx: &mut TestAppContext) {
29029 init_test(cx, |_| {});
29030 let mut cx = EditorTestContext::new(cx).await;
29031
29032 cx.update_editor(|editor, _, cx| {
29033 editor.project().unwrap().update(cx, |project, cx| {
29034 project.snippets().update(cx, |snippets, _cx| {
29035 let snippet = project::snippet_provider::Snippet {
29036 prefix: vec![], // no prefix needed!
29037 body: "an Unspecified".to_string(),
29038 description: Some("shhhh it's a secret".to_string()),
29039 name: "super secret snippet".to_string(),
29040 };
29041 snippets.add_snippet_for_test(
29042 None,
29043 PathBuf::from("test_snippets.json"),
29044 vec![Arc::new(snippet)],
29045 );
29046
29047 let snippet = project::snippet_provider::Snippet {
29048 prefix: vec![], // no prefix needed!
29049 body: " Location".to_string(),
29050 description: Some("the word 'location'".to_string()),
29051 name: "location word".to_string(),
29052 };
29053 snippets.add_snippet_for_test(
29054 Some("Markdown".to_string()),
29055 PathBuf::from("test_snippets.json"),
29056 vec![Arc::new(snippet)],
29057 );
29058 });
29059 })
29060 });
29061
29062 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
29063
29064 cx.update_editor(|editor, window, cx| {
29065 editor.insert_snippet_at_selections(
29066 &InsertSnippet {
29067 language: None,
29068 name: Some("super secret snippet".to_string()),
29069 snippet: None,
29070 },
29071 window,
29072 cx,
29073 );
29074
29075 // Language is specified in the action,
29076 // so the buffer language does not need to match
29077 editor.insert_snippet_at_selections(
29078 &InsertSnippet {
29079 language: Some("Markdown".to_string()),
29080 name: Some("location word".to_string()),
29081 snippet: None,
29082 },
29083 window,
29084 cx,
29085 );
29086
29087 editor.insert_snippet_at_selections(
29088 &InsertSnippet {
29089 language: None,
29090 name: None,
29091 snippet: Some("$0 after".to_string()),
29092 },
29093 window,
29094 cx,
29095 );
29096 });
29097
29098 cx.assert_editor_state(
29099 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
29100 );
29101}
29102
29103#[gpui::test]
29104async fn test_inlay_hints_request_timeout(cx: &mut TestAppContext) {
29105 use crate::inlays::inlay_hints::InlayHintRefreshReason;
29106 use crate::inlays::inlay_hints::tests::{cached_hint_labels, init_test, visible_hint_labels};
29107 use settings::InlayHintSettingsContent;
29108 use std::sync::atomic::AtomicU32;
29109 use std::time::Duration;
29110
29111 const BASE_TIMEOUT_SECS: u64 = 1;
29112
29113 let request_count = Arc::new(AtomicU32::new(0));
29114 let closure_request_count = request_count.clone();
29115
29116 init_test(cx, &|settings| {
29117 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
29118 enabled: Some(true),
29119 ..InlayHintSettingsContent::default()
29120 })
29121 });
29122 cx.update(|cx| {
29123 SettingsStore::update_global(cx, |store, cx| {
29124 store.update_user_settings(cx, &|settings: &mut SettingsContent| {
29125 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
29126 request_timeout: Some(BASE_TIMEOUT_SECS),
29127 button: Some(true),
29128 notifications: None,
29129 semantic_token_rules: None,
29130 });
29131 });
29132 });
29133 });
29134
29135 let fs = FakeFs::new(cx.executor());
29136 fs.insert_tree(
29137 path!("/a"),
29138 json!({
29139 "main.rs": "fn main() { let a = 5; }",
29140 }),
29141 )
29142 .await;
29143
29144 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
29145 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
29146 language_registry.add(rust_lang());
29147 let mut fake_servers = language_registry.register_fake_lsp(
29148 "Rust",
29149 FakeLspAdapter {
29150 capabilities: lsp::ServerCapabilities {
29151 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
29152 ..lsp::ServerCapabilities::default()
29153 },
29154 initializer: Some(Box::new(move |fake_server| {
29155 let request_count = closure_request_count.clone();
29156 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
29157 move |params, cx| {
29158 let request_count = request_count.clone();
29159 async move {
29160 cx.background_executor()
29161 .timer(Duration::from_secs(BASE_TIMEOUT_SECS * 2))
29162 .await;
29163 let count = request_count.fetch_add(1, atomic::Ordering::Release) + 1;
29164 assert_eq!(
29165 params.text_document.uri,
29166 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
29167 );
29168 Ok(Some(vec![lsp::InlayHint {
29169 position: lsp::Position::new(0, 1),
29170 label: lsp::InlayHintLabel::String(count.to_string()),
29171 kind: None,
29172 text_edits: None,
29173 tooltip: None,
29174 padding_left: None,
29175 padding_right: None,
29176 data: None,
29177 }]))
29178 }
29179 },
29180 );
29181 })),
29182 ..FakeLspAdapter::default()
29183 },
29184 );
29185
29186 let buffer = project
29187 .update(cx, |project, cx| {
29188 project.open_local_buffer(path!("/a/main.rs"), cx)
29189 })
29190 .await
29191 .unwrap();
29192 let editor = cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
29193
29194 cx.executor().run_until_parked();
29195 let fake_server = fake_servers.next().await.unwrap();
29196
29197 cx.executor()
29198 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS) + Duration::from_millis(100));
29199 cx.executor().run_until_parked();
29200 editor
29201 .update(cx, |editor, _window, cx| {
29202 assert!(
29203 cached_hint_labels(editor, cx).is_empty(),
29204 "First request should time out, no hints cached"
29205 );
29206 })
29207 .unwrap();
29208
29209 editor
29210 .update(cx, |editor, _window, cx| {
29211 editor.refresh_inlay_hints(
29212 InlayHintRefreshReason::RefreshRequested {
29213 server_id: fake_server.server.server_id(),
29214 request_id: Some(1),
29215 },
29216 cx,
29217 );
29218 })
29219 .unwrap();
29220 cx.executor()
29221 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS) + Duration::from_millis(100));
29222 cx.executor().run_until_parked();
29223 editor
29224 .update(cx, |editor, _window, cx| {
29225 assert!(
29226 cached_hint_labels(editor, cx).is_empty(),
29227 "Second request should also time out with BASE_TIMEOUT, no hints cached"
29228 );
29229 })
29230 .unwrap();
29231
29232 cx.update(|cx| {
29233 SettingsStore::update_global(cx, |store, cx| {
29234 store.update_user_settings(cx, |settings| {
29235 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
29236 request_timeout: Some(BASE_TIMEOUT_SECS * 4),
29237 button: Some(true),
29238 notifications: None,
29239 semantic_token_rules: None,
29240 });
29241 });
29242 });
29243 });
29244 editor
29245 .update(cx, |editor, _window, cx| {
29246 editor.refresh_inlay_hints(
29247 InlayHintRefreshReason::RefreshRequested {
29248 server_id: fake_server.server.server_id(),
29249 request_id: Some(2),
29250 },
29251 cx,
29252 );
29253 })
29254 .unwrap();
29255 cx.executor()
29256 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS * 4) + Duration::from_millis(100));
29257 cx.executor().run_until_parked();
29258 editor
29259 .update(cx, |editor, _window, cx| {
29260 assert_eq!(
29261 vec!["1".to_string()],
29262 cached_hint_labels(editor, cx),
29263 "With extended timeout (BASE * 4), hints should arrive successfully"
29264 );
29265 assert_eq!(vec!["1".to_string()], visible_hint_labels(editor, cx));
29266 })
29267 .unwrap();
29268}
29269
29270#[gpui::test]
29271async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
29272 init_test(cx, |_| {});
29273 let (editor, cx) = cx.add_window_view(Editor::single_line);
29274 editor.update_in(cx, |editor, window, cx| {
29275 editor.set_text("oops\n\nwow\n", window, cx)
29276 });
29277 cx.run_until_parked();
29278 editor.update(cx, |editor, cx| {
29279 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
29280 });
29281 editor.update(cx, |editor, cx| {
29282 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
29283 });
29284 cx.run_until_parked();
29285 editor.update(cx, |editor, cx| {
29286 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
29287 });
29288}
29289
29290#[gpui::test]
29291async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
29292 init_test(cx, |_| {});
29293
29294 cx.update(|cx| {
29295 register_project_item::<Editor>(cx);
29296 });
29297
29298 let fs = FakeFs::new(cx.executor());
29299 fs.insert_tree("/root1", json!({})).await;
29300 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
29301 .await;
29302
29303 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
29304 let (multi_workspace, cx) =
29305 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
29306 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
29307
29308 let worktree_id = project.update(cx, |project, cx| {
29309 project.worktrees(cx).next().unwrap().read(cx).id()
29310 });
29311
29312 let handle = workspace
29313 .update_in(cx, |workspace, window, cx| {
29314 let project_path = (worktree_id, rel_path("one.pdf"));
29315 workspace.open_path(project_path, None, true, window, cx)
29316 })
29317 .await
29318 .unwrap();
29319 // The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
29320 // Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
29321 // With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
29322 assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
29323}
29324
29325#[gpui::test]
29326async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
29327 init_test(cx, |_| {});
29328
29329 let language = Arc::new(Language::new(
29330 LanguageConfig::default(),
29331 Some(tree_sitter_rust::LANGUAGE.into()),
29332 ));
29333
29334 // Test hierarchical sibling navigation
29335 let text = r#"
29336 fn outer() {
29337 if condition {
29338 let a = 1;
29339 }
29340 let b = 2;
29341 }
29342
29343 fn another() {
29344 let c = 3;
29345 }
29346 "#;
29347
29348 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
29349 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
29350 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
29351
29352 // Wait for parsing to complete
29353 editor
29354 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
29355 .await;
29356
29357 editor.update_in(cx, |editor, window, cx| {
29358 // Start by selecting "let a = 1;" inside the if block
29359 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
29360 s.select_display_ranges([
29361 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
29362 ]);
29363 });
29364
29365 let initial_selection = editor
29366 .selections
29367 .display_ranges(&editor.display_snapshot(cx));
29368 assert_eq!(initial_selection.len(), 1, "Should have one selection");
29369
29370 // Test select next sibling - should move up levels to find the next sibling
29371 // Since "let a = 1;" has no siblings in the if block, it should move up
29372 // to find "let b = 2;" which is a sibling of the if block
29373 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
29374 let next_selection = editor
29375 .selections
29376 .display_ranges(&editor.display_snapshot(cx));
29377
29378 // Should have a selection and it should be different from the initial
29379 assert_eq!(
29380 next_selection.len(),
29381 1,
29382 "Should have one selection after next"
29383 );
29384 assert_ne!(
29385 next_selection[0], initial_selection[0],
29386 "Next sibling selection should be different"
29387 );
29388
29389 // Test hierarchical navigation by going to the end of the current function
29390 // and trying to navigate to the next function
29391 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
29392 s.select_display_ranges([
29393 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
29394 ]);
29395 });
29396
29397 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
29398 let function_next_selection = editor
29399 .selections
29400 .display_ranges(&editor.display_snapshot(cx));
29401
29402 // Should move to the next function
29403 assert_eq!(
29404 function_next_selection.len(),
29405 1,
29406 "Should have one selection after function next"
29407 );
29408
29409 // Test select previous sibling navigation
29410 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
29411 let prev_selection = editor
29412 .selections
29413 .display_ranges(&editor.display_snapshot(cx));
29414
29415 // Should have a selection and it should be different
29416 assert_eq!(
29417 prev_selection.len(),
29418 1,
29419 "Should have one selection after prev"
29420 );
29421 assert_ne!(
29422 prev_selection[0], function_next_selection[0],
29423 "Previous sibling selection should be different from next"
29424 );
29425 });
29426}
29427
29428#[gpui::test]
29429async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
29430 init_test(cx, |_| {});
29431
29432 let mut cx = EditorTestContext::new(cx).await;
29433 cx.set_state(
29434 "let ˇvariable = 42;
29435let another = variable + 1;
29436let result = variable * 2;",
29437 );
29438
29439 // Set up document highlights manually (simulating LSP response)
29440 cx.update_editor(|editor, _window, cx| {
29441 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
29442
29443 // Create highlights for "variable" occurrences
29444 let highlight_ranges = [
29445 Point::new(0, 4)..Point::new(0, 12), // First "variable"
29446 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
29447 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
29448 ];
29449
29450 let anchor_ranges: Vec<_> = highlight_ranges
29451 .iter()
29452 .map(|range| range.clone().to_anchors(&buffer_snapshot))
29453 .collect();
29454
29455 editor.highlight_background(
29456 HighlightKey::DocumentHighlightRead,
29457 &anchor_ranges,
29458 |_, theme| theme.colors().editor_document_highlight_read_background,
29459 cx,
29460 );
29461 });
29462
29463 // Go to next highlight - should move to second "variable"
29464 cx.update_editor(|editor, window, cx| {
29465 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
29466 });
29467 cx.assert_editor_state(
29468 "let variable = 42;
29469let another = ˇvariable + 1;
29470let result = variable * 2;",
29471 );
29472
29473 // Go to next highlight - should move to third "variable"
29474 cx.update_editor(|editor, window, cx| {
29475 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
29476 });
29477 cx.assert_editor_state(
29478 "let variable = 42;
29479let another = variable + 1;
29480let result = ˇvariable * 2;",
29481 );
29482
29483 // Go to next highlight - should stay at third "variable" (no wrap-around)
29484 cx.update_editor(|editor, window, cx| {
29485 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
29486 });
29487 cx.assert_editor_state(
29488 "let variable = 42;
29489let another = variable + 1;
29490let result = ˇvariable * 2;",
29491 );
29492
29493 // Now test going backwards from third position
29494 cx.update_editor(|editor, window, cx| {
29495 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
29496 });
29497 cx.assert_editor_state(
29498 "let variable = 42;
29499let another = ˇvariable + 1;
29500let result = variable * 2;",
29501 );
29502
29503 // Go to previous highlight - should move to first "variable"
29504 cx.update_editor(|editor, window, cx| {
29505 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
29506 });
29507 cx.assert_editor_state(
29508 "let ˇvariable = 42;
29509let another = variable + 1;
29510let result = variable * 2;",
29511 );
29512
29513 // Go to previous highlight - should stay on first "variable"
29514 cx.update_editor(|editor, window, cx| {
29515 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
29516 });
29517 cx.assert_editor_state(
29518 "let ˇvariable = 42;
29519let another = variable + 1;
29520let result = variable * 2;",
29521 );
29522}
29523
29524#[gpui::test]
29525async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
29526 cx: &mut gpui::TestAppContext,
29527) {
29528 init_test(cx, |_| {});
29529
29530 let url = "https://zed.dev";
29531
29532 let markdown_language = Arc::new(Language::new(
29533 LanguageConfig {
29534 name: "Markdown".into(),
29535 ..LanguageConfig::default()
29536 },
29537 None,
29538 ));
29539
29540 let mut cx = EditorTestContext::new(cx).await;
29541 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29542 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
29543
29544 cx.update_editor(|editor, window, cx| {
29545 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
29546 editor.paste(&Paste, window, cx);
29547 });
29548
29549 cx.assert_editor_state(&format!(
29550 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
29551 ));
29552}
29553
29554#[gpui::test]
29555async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
29556 init_test(cx, |_| {});
29557
29558 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29559 let mut cx = EditorTestContext::new(cx).await;
29560
29561 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29562
29563 // Case 1: Test if adding a character with multi cursors preserves nested list indents
29564 cx.set_state(&indoc! {"
29565 - [ ] Item 1
29566 - [ ] Item 1.a
29567 - [ˇ] Item 2
29568 - [ˇ] Item 2.a
29569 - [ˇ] Item 2.b
29570 "
29571 });
29572 cx.update_editor(|editor, window, cx| {
29573 editor.handle_input("x", window, cx);
29574 });
29575 cx.run_until_parked();
29576 cx.assert_editor_state(indoc! {"
29577 - [ ] Item 1
29578 - [ ] Item 1.a
29579 - [xˇ] Item 2
29580 - [xˇ] Item 2.a
29581 - [xˇ] Item 2.b
29582 "
29583 });
29584
29585 // Case 2: Test adding new line after nested list continues the list with unchecked task
29586 cx.set_state(&indoc! {"
29587 - [ ] Item 1
29588 - [ ] Item 1.a
29589 - [x] Item 2
29590 - [x] Item 2.a
29591 - [x] Item 2.bˇ"
29592 });
29593 cx.update_editor(|editor, window, cx| {
29594 editor.newline(&Newline, window, cx);
29595 });
29596 cx.assert_editor_state(indoc! {"
29597 - [ ] Item 1
29598 - [ ] Item 1.a
29599 - [x] Item 2
29600 - [x] Item 2.a
29601 - [x] Item 2.b
29602 - [ ] ˇ"
29603 });
29604
29605 // Case 3: Test adding content to continued list item
29606 cx.update_editor(|editor, window, cx| {
29607 editor.handle_input("Item 2.c", window, cx);
29608 });
29609 cx.run_until_parked();
29610 cx.assert_editor_state(indoc! {"
29611 - [ ] Item 1
29612 - [ ] Item 1.a
29613 - [x] Item 2
29614 - [x] Item 2.a
29615 - [x] Item 2.b
29616 - [ ] Item 2.cˇ"
29617 });
29618
29619 // Case 4: Test adding new line after nested ordered list continues with next number
29620 cx.set_state(indoc! {"
29621 1. Item 1
29622 1. Item 1.a
29623 2. Item 2
29624 1. Item 2.a
29625 2. Item 2.bˇ"
29626 });
29627 cx.update_editor(|editor, window, cx| {
29628 editor.newline(&Newline, window, cx);
29629 });
29630 cx.assert_editor_state(indoc! {"
29631 1. Item 1
29632 1. Item 1.a
29633 2. Item 2
29634 1. Item 2.a
29635 2. Item 2.b
29636 3. ˇ"
29637 });
29638
29639 // Case 5: Adding content to continued ordered list item
29640 cx.update_editor(|editor, window, cx| {
29641 editor.handle_input("Item 2.c", window, cx);
29642 });
29643 cx.run_until_parked();
29644 cx.assert_editor_state(indoc! {"
29645 1. Item 1
29646 1. Item 1.a
29647 2. Item 2
29648 1. Item 2.a
29649 2. Item 2.b
29650 3. Item 2.cˇ"
29651 });
29652
29653 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
29654 cx.set_state(indoc! {"
29655 - Item 1
29656 - Item 1.a
29657 - Item 1.a
29658 ˇ"});
29659 cx.update_editor(|editor, window, cx| {
29660 editor.handle_input("-", window, cx);
29661 });
29662 cx.run_until_parked();
29663 cx.assert_editor_state(indoc! {"
29664 - Item 1
29665 - Item 1.a
29666 - Item 1.a
29667 -ˇ"});
29668
29669 // Case 7: Test blockquote newline preserves something
29670 cx.set_state(indoc! {"
29671 > Item 1ˇ"
29672 });
29673 cx.update_editor(|editor, window, cx| {
29674 editor.newline(&Newline, window, cx);
29675 });
29676 cx.assert_editor_state(indoc! {"
29677 > Item 1
29678 ˇ"
29679 });
29680}
29681
29682#[gpui::test]
29683async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
29684 cx: &mut gpui::TestAppContext,
29685) {
29686 init_test(cx, |_| {});
29687
29688 let url = "https://zed.dev";
29689
29690 let markdown_language = Arc::new(Language::new(
29691 LanguageConfig {
29692 name: "Markdown".into(),
29693 ..LanguageConfig::default()
29694 },
29695 None,
29696 ));
29697
29698 let mut cx = EditorTestContext::new(cx).await;
29699 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29700 cx.set_state(&format!(
29701 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
29702 ));
29703
29704 cx.update_editor(|editor, window, cx| {
29705 editor.copy(&Copy, window, cx);
29706 });
29707
29708 cx.set_state(&format!(
29709 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
29710 ));
29711
29712 cx.update_editor(|editor, window, cx| {
29713 editor.paste(&Paste, window, cx);
29714 });
29715
29716 cx.assert_editor_state(&format!(
29717 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
29718 ));
29719}
29720
29721#[gpui::test]
29722async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
29723 cx: &mut gpui::TestAppContext,
29724) {
29725 init_test(cx, |_| {});
29726
29727 let url = "https://zed.dev";
29728
29729 let markdown_language = Arc::new(Language::new(
29730 LanguageConfig {
29731 name: "Markdown".into(),
29732 ..LanguageConfig::default()
29733 },
29734 None,
29735 ));
29736
29737 let mut cx = EditorTestContext::new(cx).await;
29738 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29739 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
29740
29741 cx.update_editor(|editor, window, cx| {
29742 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
29743 editor.paste(&Paste, window, cx);
29744 });
29745
29746 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
29747}
29748
29749#[gpui::test]
29750async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
29751 cx: &mut gpui::TestAppContext,
29752) {
29753 init_test(cx, |_| {});
29754
29755 let text = "Awesome";
29756
29757 let markdown_language = Arc::new(Language::new(
29758 LanguageConfig {
29759 name: "Markdown".into(),
29760 ..LanguageConfig::default()
29761 },
29762 None,
29763 ));
29764
29765 let mut cx = EditorTestContext::new(cx).await;
29766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29767 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
29768
29769 cx.update_editor(|editor, window, cx| {
29770 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
29771 editor.paste(&Paste, window, cx);
29772 });
29773
29774 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
29775}
29776
29777#[gpui::test]
29778async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
29779 cx: &mut gpui::TestAppContext,
29780) {
29781 init_test(cx, |_| {});
29782
29783 let url = "https://zed.dev";
29784
29785 let markdown_language = Arc::new(Language::new(
29786 LanguageConfig {
29787 name: "Rust".into(),
29788 ..LanguageConfig::default()
29789 },
29790 None,
29791 ));
29792
29793 let mut cx = EditorTestContext::new(cx).await;
29794 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29795 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
29796
29797 cx.update_editor(|editor, window, cx| {
29798 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
29799 editor.paste(&Paste, window, cx);
29800 });
29801
29802 cx.assert_editor_state(&format!(
29803 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
29804 ));
29805}
29806
29807#[gpui::test]
29808async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
29809 cx: &mut TestAppContext,
29810) {
29811 init_test(cx, |_| {});
29812
29813 let url = "https://zed.dev";
29814
29815 let markdown_language = Arc::new(Language::new(
29816 LanguageConfig {
29817 name: "Markdown".into(),
29818 ..LanguageConfig::default()
29819 },
29820 None,
29821 ));
29822
29823 let (editor, cx) = cx.add_window_view(|window, cx| {
29824 let multi_buffer = MultiBuffer::build_multi(
29825 [
29826 ("this will embed -> link", vec![Point::row_range(0..1)]),
29827 ("this will replace -> link", vec![Point::row_range(0..1)]),
29828 ],
29829 cx,
29830 );
29831 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
29832 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
29833 s.select_ranges(vec![
29834 Point::new(0, 19)..Point::new(0, 23),
29835 Point::new(1, 21)..Point::new(1, 25),
29836 ])
29837 });
29838 let first_buffer_id = multi_buffer
29839 .read(cx)
29840 .excerpt_buffer_ids()
29841 .into_iter()
29842 .next()
29843 .unwrap();
29844 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
29845 first_buffer.update(cx, |buffer, cx| {
29846 buffer.set_language(Some(markdown_language.clone()), cx);
29847 });
29848
29849 editor
29850 });
29851 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29852
29853 cx.update_editor(|editor, window, cx| {
29854 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
29855 editor.paste(&Paste, window, cx);
29856 });
29857
29858 cx.assert_editor_state(&format!(
29859 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
29860 ));
29861}
29862
29863#[gpui::test]
29864async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
29865 init_test(cx, |_| {});
29866
29867 let fs = FakeFs::new(cx.executor());
29868 fs.insert_tree(
29869 path!("/project"),
29870 json!({
29871 "first.rs": "# First Document\nSome content here.",
29872 "second.rs": "Plain text content for second file.",
29873 }),
29874 )
29875 .await;
29876
29877 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
29878 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
29879 let cx = &mut VisualTestContext::from_window(*window, cx);
29880
29881 let language = rust_lang();
29882 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
29883 language_registry.add(language.clone());
29884 let mut fake_servers = language_registry.register_fake_lsp(
29885 "Rust",
29886 FakeLspAdapter {
29887 ..FakeLspAdapter::default()
29888 },
29889 );
29890
29891 let buffer1 = project
29892 .update(cx, |project, cx| {
29893 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
29894 })
29895 .await
29896 .unwrap();
29897 let buffer2 = project
29898 .update(cx, |project, cx| {
29899 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
29900 })
29901 .await
29902 .unwrap();
29903
29904 let multi_buffer = cx.new(|cx| {
29905 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
29906 multi_buffer.set_excerpts_for_path(
29907 PathKey::for_buffer(&buffer1, cx),
29908 buffer1.clone(),
29909 [Point::zero()..buffer1.read(cx).max_point()],
29910 3,
29911 cx,
29912 );
29913 multi_buffer.set_excerpts_for_path(
29914 PathKey::for_buffer(&buffer2, cx),
29915 buffer2.clone(),
29916 [Point::zero()..buffer1.read(cx).max_point()],
29917 3,
29918 cx,
29919 );
29920 multi_buffer
29921 });
29922
29923 let (editor, cx) = cx.add_window_view(|window, cx| {
29924 Editor::new(
29925 EditorMode::full(),
29926 multi_buffer,
29927 Some(project.clone()),
29928 window,
29929 cx,
29930 )
29931 });
29932
29933 let fake_language_server = fake_servers.next().await.unwrap();
29934
29935 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
29936
29937 let save = editor.update_in(cx, |editor, window, cx| {
29938 assert!(editor.is_dirty(cx));
29939
29940 editor.save(
29941 SaveOptions {
29942 format: true,
29943 autosave: true,
29944 },
29945 project,
29946 window,
29947 cx,
29948 )
29949 });
29950 let (start_edit_tx, start_edit_rx) = oneshot::channel();
29951 let (done_edit_tx, done_edit_rx) = oneshot::channel();
29952 let mut done_edit_rx = Some(done_edit_rx);
29953 let mut start_edit_tx = Some(start_edit_tx);
29954
29955 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
29956 start_edit_tx.take().unwrap().send(()).unwrap();
29957 let done_edit_rx = done_edit_rx.take().unwrap();
29958 async move {
29959 done_edit_rx.await.unwrap();
29960 Ok(None)
29961 }
29962 });
29963
29964 start_edit_rx.await.unwrap();
29965 buffer2
29966 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
29967 .unwrap();
29968
29969 done_edit_tx.send(()).unwrap();
29970
29971 save.await.unwrap();
29972 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
29973}
29974
29975#[gpui::test]
29976fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
29977 init_test(cx, |_| {});
29978
29979 let editor = cx.add_window(|window, cx| {
29980 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
29981 build_editor(buffer, window, cx)
29982 });
29983
29984 editor
29985 .update(cx, |editor, window, cx| {
29986 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
29987 s.select_display_ranges([
29988 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
29989 ])
29990 });
29991
29992 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
29993
29994 assert_eq!(
29995 editor.display_text(cx),
29996 "line1\nline2\nline2",
29997 "Duplicating last line upward should create duplicate above, not on same line"
29998 );
29999
30000 assert_eq!(
30001 editor
30002 .selections
30003 .display_ranges(&editor.display_snapshot(cx)),
30004 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
30005 "Selection should move to the duplicated line"
30006 );
30007 })
30008 .unwrap();
30009}
30010
30011#[gpui::test]
30012async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
30013 init_test(cx, |_| {});
30014
30015 let mut cx = EditorTestContext::new(cx).await;
30016
30017 cx.set_state("line1\nline2ˇ");
30018
30019 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
30020
30021 let clipboard_text = cx
30022 .read_from_clipboard()
30023 .and_then(|item| item.text().as_deref().map(str::to_string));
30024
30025 assert_eq!(
30026 clipboard_text,
30027 Some("line2\n".to_string()),
30028 "Copying a line without trailing newline should include a newline"
30029 );
30030
30031 cx.set_state("line1\nˇ");
30032
30033 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
30034
30035 cx.assert_editor_state("line1\nline2\nˇ");
30036}
30037
30038#[gpui::test]
30039async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
30040 init_test(cx, |_| {});
30041
30042 let mut cx = EditorTestContext::new(cx).await;
30043
30044 cx.set_state("ˇline1\nˇline2\nˇline3\n");
30045
30046 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
30047
30048 let clipboard_text = cx
30049 .read_from_clipboard()
30050 .and_then(|item| item.text().as_deref().map(str::to_string));
30051
30052 assert_eq!(
30053 clipboard_text,
30054 Some("line1\nline2\nline3\n".to_string()),
30055 "Copying multiple lines should include a single newline between lines"
30056 );
30057
30058 cx.set_state("lineA\nˇ");
30059
30060 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
30061
30062 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
30063}
30064
30065#[gpui::test]
30066async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
30067 init_test(cx, |_| {});
30068
30069 let mut cx = EditorTestContext::new(cx).await;
30070
30071 cx.set_state("ˇline1\nˇline2\nˇline3\n");
30072
30073 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
30074
30075 let clipboard_text = cx
30076 .read_from_clipboard()
30077 .and_then(|item| item.text().as_deref().map(str::to_string));
30078
30079 assert_eq!(
30080 clipboard_text,
30081 Some("line1\nline2\nline3\n".to_string()),
30082 "Copying multiple lines should include a single newline between lines"
30083 );
30084
30085 cx.set_state("lineA\nˇ");
30086
30087 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
30088
30089 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
30090}
30091
30092#[gpui::test]
30093async fn test_end_of_editor_context(cx: &mut TestAppContext) {
30094 init_test(cx, |_| {});
30095
30096 let mut cx = EditorTestContext::new(cx).await;
30097
30098 cx.set_state("line1\nline2ˇ");
30099 cx.update_editor(|e, window, cx| {
30100 e.set_mode(EditorMode::SingleLine);
30101 assert!(e.key_context(window, cx).contains("end_of_input"));
30102 });
30103 cx.set_state("ˇline1\nline2");
30104 cx.update_editor(|e, window, cx| {
30105 assert!(!e.key_context(window, cx).contains("end_of_input"));
30106 });
30107 cx.set_state("line1ˇ\nline2");
30108 cx.update_editor(|e, window, cx| {
30109 assert!(!e.key_context(window, cx).contains("end_of_input"));
30110 });
30111}
30112
30113#[gpui::test]
30114async fn test_sticky_scroll(cx: &mut TestAppContext) {
30115 init_test(cx, |_| {});
30116 let mut cx = EditorTestContext::new(cx).await;
30117
30118 let buffer = indoc! {"
30119 ˇfn foo() {
30120 let abc = 123;
30121 }
30122 struct Bar;
30123 impl Bar {
30124 fn new() -> Self {
30125 Self
30126 }
30127 }
30128 fn baz() {
30129 }
30130 "};
30131 cx.set_state(&buffer);
30132
30133 cx.update_editor(|e, _, cx| {
30134 e.buffer()
30135 .read(cx)
30136 .as_singleton()
30137 .unwrap()
30138 .update(cx, |buffer, cx| {
30139 buffer.set_language(Some(rust_lang()), cx);
30140 })
30141 });
30142
30143 let mut sticky_headers = |offset: ScrollOffset| {
30144 cx.update_editor(|e, window, cx| {
30145 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
30146 });
30147 cx.run_until_parked();
30148 cx.update_editor(|e, window, cx| {
30149 EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
30150 .into_iter()
30151 .map(
30152 |StickyHeader {
30153 start_point,
30154 offset,
30155 ..
30156 }| { (start_point, offset) },
30157 )
30158 .collect::<Vec<_>>()
30159 })
30160 };
30161
30162 let fn_foo = Point { row: 0, column: 0 };
30163 let impl_bar = Point { row: 4, column: 0 };
30164 let fn_new = Point { row: 5, column: 4 };
30165
30166 assert_eq!(sticky_headers(0.0), vec![]);
30167 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
30168 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
30169 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
30170 assert_eq!(sticky_headers(2.0), vec![]);
30171 assert_eq!(sticky_headers(2.5), vec![]);
30172 assert_eq!(sticky_headers(3.0), vec![]);
30173 assert_eq!(sticky_headers(3.5), vec![]);
30174 assert_eq!(sticky_headers(4.0), vec![]);
30175 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
30176 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
30177 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
30178 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
30179 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
30180 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
30181 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
30182 assert_eq!(sticky_headers(8.0), vec![]);
30183 assert_eq!(sticky_headers(8.5), vec![]);
30184 assert_eq!(sticky_headers(9.0), vec![]);
30185 assert_eq!(sticky_headers(9.5), vec![]);
30186 assert_eq!(sticky_headers(10.0), vec![]);
30187}
30188
30189#[gpui::test]
30190async fn test_sticky_scroll_with_expanded_deleted_diff_hunks(
30191 executor: BackgroundExecutor,
30192 cx: &mut TestAppContext,
30193) {
30194 init_test(cx, |_| {});
30195 let mut cx = EditorTestContext::new(cx).await;
30196
30197 let diff_base = indoc! {"
30198 fn foo() {
30199 let a = 1;
30200 let b = 2;
30201 let c = 3;
30202 let d = 4;
30203 let e = 5;
30204 }
30205 "};
30206
30207 let buffer = indoc! {"
30208 ˇfn foo() {
30209 }
30210 "};
30211
30212 cx.set_state(&buffer);
30213
30214 cx.update_editor(|e, _, cx| {
30215 e.buffer()
30216 .read(cx)
30217 .as_singleton()
30218 .unwrap()
30219 .update(cx, |buffer, cx| {
30220 buffer.set_language(Some(rust_lang()), cx);
30221 })
30222 });
30223
30224 cx.set_head_text(diff_base);
30225 executor.run_until_parked();
30226
30227 cx.update_editor(|editor, window, cx| {
30228 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
30229 });
30230 executor.run_until_parked();
30231
30232 // After expanding, the display should look like:
30233 // row 0: fn foo() {
30234 // row 1: - let a = 1; (deleted)
30235 // row 2: - let b = 2; (deleted)
30236 // row 3: - let c = 3; (deleted)
30237 // row 4: - let d = 4; (deleted)
30238 // row 5: - let e = 5; (deleted)
30239 // row 6: }
30240 //
30241 // fn foo() spans display rows 0-6. Scrolling into the deleted region
30242 // (rows 1-5) should still show fn foo() as a sticky header.
30243
30244 let fn_foo = Point { row: 0, column: 0 };
30245
30246 let mut sticky_headers = |offset: ScrollOffset| {
30247 cx.update_editor(|e, window, cx| {
30248 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
30249 });
30250 cx.run_until_parked();
30251 cx.update_editor(|e, window, cx| {
30252 EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
30253 .into_iter()
30254 .map(
30255 |StickyHeader {
30256 start_point,
30257 offset,
30258 ..
30259 }| { (start_point, offset) },
30260 )
30261 .collect::<Vec<_>>()
30262 })
30263 };
30264
30265 assert_eq!(sticky_headers(0.0), vec![]);
30266 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
30267 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
30268 // Scrolling into deleted lines: fn foo() should still be a sticky header.
30269 assert_eq!(sticky_headers(2.0), vec![(fn_foo, 0.0)]);
30270 assert_eq!(sticky_headers(3.0), vec![(fn_foo, 0.0)]);
30271 assert_eq!(sticky_headers(4.0), vec![(fn_foo, 0.0)]);
30272 assert_eq!(sticky_headers(5.0), vec![(fn_foo, 0.0)]);
30273 assert_eq!(sticky_headers(5.5), vec![(fn_foo, -0.5)]);
30274 // Past the closing brace: no more sticky header.
30275 assert_eq!(sticky_headers(6.0), vec![]);
30276}
30277
30278#[gpui::test]
30279fn test_relative_line_numbers(cx: &mut TestAppContext) {
30280 init_test(cx, |_| {});
30281
30282 let buffer_1 = cx.new(|cx| Buffer::local("aaaaaaaaaa\nbbb\n", cx));
30283 let buffer_2 = cx.new(|cx| Buffer::local("cccccccccc\nddd\n", cx));
30284 let buffer_3 = cx.new(|cx| Buffer::local("eee\nffffffffff\n", cx));
30285
30286 let multibuffer = cx.new(|cx| {
30287 let mut multibuffer = MultiBuffer::new(ReadWrite);
30288 multibuffer.set_excerpts_for_path(
30289 PathKey::sorted(0),
30290 buffer_1.clone(),
30291 [Point::new(0, 0)..Point::new(2, 0)],
30292 0,
30293 cx,
30294 );
30295 multibuffer.set_excerpts_for_path(
30296 PathKey::sorted(1),
30297 buffer_2.clone(),
30298 [Point::new(0, 0)..Point::new(2, 0)],
30299 0,
30300 cx,
30301 );
30302 multibuffer.set_excerpts_for_path(
30303 PathKey::sorted(2),
30304 buffer_3.clone(),
30305 [Point::new(0, 0)..Point::new(2, 0)],
30306 0,
30307 cx,
30308 );
30309 multibuffer
30310 });
30311
30312 // wrapped contents of multibuffer:
30313 // aaa
30314 // aaa
30315 // aaa
30316 // a
30317 // bbb
30318 //
30319 // ccc
30320 // ccc
30321 // ccc
30322 // c
30323 // ddd
30324 //
30325 // eee
30326 // fff
30327 // fff
30328 // fff
30329 // f
30330
30331 let editor = cx.add_window(|window, cx| build_editor(multibuffer, window, cx));
30332 _ = editor.update(cx, |editor, window, cx| {
30333 editor.set_wrap_width(Some(30.0.into()), cx); // every 3 characters
30334
30335 // includes trailing newlines.
30336 let expected_line_numbers = [2, 6, 7, 10, 14, 15, 18, 19, 23];
30337 let expected_wrapped_line_numbers = [
30338 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23,
30339 ];
30340
30341 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
30342 s.select_ranges([
30343 Point::new(7, 0)..Point::new(7, 1), // second row of `ccc`
30344 ]);
30345 });
30346
30347 let snapshot = editor.snapshot(window, cx);
30348
30349 // these are all 0-indexed
30350 let base_display_row = DisplayRow(11);
30351 let base_row = 3;
30352 let wrapped_base_row = 7;
30353
30354 // test not counting wrapped lines
30355 let expected_relative_numbers = expected_line_numbers
30356 .into_iter()
30357 .enumerate()
30358 .map(|(i, row)| (DisplayRow(row), i.abs_diff(base_row) as u32))
30359 .filter(|(_, relative_line_number)| *relative_line_number != 0)
30360 .collect_vec();
30361 let actual_relative_numbers = snapshot
30362 .calculate_relative_line_numbers(
30363 &(DisplayRow(0)..DisplayRow(24)),
30364 base_display_row,
30365 false,
30366 )
30367 .into_iter()
30368 .sorted()
30369 .collect_vec();
30370 assert_eq!(expected_relative_numbers, actual_relative_numbers);
30371 // check `calculate_relative_line_numbers()` against `relative_line_delta()` for each line
30372 for (display_row, relative_number) in expected_relative_numbers {
30373 assert_eq!(
30374 relative_number,
30375 snapshot
30376 .relative_line_delta(display_row, base_display_row, false)
30377 .unsigned_abs() as u32,
30378 );
30379 }
30380
30381 // test counting wrapped lines
30382 let expected_wrapped_relative_numbers = expected_wrapped_line_numbers
30383 .into_iter()
30384 .enumerate()
30385 .map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32))
30386 .filter(|(row, _)| *row != base_display_row)
30387 .collect_vec();
30388 let actual_relative_numbers = snapshot
30389 .calculate_relative_line_numbers(
30390 &(DisplayRow(0)..DisplayRow(24)),
30391 base_display_row,
30392 true,
30393 )
30394 .into_iter()
30395 .sorted()
30396 .collect_vec();
30397 assert_eq!(expected_wrapped_relative_numbers, actual_relative_numbers);
30398 // check `calculate_relative_line_numbers()` against `relative_wrapped_line_delta()` for each line
30399 for (display_row, relative_number) in expected_wrapped_relative_numbers {
30400 assert_eq!(
30401 relative_number,
30402 snapshot
30403 .relative_line_delta(display_row, base_display_row, true)
30404 .unsigned_abs() as u32,
30405 );
30406 }
30407 });
30408}
30409
30410#[gpui::test]
30411async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
30412 init_test(cx, |_| {});
30413 cx.update(|cx| {
30414 SettingsStore::update_global(cx, |store, cx| {
30415 store.update_user_settings(cx, |settings| {
30416 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
30417 enabled: Some(true),
30418 })
30419 });
30420 });
30421 });
30422 let mut cx = EditorTestContext::new(cx).await;
30423
30424 let line_height = cx.update_editor(|editor, window, cx| {
30425 editor
30426 .style(cx)
30427 .text
30428 .line_height_in_pixels(window.rem_size())
30429 });
30430
30431 let buffer = indoc! {"
30432 ˇfn foo() {
30433 let abc = 123;
30434 }
30435 struct Bar;
30436 impl Bar {
30437 fn new() -> Self {
30438 Self
30439 }
30440 }
30441 fn baz() {
30442 }
30443 "};
30444 cx.set_state(&buffer);
30445
30446 cx.update_editor(|e, _, cx| {
30447 e.buffer()
30448 .read(cx)
30449 .as_singleton()
30450 .unwrap()
30451 .update(cx, |buffer, cx| {
30452 buffer.set_language(Some(rust_lang()), cx);
30453 })
30454 });
30455
30456 let fn_foo = || empty_range(0, 0);
30457 let impl_bar = || empty_range(4, 0);
30458 let fn_new = || empty_range(5, 4);
30459
30460 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
30461 cx.update_editor(|e, window, cx| {
30462 e.scroll(
30463 gpui::Point {
30464 x: 0.,
30465 y: scroll_offset,
30466 },
30467 None,
30468 window,
30469 cx,
30470 );
30471 });
30472 cx.run_until_parked();
30473 cx.simulate_click(
30474 gpui::Point {
30475 x: px(0.),
30476 y: click_offset as f32 * line_height,
30477 },
30478 Modifiers::none(),
30479 );
30480 cx.run_until_parked();
30481 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
30482 };
30483 assert_eq!(
30484 scroll_and_click(
30485 4.5, // impl Bar is halfway off the screen
30486 0.0 // click top of screen
30487 ),
30488 // scrolled to impl Bar
30489 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
30490 );
30491
30492 assert_eq!(
30493 scroll_and_click(
30494 4.5, // impl Bar is halfway off the screen
30495 0.25 // click middle of impl Bar
30496 ),
30497 // scrolled to impl Bar
30498 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
30499 );
30500
30501 assert_eq!(
30502 scroll_and_click(
30503 4.5, // impl Bar is halfway off the screen
30504 1.5 // click below impl Bar (e.g. fn new())
30505 ),
30506 // scrolled to fn new() - this is below the impl Bar header which has persisted
30507 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
30508 );
30509
30510 assert_eq!(
30511 scroll_and_click(
30512 5.5, // fn new is halfway underneath impl Bar
30513 0.75 // click on the overlap of impl Bar and fn new()
30514 ),
30515 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
30516 );
30517
30518 assert_eq!(
30519 scroll_and_click(
30520 5.5, // fn new is halfway underneath impl Bar
30521 1.25 // click on the visible part of fn new()
30522 ),
30523 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
30524 );
30525
30526 assert_eq!(
30527 scroll_and_click(
30528 1.5, // fn foo is halfway off the screen
30529 0.0 // click top of screen
30530 ),
30531 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
30532 );
30533
30534 assert_eq!(
30535 scroll_and_click(
30536 1.5, // fn foo is halfway off the screen
30537 0.75 // click visible part of let abc...
30538 )
30539 .0,
30540 // no change in scroll
30541 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
30542 (gpui::Point { x: 0., y: 1.5 })
30543 );
30544}
30545
30546#[gpui::test]
30547async fn test_next_prev_reference(cx: &mut TestAppContext) {
30548 const CYCLE_POSITIONS: &[&'static str] = &[
30549 indoc! {"
30550 fn foo() {
30551 let ˇabc = 123;
30552 let x = abc + 1;
30553 let y = abc + 2;
30554 let z = abc + 2;
30555 }
30556 "},
30557 indoc! {"
30558 fn foo() {
30559 let abc = 123;
30560 let x = ˇabc + 1;
30561 let y = abc + 2;
30562 let z = abc + 2;
30563 }
30564 "},
30565 indoc! {"
30566 fn foo() {
30567 let abc = 123;
30568 let x = abc + 1;
30569 let y = ˇabc + 2;
30570 let z = abc + 2;
30571 }
30572 "},
30573 indoc! {"
30574 fn foo() {
30575 let abc = 123;
30576 let x = abc + 1;
30577 let y = abc + 2;
30578 let z = ˇabc + 2;
30579 }
30580 "},
30581 ];
30582
30583 init_test(cx, |_| {});
30584
30585 let mut cx = EditorLspTestContext::new_rust(
30586 lsp::ServerCapabilities {
30587 references_provider: Some(lsp::OneOf::Left(true)),
30588 ..Default::default()
30589 },
30590 cx,
30591 )
30592 .await;
30593
30594 // importantly, the cursor is in the middle
30595 cx.set_state(indoc! {"
30596 fn foo() {
30597 let aˇbc = 123;
30598 let x = abc + 1;
30599 let y = abc + 2;
30600 let z = abc + 2;
30601 }
30602 "});
30603
30604 let reference_ranges = [
30605 lsp::Position::new(1, 8),
30606 lsp::Position::new(2, 12),
30607 lsp::Position::new(3, 12),
30608 lsp::Position::new(4, 12),
30609 ]
30610 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
30611
30612 cx.lsp
30613 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
30614 Ok(Some(
30615 reference_ranges
30616 .map(|range| lsp::Location {
30617 uri: params.text_document_position.text_document.uri.clone(),
30618 range,
30619 })
30620 .to_vec(),
30621 ))
30622 });
30623
30624 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
30625 cx.update_editor(|editor, window, cx| {
30626 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
30627 })
30628 .unwrap()
30629 .await
30630 .unwrap()
30631 };
30632
30633 _move(Direction::Next, 1, &mut cx).await;
30634 cx.assert_editor_state(CYCLE_POSITIONS[1]);
30635
30636 _move(Direction::Next, 1, &mut cx).await;
30637 cx.assert_editor_state(CYCLE_POSITIONS[2]);
30638
30639 _move(Direction::Next, 1, &mut cx).await;
30640 cx.assert_editor_state(CYCLE_POSITIONS[3]);
30641
30642 // loops back to the start
30643 _move(Direction::Next, 1, &mut cx).await;
30644 cx.assert_editor_state(CYCLE_POSITIONS[0]);
30645
30646 // loops back to the end
30647 _move(Direction::Prev, 1, &mut cx).await;
30648 cx.assert_editor_state(CYCLE_POSITIONS[3]);
30649
30650 _move(Direction::Prev, 1, &mut cx).await;
30651 cx.assert_editor_state(CYCLE_POSITIONS[2]);
30652
30653 _move(Direction::Prev, 1, &mut cx).await;
30654 cx.assert_editor_state(CYCLE_POSITIONS[1]);
30655
30656 _move(Direction::Prev, 1, &mut cx).await;
30657 cx.assert_editor_state(CYCLE_POSITIONS[0]);
30658
30659 _move(Direction::Next, 3, &mut cx).await;
30660 cx.assert_editor_state(CYCLE_POSITIONS[3]);
30661
30662 _move(Direction::Prev, 2, &mut cx).await;
30663 cx.assert_editor_state(CYCLE_POSITIONS[1]);
30664}
30665
30666#[gpui::test]
30667async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
30668 init_test(cx, |_| {});
30669
30670 let (editor, cx) = cx.add_window_view(|window, cx| {
30671 let multi_buffer = MultiBuffer::build_multi(
30672 [
30673 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
30674 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
30675 ],
30676 cx,
30677 );
30678 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
30679 });
30680
30681 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
30682 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
30683
30684 cx.assert_excerpts_with_selections(indoc! {"
30685 [EXCERPT]
30686 ˇ1
30687 2
30688 3
30689 [EXCERPT]
30690 1
30691 2
30692 3
30693 "});
30694
30695 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
30696 cx.update_editor(|editor, window, cx| {
30697 editor.change_selections(None.into(), window, cx, |s| {
30698 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
30699 });
30700 });
30701 cx.assert_excerpts_with_selections(indoc! {"
30702 [EXCERPT]
30703 1
30704 2ˇ
30705 3
30706 [EXCERPT]
30707 1
30708 2
30709 3
30710 "});
30711
30712 cx.update_editor(|editor, window, cx| {
30713 editor
30714 .select_all_matches(&SelectAllMatches, window, cx)
30715 .unwrap();
30716 });
30717 cx.assert_excerpts_with_selections(indoc! {"
30718 [EXCERPT]
30719 1
30720 2ˇ
30721 3
30722 [EXCERPT]
30723 1
30724 2ˇ
30725 3
30726 "});
30727
30728 cx.update_editor(|editor, window, cx| {
30729 editor.handle_input("X", window, cx);
30730 });
30731 cx.assert_excerpts_with_selections(indoc! {"
30732 [EXCERPT]
30733 1
30734 Xˇ
30735 3
30736 [EXCERPT]
30737 1
30738 Xˇ
30739 3
30740 "});
30741
30742 // Scenario 2: Select "2", then fold second buffer before insertion
30743 cx.update_multibuffer(|mb, cx| {
30744 for buffer_id in buffer_ids.iter() {
30745 let buffer = mb.buffer(*buffer_id).unwrap();
30746 buffer.update(cx, |buffer, cx| {
30747 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
30748 });
30749 }
30750 });
30751
30752 // Select "2" and select all matches
30753 cx.update_editor(|editor, window, cx| {
30754 editor.change_selections(None.into(), window, cx, |s| {
30755 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
30756 });
30757 editor
30758 .select_all_matches(&SelectAllMatches, window, cx)
30759 .unwrap();
30760 });
30761
30762 // Fold second buffer - should remove selections from folded buffer
30763 cx.update_editor(|editor, _, cx| {
30764 editor.fold_buffer(buffer_ids[1], cx);
30765 });
30766 cx.assert_excerpts_with_selections(indoc! {"
30767 [EXCERPT]
30768 1
30769 2ˇ
30770 3
30771 [EXCERPT]
30772 [FOLDED]
30773 "});
30774
30775 // Insert text - should only affect first buffer
30776 cx.update_editor(|editor, window, cx| {
30777 editor.handle_input("Y", window, cx);
30778 });
30779 cx.update_editor(|editor, _, cx| {
30780 editor.unfold_buffer(buffer_ids[1], cx);
30781 });
30782 cx.assert_excerpts_with_selections(indoc! {"
30783 [EXCERPT]
30784 1
30785 Yˇ
30786 3
30787 [EXCERPT]
30788 1
30789 2
30790 3
30791 "});
30792
30793 // Scenario 3: Select "2", then fold first buffer before insertion
30794 cx.update_multibuffer(|mb, cx| {
30795 for buffer_id in buffer_ids.iter() {
30796 let buffer = mb.buffer(*buffer_id).unwrap();
30797 buffer.update(cx, |buffer, cx| {
30798 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
30799 });
30800 }
30801 });
30802
30803 // Select "2" and select all matches
30804 cx.update_editor(|editor, window, cx| {
30805 editor.change_selections(None.into(), window, cx, |s| {
30806 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
30807 });
30808 editor
30809 .select_all_matches(&SelectAllMatches, window, cx)
30810 .unwrap();
30811 });
30812
30813 // Fold first buffer - should remove selections from folded buffer
30814 cx.update_editor(|editor, _, cx| {
30815 editor.fold_buffer(buffer_ids[0], cx);
30816 });
30817 cx.assert_excerpts_with_selections(indoc! {"
30818 [EXCERPT]
30819 [FOLDED]
30820 [EXCERPT]
30821 1
30822 2ˇ
30823 3
30824 "});
30825
30826 // Insert text - should only affect second buffer
30827 cx.update_editor(|editor, window, cx| {
30828 editor.handle_input("Z", window, cx);
30829 });
30830 cx.update_editor(|editor, _, cx| {
30831 editor.unfold_buffer(buffer_ids[0], cx);
30832 });
30833 cx.assert_excerpts_with_selections(indoc! {"
30834 [EXCERPT]
30835 1
30836 2
30837 3
30838 [EXCERPT]
30839 1
30840 Zˇ
30841 3
30842 "});
30843
30844 // Test correct folded header is selected upon fold
30845 cx.update_editor(|editor, _, cx| {
30846 editor.fold_buffer(buffer_ids[0], cx);
30847 editor.fold_buffer(buffer_ids[1], cx);
30848 });
30849 cx.assert_excerpts_with_selections(indoc! {"
30850 [EXCERPT]
30851 [FOLDED]
30852 [EXCERPT]
30853 ˇ[FOLDED]
30854 "});
30855
30856 // Test selection inside folded buffer unfolds it on type
30857 cx.update_editor(|editor, window, cx| {
30858 editor.handle_input("W", window, cx);
30859 });
30860 cx.update_editor(|editor, _, cx| {
30861 editor.unfold_buffer(buffer_ids[0], cx);
30862 });
30863 cx.assert_excerpts_with_selections(indoc! {"
30864 [EXCERPT]
30865 1
30866 2
30867 3
30868 [EXCERPT]
30869 Wˇ1
30870 Z
30871 3
30872 "});
30873}
30874
30875#[gpui::test]
30876async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
30877 init_test(cx, |_| {});
30878
30879 let (editor, cx) = cx.add_window_view(|window, cx| {
30880 let multi_buffer = MultiBuffer::build_multi(
30881 [
30882 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
30883 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
30884 ],
30885 cx,
30886 );
30887 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
30888 });
30889
30890 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
30891
30892 cx.assert_excerpts_with_selections(indoc! {"
30893 [EXCERPT]
30894 ˇ1
30895 2
30896 3
30897 [EXCERPT]
30898 1
30899 2
30900 3
30901 4
30902 5
30903 6
30904 7
30905 8
30906 9
30907 "});
30908
30909 cx.update_editor(|editor, window, cx| {
30910 editor.change_selections(None.into(), window, cx, |s| {
30911 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
30912 });
30913 });
30914
30915 cx.assert_excerpts_with_selections(indoc! {"
30916 [EXCERPT]
30917 1
30918 2
30919 3
30920 [EXCERPT]
30921 1
30922 2
30923 3
30924 4
30925 5
30926 6
30927 ˇ7
30928 8
30929 9
30930 "});
30931
30932 cx.update_editor(|editor, _window, cx| {
30933 editor.set_vertical_scroll_margin(0, cx);
30934 });
30935
30936 cx.update_editor(|editor, window, cx| {
30937 assert_eq!(editor.vertical_scroll_margin(), 0);
30938 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
30939 assert_eq!(
30940 editor.snapshot(window, cx).scroll_position(),
30941 gpui::Point::new(0., 12.0)
30942 );
30943 });
30944
30945 cx.update_editor(|editor, _window, cx| {
30946 editor.set_vertical_scroll_margin(3, cx);
30947 });
30948
30949 cx.update_editor(|editor, window, cx| {
30950 assert_eq!(editor.vertical_scroll_margin(), 3);
30951 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
30952 assert_eq!(
30953 editor.snapshot(window, cx).scroll_position(),
30954 gpui::Point::new(0., 9.0)
30955 );
30956 });
30957}
30958
30959#[gpui::test]
30960async fn test_find_references_single_case(cx: &mut TestAppContext) {
30961 init_test(cx, |_| {});
30962 let mut cx = EditorLspTestContext::new_rust(
30963 lsp::ServerCapabilities {
30964 references_provider: Some(lsp::OneOf::Left(true)),
30965 ..lsp::ServerCapabilities::default()
30966 },
30967 cx,
30968 )
30969 .await;
30970
30971 let before = indoc!(
30972 r#"
30973 fn main() {
30974 let aˇbc = 123;
30975 let xyz = abc;
30976 }
30977 "#
30978 );
30979 let after = indoc!(
30980 r#"
30981 fn main() {
30982 let abc = 123;
30983 let xyz = ˇabc;
30984 }
30985 "#
30986 );
30987
30988 cx.lsp
30989 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
30990 Ok(Some(vec![
30991 lsp::Location {
30992 uri: params.text_document_position.text_document.uri.clone(),
30993 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
30994 },
30995 lsp::Location {
30996 uri: params.text_document_position.text_document.uri,
30997 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
30998 },
30999 ]))
31000 });
31001
31002 cx.set_state(before);
31003
31004 let action = FindAllReferences {
31005 always_open_multibuffer: false,
31006 };
31007
31008 let navigated = cx
31009 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
31010 .expect("should have spawned a task")
31011 .await
31012 .unwrap();
31013
31014 assert_eq!(navigated, Navigated::No);
31015
31016 cx.run_until_parked();
31017
31018 cx.assert_editor_state(after);
31019}
31020
31021#[gpui::test]
31022async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
31023 init_test(cx, |settings| {
31024 settings.defaults.tab_size = Some(2.try_into().unwrap());
31025 });
31026
31027 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
31028 let mut cx = EditorTestContext::new(cx).await;
31029 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31030
31031 // Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
31032 cx.set_state(indoc! {"
31033 - [ ] taskˇ
31034 "});
31035 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31036 cx.wait_for_autoindent_applied().await;
31037 cx.assert_editor_state(indoc! {"
31038 - [ ] task
31039 - [ ] ˇ
31040 "});
31041
31042 // Case 2: Works with checked task items too
31043 cx.set_state(indoc! {"
31044 - [x] completed taskˇ
31045 "});
31046 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31047 cx.wait_for_autoindent_applied().await;
31048 cx.assert_editor_state(indoc! {"
31049 - [x] completed task
31050 - [ ] ˇ
31051 "});
31052
31053 // Case 2.1: Works with uppercase checked marker too
31054 cx.set_state(indoc! {"
31055 - [X] completed taskˇ
31056 "});
31057 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31058 cx.wait_for_autoindent_applied().await;
31059 cx.assert_editor_state(indoc! {"
31060 - [X] completed task
31061 - [ ] ˇ
31062 "});
31063
31064 // Case 3: Cursor position doesn't matter - content after marker is what counts
31065 cx.set_state(indoc! {"
31066 - [ ] taˇsk
31067 "});
31068 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31069 cx.wait_for_autoindent_applied().await;
31070 cx.assert_editor_state(indoc! {"
31071 - [ ] ta
31072 - [ ] ˇsk
31073 "});
31074
31075 // Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
31076 cx.set_state(indoc! {"
31077 - [ ] ˇ
31078 "});
31079 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31080 cx.wait_for_autoindent_applied().await;
31081 cx.assert_editor_state(
31082 indoc! {"
31083 - [ ]$$
31084 ˇ
31085 "}
31086 .replace("$", " ")
31087 .as_str(),
31088 );
31089
31090 // Case 5: Adding newline with content adds marker preserving indentation
31091 cx.set_state(indoc! {"
31092 - [ ] task
31093 - [ ] indentedˇ
31094 "});
31095 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31096 cx.wait_for_autoindent_applied().await;
31097 cx.assert_editor_state(indoc! {"
31098 - [ ] task
31099 - [ ] indented
31100 - [ ] ˇ
31101 "});
31102
31103 // Case 6: Adding newline with cursor right after prefix, unindents
31104 cx.set_state(indoc! {"
31105 - [ ] task
31106 - [ ] sub task
31107 - [ ] ˇ
31108 "});
31109 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31110 cx.wait_for_autoindent_applied().await;
31111 cx.assert_editor_state(indoc! {"
31112 - [ ] task
31113 - [ ] sub task
31114 - [ ] ˇ
31115 "});
31116 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31117 cx.wait_for_autoindent_applied().await;
31118
31119 // Case 7: Adding newline with cursor right after prefix, removes marker
31120 cx.assert_editor_state(indoc! {"
31121 - [ ] task
31122 - [ ] sub task
31123 - [ ] ˇ
31124 "});
31125 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31126 cx.wait_for_autoindent_applied().await;
31127 cx.assert_editor_state(indoc! {"
31128 - [ ] task
31129 - [ ] sub task
31130 ˇ
31131 "});
31132
31133 // Case 8: Cursor before or inside prefix does not add marker
31134 cx.set_state(indoc! {"
31135 ˇ- [ ] task
31136 "});
31137 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31138 cx.wait_for_autoindent_applied().await;
31139 cx.assert_editor_state(indoc! {"
31140
31141 ˇ- [ ] task
31142 "});
31143
31144 cx.set_state(indoc! {"
31145 - [ˇ ] task
31146 "});
31147 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31148 cx.wait_for_autoindent_applied().await;
31149 cx.assert_editor_state(indoc! {"
31150 - [
31151 ˇ
31152 ] task
31153 "});
31154}
31155
31156#[gpui::test]
31157async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
31158 init_test(cx, |settings| {
31159 settings.defaults.tab_size = Some(2.try_into().unwrap());
31160 });
31161
31162 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
31163 let mut cx = EditorTestContext::new(cx).await;
31164 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31165
31166 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
31167 cx.set_state(indoc! {"
31168 - itemˇ
31169 "});
31170 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31171 cx.wait_for_autoindent_applied().await;
31172 cx.assert_editor_state(indoc! {"
31173 - item
31174 - ˇ
31175 "});
31176
31177 // Case 2: Works with different markers
31178 cx.set_state(indoc! {"
31179 * starred itemˇ
31180 "});
31181 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31182 cx.wait_for_autoindent_applied().await;
31183 cx.assert_editor_state(indoc! {"
31184 * starred item
31185 * ˇ
31186 "});
31187
31188 cx.set_state(indoc! {"
31189 + plus itemˇ
31190 "});
31191 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31192 cx.wait_for_autoindent_applied().await;
31193 cx.assert_editor_state(indoc! {"
31194 + plus item
31195 + ˇ
31196 "});
31197
31198 // Case 3: Cursor position doesn't matter - content after marker is what counts
31199 cx.set_state(indoc! {"
31200 - itˇem
31201 "});
31202 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31203 cx.wait_for_autoindent_applied().await;
31204 cx.assert_editor_state(indoc! {"
31205 - it
31206 - ˇem
31207 "});
31208
31209 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
31210 cx.set_state(indoc! {"
31211 - ˇ
31212 "});
31213 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31214 cx.wait_for_autoindent_applied().await;
31215 cx.assert_editor_state(
31216 indoc! {"
31217 - $
31218 ˇ
31219 "}
31220 .replace("$", " ")
31221 .as_str(),
31222 );
31223
31224 // Case 5: Adding newline with content adds marker preserving indentation
31225 cx.set_state(indoc! {"
31226 - item
31227 - indentedˇ
31228 "});
31229 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31230 cx.wait_for_autoindent_applied().await;
31231 cx.assert_editor_state(indoc! {"
31232 - item
31233 - indented
31234 - ˇ
31235 "});
31236
31237 // Case 6: Adding newline with cursor right after marker, unindents
31238 cx.set_state(indoc! {"
31239 - item
31240 - sub item
31241 - ˇ
31242 "});
31243 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31244 cx.wait_for_autoindent_applied().await;
31245 cx.assert_editor_state(indoc! {"
31246 - item
31247 - sub item
31248 - ˇ
31249 "});
31250 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31251 cx.wait_for_autoindent_applied().await;
31252
31253 // Case 7: Adding newline with cursor right after marker, removes marker
31254 cx.assert_editor_state(indoc! {"
31255 - item
31256 - sub item
31257 - ˇ
31258 "});
31259 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31260 cx.wait_for_autoindent_applied().await;
31261 cx.assert_editor_state(indoc! {"
31262 - item
31263 - sub item
31264 ˇ
31265 "});
31266
31267 // Case 8: Cursor before or inside prefix does not add marker
31268 cx.set_state(indoc! {"
31269 ˇ- item
31270 "});
31271 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31272 cx.wait_for_autoindent_applied().await;
31273 cx.assert_editor_state(indoc! {"
31274
31275 ˇ- item
31276 "});
31277
31278 cx.set_state(indoc! {"
31279 -ˇ item
31280 "});
31281 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31282 cx.wait_for_autoindent_applied().await;
31283 cx.assert_editor_state(indoc! {"
31284 -
31285 ˇitem
31286 "});
31287}
31288
31289#[gpui::test]
31290async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
31291 init_test(cx, |settings| {
31292 settings.defaults.tab_size = Some(2.try_into().unwrap());
31293 });
31294
31295 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
31296 let mut cx = EditorTestContext::new(cx).await;
31297 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31298
31299 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
31300 cx.set_state(indoc! {"
31301 1. first itemˇ
31302 "});
31303 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31304 cx.wait_for_autoindent_applied().await;
31305 cx.assert_editor_state(indoc! {"
31306 1. first item
31307 2. ˇ
31308 "});
31309
31310 // Case 2: Works with larger numbers
31311 cx.set_state(indoc! {"
31312 10. tenth itemˇ
31313 "});
31314 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31315 cx.wait_for_autoindent_applied().await;
31316 cx.assert_editor_state(indoc! {"
31317 10. tenth item
31318 11. ˇ
31319 "});
31320
31321 // Case 3: Cursor position doesn't matter - content after marker is what counts
31322 cx.set_state(indoc! {"
31323 1. itˇem
31324 "});
31325 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31326 cx.wait_for_autoindent_applied().await;
31327 cx.assert_editor_state(indoc! {"
31328 1. it
31329 2. ˇem
31330 "});
31331
31332 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
31333 cx.set_state(indoc! {"
31334 1. ˇ
31335 "});
31336 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31337 cx.wait_for_autoindent_applied().await;
31338 cx.assert_editor_state(
31339 indoc! {"
31340 1. $
31341 ˇ
31342 "}
31343 .replace("$", " ")
31344 .as_str(),
31345 );
31346
31347 // Case 5: Adding newline with content adds marker preserving indentation
31348 cx.set_state(indoc! {"
31349 1. item
31350 2. indentedˇ
31351 "});
31352 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31353 cx.wait_for_autoindent_applied().await;
31354 cx.assert_editor_state(indoc! {"
31355 1. item
31356 2. indented
31357 3. ˇ
31358 "});
31359
31360 // Case 6: Adding newline with cursor right after marker, unindents
31361 cx.set_state(indoc! {"
31362 1. item
31363 2. sub item
31364 3. ˇ
31365 "});
31366 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31367 cx.wait_for_autoindent_applied().await;
31368 cx.assert_editor_state(indoc! {"
31369 1. item
31370 2. sub item
31371 1. ˇ
31372 "});
31373 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31374 cx.wait_for_autoindent_applied().await;
31375
31376 // Case 7: Adding newline with cursor right after marker, removes marker
31377 cx.assert_editor_state(indoc! {"
31378 1. item
31379 2. sub item
31380 1. ˇ
31381 "});
31382 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31383 cx.wait_for_autoindent_applied().await;
31384 cx.assert_editor_state(indoc! {"
31385 1. item
31386 2. sub item
31387 ˇ
31388 "});
31389
31390 // Case 8: Cursor before or inside prefix does not add marker
31391 cx.set_state(indoc! {"
31392 ˇ1. item
31393 "});
31394 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31395 cx.wait_for_autoindent_applied().await;
31396 cx.assert_editor_state(indoc! {"
31397
31398 ˇ1. item
31399 "});
31400
31401 cx.set_state(indoc! {"
31402 1ˇ. item
31403 "});
31404 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31405 cx.wait_for_autoindent_applied().await;
31406 cx.assert_editor_state(indoc! {"
31407 1
31408 ˇ. item
31409 "});
31410}
31411
31412#[gpui::test]
31413async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
31414 init_test(cx, |settings| {
31415 settings.defaults.tab_size = Some(2.try_into().unwrap());
31416 });
31417
31418 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
31419 let mut cx = EditorTestContext::new(cx).await;
31420 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31421
31422 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
31423 cx.set_state(indoc! {"
31424 1. first item
31425 1. sub first item
31426 2. sub second item
31427 3. ˇ
31428 "});
31429 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
31430 cx.wait_for_autoindent_applied().await;
31431 cx.assert_editor_state(indoc! {"
31432 1. first item
31433 1. sub first item
31434 2. sub second item
31435 1. ˇ
31436 "});
31437}
31438
31439#[gpui::test]
31440async fn test_tab_list_indent(cx: &mut TestAppContext) {
31441 init_test(cx, |settings| {
31442 settings.defaults.tab_size = Some(2.try_into().unwrap());
31443 });
31444
31445 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
31446 let mut cx = EditorTestContext::new(cx).await;
31447 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
31448
31449 // Case 1: Unordered list - cursor after prefix, adds indent before prefix
31450 cx.set_state(indoc! {"
31451 - ˇitem
31452 "});
31453 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31454 cx.wait_for_autoindent_applied().await;
31455 let expected = indoc! {"
31456 $$- ˇitem
31457 "};
31458 cx.assert_editor_state(expected.replace("$", " ").as_str());
31459
31460 // Case 2: Task list - cursor after prefix
31461 cx.set_state(indoc! {"
31462 - [ ] ˇtask
31463 "});
31464 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31465 cx.wait_for_autoindent_applied().await;
31466 let expected = indoc! {"
31467 $$- [ ] ˇtask
31468 "};
31469 cx.assert_editor_state(expected.replace("$", " ").as_str());
31470
31471 // Case 3: Ordered list - cursor after prefix
31472 cx.set_state(indoc! {"
31473 1. ˇfirst
31474 "});
31475 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31476 cx.wait_for_autoindent_applied().await;
31477 let expected = indoc! {"
31478 $$1. ˇfirst
31479 "};
31480 cx.assert_editor_state(expected.replace("$", " ").as_str());
31481
31482 // Case 4: With existing indentation - adds more indent
31483 let initial = indoc! {"
31484 $$- ˇitem
31485 "};
31486 cx.set_state(initial.replace("$", " ").as_str());
31487 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31488 cx.wait_for_autoindent_applied().await;
31489 let expected = indoc! {"
31490 $$$$- ˇitem
31491 "};
31492 cx.assert_editor_state(expected.replace("$", " ").as_str());
31493
31494 // Case 5: Empty list item
31495 cx.set_state(indoc! {"
31496 - ˇ
31497 "});
31498 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31499 cx.wait_for_autoindent_applied().await;
31500 let expected = indoc! {"
31501 $$- ˇ
31502 "};
31503 cx.assert_editor_state(expected.replace("$", " ").as_str());
31504
31505 // Case 6: Cursor at end of line with content
31506 cx.set_state(indoc! {"
31507 - itemˇ
31508 "});
31509 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31510 cx.wait_for_autoindent_applied().await;
31511 let expected = indoc! {"
31512 $$- itemˇ
31513 "};
31514 cx.assert_editor_state(expected.replace("$", " ").as_str());
31515
31516 // Case 7: Cursor at start of list item, indents it
31517 cx.set_state(indoc! {"
31518 - item
31519 ˇ - sub item
31520 "});
31521 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31522 cx.wait_for_autoindent_applied().await;
31523 let expected = indoc! {"
31524 - item
31525 ˇ - sub item
31526 "};
31527 cx.assert_editor_state(expected);
31528
31529 // Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
31530 cx.update_editor(|_, _, cx| {
31531 SettingsStore::update_global(cx, |store, cx| {
31532 store.update_user_settings(cx, |settings| {
31533 settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
31534 });
31535 });
31536 });
31537 cx.set_state(indoc! {"
31538 - item
31539 ˇ - sub item
31540 "});
31541 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
31542 cx.wait_for_autoindent_applied().await;
31543 let expected = indoc! {"
31544 - item
31545 ˇ- sub item
31546 "};
31547 cx.assert_editor_state(expected);
31548}
31549
31550#[gpui::test]
31551async fn test_local_worktree_trust(cx: &mut TestAppContext) {
31552 init_test(cx, |_| {});
31553 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), cx));
31554
31555 cx.update(|cx| {
31556 SettingsStore::update_global(cx, |store, cx| {
31557 store.update_user_settings(cx, |settings| {
31558 settings.project.all_languages.defaults.inlay_hints =
31559 Some(InlayHintSettingsContent {
31560 enabled: Some(true),
31561 ..InlayHintSettingsContent::default()
31562 });
31563 });
31564 });
31565 });
31566
31567 let fs = FakeFs::new(cx.executor());
31568 fs.insert_tree(
31569 path!("/project"),
31570 json!({
31571 ".zed": {
31572 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
31573 },
31574 "main.rs": "fn main() {}"
31575 }),
31576 )
31577 .await;
31578
31579 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
31580 let server_name = "override-rust-analyzer";
31581 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
31582
31583 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
31584 language_registry.add(rust_lang());
31585
31586 let capabilities = lsp::ServerCapabilities {
31587 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
31588 ..lsp::ServerCapabilities::default()
31589 };
31590 let mut fake_language_servers = language_registry.register_fake_lsp(
31591 "Rust",
31592 FakeLspAdapter {
31593 name: server_name,
31594 capabilities,
31595 initializer: Some(Box::new({
31596 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
31597 move |fake_server| {
31598 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
31599 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
31600 move |_params, _| {
31601 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
31602 async move {
31603 Ok(Some(vec![lsp::InlayHint {
31604 position: lsp::Position::new(0, 0),
31605 label: lsp::InlayHintLabel::String("hint".to_string()),
31606 kind: None,
31607 text_edits: None,
31608 tooltip: None,
31609 padding_left: None,
31610 padding_right: None,
31611 data: None,
31612 }]))
31613 }
31614 },
31615 );
31616 }
31617 })),
31618 ..FakeLspAdapter::default()
31619 },
31620 );
31621
31622 cx.run_until_parked();
31623
31624 let worktree_id = project.read_with(cx, |project, cx| {
31625 project
31626 .worktrees(cx)
31627 .next()
31628 .map(|wt| wt.read(cx).id())
31629 .expect("should have a worktree")
31630 });
31631 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
31632
31633 let trusted_worktrees =
31634 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
31635
31636 let can_trust = trusted_worktrees.update(cx, |store, cx| {
31637 store.can_trust(&worktree_store, worktree_id, cx)
31638 });
31639 assert!(!can_trust, "worktree should be restricted initially");
31640
31641 let buffer_before_approval = project
31642 .update(cx, |project, cx| {
31643 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
31644 })
31645 .await
31646 .unwrap();
31647
31648 let (editor, cx) = cx.add_window_view(|window, cx| {
31649 Editor::new(
31650 EditorMode::full(),
31651 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
31652 Some(project.clone()),
31653 window,
31654 cx,
31655 )
31656 });
31657 cx.run_until_parked();
31658 let fake_language_server = fake_language_servers.next();
31659
31660 cx.read(|cx| {
31661 let file = buffer_before_approval.read(cx).file();
31662 assert_eq!(
31663 language::language_settings::language_settings(Some("Rust".into()), file, cx)
31664 .language_servers,
31665 ["...".to_string()],
31666 "local .zed/settings.json must not apply before trust approval"
31667 )
31668 });
31669
31670 editor.update_in(cx, |editor, window, cx| {
31671 editor.handle_input("1", window, cx);
31672 });
31673 cx.run_until_parked();
31674 cx.executor()
31675 .advance_clock(std::time::Duration::from_secs(1));
31676 assert_eq!(
31677 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
31678 0,
31679 "inlay hints must not be queried before trust approval"
31680 );
31681
31682 trusted_worktrees.update(cx, |store, cx| {
31683 store.trust(
31684 &worktree_store,
31685 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
31686 cx,
31687 );
31688 });
31689 cx.run_until_parked();
31690
31691 cx.read(|cx| {
31692 let file = buffer_before_approval.read(cx).file();
31693 assert_eq!(
31694 language::language_settings::language_settings(Some("Rust".into()), file, cx)
31695 .language_servers,
31696 ["override-rust-analyzer".to_string()],
31697 "local .zed/settings.json should apply after trust approval"
31698 )
31699 });
31700 let _fake_language_server = fake_language_server.await.unwrap();
31701 editor.update_in(cx, |editor, window, cx| {
31702 editor.handle_input("1", window, cx);
31703 });
31704 cx.run_until_parked();
31705 cx.executor()
31706 .advance_clock(std::time::Duration::from_secs(1));
31707 assert!(
31708 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
31709 "inlay hints should be queried after trust approval"
31710 );
31711
31712 let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
31713 store.can_trust(&worktree_store, worktree_id, cx)
31714 });
31715 assert!(can_trust_after, "worktree should be trusted after trust()");
31716}
31717
31718#[gpui::test]
31719fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
31720 // This test reproduces a bug where drawing an editor at a position above the viewport
31721 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
31722 // causes an infinite loop in blocks_in_range.
31723 //
31724 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
31725 // the content mask intersection produces visible_bounds with origin at the viewport top.
31726 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
31727 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
31728 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
31729 init_test(cx, |_| {});
31730
31731 let window = cx.add_window(|_, _| gpui::Empty);
31732 let mut cx = VisualTestContext::from_window(*window, cx);
31733
31734 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
31735 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
31736
31737 // Simulate a small viewport (500x500 pixels at origin 0,0)
31738 cx.simulate_resize(gpui::size(px(500.), px(500.)));
31739
31740 // Draw the editor at a very negative Y position, simulating an editor that's been
31741 // scrolled way above the visible viewport (like in a List that has scrolled past it).
31742 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
31743 // This should NOT hang - it should just render nothing.
31744 cx.draw(
31745 gpui::point(px(0.), px(-10000.)),
31746 gpui::size(px(500.), px(3000.)),
31747 |_, _| editor.clone().into_any_element(),
31748 );
31749
31750 // If we get here without hanging, the test passes
31751}
31752
31753#[gpui::test]
31754async fn test_diff_review_indicator_created_on_gutter_hover(cx: &mut TestAppContext) {
31755 init_test(cx, |_| {});
31756
31757 let fs = FakeFs::new(cx.executor());
31758 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
31759 .await;
31760
31761 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
31762 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
31763 let workspace = window
31764 .read_with(cx, |mw, _| mw.workspace().clone())
31765 .unwrap();
31766 let cx = &mut VisualTestContext::from_window(*window, cx);
31767
31768 let editor = workspace
31769 .update_in(cx, |workspace, window, cx| {
31770 workspace.open_abs_path(
31771 PathBuf::from(path!("/root/file.txt")),
31772 OpenOptions::default(),
31773 window,
31774 cx,
31775 )
31776 })
31777 .await
31778 .unwrap()
31779 .downcast::<Editor>()
31780 .unwrap();
31781
31782 // Enable diff review button mode
31783 editor.update(cx, |editor, cx| {
31784 editor.set_show_diff_review_button(true, cx);
31785 });
31786
31787 // Initially, no indicator should be present
31788 editor.update(cx, |editor, _cx| {
31789 assert!(
31790 editor.gutter_diff_review_indicator.0.is_none(),
31791 "Indicator should be None initially"
31792 );
31793 });
31794}
31795
31796#[gpui::test]
31797async fn test_diff_review_button_hidden_when_ai_disabled(cx: &mut TestAppContext) {
31798 init_test(cx, |_| {});
31799
31800 // Register DisableAiSettings and set disable_ai to true
31801 cx.update(|cx| {
31802 project::DisableAiSettings::register(cx);
31803 project::DisableAiSettings::override_global(
31804 project::DisableAiSettings { disable_ai: true },
31805 cx,
31806 );
31807 });
31808
31809 let fs = FakeFs::new(cx.executor());
31810 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
31811 .await;
31812
31813 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
31814 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
31815 let workspace = window
31816 .read_with(cx, |mw, _| mw.workspace().clone())
31817 .unwrap();
31818 let cx = &mut VisualTestContext::from_window(*window, cx);
31819
31820 let editor = workspace
31821 .update_in(cx, |workspace, window, cx| {
31822 workspace.open_abs_path(
31823 PathBuf::from(path!("/root/file.txt")),
31824 OpenOptions::default(),
31825 window,
31826 cx,
31827 )
31828 })
31829 .await
31830 .unwrap()
31831 .downcast::<Editor>()
31832 .unwrap();
31833
31834 // Enable diff review button mode
31835 editor.update(cx, |editor, cx| {
31836 editor.set_show_diff_review_button(true, cx);
31837 });
31838
31839 // Verify AI is disabled
31840 cx.read(|cx| {
31841 assert!(
31842 project::DisableAiSettings::get_global(cx).disable_ai,
31843 "AI should be disabled"
31844 );
31845 });
31846
31847 // The indicator should not be created when AI is disabled
31848 // (The mouse_moved handler checks DisableAiSettings before creating the indicator)
31849 editor.update(cx, |editor, _cx| {
31850 assert!(
31851 editor.gutter_diff_review_indicator.0.is_none(),
31852 "Indicator should be None when AI is disabled"
31853 );
31854 });
31855}
31856
31857#[gpui::test]
31858async fn test_diff_review_button_shown_when_ai_enabled(cx: &mut TestAppContext) {
31859 init_test(cx, |_| {});
31860
31861 // Register DisableAiSettings and set disable_ai to false
31862 cx.update(|cx| {
31863 project::DisableAiSettings::register(cx);
31864 project::DisableAiSettings::override_global(
31865 project::DisableAiSettings { disable_ai: false },
31866 cx,
31867 );
31868 });
31869
31870 let fs = FakeFs::new(cx.executor());
31871 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
31872 .await;
31873
31874 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
31875 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
31876 let workspace = window
31877 .read_with(cx, |mw, _| mw.workspace().clone())
31878 .unwrap();
31879 let cx = &mut VisualTestContext::from_window(*window, cx);
31880
31881 let editor = workspace
31882 .update_in(cx, |workspace, window, cx| {
31883 workspace.open_abs_path(
31884 PathBuf::from(path!("/root/file.txt")),
31885 OpenOptions::default(),
31886 window,
31887 cx,
31888 )
31889 })
31890 .await
31891 .unwrap()
31892 .downcast::<Editor>()
31893 .unwrap();
31894
31895 // Enable diff review button mode
31896 editor.update(cx, |editor, cx| {
31897 editor.set_show_diff_review_button(true, cx);
31898 });
31899
31900 // Verify AI is enabled
31901 cx.read(|cx| {
31902 assert!(
31903 !project::DisableAiSettings::get_global(cx).disable_ai,
31904 "AI should be enabled"
31905 );
31906 });
31907
31908 // The show_diff_review_button flag should be true
31909 editor.update(cx, |editor, _cx| {
31910 assert!(
31911 editor.show_diff_review_button(),
31912 "show_diff_review_button should be true"
31913 );
31914 });
31915}
31916
31917/// Helper function to create a DiffHunkKey for testing.
31918/// Uses Anchor::min() as a placeholder anchor since these tests don't need
31919/// real buffer positioning.
31920fn test_hunk_key(file_path: &str) -> DiffHunkKey {
31921 DiffHunkKey {
31922 file_path: if file_path.is_empty() {
31923 Arc::from(util::rel_path::RelPath::empty())
31924 } else {
31925 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
31926 },
31927 hunk_start_anchor: Anchor::min(),
31928 }
31929}
31930
31931/// Helper function to create a DiffHunkKey with a specific anchor for testing.
31932fn test_hunk_key_with_anchor(file_path: &str, anchor: Anchor) -> DiffHunkKey {
31933 DiffHunkKey {
31934 file_path: if file_path.is_empty() {
31935 Arc::from(util::rel_path::RelPath::empty())
31936 } else {
31937 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
31938 },
31939 hunk_start_anchor: anchor,
31940 }
31941}
31942
31943/// Helper function to add a review comment with default anchors for testing.
31944fn add_test_comment(
31945 editor: &mut Editor,
31946 key: DiffHunkKey,
31947 comment: &str,
31948 cx: &mut Context<Editor>,
31949) -> usize {
31950 editor.add_review_comment(key, comment.to_string(), Anchor::min()..Anchor::max(), cx)
31951}
31952
31953#[gpui::test]
31954fn test_review_comment_add_to_hunk(cx: &mut TestAppContext) {
31955 init_test(cx, |_| {});
31956
31957 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31958
31959 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
31960 let key = test_hunk_key("");
31961
31962 let id = add_test_comment(editor, key.clone(), "Test comment", cx);
31963
31964 let snapshot = editor.buffer().read(cx).snapshot(cx);
31965 assert_eq!(editor.total_review_comment_count(), 1);
31966 assert_eq!(editor.hunk_comment_count(&key, &snapshot), 1);
31967
31968 let comments = editor.comments_for_hunk(&key, &snapshot);
31969 assert_eq!(comments.len(), 1);
31970 assert_eq!(comments[0].comment, "Test comment");
31971 assert_eq!(comments[0].id, id);
31972 });
31973}
31974
31975#[gpui::test]
31976fn test_review_comments_are_per_hunk(cx: &mut TestAppContext) {
31977 init_test(cx, |_| {});
31978
31979 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31980
31981 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
31982 let snapshot = editor.buffer().read(cx).snapshot(cx);
31983 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
31984 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
31985 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
31986 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
31987
31988 add_test_comment(editor, key1.clone(), "Comment for file1", cx);
31989 add_test_comment(editor, key2.clone(), "Comment for file2", cx);
31990
31991 let snapshot = editor.buffer().read(cx).snapshot(cx);
31992 assert_eq!(editor.total_review_comment_count(), 2);
31993 assert_eq!(editor.hunk_comment_count(&key1, &snapshot), 1);
31994 assert_eq!(editor.hunk_comment_count(&key2, &snapshot), 1);
31995
31996 assert_eq!(
31997 editor.comments_for_hunk(&key1, &snapshot)[0].comment,
31998 "Comment for file1"
31999 );
32000 assert_eq!(
32001 editor.comments_for_hunk(&key2, &snapshot)[0].comment,
32002 "Comment for file2"
32003 );
32004 });
32005}
32006
32007#[gpui::test]
32008fn test_review_comment_remove(cx: &mut TestAppContext) {
32009 init_test(cx, |_| {});
32010
32011 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32012
32013 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
32014 let key = test_hunk_key("");
32015
32016 let id = add_test_comment(editor, key, "To be removed", cx);
32017
32018 assert_eq!(editor.total_review_comment_count(), 1);
32019
32020 let removed = editor.remove_review_comment(id, cx);
32021 assert!(removed);
32022 assert_eq!(editor.total_review_comment_count(), 0);
32023
32024 // Try to remove again
32025 let removed_again = editor.remove_review_comment(id, cx);
32026 assert!(!removed_again);
32027 });
32028}
32029
32030#[gpui::test]
32031fn test_review_comment_update(cx: &mut TestAppContext) {
32032 init_test(cx, |_| {});
32033
32034 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32035
32036 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
32037 let key = test_hunk_key("");
32038
32039 let id = add_test_comment(editor, key.clone(), "Original text", cx);
32040
32041 let updated = editor.update_review_comment(id, "Updated text".to_string(), cx);
32042 assert!(updated);
32043
32044 let snapshot = editor.buffer().read(cx).snapshot(cx);
32045 let comments = editor.comments_for_hunk(&key, &snapshot);
32046 assert_eq!(comments[0].comment, "Updated text");
32047 assert!(!comments[0].is_editing); // Should clear editing flag
32048 });
32049}
32050
32051#[gpui::test]
32052fn test_review_comment_take_all(cx: &mut TestAppContext) {
32053 init_test(cx, |_| {});
32054
32055 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32056
32057 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
32058 let snapshot = editor.buffer().read(cx).snapshot(cx);
32059 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
32060 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
32061 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
32062 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
32063
32064 let id1 = add_test_comment(editor, key1.clone(), "Comment 1", cx);
32065 let id2 = add_test_comment(editor, key1.clone(), "Comment 2", cx);
32066 let id3 = add_test_comment(editor, key2.clone(), "Comment 3", cx);
32067
32068 // IDs should be sequential starting from 0
32069 assert_eq!(id1, 0);
32070 assert_eq!(id2, 1);
32071 assert_eq!(id3, 2);
32072
32073 assert_eq!(editor.total_review_comment_count(), 3);
32074
32075 let taken = editor.take_all_review_comments(cx);
32076
32077 // Should have 2 entries (one per hunk)
32078 assert_eq!(taken.len(), 2);
32079
32080 // Total comments should be 3
32081 let total: usize = taken
32082 .iter()
32083 .map(|(_, comments): &(DiffHunkKey, Vec<StoredReviewComment>)| comments.len())
32084 .sum();
32085 assert_eq!(total, 3);
32086
32087 // Storage should be empty
32088 assert_eq!(editor.total_review_comment_count(), 0);
32089
32090 // After taking all comments, ID counter should reset
32091 // New comments should get IDs starting from 0 again
32092 let new_id1 = add_test_comment(editor, key1, "New Comment 1", cx);
32093 let new_id2 = add_test_comment(editor, key2, "New Comment 2", cx);
32094
32095 assert_eq!(new_id1, 0, "ID counter should reset after take_all");
32096 assert_eq!(new_id2, 1, "IDs should be sequential after reset");
32097 });
32098}
32099
32100#[gpui::test]
32101fn test_diff_review_overlay_show_and_dismiss(cx: &mut TestAppContext) {
32102 init_test(cx, |_| {});
32103
32104 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32105
32106 // Show overlay
32107 editor
32108 .update(cx, |editor, window, cx| {
32109 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
32110 })
32111 .unwrap();
32112
32113 // Verify overlay is shown
32114 editor
32115 .update(cx, |editor, _window, cx| {
32116 assert!(!editor.diff_review_overlays.is_empty());
32117 assert_eq!(editor.diff_review_line_range(cx), Some((0, 0)));
32118 assert!(editor.diff_review_prompt_editor().is_some());
32119 })
32120 .unwrap();
32121
32122 // Dismiss overlay
32123 editor
32124 .update(cx, |editor, _window, cx| {
32125 editor.dismiss_all_diff_review_overlays(cx);
32126 })
32127 .unwrap();
32128
32129 // Verify overlay is dismissed
32130 editor
32131 .update(cx, |editor, _window, cx| {
32132 assert!(editor.diff_review_overlays.is_empty());
32133 assert_eq!(editor.diff_review_line_range(cx), None);
32134 assert!(editor.diff_review_prompt_editor().is_none());
32135 })
32136 .unwrap();
32137}
32138
32139#[gpui::test]
32140fn test_diff_review_overlay_dismiss_via_cancel(cx: &mut TestAppContext) {
32141 init_test(cx, |_| {});
32142
32143 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32144
32145 // Show overlay
32146 editor
32147 .update(cx, |editor, window, cx| {
32148 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
32149 })
32150 .unwrap();
32151
32152 // Verify overlay is shown
32153 editor
32154 .update(cx, |editor, _window, _cx| {
32155 assert!(!editor.diff_review_overlays.is_empty());
32156 })
32157 .unwrap();
32158
32159 // Dismiss via dismiss_menus_and_popups (which is called by cancel action)
32160 editor
32161 .update(cx, |editor, window, cx| {
32162 editor.dismiss_menus_and_popups(true, window, cx);
32163 })
32164 .unwrap();
32165
32166 // Verify overlay is dismissed
32167 editor
32168 .update(cx, |editor, _window, _cx| {
32169 assert!(editor.diff_review_overlays.is_empty());
32170 })
32171 .unwrap();
32172}
32173
32174#[gpui::test]
32175fn test_diff_review_empty_comment_not_submitted(cx: &mut TestAppContext) {
32176 init_test(cx, |_| {});
32177
32178 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32179
32180 // Show overlay
32181 editor
32182 .update(cx, |editor, window, cx| {
32183 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
32184 })
32185 .unwrap();
32186
32187 // Try to submit without typing anything (empty comment)
32188 editor
32189 .update(cx, |editor, window, cx| {
32190 editor.submit_diff_review_comment(window, cx);
32191 })
32192 .unwrap();
32193
32194 // Verify no comment was added
32195 editor
32196 .update(cx, |editor, _window, _cx| {
32197 assert_eq!(editor.total_review_comment_count(), 0);
32198 })
32199 .unwrap();
32200
32201 // Try to submit with whitespace-only comment
32202 editor
32203 .update(cx, |editor, window, cx| {
32204 if let Some(prompt_editor) = editor.diff_review_prompt_editor().cloned() {
32205 prompt_editor.update(cx, |pe, cx| {
32206 pe.insert(" \n\t ", window, cx);
32207 });
32208 }
32209 editor.submit_diff_review_comment(window, cx);
32210 })
32211 .unwrap();
32212
32213 // Verify still no comment was added
32214 editor
32215 .update(cx, |editor, _window, _cx| {
32216 assert_eq!(editor.total_review_comment_count(), 0);
32217 })
32218 .unwrap();
32219}
32220
32221#[gpui::test]
32222fn test_diff_review_inline_edit_flow(cx: &mut TestAppContext) {
32223 init_test(cx, |_| {});
32224
32225 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32226
32227 // Add a comment directly
32228 let comment_id = editor
32229 .update(cx, |editor, _window, cx| {
32230 let key = test_hunk_key("");
32231 add_test_comment(editor, key, "Original comment", cx)
32232 })
32233 .unwrap();
32234
32235 // Set comment to editing mode
32236 editor
32237 .update(cx, |editor, _window, cx| {
32238 editor.set_comment_editing(comment_id, true, cx);
32239 })
32240 .unwrap();
32241
32242 // Verify editing flag is set
32243 editor
32244 .update(cx, |editor, _window, cx| {
32245 let key = test_hunk_key("");
32246 let snapshot = editor.buffer().read(cx).snapshot(cx);
32247 let comments = editor.comments_for_hunk(&key, &snapshot);
32248 assert_eq!(comments.len(), 1);
32249 assert!(comments[0].is_editing);
32250 })
32251 .unwrap();
32252
32253 // Update the comment
32254 editor
32255 .update(cx, |editor, _window, cx| {
32256 let updated =
32257 editor.update_review_comment(comment_id, "Updated comment".to_string(), cx);
32258 assert!(updated);
32259 })
32260 .unwrap();
32261
32262 // Verify comment was updated and editing flag is cleared
32263 editor
32264 .update(cx, |editor, _window, cx| {
32265 let key = test_hunk_key("");
32266 let snapshot = editor.buffer().read(cx).snapshot(cx);
32267 let comments = editor.comments_for_hunk(&key, &snapshot);
32268 assert_eq!(comments[0].comment, "Updated comment");
32269 assert!(!comments[0].is_editing);
32270 })
32271 .unwrap();
32272}
32273
32274#[gpui::test]
32275fn test_orphaned_comments_are_cleaned_up(cx: &mut TestAppContext) {
32276 init_test(cx, |_| {});
32277
32278 // Create an editor with some text
32279 let editor = cx.add_window(|window, cx| {
32280 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
32281 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32282 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
32283 });
32284
32285 // Add a comment with an anchor on line 2
32286 editor
32287 .update(cx, |editor, _window, cx| {
32288 let snapshot = editor.buffer().read(cx).snapshot(cx);
32289 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
32290 let key = DiffHunkKey {
32291 file_path: Arc::from(util::rel_path::RelPath::empty()),
32292 hunk_start_anchor: anchor,
32293 };
32294 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
32295 assert_eq!(editor.total_review_comment_count(), 1);
32296 })
32297 .unwrap();
32298
32299 // Delete all content (this should orphan the comment's anchor)
32300 editor
32301 .update(cx, |editor, window, cx| {
32302 editor.select_all(&SelectAll, window, cx);
32303 editor.insert("completely new content", window, cx);
32304 })
32305 .unwrap();
32306
32307 // Trigger cleanup
32308 editor
32309 .update(cx, |editor, _window, cx| {
32310 editor.cleanup_orphaned_review_comments(cx);
32311 // Comment should be removed because its anchor is invalid
32312 assert_eq!(editor.total_review_comment_count(), 0);
32313 })
32314 .unwrap();
32315}
32316
32317#[gpui::test]
32318fn test_orphaned_comments_cleanup_called_on_buffer_edit(cx: &mut TestAppContext) {
32319 init_test(cx, |_| {});
32320
32321 // Create an editor with some text
32322 let editor = cx.add_window(|window, cx| {
32323 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
32324 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32325 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
32326 });
32327
32328 // Add a comment with an anchor on line 2
32329 editor
32330 .update(cx, |editor, _window, cx| {
32331 let snapshot = editor.buffer().read(cx).snapshot(cx);
32332 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
32333 let key = DiffHunkKey {
32334 file_path: Arc::from(util::rel_path::RelPath::empty()),
32335 hunk_start_anchor: anchor,
32336 };
32337 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
32338 assert_eq!(editor.total_review_comment_count(), 1);
32339 })
32340 .unwrap();
32341
32342 // Edit the buffer - this should trigger cleanup via on_buffer_event
32343 // Delete all content which orphans the anchor
32344 editor
32345 .update(cx, |editor, window, cx| {
32346 editor.select_all(&SelectAll, window, cx);
32347 editor.insert("completely new content", window, cx);
32348 // The cleanup is called automatically in on_buffer_event when Edited fires
32349 })
32350 .unwrap();
32351
32352 // Verify cleanup happened automatically (not manually triggered)
32353 editor
32354 .update(cx, |editor, _window, _cx| {
32355 // Comment should be removed because its anchor became invalid
32356 // and cleanup was called automatically on buffer edit
32357 assert_eq!(editor.total_review_comment_count(), 0);
32358 })
32359 .unwrap();
32360}
32361
32362#[gpui::test]
32363fn test_comments_stored_for_multiple_hunks(cx: &mut TestAppContext) {
32364 init_test(cx, |_| {});
32365
32366 // This test verifies that comments can be stored for multiple different hunks
32367 // and that hunk_comment_count correctly identifies comments per hunk.
32368 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32369
32370 _ = editor.update(cx, |editor, _window, cx| {
32371 let snapshot = editor.buffer().read(cx).snapshot(cx);
32372
32373 // Create two different hunk keys (simulating two different files)
32374 let anchor = snapshot.anchor_before(Point::new(0, 0));
32375 let key1 = DiffHunkKey {
32376 file_path: Arc::from(util::rel_path::RelPath::unix("file1.rs").unwrap()),
32377 hunk_start_anchor: anchor,
32378 };
32379 let key2 = DiffHunkKey {
32380 file_path: Arc::from(util::rel_path::RelPath::unix("file2.rs").unwrap()),
32381 hunk_start_anchor: anchor,
32382 };
32383
32384 // Add comments to first hunk
32385 editor.add_review_comment(
32386 key1.clone(),
32387 "Comment 1 for file1".to_string(),
32388 anchor..anchor,
32389 cx,
32390 );
32391 editor.add_review_comment(
32392 key1.clone(),
32393 "Comment 2 for file1".to_string(),
32394 anchor..anchor,
32395 cx,
32396 );
32397
32398 // Add comment to second hunk
32399 editor.add_review_comment(
32400 key2.clone(),
32401 "Comment for file2".to_string(),
32402 anchor..anchor,
32403 cx,
32404 );
32405
32406 // Verify total count
32407 assert_eq!(editor.total_review_comment_count(), 3);
32408
32409 // Verify per-hunk counts
32410 let snapshot = editor.buffer().read(cx).snapshot(cx);
32411 assert_eq!(
32412 editor.hunk_comment_count(&key1, &snapshot),
32413 2,
32414 "file1 should have 2 comments"
32415 );
32416 assert_eq!(
32417 editor.hunk_comment_count(&key2, &snapshot),
32418 1,
32419 "file2 should have 1 comment"
32420 );
32421
32422 // Verify comments_for_hunk returns correct comments
32423 let file1_comments = editor.comments_for_hunk(&key1, &snapshot);
32424 assert_eq!(file1_comments.len(), 2);
32425 assert_eq!(file1_comments[0].comment, "Comment 1 for file1");
32426 assert_eq!(file1_comments[1].comment, "Comment 2 for file1");
32427
32428 let file2_comments = editor.comments_for_hunk(&key2, &snapshot);
32429 assert_eq!(file2_comments.len(), 1);
32430 assert_eq!(file2_comments[0].comment, "Comment for file2");
32431 });
32432}
32433
32434#[gpui::test]
32435fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
32436 init_test(cx, |_| {});
32437
32438 // This test verifies that hunk_keys_match correctly identifies when two
32439 // DiffHunkKeys refer to the same hunk (same file path and anchor point).
32440 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32441
32442 _ = editor.update(cx, |editor, _window, cx| {
32443 let snapshot = editor.buffer().read(cx).snapshot(cx);
32444 let anchor = snapshot.anchor_before(Point::new(0, 0));
32445
32446 // Create two keys with the same file path and anchor
32447 let key1 = DiffHunkKey {
32448 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
32449 hunk_start_anchor: anchor,
32450 };
32451 let key2 = DiffHunkKey {
32452 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
32453 hunk_start_anchor: anchor,
32454 };
32455
32456 // Add comment to first key
32457 editor.add_review_comment(key1, "Test comment".to_string(), anchor..anchor, cx);
32458
32459 // Verify second key (same hunk) finds the comment
32460 let snapshot = editor.buffer().read(cx).snapshot(cx);
32461 assert_eq!(
32462 editor.hunk_comment_count(&key2, &snapshot),
32463 1,
32464 "Same hunk should find the comment"
32465 );
32466
32467 // Create a key with different file path
32468 let different_file_key = DiffHunkKey {
32469 file_path: Arc::from(util::rel_path::RelPath::unix("other.rs").unwrap()),
32470 hunk_start_anchor: anchor,
32471 };
32472
32473 // Different file should not find the comment
32474 assert_eq!(
32475 editor.hunk_comment_count(&different_file_key, &snapshot),
32476 0,
32477 "Different file should not find the comment"
32478 );
32479 });
32480}
32481
32482#[gpui::test]
32483fn test_overlay_comments_expanded_state(cx: &mut TestAppContext) {
32484 init_test(cx, |_| {});
32485
32486 // This test verifies that set_diff_review_comments_expanded correctly
32487 // updates the expanded state of overlays.
32488 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32489
32490 // Show overlay
32491 editor
32492 .update(cx, |editor, window, cx| {
32493 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
32494 })
32495 .unwrap();
32496
32497 // Verify initially expanded (default)
32498 editor
32499 .update(cx, |editor, _window, _cx| {
32500 assert!(
32501 editor.diff_review_overlays[0].comments_expanded,
32502 "Should be expanded by default"
32503 );
32504 })
32505 .unwrap();
32506
32507 // Set to collapsed using the public method
32508 editor
32509 .update(cx, |editor, _window, cx| {
32510 editor.set_diff_review_comments_expanded(false, cx);
32511 })
32512 .unwrap();
32513
32514 // Verify collapsed
32515 editor
32516 .update(cx, |editor, _window, _cx| {
32517 assert!(
32518 !editor.diff_review_overlays[0].comments_expanded,
32519 "Should be collapsed after setting to false"
32520 );
32521 })
32522 .unwrap();
32523
32524 // Set back to expanded
32525 editor
32526 .update(cx, |editor, _window, cx| {
32527 editor.set_diff_review_comments_expanded(true, cx);
32528 })
32529 .unwrap();
32530
32531 // Verify expanded again
32532 editor
32533 .update(cx, |editor, _window, _cx| {
32534 assert!(
32535 editor.diff_review_overlays[0].comments_expanded,
32536 "Should be expanded after setting to true"
32537 );
32538 })
32539 .unwrap();
32540}
32541
32542#[gpui::test]
32543fn test_diff_review_multiline_selection(cx: &mut TestAppContext) {
32544 init_test(cx, |_| {});
32545
32546 // Create an editor with multiple lines of text
32547 let editor = cx.add_window(|window, cx| {
32548 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\nline 4\nline 5\n", cx));
32549 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32550 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
32551 });
32552
32553 // Test showing overlay with a multi-line selection (lines 1-3, which are rows 0-2)
32554 editor
32555 .update(cx, |editor, window, cx| {
32556 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(2), window, cx);
32557 })
32558 .unwrap();
32559
32560 // Verify line range
32561 editor
32562 .update(cx, |editor, _window, cx| {
32563 assert!(!editor.diff_review_overlays.is_empty());
32564 assert_eq!(editor.diff_review_line_range(cx), Some((0, 2)));
32565 })
32566 .unwrap();
32567
32568 // Dismiss and test with reversed range (end < start)
32569 editor
32570 .update(cx, |editor, _window, cx| {
32571 editor.dismiss_all_diff_review_overlays(cx);
32572 })
32573 .unwrap();
32574
32575 // Show overlay with reversed range - should normalize it
32576 editor
32577 .update(cx, |editor, window, cx| {
32578 editor.show_diff_review_overlay(DisplayRow(3)..DisplayRow(1), window, cx);
32579 })
32580 .unwrap();
32581
32582 // Verify range is normalized (start <= end)
32583 editor
32584 .update(cx, |editor, _window, cx| {
32585 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
32586 })
32587 .unwrap();
32588}
32589
32590#[gpui::test]
32591fn test_diff_review_drag_state(cx: &mut TestAppContext) {
32592 init_test(cx, |_| {});
32593
32594 let editor = cx.add_window(|window, cx| {
32595 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
32596 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32597 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
32598 });
32599
32600 // Initially no drag state
32601 editor
32602 .update(cx, |editor, _window, _cx| {
32603 assert!(editor.diff_review_drag_state.is_none());
32604 })
32605 .unwrap();
32606
32607 // Start drag at row 1
32608 editor
32609 .update(cx, |editor, window, cx| {
32610 editor.start_diff_review_drag(DisplayRow(1), window, cx);
32611 })
32612 .unwrap();
32613
32614 // Verify drag state is set
32615 editor
32616 .update(cx, |editor, window, cx| {
32617 assert!(editor.diff_review_drag_state.is_some());
32618 let snapshot = editor.snapshot(window, cx);
32619 let range = editor
32620 .diff_review_drag_state
32621 .as_ref()
32622 .unwrap()
32623 .row_range(&snapshot.display_snapshot);
32624 assert_eq!(*range.start(), DisplayRow(1));
32625 assert_eq!(*range.end(), DisplayRow(1));
32626 })
32627 .unwrap();
32628
32629 // Update drag to row 3
32630 editor
32631 .update(cx, |editor, window, cx| {
32632 editor.update_diff_review_drag(DisplayRow(3), window, cx);
32633 })
32634 .unwrap();
32635
32636 // Verify drag state is updated
32637 editor
32638 .update(cx, |editor, window, cx| {
32639 assert!(editor.diff_review_drag_state.is_some());
32640 let snapshot = editor.snapshot(window, cx);
32641 let range = editor
32642 .diff_review_drag_state
32643 .as_ref()
32644 .unwrap()
32645 .row_range(&snapshot.display_snapshot);
32646 assert_eq!(*range.start(), DisplayRow(1));
32647 assert_eq!(*range.end(), DisplayRow(3));
32648 })
32649 .unwrap();
32650
32651 // End drag - should show overlay
32652 editor
32653 .update(cx, |editor, window, cx| {
32654 editor.end_diff_review_drag(window, cx);
32655 })
32656 .unwrap();
32657
32658 // Verify drag state is cleared and overlay is shown
32659 editor
32660 .update(cx, |editor, _window, cx| {
32661 assert!(editor.diff_review_drag_state.is_none());
32662 assert!(!editor.diff_review_overlays.is_empty());
32663 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
32664 })
32665 .unwrap();
32666}
32667
32668#[gpui::test]
32669fn test_diff_review_drag_cancel(cx: &mut TestAppContext) {
32670 init_test(cx, |_| {});
32671
32672 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32673
32674 // Start drag
32675 editor
32676 .update(cx, |editor, window, cx| {
32677 editor.start_diff_review_drag(DisplayRow(0), window, cx);
32678 })
32679 .unwrap();
32680
32681 // Verify drag state is set
32682 editor
32683 .update(cx, |editor, _window, _cx| {
32684 assert!(editor.diff_review_drag_state.is_some());
32685 })
32686 .unwrap();
32687
32688 // Cancel drag
32689 editor
32690 .update(cx, |editor, _window, cx| {
32691 editor.cancel_diff_review_drag(cx);
32692 })
32693 .unwrap();
32694
32695 // Verify drag state is cleared and no overlay was created
32696 editor
32697 .update(cx, |editor, _window, _cx| {
32698 assert!(editor.diff_review_drag_state.is_none());
32699 assert!(editor.diff_review_overlays.is_empty());
32700 })
32701 .unwrap();
32702}
32703
32704#[gpui::test]
32705fn test_calculate_overlay_height(cx: &mut TestAppContext) {
32706 init_test(cx, |_| {});
32707
32708 // This test verifies that calculate_overlay_height returns correct heights
32709 // based on comment count and expanded state.
32710 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
32711
32712 _ = editor.update(cx, |editor, _window, cx| {
32713 let snapshot = editor.buffer().read(cx).snapshot(cx);
32714 let anchor = snapshot.anchor_before(Point::new(0, 0));
32715 let key = DiffHunkKey {
32716 file_path: Arc::from(util::rel_path::RelPath::empty()),
32717 hunk_start_anchor: anchor,
32718 };
32719
32720 // No comments: base height of 2
32721 let height_no_comments = editor.calculate_overlay_height(&key, true, &snapshot);
32722 assert_eq!(
32723 height_no_comments, 2,
32724 "Base height should be 2 with no comments"
32725 );
32726
32727 // Add one comment
32728 editor.add_review_comment(key.clone(), "Comment 1".to_string(), anchor..anchor, cx);
32729
32730 let snapshot = editor.buffer().read(cx).snapshot(cx);
32731
32732 // With comments expanded: base (2) + header (1) + 2 per comment
32733 let height_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
32734 assert_eq!(
32735 height_expanded,
32736 2 + 1 + 2, // base + header + 1 comment * 2
32737 "Height with 1 comment expanded"
32738 );
32739
32740 // With comments collapsed: base (2) + header (1)
32741 let height_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
32742 assert_eq!(
32743 height_collapsed,
32744 2 + 1, // base + header only
32745 "Height with comments collapsed"
32746 );
32747
32748 // Add more comments
32749 editor.add_review_comment(key.clone(), "Comment 2".to_string(), anchor..anchor, cx);
32750 editor.add_review_comment(key.clone(), "Comment 3".to_string(), anchor..anchor, cx);
32751
32752 let snapshot = editor.buffer().read(cx).snapshot(cx);
32753
32754 // With 3 comments expanded
32755 let height_3_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
32756 assert_eq!(
32757 height_3_expanded,
32758 2 + 1 + (3 * 2), // base + header + 3 comments * 2
32759 "Height with 3 comments expanded"
32760 );
32761
32762 // Collapsed height stays the same regardless of comment count
32763 let height_3_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
32764 assert_eq!(
32765 height_3_collapsed,
32766 2 + 1, // base + header only
32767 "Height with 3 comments collapsed should be same as 1 comment collapsed"
32768 );
32769 });
32770}
32771
32772#[gpui::test]
32773async fn test_move_to_start_end_of_larger_syntax_node_single_cursor(cx: &mut TestAppContext) {
32774 init_test(cx, |_| {});
32775
32776 let language = Arc::new(Language::new(
32777 LanguageConfig::default(),
32778 Some(tree_sitter_rust::LANGUAGE.into()),
32779 ));
32780
32781 let text = r#"
32782 fn main() {
32783 let x = foo(1, 2);
32784 }
32785 "#
32786 .unindent();
32787
32788 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
32789 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32790 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32791
32792 editor
32793 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32794 .await;
32795
32796 // Test case 1: Move to end of syntax nodes
32797 editor.update_in(cx, |editor, window, cx| {
32798 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32799 s.select_display_ranges([
32800 DisplayPoint::new(DisplayRow(1), 16)..DisplayPoint::new(DisplayRow(1), 16)
32801 ]);
32802 });
32803 });
32804 editor.update(cx, |editor, cx| {
32805 assert_text_with_selections(
32806 editor,
32807 indoc! {r#"
32808 fn main() {
32809 let x = foo(ˇ1, 2);
32810 }
32811 "#},
32812 cx,
32813 );
32814 });
32815 editor.update_in(cx, |editor, window, cx| {
32816 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
32817 });
32818 editor.update(cx, |editor, cx| {
32819 assert_text_with_selections(
32820 editor,
32821 indoc! {r#"
32822 fn main() {
32823 let x = foo(1ˇ, 2);
32824 }
32825 "#},
32826 cx,
32827 );
32828 });
32829 editor.update_in(cx, |editor, window, cx| {
32830 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
32831 });
32832 editor.update(cx, |editor, cx| {
32833 assert_text_with_selections(
32834 editor,
32835 indoc! {r#"
32836 fn main() {
32837 let x = foo(1, 2)ˇ;
32838 }
32839 "#},
32840 cx,
32841 );
32842 });
32843 editor.update_in(cx, |editor, window, cx| {
32844 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
32845 });
32846 editor.update(cx, |editor, cx| {
32847 assert_text_with_selections(
32848 editor,
32849 indoc! {r#"
32850 fn main() {
32851 let x = foo(1, 2);ˇ
32852 }
32853 "#},
32854 cx,
32855 );
32856 });
32857 editor.update_in(cx, |editor, window, cx| {
32858 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
32859 });
32860 editor.update(cx, |editor, cx| {
32861 assert_text_with_selections(
32862 editor,
32863 indoc! {r#"
32864 fn main() {
32865 let x = foo(1, 2);
32866 }ˇ
32867 "#},
32868 cx,
32869 );
32870 });
32871
32872 // Test case 2: Move to start of syntax nodes
32873 editor.update_in(cx, |editor, window, cx| {
32874 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32875 s.select_display_ranges([
32876 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20)
32877 ]);
32878 });
32879 });
32880 editor.update(cx, |editor, cx| {
32881 assert_text_with_selections(
32882 editor,
32883 indoc! {r#"
32884 fn main() {
32885 let x = foo(1, 2ˇ);
32886 }
32887 "#},
32888 cx,
32889 );
32890 });
32891 editor.update_in(cx, |editor, window, cx| {
32892 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
32893 });
32894 editor.update(cx, |editor, cx| {
32895 assert_text_with_selections(
32896 editor,
32897 indoc! {r#"
32898 fn main() {
32899 let x = fooˇ(1, 2);
32900 }
32901 "#},
32902 cx,
32903 );
32904 });
32905 editor.update_in(cx, |editor, window, cx| {
32906 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
32907 });
32908 editor.update(cx, |editor, cx| {
32909 assert_text_with_selections(
32910 editor,
32911 indoc! {r#"
32912 fn main() {
32913 let x = ˇfoo(1, 2);
32914 }
32915 "#},
32916 cx,
32917 );
32918 });
32919 editor.update_in(cx, |editor, window, cx| {
32920 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
32921 });
32922 editor.update(cx, |editor, cx| {
32923 assert_text_with_selections(
32924 editor,
32925 indoc! {r#"
32926 fn main() {
32927 ˇlet x = foo(1, 2);
32928 }
32929 "#},
32930 cx,
32931 );
32932 });
32933 editor.update_in(cx, |editor, window, cx| {
32934 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
32935 });
32936 editor.update(cx, |editor, cx| {
32937 assert_text_with_selections(
32938 editor,
32939 indoc! {r#"
32940 fn main() ˇ{
32941 let x = foo(1, 2);
32942 }
32943 "#},
32944 cx,
32945 );
32946 });
32947 editor.update_in(cx, |editor, window, cx| {
32948 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
32949 });
32950 editor.update(cx, |editor, cx| {
32951 assert_text_with_selections(
32952 editor,
32953 indoc! {r#"
32954 ˇfn main() {
32955 let x = foo(1, 2);
32956 }
32957 "#},
32958 cx,
32959 );
32960 });
32961}
32962
32963#[gpui::test]
32964async fn test_move_to_start_end_of_larger_syntax_node_two_cursors(cx: &mut TestAppContext) {
32965 init_test(cx, |_| {});
32966
32967 let language = Arc::new(Language::new(
32968 LanguageConfig::default(),
32969 Some(tree_sitter_rust::LANGUAGE.into()),
32970 ));
32971
32972 let text = r#"
32973 fn main() {
32974 let x = foo(1, 2);
32975 let y = bar(3, 4);
32976 }
32977 "#
32978 .unindent();
32979
32980 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
32981 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32982 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32983
32984 editor
32985 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32986 .await;
32987
32988 // Test case 1: Move to end of syntax nodes with two cursors
32989 editor.update_in(cx, |editor, window, cx| {
32990 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32991 s.select_display_ranges([
32992 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20),
32993 DisplayPoint::new(DisplayRow(2), 20)..DisplayPoint::new(DisplayRow(2), 20),
32994 ]);
32995 });
32996 });
32997 editor.update(cx, |editor, cx| {
32998 assert_text_with_selections(
32999 editor,
33000 indoc! {r#"
33001 fn main() {
33002 let x = foo(1, 2ˇ);
33003 let y = bar(3, 4ˇ);
33004 }
33005 "#},
33006 cx,
33007 );
33008 });
33009 editor.update_in(cx, |editor, window, cx| {
33010 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
33011 });
33012 editor.update(cx, |editor, cx| {
33013 assert_text_with_selections(
33014 editor,
33015 indoc! {r#"
33016 fn main() {
33017 let x = foo(1, 2)ˇ;
33018 let y = bar(3, 4)ˇ;
33019 }
33020 "#},
33021 cx,
33022 );
33023 });
33024 editor.update_in(cx, |editor, window, cx| {
33025 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
33026 });
33027 editor.update(cx, |editor, cx| {
33028 assert_text_with_selections(
33029 editor,
33030 indoc! {r#"
33031 fn main() {
33032 let x = foo(1, 2);ˇ
33033 let y = bar(3, 4);ˇ
33034 }
33035 "#},
33036 cx,
33037 );
33038 });
33039
33040 // Test case 2: Move to start of syntax nodes with two cursors
33041 editor.update_in(cx, |editor, window, cx| {
33042 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33043 s.select_display_ranges([
33044 DisplayPoint::new(DisplayRow(1), 19)..DisplayPoint::new(DisplayRow(1), 19),
33045 DisplayPoint::new(DisplayRow(2), 19)..DisplayPoint::new(DisplayRow(2), 19),
33046 ]);
33047 });
33048 });
33049 editor.update(cx, |editor, cx| {
33050 assert_text_with_selections(
33051 editor,
33052 indoc! {r#"
33053 fn main() {
33054 let x = foo(1, ˇ2);
33055 let y = bar(3, ˇ4);
33056 }
33057 "#},
33058 cx,
33059 );
33060 });
33061 editor.update_in(cx, |editor, window, cx| {
33062 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
33063 });
33064 editor.update(cx, |editor, cx| {
33065 assert_text_with_selections(
33066 editor,
33067 indoc! {r#"
33068 fn main() {
33069 let x = fooˇ(1, 2);
33070 let y = barˇ(3, 4);
33071 }
33072 "#},
33073 cx,
33074 );
33075 });
33076 editor.update_in(cx, |editor, window, cx| {
33077 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
33078 });
33079 editor.update(cx, |editor, cx| {
33080 assert_text_with_selections(
33081 editor,
33082 indoc! {r#"
33083 fn main() {
33084 let x = ˇfoo(1, 2);
33085 let y = ˇbar(3, 4);
33086 }
33087 "#},
33088 cx,
33089 );
33090 });
33091 editor.update_in(cx, |editor, window, cx| {
33092 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
33093 });
33094 editor.update(cx, |editor, cx| {
33095 assert_text_with_selections(
33096 editor,
33097 indoc! {r#"
33098 fn main() {
33099 ˇlet x = foo(1, 2);
33100 ˇlet y = bar(3, 4);
33101 }
33102 "#},
33103 cx,
33104 );
33105 });
33106}
33107
33108#[gpui::test]
33109async fn test_move_to_start_end_of_larger_syntax_node_with_selections_and_strings(
33110 cx: &mut TestAppContext,
33111) {
33112 init_test(cx, |_| {});
33113
33114 let language = Arc::new(Language::new(
33115 LanguageConfig::default(),
33116 Some(tree_sitter_rust::LANGUAGE.into()),
33117 ));
33118
33119 let text = r#"
33120 fn main() {
33121 let x = foo(1, 2);
33122 let msg = "hello world";
33123 }
33124 "#
33125 .unindent();
33126
33127 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
33128 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33129 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33130
33131 editor
33132 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33133 .await;
33134
33135 // Test case 1: With existing selection, move_to_end keeps selection
33136 editor.update_in(cx, |editor, window, cx| {
33137 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33138 s.select_display_ranges([
33139 DisplayPoint::new(DisplayRow(1), 12)..DisplayPoint::new(DisplayRow(1), 21)
33140 ]);
33141 });
33142 });
33143 editor.update(cx, |editor, cx| {
33144 assert_text_with_selections(
33145 editor,
33146 indoc! {r#"
33147 fn main() {
33148 let x = «foo(1, 2)ˇ»;
33149 let msg = "hello world";
33150 }
33151 "#},
33152 cx,
33153 );
33154 });
33155 editor.update_in(cx, |editor, window, cx| {
33156 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
33157 });
33158 editor.update(cx, |editor, cx| {
33159 assert_text_with_selections(
33160 editor,
33161 indoc! {r#"
33162 fn main() {
33163 let x = «foo(1, 2)ˇ»;
33164 let msg = "hello world";
33165 }
33166 "#},
33167 cx,
33168 );
33169 });
33170
33171 // Test case 2: Move to end within a string
33172 editor.update_in(cx, |editor, window, cx| {
33173 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33174 s.select_display_ranges([
33175 DisplayPoint::new(DisplayRow(2), 15)..DisplayPoint::new(DisplayRow(2), 15)
33176 ]);
33177 });
33178 });
33179 editor.update(cx, |editor, cx| {
33180 assert_text_with_selections(
33181 editor,
33182 indoc! {r#"
33183 fn main() {
33184 let x = foo(1, 2);
33185 let msg = "ˇhello world";
33186 }
33187 "#},
33188 cx,
33189 );
33190 });
33191 editor.update_in(cx, |editor, window, cx| {
33192 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
33193 });
33194 editor.update(cx, |editor, cx| {
33195 assert_text_with_selections(
33196 editor,
33197 indoc! {r#"
33198 fn main() {
33199 let x = foo(1, 2);
33200 let msg = "hello worldˇ";
33201 }
33202 "#},
33203 cx,
33204 );
33205 });
33206
33207 // Test case 3: Move to start within a string
33208 editor.update_in(cx, |editor, window, cx| {
33209 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33210 s.select_display_ranges([
33211 DisplayPoint::new(DisplayRow(2), 21)..DisplayPoint::new(DisplayRow(2), 21)
33212 ]);
33213 });
33214 });
33215 editor.update(cx, |editor, cx| {
33216 assert_text_with_selections(
33217 editor,
33218 indoc! {r#"
33219 fn main() {
33220 let x = foo(1, 2);
33221 let msg = "hello ˇworld";
33222 }
33223 "#},
33224 cx,
33225 );
33226 });
33227 editor.update_in(cx, |editor, window, cx| {
33228 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
33229 });
33230 editor.update(cx, |editor, cx| {
33231 assert_text_with_selections(
33232 editor,
33233 indoc! {r#"
33234 fn main() {
33235 let x = foo(1, 2);
33236 let msg = "ˇhello world";
33237 }
33238 "#},
33239 cx,
33240 );
33241 });
33242}
33243
33244#[gpui::test]
33245async fn test_select_to_start_end_of_larger_syntax_node(cx: &mut TestAppContext) {
33246 init_test(cx, |_| {});
33247
33248 let language = Arc::new(Language::new(
33249 LanguageConfig::default(),
33250 Some(tree_sitter_rust::LANGUAGE.into()),
33251 ));
33252
33253 // Test Group 1.1: Cursor in String - First Jump (Select to End)
33254 let text = r#"let msg = "foo bar baz";"#.unindent();
33255
33256 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33257 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33258 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33259
33260 editor
33261 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33262 .await;
33263
33264 editor.update_in(cx, |editor, window, cx| {
33265 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33266 s.select_display_ranges([
33267 DisplayPoint::new(DisplayRow(0), 14)..DisplayPoint::new(DisplayRow(0), 14)
33268 ]);
33269 });
33270 });
33271 editor.update(cx, |editor, cx| {
33272 assert_text_with_selections(editor, indoc! {r#"let msg = "fooˇ bar baz";"#}, cx);
33273 });
33274 editor.update_in(cx, |editor, window, cx| {
33275 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33276 });
33277 editor.update(cx, |editor, cx| {
33278 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar bazˇ»";"#}, cx);
33279 });
33280
33281 // Test Group 1.2: Cursor in String - Second Jump (Select to End)
33282 editor.update_in(cx, |editor, window, cx| {
33283 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33284 });
33285 editor.update(cx, |editor, cx| {
33286 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz"ˇ»;"#}, cx);
33287 });
33288
33289 // Test Group 1.3: Cursor in String - Third Jump (Select to End)
33290 editor.update_in(cx, |editor, window, cx| {
33291 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33292 });
33293 editor.update(cx, |editor, cx| {
33294 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz";ˇ»"#}, cx);
33295 });
33296
33297 // Test Group 1.4: Cursor in String - First Jump (Select to Start)
33298 editor.update_in(cx, |editor, window, cx| {
33299 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33300 s.select_display_ranges([
33301 DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18)
33302 ]);
33303 });
33304 });
33305 editor.update(cx, |editor, cx| {
33306 assert_text_with_selections(editor, indoc! {r#"let msg = "foo barˇ baz";"#}, cx);
33307 });
33308 editor.update_in(cx, |editor, window, cx| {
33309 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
33310 });
33311 editor.update(cx, |editor, cx| {
33312 assert_text_with_selections(editor, indoc! {r#"let msg = "«ˇfoo bar» baz";"#}, cx);
33313 });
33314
33315 // Test Group 1.5: Cursor in String - Second Jump (Select to Start)
33316 editor.update_in(cx, |editor, window, cx| {
33317 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
33318 });
33319 editor.update(cx, |editor, cx| {
33320 assert_text_with_selections(editor, indoc! {r#"let msg = «ˇ"foo bar» baz";"#}, cx);
33321 });
33322
33323 // Test Group 1.6: Cursor in String - Third Jump (Select to Start)
33324 editor.update_in(cx, |editor, window, cx| {
33325 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
33326 });
33327 editor.update(cx, |editor, cx| {
33328 assert_text_with_selections(editor, indoc! {r#"«ˇlet msg = "foo bar» baz";"#}, cx);
33329 });
33330
33331 // Test Group 2.1: Let Statement Progression (Select to End)
33332 let text = r#"
33333fn main() {
33334 let x = "hello";
33335}
33336"#
33337 .unindent();
33338
33339 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33340 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33341 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33342
33343 editor
33344 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33345 .await;
33346
33347 editor.update_in(cx, |editor, window, cx| {
33348 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33349 s.select_display_ranges([
33350 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)
33351 ]);
33352 });
33353 });
33354 editor.update(cx, |editor, cx| {
33355 assert_text_with_selections(
33356 editor,
33357 indoc! {r#"
33358 fn main() {
33359 let xˇ = "hello";
33360 }
33361 "#},
33362 cx,
33363 );
33364 });
33365 editor.update_in(cx, |editor, window, cx| {
33366 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33367 });
33368 editor.update(cx, |editor, cx| {
33369 assert_text_with_selections(
33370 editor,
33371 indoc! {r##"
33372 fn main() {
33373 let x« = "hello";ˇ»
33374 }
33375 "##},
33376 cx,
33377 );
33378 });
33379 editor.update_in(cx, |editor, window, cx| {
33380 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33381 });
33382 editor.update(cx, |editor, cx| {
33383 assert_text_with_selections(
33384 editor,
33385 indoc! {r#"
33386 fn main() {
33387 let x« = "hello";
33388 }ˇ»
33389 "#},
33390 cx,
33391 );
33392 });
33393
33394 // Test Group 2.2a: From Inside String Content Node To String Content Boundary
33395 let text = r#"let x = "hello";"#.unindent();
33396
33397 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33398 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33399 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33400
33401 editor
33402 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33403 .await;
33404
33405 editor.update_in(cx, |editor, window, cx| {
33406 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33407 s.select_display_ranges([
33408 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12)
33409 ]);
33410 });
33411 });
33412 editor.update(cx, |editor, cx| {
33413 assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo";"#}, cx);
33414 });
33415 editor.update_in(cx, |editor, window, cx| {
33416 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
33417 });
33418 editor.update(cx, |editor, cx| {
33419 assert_text_with_selections(editor, indoc! {r#"let x = "«ˇhel»lo";"#}, cx);
33420 });
33421
33422 // Test Group 2.2b: From Edge of String Content Node To String Literal Boundary
33423 editor.update_in(cx, |editor, window, cx| {
33424 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33425 s.select_display_ranges([
33426 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
33427 ]);
33428 });
33429 });
33430 editor.update(cx, |editor, cx| {
33431 assert_text_with_selections(editor, indoc! {r#"let x = "ˇhello";"#}, cx);
33432 });
33433 editor.update_in(cx, |editor, window, cx| {
33434 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
33435 });
33436 editor.update(cx, |editor, cx| {
33437 assert_text_with_selections(editor, indoc! {r#"let x = «ˇ"»hello";"#}, cx);
33438 });
33439
33440 // Test Group 3.1: Create Selection from Cursor (Select to End)
33441 let text = r#"let x = "hello world";"#.unindent();
33442
33443 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33444 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33445 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33446
33447 editor
33448 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33449 .await;
33450
33451 editor.update_in(cx, |editor, window, cx| {
33452 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33453 s.select_display_ranges([
33454 DisplayPoint::new(DisplayRow(0), 14)..DisplayPoint::new(DisplayRow(0), 14)
33455 ]);
33456 });
33457 });
33458 editor.update(cx, |editor, cx| {
33459 assert_text_with_selections(editor, indoc! {r#"let x = "helloˇ world";"#}, cx);
33460 });
33461 editor.update_in(cx, |editor, window, cx| {
33462 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33463 });
33464 editor.update(cx, |editor, cx| {
33465 assert_text_with_selections(editor, indoc! {r#"let x = "hello« worldˇ»";"#}, cx);
33466 });
33467
33468 // Test Group 3.2: Extend Existing Selection (Select to End)
33469 editor.update_in(cx, |editor, window, cx| {
33470 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33471 s.select_display_ranges([
33472 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 17)
33473 ]);
33474 });
33475 });
33476 editor.update(cx, |editor, cx| {
33477 assert_text_with_selections(editor, indoc! {r#"let x = "he«llo woˇ»rld";"#}, cx);
33478 });
33479 editor.update_in(cx, |editor, window, cx| {
33480 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33481 });
33482 editor.update(cx, |editor, cx| {
33483 assert_text_with_selections(editor, indoc! {r#"let x = "he«llo worldˇ»";"#}, cx);
33484 });
33485
33486 // Test Group 4.1: Multiple Cursors - All Expand to Different Syntax Nodes
33487 let text = r#"let x = "hello"; let y = 42;"#.unindent();
33488
33489 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33490 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33491 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33492
33493 editor
33494 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33495 .await;
33496
33497 editor.update_in(cx, |editor, window, cx| {
33498 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33499 s.select_display_ranges([
33500 // Cursor inside string content
33501 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12),
33502 // Cursor at let statement semicolon
33503 DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18),
33504 // Cursor inside integer literal
33505 DisplayPoint::new(DisplayRow(0), 26)..DisplayPoint::new(DisplayRow(0), 26),
33506 ]);
33507 });
33508 });
33509 editor.update(cx, |editor, cx| {
33510 assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo"; lˇet y = 4ˇ2;"#}, cx);
33511 });
33512 editor.update_in(cx, |editor, window, cx| {
33513 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33514 });
33515 editor.update(cx, |editor, cx| {
33516 assert_text_with_selections(editor, indoc! {r#"let x = "hel«loˇ»"; l«et y = 42;ˇ»"#}, cx);
33517 });
33518
33519 // Test Group 4.2: Multiple Cursors on Separate Lines
33520 let text = r#"
33521let x = "hello";
33522let y = 42;
33523"#
33524 .unindent();
33525
33526 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33527 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33528 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33529
33530 editor
33531 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33532 .await;
33533
33534 editor.update_in(cx, |editor, window, cx| {
33535 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33536 s.select_display_ranges([
33537 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12),
33538 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9),
33539 ]);
33540 });
33541 });
33542
33543 editor.update(cx, |editor, cx| {
33544 assert_text_with_selections(
33545 editor,
33546 indoc! {r#"
33547 let x = "helˇlo";
33548 let y = 4ˇ2;
33549 "#},
33550 cx,
33551 );
33552 });
33553 editor.update_in(cx, |editor, window, cx| {
33554 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33555 });
33556 editor.update(cx, |editor, cx| {
33557 assert_text_with_selections(
33558 editor,
33559 indoc! {r#"
33560 let x = "hel«loˇ»";
33561 let y = 4«2ˇ»;
33562 "#},
33563 cx,
33564 );
33565 });
33566
33567 // Test Group 5.1: Nested Function Calls
33568 let text = r#"let result = foo(bar("arg"));"#.unindent();
33569
33570 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33571 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33572 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33573
33574 editor
33575 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33576 .await;
33577
33578 editor.update_in(cx, |editor, window, cx| {
33579 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33580 s.select_display_ranges([
33581 DisplayPoint::new(DisplayRow(0), 22)..DisplayPoint::new(DisplayRow(0), 22)
33582 ]);
33583 });
33584 });
33585 editor.update(cx, |editor, cx| {
33586 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("ˇarg"));"#}, cx);
33587 });
33588 editor.update_in(cx, |editor, window, cx| {
33589 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33590 });
33591 editor.update(cx, |editor, cx| {
33592 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«argˇ»"));"#}, cx);
33593 });
33594 editor.update_in(cx, |editor, window, cx| {
33595 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33596 });
33597 editor.update(cx, |editor, cx| {
33598 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg"ˇ»));"#}, cx);
33599 });
33600 editor.update_in(cx, |editor, window, cx| {
33601 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33602 });
33603 editor.update(cx, |editor, cx| {
33604 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg")ˇ»);"#}, cx);
33605 });
33606
33607 // Test Group 6.1: Block Comments
33608 let text = r#"let x = /* multi
33609 line
33610 comment */;"#
33611 .unindent();
33612
33613 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33614 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33615 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33616
33617 editor
33618 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33619 .await;
33620
33621 editor.update_in(cx, |editor, window, cx| {
33622 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33623 s.select_display_ranges([
33624 DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16)
33625 ]);
33626 });
33627 });
33628 editor.update(cx, |editor, cx| {
33629 assert_text_with_selections(
33630 editor,
33631 indoc! {r#"
33632let x = /* multiˇ
33633line
33634comment */;"#},
33635 cx,
33636 );
33637 });
33638 editor.update_in(cx, |editor, window, cx| {
33639 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33640 });
33641 editor.update(cx, |editor, cx| {
33642 assert_text_with_selections(
33643 editor,
33644 indoc! {r#"
33645let x = /* multi«
33646line
33647comment */ˇ»;"#},
33648 cx,
33649 );
33650 });
33651
33652 // Test Group 6.2: Array/Vector Literals
33653 let text = r#"let arr = [1, 2, 3];"#.unindent();
33654
33655 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
33656 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
33657 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
33658
33659 editor
33660 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
33661 .await;
33662
33663 editor.update_in(cx, |editor, window, cx| {
33664 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
33665 s.select_display_ranges([
33666 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
33667 ]);
33668 });
33669 });
33670 editor.update(cx, |editor, cx| {
33671 assert_text_with_selections(editor, indoc! {r#"let arr = [ˇ1, 2, 3];"#}, cx);
33672 });
33673 editor.update_in(cx, |editor, window, cx| {
33674 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33675 });
33676 editor.update(cx, |editor, cx| {
33677 assert_text_with_selections(editor, indoc! {r#"let arr = [«1ˇ», 2, 3];"#}, cx);
33678 });
33679 editor.update_in(cx, |editor, window, cx| {
33680 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
33681 });
33682 editor.update(cx, |editor, cx| {
33683 assert_text_with_selections(editor, indoc! {r#"let arr = [«1, 2, 3]ˇ»;"#}, cx);
33684 });
33685}
33686
33687#[gpui::test]
33688async fn test_restore_and_next(cx: &mut TestAppContext) {
33689 init_test(cx, |_| {});
33690 let mut cx = EditorTestContext::new(cx).await;
33691
33692 let diff_base = r#"
33693 one
33694 two
33695 three
33696 four
33697 five
33698 "#
33699 .unindent();
33700
33701 cx.set_state(
33702 &r#"
33703 ONE
33704 two
33705 ˇTHREE
33706 four
33707 FIVE
33708 "#
33709 .unindent(),
33710 );
33711 cx.set_head_text(&diff_base);
33712
33713 cx.update_editor(|editor, window, cx| {
33714 editor.set_expand_all_diff_hunks(cx);
33715 editor.restore_and_next(&Default::default(), window, cx);
33716 });
33717 cx.run_until_parked();
33718
33719 cx.assert_state_with_diff(
33720 r#"
33721 - one
33722 + ONE
33723 two
33724 three
33725 four
33726 - ˇfive
33727 + FIVE
33728 "#
33729 .unindent(),
33730 );
33731
33732 cx.update_editor(|editor, window, cx| {
33733 editor.restore_and_next(&Default::default(), window, cx);
33734 });
33735 cx.run_until_parked();
33736
33737 cx.assert_state_with_diff(
33738 r#"
33739 - one
33740 + ONE
33741 two
33742 three
33743 four
33744 ˇfive
33745 "#
33746 .unindent(),
33747 );
33748}