1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionDelegate,
6 element::StickyHeader,
7 linked_editing_ranges::LinkedEditingRanges,
8 scroll::scroll_amount::ScrollAmount,
9 test::{
10 assert_text_with_selections, build_editor,
11 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
12 editor_test_context::EditorTestContext,
13 select_ranges,
14 },
15};
16use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
17use collections::HashMap;
18use futures::{StreamExt, channel::oneshot};
19use gpui::{
20 BackgroundExecutor, DismissEvent, TestAppContext, UpdateGlobal, VisualTestContext,
21 WindowBounds, WindowOptions, div,
22};
23use indoc::indoc;
24use language::{
25 BracketPairConfig,
26 Capability::ReadWrite,
27 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
28 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
29 language_settings::{
30 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use languages::markdown_lang;
36use languages::rust_lang;
37use lsp::{CompletionParams, DEFAULT_LSP_REQUEST_TIMEOUT};
38use multi_buffer::{
39 ExcerptRange, IndentGuide, MultiBuffer, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey,
40};
41use parking_lot::Mutex;
42use pretty_assertions::{assert_eq, assert_ne};
43use project::{
44 FakeFs, Project,
45 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
46 project_settings::LspSettings,
47 trusted_worktrees::{PathTrust, TrustedWorktrees},
48};
49use serde_json::{self, json};
50use settings::{
51 AllLanguageSettingsContent, DelayMs, EditorSettingsContent, GlobalLspSettingsContent,
52 IndentGuideBackgroundColoring, IndentGuideColoring, InlayHintSettingsContent,
53 ProjectSettingsContent, SearchSettingsContent, SettingsStore,
54};
55use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
56use std::{
57 iter,
58 sync::atomic::{self, AtomicUsize},
59};
60use test::build_editor_with_project;
61use text::ToPoint as _;
62use unindent::Unindent;
63use util::{
64 assert_set_eq, path,
65 rel_path::rel_path,
66 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
67 uri,
68};
69use workspace::{
70 CloseActiveItem, CloseAllItems, CloseOtherItems, MultiWorkspace, NavigationEntry, OpenOptions,
71 ViewId,
72 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
73 register_project_item,
74};
75
76fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
77 editor
78 .selections
79 .display_ranges(&editor.display_snapshot(cx))
80}
81
82#[gpui::test]
83fn test_edit_events(cx: &mut TestAppContext) {
84 init_test(cx, |_| {});
85
86 let buffer = cx.new(|cx| {
87 let mut buffer = language::Buffer::local("123456", cx);
88 buffer.set_group_interval(Duration::from_secs(1));
89 buffer
90 });
91
92 let events = Rc::new(RefCell::new(Vec::new()));
93 let editor1 = cx.add_window({
94 let events = events.clone();
95 |window, cx| {
96 let entity = cx.entity();
97 cx.subscribe_in(
98 &entity,
99 window,
100 move |_, _, event: &EditorEvent, _, _| match event {
101 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
102 EditorEvent::BufferEdited => {
103 events.borrow_mut().push(("editor1", "buffer edited"))
104 }
105 _ => {}
106 },
107 )
108 .detach();
109 Editor::for_buffer(buffer.clone(), None, window, cx)
110 }
111 });
112
113 let editor2 = cx.add_window({
114 let events = events.clone();
115 |window, cx| {
116 cx.subscribe_in(
117 &cx.entity(),
118 window,
119 move |_, _, event: &EditorEvent, _, _| match event {
120 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
121 EditorEvent::BufferEdited => {
122 events.borrow_mut().push(("editor2", "buffer edited"))
123 }
124 _ => {}
125 },
126 )
127 .detach();
128 Editor::for_buffer(buffer.clone(), None, window, cx)
129 }
130 });
131
132 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
133
134 // Mutating editor 1 will emit an `Edited` event only for that editor.
135 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
136 assert_eq!(
137 mem::take(&mut *events.borrow_mut()),
138 [
139 ("editor1", "edited"),
140 ("editor1", "buffer edited"),
141 ("editor2", "buffer edited"),
142 ]
143 );
144
145 // Mutating editor 2 will emit an `Edited` event only for that editor.
146 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
147 assert_eq!(
148 mem::take(&mut *events.borrow_mut()),
149 [
150 ("editor2", "edited"),
151 ("editor1", "buffer edited"),
152 ("editor2", "buffer edited"),
153 ]
154 );
155
156 // Undoing on editor 1 will emit an `Edited` event only for that editor.
157 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
158 assert_eq!(
159 mem::take(&mut *events.borrow_mut()),
160 [
161 ("editor1", "edited"),
162 ("editor1", "buffer edited"),
163 ("editor2", "buffer edited"),
164 ]
165 );
166
167 // Redoing on editor 1 will emit an `Edited` event only for that editor.
168 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
169 assert_eq!(
170 mem::take(&mut *events.borrow_mut()),
171 [
172 ("editor1", "edited"),
173 ("editor1", "buffer edited"),
174 ("editor2", "buffer edited"),
175 ]
176 );
177
178 // Undoing on editor 2 will emit an `Edited` event only for that editor.
179 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
180 assert_eq!(
181 mem::take(&mut *events.borrow_mut()),
182 [
183 ("editor2", "edited"),
184 ("editor1", "buffer edited"),
185 ("editor2", "buffer edited"),
186 ]
187 );
188
189 // Redoing on editor 2 will emit an `Edited` event only for that editor.
190 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
191 assert_eq!(
192 mem::take(&mut *events.borrow_mut()),
193 [
194 ("editor2", "edited"),
195 ("editor1", "buffer edited"),
196 ("editor2", "buffer edited"),
197 ]
198 );
199
200 // No event is emitted when the mutation is a no-op.
201 _ = editor2.update(cx, |editor, window, cx| {
202 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
203 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
204 });
205
206 editor.backspace(&Backspace, window, cx);
207 });
208 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
209}
210
211#[gpui::test]
212fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
213 init_test(cx, |_| {});
214
215 let mut now = Instant::now();
216 let group_interval = Duration::from_millis(1);
217 let buffer = cx.new(|cx| {
218 let mut buf = language::Buffer::local("123456", cx);
219 buf.set_group_interval(group_interval);
220 buf
221 });
222 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
223 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
224
225 _ = editor.update(cx, |editor, window, cx| {
226 editor.start_transaction_at(now, window, cx);
227 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
228 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(4)])
229 });
230
231 editor.insert("cd", window, cx);
232 editor.end_transaction_at(now, cx);
233 assert_eq!(editor.text(cx), "12cd56");
234 assert_eq!(
235 editor.selections.ranges(&editor.display_snapshot(cx)),
236 vec![MultiBufferOffset(4)..MultiBufferOffset(4)]
237 );
238
239 editor.start_transaction_at(now, window, cx);
240 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
241 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(5)])
242 });
243 editor.insert("e", window, cx);
244 editor.end_transaction_at(now, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(
247 editor.selections.ranges(&editor.display_snapshot(cx)),
248 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
249 );
250
251 now += group_interval + Duration::from_millis(1);
252 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
253 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)])
254 });
255
256 // Simulate an edit in another editor
257 buffer.update(cx, |buffer, cx| {
258 buffer.start_transaction_at(now, cx);
259 buffer.edit(
260 [(MultiBufferOffset(0)..MultiBufferOffset(1), "a")],
261 None,
262 cx,
263 );
264 buffer.edit(
265 [(MultiBufferOffset(1)..MultiBufferOffset(1), "b")],
266 None,
267 cx,
268 );
269 buffer.end_transaction_at(now, cx);
270 });
271
272 assert_eq!(editor.text(cx), "ab2cde6");
273 assert_eq!(
274 editor.selections.ranges(&editor.display_snapshot(cx)),
275 vec![MultiBufferOffset(3)..MultiBufferOffset(3)]
276 );
277
278 // Last transaction happened past the group interval in a different editor.
279 // Undo it individually and don't restore selections.
280 editor.undo(&Undo, window, cx);
281 assert_eq!(editor.text(cx), "12cde6");
282 assert_eq!(
283 editor.selections.ranges(&editor.display_snapshot(cx)),
284 vec![MultiBufferOffset(2)..MultiBufferOffset(2)]
285 );
286
287 // First two transactions happened within the group interval in this editor.
288 // Undo them together and restore selections.
289 editor.undo(&Undo, window, cx);
290 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
291 assert_eq!(editor.text(cx), "123456");
292 assert_eq!(
293 editor.selections.ranges(&editor.display_snapshot(cx)),
294 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
295 );
296
297 // Redo the first two transactions together.
298 editor.redo(&Redo, window, cx);
299 assert_eq!(editor.text(cx), "12cde6");
300 assert_eq!(
301 editor.selections.ranges(&editor.display_snapshot(cx)),
302 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
303 );
304
305 // Redo the last transaction on its own.
306 editor.redo(&Redo, window, cx);
307 assert_eq!(editor.text(cx), "ab2cde6");
308 assert_eq!(
309 editor.selections.ranges(&editor.display_snapshot(cx)),
310 vec![MultiBufferOffset(6)..MultiBufferOffset(6)]
311 );
312
313 // Test empty transactions.
314 editor.start_transaction_at(now, window, cx);
315 editor.end_transaction_at(now, cx);
316 editor.undo(&Undo, window, cx);
317 assert_eq!(editor.text(cx), "12cde6");
318 });
319}
320
321#[gpui::test]
322fn test_ime_composition(cx: &mut TestAppContext) {
323 init_test(cx, |_| {});
324
325 let buffer = cx.new(|cx| {
326 let mut buffer = language::Buffer::local("abcde", cx);
327 // Ensure automatic grouping doesn't occur.
328 buffer.set_group_interval(Duration::ZERO);
329 buffer
330 });
331
332 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
333 cx.add_window(|window, cx| {
334 let mut editor = build_editor(buffer.clone(), window, cx);
335
336 // Start a new IME composition.
337 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
338 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
339 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
340 assert_eq!(editor.text(cx), "äbcde");
341 assert_eq!(
342 editor.marked_text_ranges(cx),
343 Some(vec![
344 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
345 ])
346 );
347
348 // Finalize IME composition.
349 editor.replace_text_in_range(None, "ā", window, cx);
350 assert_eq!(editor.text(cx), "ābcde");
351 assert_eq!(editor.marked_text_ranges(cx), None);
352
353 // IME composition edits are grouped and are undone/redone at once.
354 editor.undo(&Default::default(), window, cx);
355 assert_eq!(editor.text(cx), "abcde");
356 assert_eq!(editor.marked_text_ranges(cx), None);
357 editor.redo(&Default::default(), window, cx);
358 assert_eq!(editor.text(cx), "ābcde");
359 assert_eq!(editor.marked_text_ranges(cx), None);
360
361 // Start a new IME composition.
362 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
363 assert_eq!(
364 editor.marked_text_ranges(cx),
365 Some(vec![
366 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
367 ])
368 );
369
370 // Undoing during an IME composition cancels it.
371 editor.undo(&Default::default(), window, cx);
372 assert_eq!(editor.text(cx), "ābcde");
373 assert_eq!(editor.marked_text_ranges(cx), None);
374
375 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
376 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
377 assert_eq!(editor.text(cx), "ābcdè");
378 assert_eq!(
379 editor.marked_text_ranges(cx),
380 Some(vec![
381 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(5))
382 ])
383 );
384
385 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
386 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
387 assert_eq!(editor.text(cx), "ābcdę");
388 assert_eq!(editor.marked_text_ranges(cx), None);
389
390 // Start a new IME composition with multiple cursors.
391 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
392 s.select_ranges([
393 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(1)),
394 MultiBufferOffsetUtf16(OffsetUtf16(3))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
395 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(5)),
396 ])
397 });
398 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
399 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
400 assert_eq!(
401 editor.marked_text_ranges(cx),
402 Some(vec![
403 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
404 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(7)),
405 MultiBufferOffsetUtf16(OffsetUtf16(8))..MultiBufferOffsetUtf16(OffsetUtf16(11))
406 ])
407 );
408
409 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
410 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
411 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
412 assert_eq!(
413 editor.marked_text_ranges(cx),
414 Some(vec![
415 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(2)),
416 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(6)),
417 MultiBufferOffsetUtf16(OffsetUtf16(9))..MultiBufferOffsetUtf16(OffsetUtf16(10))
418 ])
419 );
420
421 // Finalize IME composition with multiple cursors.
422 editor.replace_text_in_range(Some(9..10), "2", window, cx);
423 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
424 assert_eq!(editor.marked_text_ranges(cx), None);
425
426 editor
427 });
428}
429
430#[gpui::test]
431fn test_selection_with_mouse(cx: &mut TestAppContext) {
432 init_test(cx, |_| {});
433
434 let editor = cx.add_window(|window, cx| {
435 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
436 build_editor(buffer, window, cx)
437 });
438
439 _ = editor.update(cx, |editor, window, cx| {
440 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
441 });
442 assert_eq!(
443 editor
444 .update(cx, |editor, _, cx| display_ranges(editor, cx))
445 .unwrap(),
446 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
447 );
448
449 _ = editor.update(cx, |editor, window, cx| {
450 editor.update_selection(
451 DisplayPoint::new(DisplayRow(3), 3),
452 0,
453 gpui::Point::<f32>::default(),
454 window,
455 cx,
456 );
457 });
458
459 assert_eq!(
460 editor
461 .update(cx, |editor, _, cx| display_ranges(editor, cx))
462 .unwrap(),
463 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
464 );
465
466 _ = editor.update(cx, |editor, window, cx| {
467 editor.update_selection(
468 DisplayPoint::new(DisplayRow(1), 1),
469 0,
470 gpui::Point::<f32>::default(),
471 window,
472 cx,
473 );
474 });
475
476 assert_eq!(
477 editor
478 .update(cx, |editor, _, cx| display_ranges(editor, cx))
479 .unwrap(),
480 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
481 );
482
483 _ = editor.update(cx, |editor, window, cx| {
484 editor.end_selection(window, cx);
485 editor.update_selection(
486 DisplayPoint::new(DisplayRow(3), 3),
487 0,
488 gpui::Point::<f32>::default(),
489 window,
490 cx,
491 );
492 });
493
494 assert_eq!(
495 editor
496 .update(cx, |editor, _, cx| display_ranges(editor, cx))
497 .unwrap(),
498 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
499 );
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
503 editor.update_selection(
504 DisplayPoint::new(DisplayRow(0), 0),
505 0,
506 gpui::Point::<f32>::default(),
507 window,
508 cx,
509 );
510 });
511
512 assert_eq!(
513 editor
514 .update(cx, |editor, _, cx| display_ranges(editor, cx))
515 .unwrap(),
516 [
517 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
518 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
519 ]
520 );
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.end_selection(window, cx);
524 });
525
526 assert_eq!(
527 editor
528 .update(cx, |editor, _, cx| display_ranges(editor, cx))
529 .unwrap(),
530 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
531 );
532}
533
534#[gpui::test]
535fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
536 init_test(cx, |_| {});
537
538 let editor = cx.add_window(|window, cx| {
539 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
540 build_editor(buffer, window, cx)
541 });
542
543 _ = editor.update(cx, |editor, window, cx| {
544 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
545 });
546
547 _ = editor.update(cx, |editor, window, cx| {
548 editor.end_selection(window, cx);
549 });
550
551 _ = editor.update(cx, |editor, window, cx| {
552 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
553 });
554
555 _ = editor.update(cx, |editor, window, cx| {
556 editor.end_selection(window, cx);
557 });
558
559 assert_eq!(
560 editor
561 .update(cx, |editor, _, cx| display_ranges(editor, cx))
562 .unwrap(),
563 [
564 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
565 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
566 ]
567 );
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
571 });
572
573 _ = editor.update(cx, |editor, window, cx| {
574 editor.end_selection(window, cx);
575 });
576
577 assert_eq!(
578 editor
579 .update(cx, |editor, _, cx| display_ranges(editor, cx))
580 .unwrap(),
581 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
582 );
583}
584
585#[gpui::test]
586fn test_canceling_pending_selection(cx: &mut TestAppContext) {
587 init_test(cx, |_| {});
588
589 let editor = cx.add_window(|window, cx| {
590 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
591 build_editor(buffer, window, cx)
592 });
593
594 _ = editor.update(cx, |editor, window, cx| {
595 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
596 assert_eq!(
597 display_ranges(editor, cx),
598 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
599 );
600 });
601
602 _ = editor.update(cx, |editor, window, cx| {
603 editor.update_selection(
604 DisplayPoint::new(DisplayRow(3), 3),
605 0,
606 gpui::Point::<f32>::default(),
607 window,
608 cx,
609 );
610 assert_eq!(
611 display_ranges(editor, cx),
612 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
613 );
614 });
615
616 _ = editor.update(cx, |editor, window, cx| {
617 editor.cancel(&Cancel, window, cx);
618 editor.update_selection(
619 DisplayPoint::new(DisplayRow(1), 1),
620 0,
621 gpui::Point::<f32>::default(),
622 window,
623 cx,
624 );
625 assert_eq!(
626 display_ranges(editor, cx),
627 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
628 );
629 });
630}
631
632#[gpui::test]
633fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
634 init_test(cx, |_| {});
635
636 let editor = cx.add_window(|window, cx| {
637 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
638 build_editor(buffer, window, cx)
639 });
640
641 _ = editor.update(cx, |editor, window, cx| {
642 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
643 assert_eq!(
644 display_ranges(editor, cx),
645 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
646 );
647
648 editor.move_down(&Default::default(), window, cx);
649 assert_eq!(
650 display_ranges(editor, cx),
651 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
652 );
653
654 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
655 assert_eq!(
656 display_ranges(editor, cx),
657 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
658 );
659
660 editor.move_up(&Default::default(), window, cx);
661 assert_eq!(
662 display_ranges(editor, cx),
663 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
664 );
665 });
666}
667
668#[gpui::test]
669fn test_extending_selection(cx: &mut TestAppContext) {
670 init_test(cx, |_| {});
671
672 let editor = cx.add_window(|window, cx| {
673 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
674 build_editor(buffer, window, cx)
675 });
676
677 _ = editor.update(cx, |editor, window, cx| {
678 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
679 editor.end_selection(window, cx);
680 assert_eq!(
681 display_ranges(editor, cx),
682 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
683 );
684
685 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
686 editor.end_selection(window, cx);
687 assert_eq!(
688 display_ranges(editor, cx),
689 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
690 );
691
692 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
693 editor.end_selection(window, cx);
694 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
695 assert_eq!(
696 display_ranges(editor, cx),
697 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
698 );
699
700 editor.update_selection(
701 DisplayPoint::new(DisplayRow(0), 1),
702 0,
703 gpui::Point::<f32>::default(),
704 window,
705 cx,
706 );
707 editor.end_selection(window, cx);
708 assert_eq!(
709 display_ranges(editor, cx),
710 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
711 );
712
713 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
714 editor.end_selection(window, cx);
715 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
716 editor.end_selection(window, cx);
717 assert_eq!(
718 display_ranges(editor, cx),
719 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
720 );
721
722 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
723 assert_eq!(
724 display_ranges(editor, cx),
725 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
726 );
727
728 editor.update_selection(
729 DisplayPoint::new(DisplayRow(0), 6),
730 0,
731 gpui::Point::<f32>::default(),
732 window,
733 cx,
734 );
735 assert_eq!(
736 display_ranges(editor, cx),
737 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
738 );
739
740 editor.update_selection(
741 DisplayPoint::new(DisplayRow(0), 1),
742 0,
743 gpui::Point::<f32>::default(),
744 window,
745 cx,
746 );
747 editor.end_selection(window, cx);
748 assert_eq!(
749 display_ranges(editor, cx),
750 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
751 );
752 });
753}
754
755#[gpui::test]
756fn test_clone(cx: &mut TestAppContext) {
757 init_test(cx, |_| {});
758
759 let (text, selection_ranges) = marked_text_ranges(
760 indoc! {"
761 one
762 two
763 threeˇ
764 four
765 fiveˇ
766 "},
767 true,
768 );
769
770 let editor = cx.add_window(|window, cx| {
771 let buffer = MultiBuffer::build_simple(&text, cx);
772 build_editor(buffer, window, cx)
773 });
774
775 _ = editor.update(cx, |editor, window, cx| {
776 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
777 s.select_ranges(
778 selection_ranges
779 .iter()
780 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
781 )
782 });
783 editor.fold_creases(
784 vec![
785 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
786 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
787 ],
788 true,
789 window,
790 cx,
791 );
792 });
793
794 let cloned_editor = editor
795 .update(cx, |editor, _, cx| {
796 cx.open_window(Default::default(), |window, cx| {
797 cx.new(|cx| editor.clone(window, cx))
798 })
799 })
800 .unwrap()
801 .unwrap();
802
803 let snapshot = editor
804 .update(cx, |e, window, cx| e.snapshot(window, cx))
805 .unwrap();
806 let cloned_snapshot = cloned_editor
807 .update(cx, |e, window, cx| e.snapshot(window, cx))
808 .unwrap();
809
810 assert_eq!(
811 cloned_editor
812 .update(cx, |e, _, cx| e.display_text(cx))
813 .unwrap(),
814 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
815 );
816 assert_eq!(
817 cloned_snapshot
818 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
819 .collect::<Vec<_>>(),
820 snapshot
821 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
822 .collect::<Vec<_>>(),
823 );
824 assert_set_eq!(
825 cloned_editor
826 .update(cx, |editor, _, cx| editor
827 .selections
828 .ranges::<Point>(&editor.display_snapshot(cx)))
829 .unwrap(),
830 editor
831 .update(cx, |editor, _, cx| editor
832 .selections
833 .ranges(&editor.display_snapshot(cx)))
834 .unwrap()
835 );
836 assert_set_eq!(
837 cloned_editor
838 .update(cx, |e, _window, cx| e
839 .selections
840 .display_ranges(&e.display_snapshot(cx)))
841 .unwrap(),
842 editor
843 .update(cx, |e, _, cx| e
844 .selections
845 .display_ranges(&e.display_snapshot(cx)))
846 .unwrap()
847 );
848}
849
850#[gpui::test]
851async fn test_navigation_history(cx: &mut TestAppContext) {
852 init_test(cx, |_| {});
853
854 use workspace::item::Item;
855
856 let fs = FakeFs::new(cx.executor());
857 let project = Project::test(fs, [], cx).await;
858 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
859 let workspace = window
860 .read_with(cx, |mw, _| mw.workspace().clone())
861 .unwrap();
862 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
863
864 _ = window.update(cx, |_mw, window, cx| {
865 cx.new(|cx| {
866 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
867 let mut editor = build_editor(buffer, window, cx);
868 let handle = cx.entity();
869 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
870
871 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
872 editor.nav_history.as_mut().unwrap().pop_backward(cx)
873 }
874
875 // Move the cursor a small distance.
876 // Nothing is added to the navigation history.
877 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
878 s.select_display_ranges([
879 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
880 ])
881 });
882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
883 s.select_display_ranges([
884 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
885 ])
886 });
887 assert!(pop_history(&mut editor, cx).is_none());
888
889 // Move the cursor a large distance.
890 // The history can jump back to the previous position.
891 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
892 s.select_display_ranges([
893 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
894 ])
895 });
896 let nav_entry = pop_history(&mut editor, cx).unwrap();
897 editor.navigate(nav_entry.data.unwrap(), window, cx);
898 assert_eq!(nav_entry.item.id(), cx.entity_id());
899 assert_eq!(
900 editor
901 .selections
902 .display_ranges(&editor.display_snapshot(cx)),
903 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
904 );
905 assert!(pop_history(&mut editor, cx).is_none());
906
907 // Move the cursor a small distance via the mouse.
908 // Nothing is added to the navigation history.
909 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
910 editor.end_selection(window, cx);
911 assert_eq!(
912 editor
913 .selections
914 .display_ranges(&editor.display_snapshot(cx)),
915 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
916 );
917 assert!(pop_history(&mut editor, cx).is_none());
918
919 // Move the cursor a large distance via the mouse.
920 // The history can jump back to the previous position.
921 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
922 editor.end_selection(window, cx);
923 assert_eq!(
924 editor
925 .selections
926 .display_ranges(&editor.display_snapshot(cx)),
927 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
928 );
929 let nav_entry = pop_history(&mut editor, cx).unwrap();
930 editor.navigate(nav_entry.data.unwrap(), window, cx);
931 assert_eq!(nav_entry.item.id(), cx.entity_id());
932 assert_eq!(
933 editor
934 .selections
935 .display_ranges(&editor.display_snapshot(cx)),
936 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
937 );
938 assert!(pop_history(&mut editor, cx).is_none());
939
940 // Set scroll position to check later
941 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
942 let original_scroll_position = editor
943 .scroll_manager
944 .native_anchor(&editor.display_snapshot(cx), cx);
945
946 // Jump to the end of the document and adjust scroll
947 editor.move_to_end(&MoveToEnd, window, cx);
948 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
949 assert_ne!(
950 editor
951 .scroll_manager
952 .native_anchor(&editor.display_snapshot(cx), cx),
953 original_scroll_position
954 );
955
956 let nav_entry = pop_history(&mut editor, cx).unwrap();
957 editor.navigate(nav_entry.data.unwrap(), window, cx);
958 assert_eq!(
959 editor
960 .scroll_manager
961 .native_anchor(&editor.display_snapshot(cx), cx),
962 original_scroll_position
963 );
964
965 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
966 let mut invalid_anchor = editor
967 .scroll_manager
968 .native_anchor(&editor.display_snapshot(cx), cx)
969 .anchor;
970 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
971 let invalid_point = Point::new(9999, 0);
972 editor.navigate(
973 Arc::new(NavigationData {
974 cursor_anchor: invalid_anchor,
975 cursor_position: invalid_point,
976 scroll_anchor: ScrollAnchor {
977 anchor: invalid_anchor,
978 offset: Default::default(),
979 },
980 scroll_top_row: invalid_point.row,
981 }),
982 window,
983 cx,
984 );
985 assert_eq!(
986 editor
987 .selections
988 .display_ranges(&editor.display_snapshot(cx)),
989 &[editor.max_point(cx)..editor.max_point(cx)]
990 );
991 assert_eq!(
992 editor.scroll_position(cx),
993 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
994 );
995
996 editor
997 })
998 });
999}
1000
1001#[gpui::test]
1002fn test_cancel(cx: &mut TestAppContext) {
1003 init_test(cx, |_| {});
1004
1005 let editor = cx.add_window(|window, cx| {
1006 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
1007 build_editor(buffer, window, cx)
1008 });
1009
1010 _ = editor.update(cx, |editor, window, cx| {
1011 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
1012 editor.update_selection(
1013 DisplayPoint::new(DisplayRow(1), 1),
1014 0,
1015 gpui::Point::<f32>::default(),
1016 window,
1017 cx,
1018 );
1019 editor.end_selection(window, cx);
1020
1021 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
1022 editor.update_selection(
1023 DisplayPoint::new(DisplayRow(0), 3),
1024 0,
1025 gpui::Point::<f32>::default(),
1026 window,
1027 cx,
1028 );
1029 editor.end_selection(window, cx);
1030 assert_eq!(
1031 display_ranges(editor, cx),
1032 [
1033 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
1034 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
1035 ]
1036 );
1037 });
1038
1039 _ = editor.update(cx, |editor, window, cx| {
1040 editor.cancel(&Cancel, window, cx);
1041 assert_eq!(
1042 display_ranges(editor, cx),
1043 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1044 );
1045 });
1046
1047 _ = editor.update(cx, |editor, window, cx| {
1048 editor.cancel(&Cancel, window, cx);
1049 assert_eq!(
1050 display_ranges(editor, cx),
1051 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1052 );
1053 });
1054}
1055
1056#[gpui::test]
1057fn test_fold_action(cx: &mut TestAppContext) {
1058 init_test(cx, |_| {});
1059
1060 let editor = cx.add_window(|window, cx| {
1061 let buffer = MultiBuffer::build_simple(
1062 &"
1063 impl Foo {
1064 // Hello!
1065
1066 fn a() {
1067 1
1068 }
1069
1070 fn b() {
1071 2
1072 }
1073
1074 fn c() {
1075 3
1076 }
1077 }
1078 "
1079 .unindent(),
1080 cx,
1081 );
1082 build_editor(buffer, window, cx)
1083 });
1084
1085 _ = editor.update(cx, |editor, window, cx| {
1086 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1087 s.select_display_ranges([
1088 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1089 ]);
1090 });
1091 editor.fold(&Fold, window, cx);
1092 assert_eq!(
1093 editor.display_text(cx),
1094 "
1095 impl Foo {
1096 // Hello!
1097
1098 fn a() {
1099 1
1100 }
1101
1102 fn b() {⋯
1103 }
1104
1105 fn c() {⋯
1106 }
1107 }
1108 "
1109 .unindent(),
1110 );
1111
1112 editor.fold(&Fold, window, cx);
1113 assert_eq!(
1114 editor.display_text(cx),
1115 "
1116 impl Foo {⋯
1117 }
1118 "
1119 .unindent(),
1120 );
1121
1122 editor.unfold_lines(&UnfoldLines, window, cx);
1123 assert_eq!(
1124 editor.display_text(cx),
1125 "
1126 impl Foo {
1127 // Hello!
1128
1129 fn a() {
1130 1
1131 }
1132
1133 fn b() {⋯
1134 }
1135
1136 fn c() {⋯
1137 }
1138 }
1139 "
1140 .unindent(),
1141 );
1142
1143 editor.unfold_lines(&UnfoldLines, window, cx);
1144 assert_eq!(
1145 editor.display_text(cx),
1146 editor.buffer.read(cx).read(cx).text()
1147 );
1148 });
1149}
1150
1151#[gpui::test]
1152fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1153 init_test(cx, |_| {});
1154
1155 let editor = cx.add_window(|window, cx| {
1156 let buffer = MultiBuffer::build_simple(
1157 &"
1158 class Foo:
1159 # Hello!
1160
1161 def a():
1162 print(1)
1163
1164 def b():
1165 print(2)
1166
1167 def c():
1168 print(3)
1169 "
1170 .unindent(),
1171 cx,
1172 );
1173 build_editor(buffer, window, cx)
1174 });
1175
1176 _ = editor.update(cx, |editor, window, cx| {
1177 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1178 s.select_display_ranges([
1179 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1180 ]);
1181 });
1182 editor.fold(&Fold, window, cx);
1183 assert_eq!(
1184 editor.display_text(cx),
1185 "
1186 class Foo:
1187 # Hello!
1188
1189 def a():
1190 print(1)
1191
1192 def b():⋯
1193
1194 def c():⋯
1195 "
1196 .unindent(),
1197 );
1198
1199 editor.fold(&Fold, window, cx);
1200 assert_eq!(
1201 editor.display_text(cx),
1202 "
1203 class Foo:⋯
1204 "
1205 .unindent(),
1206 );
1207
1208 editor.unfold_lines(&UnfoldLines, window, cx);
1209 assert_eq!(
1210 editor.display_text(cx),
1211 "
1212 class Foo:
1213 # Hello!
1214
1215 def a():
1216 print(1)
1217
1218 def b():⋯
1219
1220 def c():⋯
1221 "
1222 .unindent(),
1223 );
1224
1225 editor.unfold_lines(&UnfoldLines, window, cx);
1226 assert_eq!(
1227 editor.display_text(cx),
1228 editor.buffer.read(cx).read(cx).text()
1229 );
1230 });
1231}
1232
1233#[gpui::test]
1234fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1235 init_test(cx, |_| {});
1236
1237 let editor = cx.add_window(|window, cx| {
1238 let buffer = MultiBuffer::build_simple(
1239 &"
1240 class Foo:
1241 # Hello!
1242
1243 def a():
1244 print(1)
1245
1246 def b():
1247 print(2)
1248
1249
1250 def c():
1251 print(3)
1252
1253
1254 "
1255 .unindent(),
1256 cx,
1257 );
1258 build_editor(buffer, window, cx)
1259 });
1260
1261 _ = editor.update(cx, |editor, window, cx| {
1262 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1263 s.select_display_ranges([
1264 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1265 ]);
1266 });
1267 editor.fold(&Fold, window, cx);
1268 assert_eq!(
1269 editor.display_text(cx),
1270 "
1271 class Foo:
1272 # Hello!
1273
1274 def a():
1275 print(1)
1276
1277 def b():⋯
1278
1279
1280 def c():⋯
1281
1282
1283 "
1284 .unindent(),
1285 );
1286
1287 editor.fold(&Fold, window, cx);
1288 assert_eq!(
1289 editor.display_text(cx),
1290 "
1291 class Foo:⋯
1292
1293
1294 "
1295 .unindent(),
1296 );
1297
1298 editor.unfold_lines(&UnfoldLines, window, cx);
1299 assert_eq!(
1300 editor.display_text(cx),
1301 "
1302 class Foo:
1303 # Hello!
1304
1305 def a():
1306 print(1)
1307
1308 def b():⋯
1309
1310
1311 def c():⋯
1312
1313
1314 "
1315 .unindent(),
1316 );
1317
1318 editor.unfold_lines(&UnfoldLines, window, cx);
1319 assert_eq!(
1320 editor.display_text(cx),
1321 editor.buffer.read(cx).read(cx).text()
1322 );
1323 });
1324}
1325
1326#[gpui::test]
1327fn test_fold_at_level(cx: &mut TestAppContext) {
1328 init_test(cx, |_| {});
1329
1330 let editor = cx.add_window(|window, cx| {
1331 let buffer = MultiBuffer::build_simple(
1332 &"
1333 class Foo:
1334 # Hello!
1335
1336 def a():
1337 print(1)
1338
1339 def b():
1340 print(2)
1341
1342
1343 class Bar:
1344 # World!
1345
1346 def a():
1347 print(1)
1348
1349 def b():
1350 print(2)
1351
1352
1353 "
1354 .unindent(),
1355 cx,
1356 );
1357 build_editor(buffer, window, cx)
1358 });
1359
1360 _ = editor.update(cx, |editor, window, cx| {
1361 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1362 assert_eq!(
1363 editor.display_text(cx),
1364 "
1365 class Foo:
1366 # Hello!
1367
1368 def a():⋯
1369
1370 def b():⋯
1371
1372
1373 class Bar:
1374 # World!
1375
1376 def a():⋯
1377
1378 def b():⋯
1379
1380
1381 "
1382 .unindent(),
1383 );
1384
1385 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1386 assert_eq!(
1387 editor.display_text(cx),
1388 "
1389 class Foo:⋯
1390
1391
1392 class Bar:⋯
1393
1394
1395 "
1396 .unindent(),
1397 );
1398
1399 editor.unfold_all(&UnfoldAll, window, cx);
1400 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1401 assert_eq!(
1402 editor.display_text(cx),
1403 "
1404 class Foo:
1405 # Hello!
1406
1407 def a():
1408 print(1)
1409
1410 def b():
1411 print(2)
1412
1413
1414 class Bar:
1415 # World!
1416
1417 def a():
1418 print(1)
1419
1420 def b():
1421 print(2)
1422
1423
1424 "
1425 .unindent(),
1426 );
1427
1428 assert_eq!(
1429 editor.display_text(cx),
1430 editor.buffer.read(cx).read(cx).text()
1431 );
1432 let (_, positions) = marked_text_ranges(
1433 &"
1434 class Foo:
1435 # Hello!
1436
1437 def a():
1438 print(1)
1439
1440 def b():
1441 p«riˇ»nt(2)
1442
1443
1444 class Bar:
1445 # World!
1446
1447 def a():
1448 «ˇprint(1)
1449
1450 def b():
1451 print(2)»
1452
1453
1454 "
1455 .unindent(),
1456 true,
1457 );
1458
1459 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1460 s.select_ranges(
1461 positions
1462 .iter()
1463 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
1464 )
1465 });
1466
1467 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1468 assert_eq!(
1469 editor.display_text(cx),
1470 "
1471 class Foo:
1472 # Hello!
1473
1474 def a():⋯
1475
1476 def b():
1477 print(2)
1478
1479
1480 class Bar:
1481 # World!
1482
1483 def a():
1484 print(1)
1485
1486 def b():
1487 print(2)
1488
1489
1490 "
1491 .unindent(),
1492 );
1493 });
1494}
1495
1496#[gpui::test]
1497fn test_move_cursor(cx: &mut TestAppContext) {
1498 init_test(cx, |_| {});
1499
1500 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1501 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1502
1503 buffer.update(cx, |buffer, cx| {
1504 buffer.edit(
1505 vec![
1506 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1507 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1508 ],
1509 None,
1510 cx,
1511 );
1512 });
1513 _ = editor.update(cx, |editor, window, cx| {
1514 assert_eq!(
1515 display_ranges(editor, cx),
1516 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1517 );
1518
1519 editor.move_down(&MoveDown, window, cx);
1520 assert_eq!(
1521 display_ranges(editor, cx),
1522 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1523 );
1524
1525 editor.move_right(&MoveRight, window, cx);
1526 assert_eq!(
1527 display_ranges(editor, cx),
1528 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1529 );
1530
1531 editor.move_left(&MoveLeft, window, cx);
1532 assert_eq!(
1533 display_ranges(editor, cx),
1534 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1535 );
1536
1537 editor.move_up(&MoveUp, window, cx);
1538 assert_eq!(
1539 display_ranges(editor, cx),
1540 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1541 );
1542
1543 editor.move_to_end(&MoveToEnd, window, cx);
1544 assert_eq!(
1545 display_ranges(editor, cx),
1546 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1547 );
1548
1549 editor.move_to_beginning(&MoveToBeginning, window, cx);
1550 assert_eq!(
1551 display_ranges(editor, cx),
1552 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1553 );
1554
1555 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1556 s.select_display_ranges([
1557 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1558 ]);
1559 });
1560 editor.select_to_beginning(&SelectToBeginning, window, cx);
1561 assert_eq!(
1562 display_ranges(editor, cx),
1563 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1564 );
1565
1566 editor.select_to_end(&SelectToEnd, window, cx);
1567 assert_eq!(
1568 display_ranges(editor, cx),
1569 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1570 );
1571 });
1572}
1573
1574#[gpui::test]
1575fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1576 init_test(cx, |_| {});
1577
1578 let editor = cx.add_window(|window, cx| {
1579 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1580 build_editor(buffer, window, cx)
1581 });
1582
1583 assert_eq!('🟥'.len_utf8(), 4);
1584 assert_eq!('α'.len_utf8(), 2);
1585
1586 _ = editor.update(cx, |editor, window, cx| {
1587 editor.fold_creases(
1588 vec![
1589 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1590 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1591 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1592 ],
1593 true,
1594 window,
1595 cx,
1596 );
1597 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1598
1599 editor.move_right(&MoveRight, window, cx);
1600 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1601 editor.move_right(&MoveRight, window, cx);
1602 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1603 editor.move_right(&MoveRight, window, cx);
1604 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1605
1606 editor.move_down(&MoveDown, window, cx);
1607 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1608 editor.move_left(&MoveLeft, window, cx);
1609 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".len())]);
1610 editor.move_left(&MoveLeft, window, cx);
1611 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab".len())]);
1612 editor.move_left(&MoveLeft, window, cx);
1613 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1614
1615 editor.move_down(&MoveDown, window, cx);
1616 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1617 editor.move_right(&MoveRight, window, cx);
1618 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ".len())]);
1619 editor.move_right(&MoveRight, window, cx);
1620 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯".len())]);
1621 editor.move_right(&MoveRight, window, cx);
1622 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1623
1624 editor.move_up(&MoveUp, window, cx);
1625 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1626 editor.move_down(&MoveDown, window, cx);
1627 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1628 editor.move_up(&MoveUp, window, cx);
1629 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1630
1631 editor.move_up(&MoveUp, window, cx);
1632 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1633 editor.move_left(&MoveLeft, window, cx);
1634 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1635 editor.move_left(&MoveLeft, window, cx);
1636 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1637 });
1638}
1639
1640#[gpui::test]
1641fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1642 init_test(cx, |_| {});
1643
1644 let editor = cx.add_window(|window, cx| {
1645 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1646 build_editor(buffer, window, cx)
1647 });
1648 _ = editor.update(cx, |editor, window, cx| {
1649 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1650 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1651 });
1652
1653 // moving above start of document should move selection to start of document,
1654 // but the next move down should still be at the original goal_x
1655 editor.move_up(&MoveUp, window, cx);
1656 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1657
1658 editor.move_down(&MoveDown, window, cx);
1659 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1660
1661 editor.move_down(&MoveDown, window, cx);
1662 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1663
1664 editor.move_down(&MoveDown, window, cx);
1665 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1666
1667 editor.move_down(&MoveDown, window, cx);
1668 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1669
1670 // moving past end of document should not change goal_x
1671 editor.move_down(&MoveDown, window, cx);
1672 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1673
1674 editor.move_down(&MoveDown, window, cx);
1675 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1676
1677 editor.move_up(&MoveUp, window, cx);
1678 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1679
1680 editor.move_up(&MoveUp, window, cx);
1681 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1682
1683 editor.move_up(&MoveUp, window, cx);
1684 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1685 });
1686}
1687
1688#[gpui::test]
1689fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1690 init_test(cx, |_| {});
1691 let move_to_beg = MoveToBeginningOfLine {
1692 stop_at_soft_wraps: true,
1693 stop_at_indent: true,
1694 };
1695
1696 let delete_to_beg = DeleteToBeginningOfLine {
1697 stop_at_indent: false,
1698 };
1699
1700 let move_to_end = MoveToEndOfLine {
1701 stop_at_soft_wraps: true,
1702 };
1703
1704 let editor = cx.add_window(|window, cx| {
1705 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1706 build_editor(buffer, window, cx)
1707 });
1708 _ = editor.update(cx, |editor, window, cx| {
1709 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1710 s.select_display_ranges([
1711 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1712 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1713 ]);
1714 });
1715 });
1716
1717 _ = editor.update(cx, |editor, window, cx| {
1718 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1719 assert_eq!(
1720 display_ranges(editor, cx),
1721 &[
1722 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1723 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1724 ]
1725 );
1726 });
1727
1728 _ = editor.update(cx, |editor, window, cx| {
1729 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1730 assert_eq!(
1731 display_ranges(editor, cx),
1732 &[
1733 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1734 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1735 ]
1736 );
1737 });
1738
1739 _ = editor.update(cx, |editor, window, cx| {
1740 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1741 assert_eq!(
1742 display_ranges(editor, cx),
1743 &[
1744 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1745 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1746 ]
1747 );
1748 });
1749
1750 _ = editor.update(cx, |editor, window, cx| {
1751 editor.move_to_end_of_line(&move_to_end, window, cx);
1752 assert_eq!(
1753 display_ranges(editor, cx),
1754 &[
1755 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1756 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1757 ]
1758 );
1759 });
1760
1761 // Moving to the end of line again is a no-op.
1762 _ = editor.update(cx, |editor, window, cx| {
1763 editor.move_to_end_of_line(&move_to_end, window, cx);
1764 assert_eq!(
1765 display_ranges(editor, cx),
1766 &[
1767 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1768 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1769 ]
1770 );
1771 });
1772
1773 _ = editor.update(cx, |editor, window, cx| {
1774 editor.move_left(&MoveLeft, window, cx);
1775 editor.select_to_beginning_of_line(
1776 &SelectToBeginningOfLine {
1777 stop_at_soft_wraps: true,
1778 stop_at_indent: true,
1779 },
1780 window,
1781 cx,
1782 );
1783 assert_eq!(
1784 display_ranges(editor, cx),
1785 &[
1786 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1787 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1788 ]
1789 );
1790 });
1791
1792 _ = editor.update(cx, |editor, window, cx| {
1793 editor.select_to_beginning_of_line(
1794 &SelectToBeginningOfLine {
1795 stop_at_soft_wraps: true,
1796 stop_at_indent: true,
1797 },
1798 window,
1799 cx,
1800 );
1801 assert_eq!(
1802 display_ranges(editor, cx),
1803 &[
1804 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1805 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1806 ]
1807 );
1808 });
1809
1810 _ = editor.update(cx, |editor, window, cx| {
1811 editor.select_to_beginning_of_line(
1812 &SelectToBeginningOfLine {
1813 stop_at_soft_wraps: true,
1814 stop_at_indent: true,
1815 },
1816 window,
1817 cx,
1818 );
1819 assert_eq!(
1820 display_ranges(editor, cx),
1821 &[
1822 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1823 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1824 ]
1825 );
1826 });
1827
1828 _ = editor.update(cx, |editor, window, cx| {
1829 editor.select_to_end_of_line(
1830 &SelectToEndOfLine {
1831 stop_at_soft_wraps: true,
1832 },
1833 window,
1834 cx,
1835 );
1836 assert_eq!(
1837 display_ranges(editor, cx),
1838 &[
1839 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1840 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1841 ]
1842 );
1843 });
1844
1845 _ = editor.update(cx, |editor, window, cx| {
1846 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1847 assert_eq!(editor.display_text(cx), "ab\n de");
1848 assert_eq!(
1849 display_ranges(editor, cx),
1850 &[
1851 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1852 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1853 ]
1854 );
1855 });
1856
1857 _ = editor.update(cx, |editor, window, cx| {
1858 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1859 assert_eq!(editor.display_text(cx), "\n");
1860 assert_eq!(
1861 display_ranges(editor, cx),
1862 &[
1863 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1864 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1865 ]
1866 );
1867 });
1868}
1869
1870#[gpui::test]
1871fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1872 init_test(cx, |_| {});
1873 let move_to_beg = MoveToBeginningOfLine {
1874 stop_at_soft_wraps: false,
1875 stop_at_indent: false,
1876 };
1877
1878 let move_to_end = MoveToEndOfLine {
1879 stop_at_soft_wraps: false,
1880 };
1881
1882 let editor = cx.add_window(|window, cx| {
1883 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1884 build_editor(buffer, window, cx)
1885 });
1886
1887 _ = editor.update(cx, |editor, window, cx| {
1888 editor.set_wrap_width(Some(140.0.into()), cx);
1889
1890 // We expect the following lines after wrapping
1891 // ```
1892 // thequickbrownfox
1893 // jumpedoverthelazydo
1894 // gs
1895 // ```
1896 // The final `gs` was soft-wrapped onto a new line.
1897 assert_eq!(
1898 "thequickbrownfox\njumpedoverthelaz\nydogs",
1899 editor.display_text(cx),
1900 );
1901
1902 // First, let's assert behavior on the first line, that was not soft-wrapped.
1903 // Start the cursor at the `k` on the first line
1904 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1905 s.select_display_ranges([
1906 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1907 ]);
1908 });
1909
1910 // Moving to the beginning of the line should put us at the beginning of the line.
1911 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1912 assert_eq!(
1913 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1914 display_ranges(editor, cx)
1915 );
1916
1917 // Moving to the end of the line should put us at the end of the line.
1918 editor.move_to_end_of_line(&move_to_end, window, cx);
1919 assert_eq!(
1920 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1921 display_ranges(editor, cx)
1922 );
1923
1924 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1925 // Start the cursor at the last line (`y` that was wrapped to a new line)
1926 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1927 s.select_display_ranges([
1928 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1929 ]);
1930 });
1931
1932 // Moving to the beginning of the line should put us at the start of the second line of
1933 // display text, i.e., the `j`.
1934 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1935 assert_eq!(
1936 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1937 display_ranges(editor, cx)
1938 );
1939
1940 // Moving to the beginning of the line again should be a no-op.
1941 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1942 assert_eq!(
1943 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1944 display_ranges(editor, cx)
1945 );
1946
1947 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1948 // next display line.
1949 editor.move_to_end_of_line(&move_to_end, window, cx);
1950 assert_eq!(
1951 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1952 display_ranges(editor, cx)
1953 );
1954
1955 // Moving to the end of the line again should be a no-op.
1956 editor.move_to_end_of_line(&move_to_end, window, cx);
1957 assert_eq!(
1958 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1959 display_ranges(editor, cx)
1960 );
1961 });
1962}
1963
1964#[gpui::test]
1965fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1966 init_test(cx, |_| {});
1967
1968 let move_to_beg = MoveToBeginningOfLine {
1969 stop_at_soft_wraps: true,
1970 stop_at_indent: true,
1971 };
1972
1973 let select_to_beg = SelectToBeginningOfLine {
1974 stop_at_soft_wraps: true,
1975 stop_at_indent: true,
1976 };
1977
1978 let delete_to_beg = DeleteToBeginningOfLine {
1979 stop_at_indent: true,
1980 };
1981
1982 let move_to_end = MoveToEndOfLine {
1983 stop_at_soft_wraps: false,
1984 };
1985
1986 let editor = cx.add_window(|window, cx| {
1987 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1988 build_editor(buffer, window, cx)
1989 });
1990
1991 _ = editor.update(cx, |editor, window, cx| {
1992 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1993 s.select_display_ranges([
1994 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1995 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1996 ]);
1997 });
1998
1999 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
2000 // and the second cursor at the first non-whitespace character in the line.
2001 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2002 assert_eq!(
2003 display_ranges(editor, cx),
2004 &[
2005 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2006 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2007 ]
2008 );
2009
2010 // Moving to the beginning of the line again should be a no-op for the first cursor,
2011 // and should move the second cursor to the beginning of the line.
2012 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2013 assert_eq!(
2014 display_ranges(editor, cx),
2015 &[
2016 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2017 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2018 ]
2019 );
2020
2021 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2022 // and should move the second cursor back to the first non-whitespace character in the line.
2023 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2024 assert_eq!(
2025 display_ranges(editor, cx),
2026 &[
2027 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2028 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2029 ]
2030 );
2031
2032 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2033 // and to the first non-whitespace character in the line for the second cursor.
2034 editor.move_to_end_of_line(&move_to_end, window, cx);
2035 editor.move_left(&MoveLeft, window, cx);
2036 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2037 assert_eq!(
2038 display_ranges(editor, cx),
2039 &[
2040 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2041 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2042 ]
2043 );
2044
2045 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2046 // and should select to the beginning of the line for the second cursor.
2047 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2048 assert_eq!(
2049 display_ranges(editor, cx),
2050 &[
2051 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2052 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2053 ]
2054 );
2055
2056 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2057 // and should delete to the first non-whitespace character in the line for the second cursor.
2058 editor.move_to_end_of_line(&move_to_end, window, cx);
2059 editor.move_left(&MoveLeft, window, cx);
2060 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2061 assert_eq!(editor.text(cx), "c\n f");
2062 });
2063}
2064
2065#[gpui::test]
2066fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2067 init_test(cx, |_| {});
2068
2069 let move_to_beg = MoveToBeginningOfLine {
2070 stop_at_soft_wraps: true,
2071 stop_at_indent: true,
2072 };
2073
2074 let editor = cx.add_window(|window, cx| {
2075 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2076 build_editor(buffer, window, cx)
2077 });
2078
2079 _ = editor.update(cx, |editor, window, cx| {
2080 // test cursor between line_start and indent_start
2081 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2082 s.select_display_ranges([
2083 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2084 ]);
2085 });
2086
2087 // cursor should move to line_start
2088 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2089 assert_eq!(
2090 display_ranges(editor, cx),
2091 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2092 );
2093
2094 // cursor should move to indent_start
2095 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2096 assert_eq!(
2097 display_ranges(editor, cx),
2098 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2099 );
2100
2101 // cursor should move to back to line_start
2102 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2103 assert_eq!(
2104 display_ranges(editor, cx),
2105 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2106 );
2107 });
2108}
2109
2110#[gpui::test]
2111fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2112 init_test(cx, |_| {});
2113
2114 let editor = cx.add_window(|window, cx| {
2115 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2116 build_editor(buffer, window, cx)
2117 });
2118 _ = editor.update(cx, |editor, window, cx| {
2119 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2120 s.select_display_ranges([
2121 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2122 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2123 ])
2124 });
2125 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2126 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2127
2128 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2129 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2130
2131 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2132 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2133
2134 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2135 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2136
2137 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2138 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2139
2140 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2141 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2142
2143 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2144 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2145
2146 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2147 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2148
2149 editor.move_right(&MoveRight, window, cx);
2150 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2151 assert_selection_ranges(
2152 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2153 editor,
2154 cx,
2155 );
2156
2157 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2158 assert_selection_ranges(
2159 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2160 editor,
2161 cx,
2162 );
2163
2164 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2165 assert_selection_ranges(
2166 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2167 editor,
2168 cx,
2169 );
2170 });
2171}
2172
2173#[gpui::test]
2174fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2175 init_test(cx, |_| {});
2176
2177 let editor = cx.add_window(|window, cx| {
2178 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2179 build_editor(buffer, window, cx)
2180 });
2181
2182 _ = editor.update(cx, |editor, window, cx| {
2183 editor.set_wrap_width(Some(140.0.into()), cx);
2184 assert_eq!(
2185 editor.display_text(cx),
2186 "use one::{\n two::three::\n four::five\n};"
2187 );
2188
2189 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2190 s.select_display_ranges([
2191 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2192 ]);
2193 });
2194
2195 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2196 assert_eq!(
2197 display_ranges(editor, cx),
2198 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2199 );
2200
2201 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2202 assert_eq!(
2203 display_ranges(editor, cx),
2204 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2205 );
2206
2207 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2208 assert_eq!(
2209 display_ranges(editor, cx),
2210 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2211 );
2212
2213 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2214 assert_eq!(
2215 display_ranges(editor, cx),
2216 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2217 );
2218
2219 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2220 assert_eq!(
2221 display_ranges(editor, cx),
2222 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2223 );
2224
2225 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2226 assert_eq!(
2227 display_ranges(editor, cx),
2228 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2229 );
2230 });
2231}
2232
2233#[gpui::test]
2234async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2235 init_test(cx, |_| {});
2236 let mut cx = EditorTestContext::new(cx).await;
2237
2238 let line_height = cx.update_editor(|editor, window, cx| {
2239 editor
2240 .style(cx)
2241 .text
2242 .line_height_in_pixels(window.rem_size())
2243 });
2244 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2245
2246 // The third line only contains a single space so we can later assert that the
2247 // editor's paragraph movement considers a non-blank line as a paragraph
2248 // boundary.
2249 cx.set_state(&"ˇone\ntwo\n \nthree\nfourˇ\nfive\n\nsix");
2250
2251 cx.update_editor(|editor, window, cx| {
2252 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2253 });
2254 cx.assert_editor_state(&"one\ntwo\nˇ \nthree\nfour\nfive\nˇ\nsix");
2255
2256 cx.update_editor(|editor, window, cx| {
2257 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2258 });
2259 cx.assert_editor_state(&"one\ntwo\n \nthree\nfour\nfive\nˇ\nsixˇ");
2260
2261 cx.update_editor(|editor, window, cx| {
2262 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2263 });
2264 cx.assert_editor_state(&"one\ntwo\n \nthree\nfour\nfive\n\nsixˇ");
2265
2266 cx.update_editor(|editor, window, cx| {
2267 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2268 });
2269 cx.assert_editor_state(&"one\ntwo\n \nthree\nfour\nfive\nˇ\nsix");
2270
2271 cx.update_editor(|editor, window, cx| {
2272 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2273 });
2274
2275 cx.assert_editor_state(&"one\ntwo\nˇ \nthree\nfour\nfive\n\nsix");
2276
2277 cx.update_editor(|editor, window, cx| {
2278 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2279 });
2280 cx.assert_editor_state(&"ˇone\ntwo\n \nthree\nfour\nfive\n\nsix");
2281}
2282
2283#[gpui::test]
2284async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2285 init_test(cx, |_| {});
2286 let mut cx = EditorTestContext::new(cx).await;
2287 let line_height = cx.update_editor(|editor, window, cx| {
2288 editor
2289 .style(cx)
2290 .text
2291 .line_height_in_pixels(window.rem_size())
2292 });
2293 let window = cx.window;
2294 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2295
2296 cx.set_state(
2297 r#"ˇone
2298 two
2299 three
2300 four
2301 five
2302 six
2303 seven
2304 eight
2305 nine
2306 ten
2307 "#,
2308 );
2309
2310 cx.update_editor(|editor, window, cx| {
2311 assert_eq!(
2312 editor.snapshot(window, cx).scroll_position(),
2313 gpui::Point::new(0., 0.)
2314 );
2315 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2316 assert_eq!(
2317 editor.snapshot(window, cx).scroll_position(),
2318 gpui::Point::new(0., 3.)
2319 );
2320 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2321 assert_eq!(
2322 editor.snapshot(window, cx).scroll_position(),
2323 gpui::Point::new(0., 6.)
2324 );
2325 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2326 assert_eq!(
2327 editor.snapshot(window, cx).scroll_position(),
2328 gpui::Point::new(0., 3.)
2329 );
2330
2331 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2332 assert_eq!(
2333 editor.snapshot(window, cx).scroll_position(),
2334 gpui::Point::new(0., 1.)
2335 );
2336 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2337 assert_eq!(
2338 editor.snapshot(window, cx).scroll_position(),
2339 gpui::Point::new(0., 3.)
2340 );
2341 });
2342}
2343
2344#[gpui::test]
2345async fn test_autoscroll(cx: &mut TestAppContext) {
2346 init_test(cx, |_| {});
2347 let mut cx = EditorTestContext::new(cx).await;
2348
2349 let line_height = cx.update_editor(|editor, window, cx| {
2350 editor.set_vertical_scroll_margin(2, cx);
2351 editor
2352 .style(cx)
2353 .text
2354 .line_height_in_pixels(window.rem_size())
2355 });
2356 let window = cx.window;
2357 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2358
2359 cx.set_state(
2360 r#"ˇone
2361 two
2362 three
2363 four
2364 five
2365 six
2366 seven
2367 eight
2368 nine
2369 ten
2370 "#,
2371 );
2372 cx.update_editor(|editor, window, cx| {
2373 assert_eq!(
2374 editor.snapshot(window, cx).scroll_position(),
2375 gpui::Point::new(0., 0.0)
2376 );
2377 });
2378
2379 // Add a cursor below the visible area. Since both cursors cannot fit
2380 // on screen, the editor autoscrolls to reveal the newest cursor, and
2381 // allows the vertical scroll margin below that cursor.
2382 cx.update_editor(|editor, window, cx| {
2383 editor.change_selections(Default::default(), window, cx, |selections| {
2384 selections.select_ranges([
2385 Point::new(0, 0)..Point::new(0, 0),
2386 Point::new(6, 0)..Point::new(6, 0),
2387 ]);
2388 })
2389 });
2390 cx.update_editor(|editor, window, cx| {
2391 assert_eq!(
2392 editor.snapshot(window, cx).scroll_position(),
2393 gpui::Point::new(0., 3.0)
2394 );
2395 });
2396
2397 // Move down. The editor cursor scrolls down to track the newest cursor.
2398 cx.update_editor(|editor, window, cx| {
2399 editor.move_down(&Default::default(), window, cx);
2400 });
2401 cx.update_editor(|editor, window, cx| {
2402 assert_eq!(
2403 editor.snapshot(window, cx).scroll_position(),
2404 gpui::Point::new(0., 4.0)
2405 );
2406 });
2407
2408 // Add a cursor above the visible area. Since both cursors fit on screen,
2409 // the editor scrolls to show both.
2410 cx.update_editor(|editor, window, cx| {
2411 editor.change_selections(Default::default(), window, cx, |selections| {
2412 selections.select_ranges([
2413 Point::new(1, 0)..Point::new(1, 0),
2414 Point::new(6, 0)..Point::new(6, 0),
2415 ]);
2416 })
2417 });
2418 cx.update_editor(|editor, window, cx| {
2419 assert_eq!(
2420 editor.snapshot(window, cx).scroll_position(),
2421 gpui::Point::new(0., 1.0)
2422 );
2423 });
2424}
2425
2426#[gpui::test]
2427async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2428 init_test(cx, |_| {});
2429 let mut cx = EditorTestContext::new(cx).await;
2430
2431 let line_height = cx.update_editor(|editor, window, cx| {
2432 editor
2433 .style(cx)
2434 .text
2435 .line_height_in_pixels(window.rem_size())
2436 });
2437 let window = cx.window;
2438 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2439 cx.set_state(
2440 &r#"
2441 ˇone
2442 two
2443 threeˇ
2444 four
2445 five
2446 six
2447 seven
2448 eight
2449 nine
2450 ten
2451 "#
2452 .unindent(),
2453 );
2454
2455 cx.update_editor(|editor, window, cx| {
2456 editor.move_page_down(&MovePageDown::default(), window, cx)
2457 });
2458 cx.assert_editor_state(
2459 &r#"
2460 one
2461 two
2462 three
2463 ˇfour
2464 five
2465 sixˇ
2466 seven
2467 eight
2468 nine
2469 ten
2470 "#
2471 .unindent(),
2472 );
2473
2474 cx.update_editor(|editor, window, cx| {
2475 editor.move_page_down(&MovePageDown::default(), window, cx)
2476 });
2477 cx.assert_editor_state(
2478 &r#"
2479 one
2480 two
2481 three
2482 four
2483 five
2484 six
2485 ˇseven
2486 eight
2487 nineˇ
2488 ten
2489 "#
2490 .unindent(),
2491 );
2492
2493 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2494 cx.assert_editor_state(
2495 &r#"
2496 one
2497 two
2498 three
2499 ˇfour
2500 five
2501 sixˇ
2502 seven
2503 eight
2504 nine
2505 ten
2506 "#
2507 .unindent(),
2508 );
2509
2510 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2511 cx.assert_editor_state(
2512 &r#"
2513 ˇone
2514 two
2515 threeˇ
2516 four
2517 five
2518 six
2519 seven
2520 eight
2521 nine
2522 ten
2523 "#
2524 .unindent(),
2525 );
2526
2527 // Test select collapsing
2528 cx.update_editor(|editor, window, cx| {
2529 editor.move_page_down(&MovePageDown::default(), window, cx);
2530 editor.move_page_down(&MovePageDown::default(), window, cx);
2531 editor.move_page_down(&MovePageDown::default(), window, cx);
2532 });
2533 cx.assert_editor_state(
2534 &r#"
2535 one
2536 two
2537 three
2538 four
2539 five
2540 six
2541 seven
2542 eight
2543 nine
2544 ˇten
2545 ˇ"#
2546 .unindent(),
2547 );
2548}
2549
2550#[gpui::test]
2551async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2552 init_test(cx, |_| {});
2553 let mut cx = EditorTestContext::new(cx).await;
2554 cx.set_state("one «two threeˇ» four");
2555 cx.update_editor(|editor, window, cx| {
2556 editor.delete_to_beginning_of_line(
2557 &DeleteToBeginningOfLine {
2558 stop_at_indent: false,
2559 },
2560 window,
2561 cx,
2562 );
2563 assert_eq!(editor.text(cx), " four");
2564 });
2565}
2566
2567#[gpui::test]
2568async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2569 init_test(cx, |_| {});
2570
2571 let mut cx = EditorTestContext::new(cx).await;
2572
2573 // For an empty selection, the preceding word fragment is deleted.
2574 // For non-empty selections, only selected characters are deleted.
2575 cx.set_state("onˇe two t«hreˇ»e four");
2576 cx.update_editor(|editor, window, cx| {
2577 editor.delete_to_previous_word_start(
2578 &DeleteToPreviousWordStart {
2579 ignore_newlines: false,
2580 ignore_brackets: false,
2581 },
2582 window,
2583 cx,
2584 );
2585 });
2586 cx.assert_editor_state("ˇe two tˇe four");
2587
2588 cx.set_state("e tˇwo te «fˇ»our");
2589 cx.update_editor(|editor, window, cx| {
2590 editor.delete_to_next_word_end(
2591 &DeleteToNextWordEnd {
2592 ignore_newlines: false,
2593 ignore_brackets: false,
2594 },
2595 window,
2596 cx,
2597 );
2598 });
2599 cx.assert_editor_state("e tˇ te ˇour");
2600}
2601
2602#[gpui::test]
2603async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2604 init_test(cx, |_| {});
2605
2606 let mut cx = EditorTestContext::new(cx).await;
2607
2608 cx.set_state("here is some text ˇwith a space");
2609 cx.update_editor(|editor, window, cx| {
2610 editor.delete_to_previous_word_start(
2611 &DeleteToPreviousWordStart {
2612 ignore_newlines: false,
2613 ignore_brackets: true,
2614 },
2615 window,
2616 cx,
2617 );
2618 });
2619 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2620 cx.assert_editor_state("here is some textˇwith a space");
2621
2622 cx.set_state("here is some text ˇwith a space");
2623 cx.update_editor(|editor, window, cx| {
2624 editor.delete_to_previous_word_start(
2625 &DeleteToPreviousWordStart {
2626 ignore_newlines: false,
2627 ignore_brackets: false,
2628 },
2629 window,
2630 cx,
2631 );
2632 });
2633 cx.assert_editor_state("here is some textˇwith a space");
2634
2635 cx.set_state("here is some textˇ with a space");
2636 cx.update_editor(|editor, window, cx| {
2637 editor.delete_to_next_word_end(
2638 &DeleteToNextWordEnd {
2639 ignore_newlines: false,
2640 ignore_brackets: true,
2641 },
2642 window,
2643 cx,
2644 );
2645 });
2646 // Same happens in the other direction.
2647 cx.assert_editor_state("here is some textˇwith a space");
2648
2649 cx.set_state("here is some textˇ with a space");
2650 cx.update_editor(|editor, window, cx| {
2651 editor.delete_to_next_word_end(
2652 &DeleteToNextWordEnd {
2653 ignore_newlines: false,
2654 ignore_brackets: false,
2655 },
2656 window,
2657 cx,
2658 );
2659 });
2660 cx.assert_editor_state("here is some textˇwith a space");
2661
2662 cx.set_state("here is some textˇ with a space");
2663 cx.update_editor(|editor, window, cx| {
2664 editor.delete_to_next_word_end(
2665 &DeleteToNextWordEnd {
2666 ignore_newlines: true,
2667 ignore_brackets: false,
2668 },
2669 window,
2670 cx,
2671 );
2672 });
2673 cx.assert_editor_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: true,
2678 ignore_brackets: false,
2679 },
2680 window,
2681 cx,
2682 );
2683 });
2684 cx.assert_editor_state("here is some ˇwith a space");
2685 cx.update_editor(|editor, window, cx| {
2686 editor.delete_to_previous_word_start(
2687 &DeleteToPreviousWordStart {
2688 ignore_newlines: true,
2689 ignore_brackets: false,
2690 },
2691 window,
2692 cx,
2693 );
2694 });
2695 // Single whitespaces are removed with the word behind them.
2696 cx.assert_editor_state("here is ˇwith a space");
2697 cx.update_editor(|editor, window, cx| {
2698 editor.delete_to_previous_word_start(
2699 &DeleteToPreviousWordStart {
2700 ignore_newlines: true,
2701 ignore_brackets: false,
2702 },
2703 window,
2704 cx,
2705 );
2706 });
2707 cx.assert_editor_state("here ˇwith a space");
2708 cx.update_editor(|editor, window, cx| {
2709 editor.delete_to_previous_word_start(
2710 &DeleteToPreviousWordStart {
2711 ignore_newlines: true,
2712 ignore_brackets: false,
2713 },
2714 window,
2715 cx,
2716 );
2717 });
2718 cx.assert_editor_state("ˇwith a space");
2719 cx.update_editor(|editor, window, cx| {
2720 editor.delete_to_previous_word_start(
2721 &DeleteToPreviousWordStart {
2722 ignore_newlines: true,
2723 ignore_brackets: false,
2724 },
2725 window,
2726 cx,
2727 );
2728 });
2729 cx.assert_editor_state("ˇwith a space");
2730 cx.update_editor(|editor, window, cx| {
2731 editor.delete_to_next_word_end(
2732 &DeleteToNextWordEnd {
2733 ignore_newlines: true,
2734 ignore_brackets: false,
2735 },
2736 window,
2737 cx,
2738 );
2739 });
2740 // Same happens in the other direction.
2741 cx.assert_editor_state("ˇ a space");
2742 cx.update_editor(|editor, window, cx| {
2743 editor.delete_to_next_word_end(
2744 &DeleteToNextWordEnd {
2745 ignore_newlines: true,
2746 ignore_brackets: false,
2747 },
2748 window,
2749 cx,
2750 );
2751 });
2752 cx.assert_editor_state("ˇ space");
2753 cx.update_editor(|editor, window, cx| {
2754 editor.delete_to_next_word_end(
2755 &DeleteToNextWordEnd {
2756 ignore_newlines: true,
2757 ignore_brackets: false,
2758 },
2759 window,
2760 cx,
2761 );
2762 });
2763 cx.assert_editor_state("ˇ");
2764 cx.update_editor(|editor, window, cx| {
2765 editor.delete_to_next_word_end(
2766 &DeleteToNextWordEnd {
2767 ignore_newlines: true,
2768 ignore_brackets: false,
2769 },
2770 window,
2771 cx,
2772 );
2773 });
2774 cx.assert_editor_state("ˇ");
2775 cx.update_editor(|editor, window, cx| {
2776 editor.delete_to_previous_word_start(
2777 &DeleteToPreviousWordStart {
2778 ignore_newlines: true,
2779 ignore_brackets: false,
2780 },
2781 window,
2782 cx,
2783 );
2784 });
2785 cx.assert_editor_state("ˇ");
2786}
2787
2788#[gpui::test]
2789async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2790 init_test(cx, |_| {});
2791
2792 let language = Arc::new(
2793 Language::new(
2794 LanguageConfig {
2795 brackets: BracketPairConfig {
2796 pairs: vec![
2797 BracketPair {
2798 start: "\"".to_string(),
2799 end: "\"".to_string(),
2800 close: true,
2801 surround: true,
2802 newline: false,
2803 },
2804 BracketPair {
2805 start: "(".to_string(),
2806 end: ")".to_string(),
2807 close: true,
2808 surround: true,
2809 newline: true,
2810 },
2811 ],
2812 ..BracketPairConfig::default()
2813 },
2814 ..LanguageConfig::default()
2815 },
2816 Some(tree_sitter_rust::LANGUAGE.into()),
2817 )
2818 .with_brackets_query(
2819 r#"
2820 ("(" @open ")" @close)
2821 ("\"" @open "\"" @close)
2822 "#,
2823 )
2824 .unwrap(),
2825 );
2826
2827 let mut cx = EditorTestContext::new(cx).await;
2828 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2829
2830 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2831 cx.update_editor(|editor, window, cx| {
2832 editor.delete_to_previous_word_start(
2833 &DeleteToPreviousWordStart {
2834 ignore_newlines: true,
2835 ignore_brackets: false,
2836 },
2837 window,
2838 cx,
2839 );
2840 });
2841 // Deletion stops before brackets if asked to not ignore them.
2842 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2843 cx.update_editor(|editor, window, cx| {
2844 editor.delete_to_previous_word_start(
2845 &DeleteToPreviousWordStart {
2846 ignore_newlines: true,
2847 ignore_brackets: false,
2848 },
2849 window,
2850 cx,
2851 );
2852 });
2853 // Deletion has to remove a single bracket and then stop again.
2854 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2855
2856 cx.update_editor(|editor, window, cx| {
2857 editor.delete_to_previous_word_start(
2858 &DeleteToPreviousWordStart {
2859 ignore_newlines: true,
2860 ignore_brackets: false,
2861 },
2862 window,
2863 cx,
2864 );
2865 });
2866 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2867
2868 cx.update_editor(|editor, window, cx| {
2869 editor.delete_to_previous_word_start(
2870 &DeleteToPreviousWordStart {
2871 ignore_newlines: true,
2872 ignore_brackets: false,
2873 },
2874 window,
2875 cx,
2876 );
2877 });
2878 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2879
2880 cx.update_editor(|editor, window, cx| {
2881 editor.delete_to_previous_word_start(
2882 &DeleteToPreviousWordStart {
2883 ignore_newlines: true,
2884 ignore_brackets: false,
2885 },
2886 window,
2887 cx,
2888 );
2889 });
2890 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2891
2892 cx.update_editor(|editor, window, cx| {
2893 editor.delete_to_next_word_end(
2894 &DeleteToNextWordEnd {
2895 ignore_newlines: true,
2896 ignore_brackets: false,
2897 },
2898 window,
2899 cx,
2900 );
2901 });
2902 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2903 cx.assert_editor_state(r#"ˇ");"#);
2904
2905 cx.update_editor(|editor, window, cx| {
2906 editor.delete_to_next_word_end(
2907 &DeleteToNextWordEnd {
2908 ignore_newlines: true,
2909 ignore_brackets: false,
2910 },
2911 window,
2912 cx,
2913 );
2914 });
2915 cx.assert_editor_state(r#"ˇ"#);
2916
2917 cx.update_editor(|editor, window, cx| {
2918 editor.delete_to_next_word_end(
2919 &DeleteToNextWordEnd {
2920 ignore_newlines: true,
2921 ignore_brackets: false,
2922 },
2923 window,
2924 cx,
2925 );
2926 });
2927 cx.assert_editor_state(r#"ˇ"#);
2928
2929 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2930 cx.update_editor(|editor, window, cx| {
2931 editor.delete_to_previous_word_start(
2932 &DeleteToPreviousWordStart {
2933 ignore_newlines: true,
2934 ignore_brackets: true,
2935 },
2936 window,
2937 cx,
2938 );
2939 });
2940 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2941}
2942
2943#[gpui::test]
2944fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2945 init_test(cx, |_| {});
2946
2947 let editor = cx.add_window(|window, cx| {
2948 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2949 build_editor(buffer, window, cx)
2950 });
2951 let del_to_prev_word_start = DeleteToPreviousWordStart {
2952 ignore_newlines: false,
2953 ignore_brackets: false,
2954 };
2955 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2956 ignore_newlines: true,
2957 ignore_brackets: false,
2958 };
2959
2960 _ = editor.update(cx, |editor, window, cx| {
2961 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2962 s.select_display_ranges([
2963 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2964 ])
2965 });
2966 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2967 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2968 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2969 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2970 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2971 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2972 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2973 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2974 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2975 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2976 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2977 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2978 });
2979}
2980
2981#[gpui::test]
2982fn test_delete_to_previous_subword_start_or_newline(cx: &mut TestAppContext) {
2983 init_test(cx, |_| {});
2984
2985 let editor = cx.add_window(|window, cx| {
2986 let buffer = MultiBuffer::build_simple("fooBar\n\nbazQux", cx);
2987 build_editor(buffer, window, cx)
2988 });
2989 let del_to_prev_sub_word_start = DeleteToPreviousSubwordStart {
2990 ignore_newlines: false,
2991 ignore_brackets: false,
2992 };
2993 let del_to_prev_sub_word_start_ignore_newlines = DeleteToPreviousSubwordStart {
2994 ignore_newlines: true,
2995 ignore_brackets: false,
2996 };
2997
2998 _ = editor.update(cx, |editor, window, cx| {
2999 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3000 s.select_display_ranges([
3001 DisplayPoint::new(DisplayRow(2), 6)..DisplayPoint::new(DisplayRow(2), 6)
3002 ])
3003 });
3004 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3005 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n\nbaz");
3006 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3007 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n\n");
3008 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3009 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n");
3010 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3011 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar");
3012 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3013 assert_eq!(editor.buffer.read(cx).read(cx).text(), "foo");
3014 editor.delete_to_previous_subword_start(
3015 &del_to_prev_sub_word_start_ignore_newlines,
3016 window,
3017 cx,
3018 );
3019 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3020 });
3021}
3022
3023#[gpui::test]
3024fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3025 init_test(cx, |_| {});
3026
3027 let editor = cx.add_window(|window, cx| {
3028 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3029 build_editor(buffer, window, cx)
3030 });
3031 let del_to_next_word_end = DeleteToNextWordEnd {
3032 ignore_newlines: false,
3033 ignore_brackets: false,
3034 };
3035 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3036 ignore_newlines: true,
3037 ignore_brackets: false,
3038 };
3039
3040 _ = editor.update(cx, |editor, window, cx| {
3041 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3042 s.select_display_ranges([
3043 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3044 ])
3045 });
3046 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3047 assert_eq!(
3048 editor.buffer.read(cx).read(cx).text(),
3049 "one\n two\nthree\n four"
3050 );
3051 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3052 assert_eq!(
3053 editor.buffer.read(cx).read(cx).text(),
3054 "\n two\nthree\n four"
3055 );
3056 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3057 assert_eq!(
3058 editor.buffer.read(cx).read(cx).text(),
3059 "two\nthree\n four"
3060 );
3061 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3062 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3063 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3064 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3065 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3066 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3067 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3068 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3069 });
3070}
3071
3072#[gpui::test]
3073fn test_delete_to_next_subword_end_or_newline(cx: &mut TestAppContext) {
3074 init_test(cx, |_| {});
3075
3076 let editor = cx.add_window(|window, cx| {
3077 let buffer = MultiBuffer::build_simple("\nfooBar\n bazQux", cx);
3078 build_editor(buffer, window, cx)
3079 });
3080 let del_to_next_subword_end = DeleteToNextSubwordEnd {
3081 ignore_newlines: false,
3082 ignore_brackets: false,
3083 };
3084 let del_to_next_subword_end_ignore_newlines = DeleteToNextSubwordEnd {
3085 ignore_newlines: true,
3086 ignore_brackets: false,
3087 };
3088
3089 _ = editor.update(cx, |editor, window, cx| {
3090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3091 s.select_display_ranges([
3092 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3093 ])
3094 });
3095 // Delete "\n" (empty line)
3096 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3097 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n bazQux");
3098 // Delete "foo" (subword boundary)
3099 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3100 assert_eq!(editor.buffer.read(cx).read(cx).text(), "Bar\n bazQux");
3101 // Delete "Bar"
3102 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3103 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n bazQux");
3104 // Delete "\n " (newline + leading whitespace)
3105 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3106 assert_eq!(editor.buffer.read(cx).read(cx).text(), "bazQux");
3107 // Delete "baz" (subword boundary)
3108 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3109 assert_eq!(editor.buffer.read(cx).read(cx).text(), "Qux");
3110 // With ignore_newlines, delete "Qux"
3111 editor.delete_to_next_subword_end(&del_to_next_subword_end_ignore_newlines, window, cx);
3112 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3113 });
3114}
3115
3116#[gpui::test]
3117fn test_newline(cx: &mut TestAppContext) {
3118 init_test(cx, |_| {});
3119
3120 let editor = cx.add_window(|window, cx| {
3121 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3122 build_editor(buffer, window, cx)
3123 });
3124
3125 _ = editor.update(cx, |editor, window, cx| {
3126 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3127 s.select_display_ranges([
3128 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3129 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3130 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3131 ])
3132 });
3133
3134 editor.newline(&Newline, window, cx);
3135 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3136 });
3137}
3138
3139#[gpui::test]
3140async fn test_newline_yaml(cx: &mut TestAppContext) {
3141 init_test(cx, |_| {});
3142
3143 let mut cx = EditorTestContext::new(cx).await;
3144 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3145 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3146
3147 // Object (between 2 fields)
3148 cx.set_state(indoc! {"
3149 test:ˇ
3150 hello: bye"});
3151 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3152 cx.assert_editor_state(indoc! {"
3153 test:
3154 ˇ
3155 hello: bye"});
3156
3157 // Object (first and single line)
3158 cx.set_state(indoc! {"
3159 test:ˇ"});
3160 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3161 cx.assert_editor_state(indoc! {"
3162 test:
3163 ˇ"});
3164
3165 // Array with objects (after first element)
3166 cx.set_state(indoc! {"
3167 test:
3168 - foo: barˇ"});
3169 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3170 cx.assert_editor_state(indoc! {"
3171 test:
3172 - foo: bar
3173 ˇ"});
3174
3175 // Array with objects and comment
3176 cx.set_state(indoc! {"
3177 test:
3178 - foo: bar
3179 - bar: # testˇ"});
3180 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3181 cx.assert_editor_state(indoc! {"
3182 test:
3183 - foo: bar
3184 - bar: # test
3185 ˇ"});
3186
3187 // Array with objects (after second element)
3188 cx.set_state(indoc! {"
3189 test:
3190 - foo: bar
3191 - bar: fooˇ"});
3192 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3193 cx.assert_editor_state(indoc! {"
3194 test:
3195 - foo: bar
3196 - bar: foo
3197 ˇ"});
3198
3199 // Array with strings (after first element)
3200 cx.set_state(indoc! {"
3201 test:
3202 - fooˇ"});
3203 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3204 cx.assert_editor_state(indoc! {"
3205 test:
3206 - foo
3207 ˇ"});
3208}
3209
3210#[gpui::test]
3211fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3212 init_test(cx, |_| {});
3213
3214 let editor = cx.add_window(|window, cx| {
3215 let buffer = MultiBuffer::build_simple(
3216 "
3217 a
3218 b(
3219 X
3220 )
3221 c(
3222 X
3223 )
3224 "
3225 .unindent()
3226 .as_str(),
3227 cx,
3228 );
3229 let mut editor = build_editor(buffer, window, cx);
3230 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3231 s.select_ranges([
3232 Point::new(2, 4)..Point::new(2, 5),
3233 Point::new(5, 4)..Point::new(5, 5),
3234 ])
3235 });
3236 editor
3237 });
3238
3239 _ = editor.update(cx, |editor, window, cx| {
3240 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3241 editor.buffer.update(cx, |buffer, cx| {
3242 buffer.edit(
3243 [
3244 (Point::new(1, 2)..Point::new(3, 0), ""),
3245 (Point::new(4, 2)..Point::new(6, 0), ""),
3246 ],
3247 None,
3248 cx,
3249 );
3250 assert_eq!(
3251 buffer.read(cx).text(),
3252 "
3253 a
3254 b()
3255 c()
3256 "
3257 .unindent()
3258 );
3259 });
3260 assert_eq!(
3261 editor.selections.ranges(&editor.display_snapshot(cx)),
3262 &[
3263 Point::new(1, 2)..Point::new(1, 2),
3264 Point::new(2, 2)..Point::new(2, 2),
3265 ],
3266 );
3267
3268 editor.newline(&Newline, window, cx);
3269 assert_eq!(
3270 editor.text(cx),
3271 "
3272 a
3273 b(
3274 )
3275 c(
3276 )
3277 "
3278 .unindent()
3279 );
3280
3281 // The selections are moved after the inserted newlines
3282 assert_eq!(
3283 editor.selections.ranges(&editor.display_snapshot(cx)),
3284 &[
3285 Point::new(2, 0)..Point::new(2, 0),
3286 Point::new(4, 0)..Point::new(4, 0),
3287 ],
3288 );
3289 });
3290}
3291
3292#[gpui::test]
3293async fn test_newline_above(cx: &mut TestAppContext) {
3294 init_test(cx, |settings| {
3295 settings.defaults.tab_size = NonZeroU32::new(4)
3296 });
3297
3298 let language = Arc::new(
3299 Language::new(
3300 LanguageConfig::default(),
3301 Some(tree_sitter_rust::LANGUAGE.into()),
3302 )
3303 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3304 .unwrap(),
3305 );
3306
3307 let mut cx = EditorTestContext::new(cx).await;
3308 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3309 cx.set_state(indoc! {"
3310 const a: ˇA = (
3311 (ˇ
3312 «const_functionˇ»(ˇ),
3313 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3314 )ˇ
3315 ˇ);ˇ
3316 "});
3317
3318 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3319 cx.assert_editor_state(indoc! {"
3320 ˇ
3321 const a: A = (
3322 ˇ
3323 (
3324 ˇ
3325 ˇ
3326 const_function(),
3327 ˇ
3328 ˇ
3329 ˇ
3330 ˇ
3331 something_else,
3332 ˇ
3333 )
3334 ˇ
3335 ˇ
3336 );
3337 "});
3338}
3339
3340#[gpui::test]
3341async fn test_newline_below(cx: &mut TestAppContext) {
3342 init_test(cx, |settings| {
3343 settings.defaults.tab_size = NonZeroU32::new(4)
3344 });
3345
3346 let language = Arc::new(
3347 Language::new(
3348 LanguageConfig::default(),
3349 Some(tree_sitter_rust::LANGUAGE.into()),
3350 )
3351 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3352 .unwrap(),
3353 );
3354
3355 let mut cx = EditorTestContext::new(cx).await;
3356 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3357 cx.set_state(indoc! {"
3358 const a: ˇA = (
3359 (ˇ
3360 «const_functionˇ»(ˇ),
3361 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3362 )ˇ
3363 ˇ);ˇ
3364 "});
3365
3366 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3367 cx.assert_editor_state(indoc! {"
3368 const a: A = (
3369 ˇ
3370 (
3371 ˇ
3372 const_function(),
3373 ˇ
3374 ˇ
3375 something_else,
3376 ˇ
3377 ˇ
3378 ˇ
3379 ˇ
3380 )
3381 ˇ
3382 );
3383 ˇ
3384 ˇ
3385 "});
3386}
3387
3388#[gpui::test]
3389fn test_newline_below_multibuffer(cx: &mut TestAppContext) {
3390 init_test(cx, |_| {});
3391
3392 let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
3393 let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
3394 let multibuffer = cx.new(|cx| {
3395 let mut multibuffer = MultiBuffer::new(ReadWrite);
3396 multibuffer.push_excerpts(
3397 buffer_1.clone(),
3398 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
3399 cx,
3400 );
3401 multibuffer.push_excerpts(
3402 buffer_2.clone(),
3403 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
3404 cx,
3405 );
3406 multibuffer
3407 });
3408
3409 cx.add_window(|window, cx| {
3410 let mut editor = build_editor(multibuffer, window, cx);
3411
3412 assert_eq!(
3413 editor.text(cx),
3414 indoc! {"
3415 aaa
3416 bbb
3417 ccc
3418 ddd
3419 eee
3420 fff"}
3421 );
3422
3423 // Cursor on the last line of the first excerpt.
3424 // The newline should be inserted within the first excerpt (buffer_1),
3425 // not in the second excerpt (buffer_2).
3426 select_ranges(
3427 &mut editor,
3428 indoc! {"
3429 aaa
3430 bbb
3431 cˇcc
3432 ddd
3433 eee
3434 fff"},
3435 window,
3436 cx,
3437 );
3438 editor.newline_below(&NewlineBelow, window, cx);
3439 assert_text_with_selections(
3440 &mut editor,
3441 indoc! {"
3442 aaa
3443 bbb
3444 ccc
3445 ˇ
3446 ddd
3447 eee
3448 fff"},
3449 cx,
3450 );
3451 buffer_1.read_with(cx, |buffer, _| {
3452 assert_eq!(buffer.text(), "aaa\nbbb\nccc\n");
3453 });
3454 buffer_2.read_with(cx, |buffer, _| {
3455 assert_eq!(buffer.text(), "ddd\neee\nfff");
3456 });
3457
3458 editor
3459 });
3460}
3461
3462#[gpui::test]
3463fn test_newline_below_multibuffer_middle_of_excerpt(cx: &mut TestAppContext) {
3464 init_test(cx, |_| {});
3465
3466 let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
3467 let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
3468 let multibuffer = cx.new(|cx| {
3469 let mut multibuffer = MultiBuffer::new(ReadWrite);
3470 multibuffer.push_excerpts(
3471 buffer_1.clone(),
3472 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
3473 cx,
3474 );
3475 multibuffer.push_excerpts(
3476 buffer_2.clone(),
3477 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
3478 cx,
3479 );
3480 multibuffer
3481 });
3482
3483 cx.add_window(|window, cx| {
3484 let mut editor = build_editor(multibuffer, window, cx);
3485
3486 // Cursor in the middle of the first excerpt.
3487 select_ranges(
3488 &mut editor,
3489 indoc! {"
3490 aˇaa
3491 bbb
3492 ccc
3493 ddd
3494 eee
3495 fff"},
3496 window,
3497 cx,
3498 );
3499 editor.newline_below(&NewlineBelow, window, cx);
3500 assert_text_with_selections(
3501 &mut editor,
3502 indoc! {"
3503 aaa
3504 ˇ
3505 bbb
3506 ccc
3507 ddd
3508 eee
3509 fff"},
3510 cx,
3511 );
3512 buffer_1.read_with(cx, |buffer, _| {
3513 assert_eq!(buffer.text(), "aaa\n\nbbb\nccc");
3514 });
3515 buffer_2.read_with(cx, |buffer, _| {
3516 assert_eq!(buffer.text(), "ddd\neee\nfff");
3517 });
3518
3519 editor
3520 });
3521}
3522
3523#[gpui::test]
3524fn test_newline_below_multibuffer_last_line_of_last_excerpt(cx: &mut TestAppContext) {
3525 init_test(cx, |_| {});
3526
3527 let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
3528 let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
3529 let multibuffer = cx.new(|cx| {
3530 let mut multibuffer = MultiBuffer::new(ReadWrite);
3531 multibuffer.push_excerpts(
3532 buffer_1.clone(),
3533 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
3534 cx,
3535 );
3536 multibuffer.push_excerpts(
3537 buffer_2.clone(),
3538 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
3539 cx,
3540 );
3541 multibuffer
3542 });
3543
3544 cx.add_window(|window, cx| {
3545 let mut editor = build_editor(multibuffer, window, cx);
3546
3547 // Cursor on the last line of the last excerpt.
3548 select_ranges(
3549 &mut editor,
3550 indoc! {"
3551 aaa
3552 bbb
3553 ccc
3554 ddd
3555 eee
3556 fˇff"},
3557 window,
3558 cx,
3559 );
3560 editor.newline_below(&NewlineBelow, window, cx);
3561 assert_text_with_selections(
3562 &mut editor,
3563 indoc! {"
3564 aaa
3565 bbb
3566 ccc
3567 ddd
3568 eee
3569 fff
3570 ˇ"},
3571 cx,
3572 );
3573 buffer_1.read_with(cx, |buffer, _| {
3574 assert_eq!(buffer.text(), "aaa\nbbb\nccc");
3575 });
3576 buffer_2.read_with(cx, |buffer, _| {
3577 assert_eq!(buffer.text(), "ddd\neee\nfff\n");
3578 });
3579
3580 editor
3581 });
3582}
3583
3584#[gpui::test]
3585fn test_newline_below_multibuffer_multiple_cursors(cx: &mut TestAppContext) {
3586 init_test(cx, |_| {});
3587
3588 let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
3589 let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
3590 let multibuffer = cx.new(|cx| {
3591 let mut multibuffer = MultiBuffer::new(ReadWrite);
3592 multibuffer.push_excerpts(
3593 buffer_1.clone(),
3594 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
3595 cx,
3596 );
3597 multibuffer.push_excerpts(
3598 buffer_2.clone(),
3599 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
3600 cx,
3601 );
3602 multibuffer
3603 });
3604
3605 cx.add_window(|window, cx| {
3606 let mut editor = build_editor(multibuffer, window, cx);
3607
3608 // Cursors on the last line of the first excerpt and the first line
3609 // of the second excerpt. Each newline should go into its respective buffer.
3610 select_ranges(
3611 &mut editor,
3612 indoc! {"
3613 aaa
3614 bbb
3615 cˇcc
3616 dˇdd
3617 eee
3618 fff"},
3619 window,
3620 cx,
3621 );
3622 editor.newline_below(&NewlineBelow, window, cx);
3623 assert_text_with_selections(
3624 &mut editor,
3625 indoc! {"
3626 aaa
3627 bbb
3628 ccc
3629 ˇ
3630 ddd
3631 ˇ
3632 eee
3633 fff"},
3634 cx,
3635 );
3636 buffer_1.read_with(cx, |buffer, _| {
3637 assert_eq!(buffer.text(), "aaa\nbbb\nccc\n");
3638 });
3639 buffer_2.read_with(cx, |buffer, _| {
3640 assert_eq!(buffer.text(), "ddd\n\neee\nfff");
3641 });
3642
3643 editor
3644 });
3645}
3646
3647#[gpui::test]
3648async fn test_newline_comments(cx: &mut TestAppContext) {
3649 init_test(cx, |settings| {
3650 settings.defaults.tab_size = NonZeroU32::new(4)
3651 });
3652
3653 let language = Arc::new(Language::new(
3654 LanguageConfig {
3655 line_comments: vec!["// ".into()],
3656 ..LanguageConfig::default()
3657 },
3658 None,
3659 ));
3660 {
3661 let mut cx = EditorTestContext::new(cx).await;
3662 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3663 cx.set_state(indoc! {"
3664 // Fooˇ
3665 "});
3666
3667 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3668 cx.assert_editor_state(indoc! {"
3669 // Foo
3670 // ˇ
3671 "});
3672 // Ensure that we add comment prefix when existing line contains space
3673 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3674 cx.assert_editor_state(
3675 indoc! {"
3676 // Foo
3677 //s
3678 // ˇ
3679 "}
3680 .replace("s", " ") // s is used as space placeholder to prevent format on save
3681 .as_str(),
3682 );
3683 // Ensure that we add comment prefix when existing line does not contain space
3684 cx.set_state(indoc! {"
3685 // Foo
3686 //ˇ
3687 "});
3688 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3689 cx.assert_editor_state(indoc! {"
3690 // Foo
3691 //
3692 // ˇ
3693 "});
3694 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3695 cx.set_state(indoc! {"
3696 ˇ// Foo
3697 "});
3698 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3699 cx.assert_editor_state(indoc! {"
3700
3701 ˇ// Foo
3702 "});
3703 }
3704 // Ensure that comment continuations can be disabled.
3705 update_test_language_settings(cx, |settings| {
3706 settings.defaults.extend_comment_on_newline = Some(false);
3707 });
3708 let mut cx = EditorTestContext::new(cx).await;
3709 cx.set_state(indoc! {"
3710 // Fooˇ
3711 "});
3712 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3713 cx.assert_editor_state(indoc! {"
3714 // Foo
3715 ˇ
3716 "});
3717}
3718
3719#[gpui::test]
3720async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3721 init_test(cx, |settings| {
3722 settings.defaults.tab_size = NonZeroU32::new(4)
3723 });
3724
3725 let language = Arc::new(Language::new(
3726 LanguageConfig {
3727 line_comments: vec!["// ".into(), "/// ".into()],
3728 ..LanguageConfig::default()
3729 },
3730 None,
3731 ));
3732 {
3733 let mut cx = EditorTestContext::new(cx).await;
3734 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3735 cx.set_state(indoc! {"
3736 //ˇ
3737 "});
3738 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3739 cx.assert_editor_state(indoc! {"
3740 //
3741 // ˇ
3742 "});
3743
3744 cx.set_state(indoc! {"
3745 ///ˇ
3746 "});
3747 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3748 cx.assert_editor_state(indoc! {"
3749 ///
3750 /// ˇ
3751 "});
3752 }
3753}
3754
3755#[gpui::test]
3756async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3757 init_test(cx, |settings| {
3758 settings.defaults.tab_size = NonZeroU32::new(4)
3759 });
3760
3761 let language = Arc::new(
3762 Language::new(
3763 LanguageConfig {
3764 documentation_comment: Some(language::BlockCommentConfig {
3765 start: "/**".into(),
3766 end: "*/".into(),
3767 prefix: "* ".into(),
3768 tab_size: 1,
3769 }),
3770
3771 ..LanguageConfig::default()
3772 },
3773 Some(tree_sitter_rust::LANGUAGE.into()),
3774 )
3775 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3776 .unwrap(),
3777 );
3778
3779 {
3780 let mut cx = EditorTestContext::new(cx).await;
3781 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3782 cx.set_state(indoc! {"
3783 /**ˇ
3784 "});
3785
3786 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3787 cx.assert_editor_state(indoc! {"
3788 /**
3789 * ˇ
3790 "});
3791 // Ensure that if cursor is before the comment start,
3792 // we do not actually insert a comment prefix.
3793 cx.set_state(indoc! {"
3794 ˇ/**
3795 "});
3796 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3797 cx.assert_editor_state(indoc! {"
3798
3799 ˇ/**
3800 "});
3801 // Ensure that if cursor is between it doesn't add comment prefix.
3802 cx.set_state(indoc! {"
3803 /*ˇ*
3804 "});
3805 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3806 cx.assert_editor_state(indoc! {"
3807 /*
3808 ˇ*
3809 "});
3810 // Ensure that if suffix exists on same line after cursor it adds new line.
3811 cx.set_state(indoc! {"
3812 /**ˇ*/
3813 "});
3814 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3815 cx.assert_editor_state(indoc! {"
3816 /**
3817 * ˇ
3818 */
3819 "});
3820 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3821 cx.set_state(indoc! {"
3822 /**ˇ */
3823 "});
3824 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3825 cx.assert_editor_state(indoc! {"
3826 /**
3827 * ˇ
3828 */
3829 "});
3830 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3831 cx.set_state(indoc! {"
3832 /** ˇ*/
3833 "});
3834 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3835 cx.assert_editor_state(
3836 indoc! {"
3837 /**s
3838 * ˇ
3839 */
3840 "}
3841 .replace("s", " ") // s is used as space placeholder to prevent format on save
3842 .as_str(),
3843 );
3844 // Ensure that delimiter space is preserved when newline on already
3845 // spaced delimiter.
3846 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3847 cx.assert_editor_state(
3848 indoc! {"
3849 /**s
3850 *s
3851 * ˇ
3852 */
3853 "}
3854 .replace("s", " ") // s is used as space placeholder to prevent format on save
3855 .as_str(),
3856 );
3857 // Ensure that delimiter space is preserved when space is not
3858 // on existing delimiter.
3859 cx.set_state(indoc! {"
3860 /**
3861 *ˇ
3862 */
3863 "});
3864 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3865 cx.assert_editor_state(indoc! {"
3866 /**
3867 *
3868 * ˇ
3869 */
3870 "});
3871 // Ensure that if suffix exists on same line after cursor it
3872 // doesn't add extra new line if prefix is not on same line.
3873 cx.set_state(indoc! {"
3874 /**
3875 ˇ*/
3876 "});
3877 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3878 cx.assert_editor_state(indoc! {"
3879 /**
3880
3881 ˇ*/
3882 "});
3883 // Ensure that it detects suffix after existing prefix.
3884 cx.set_state(indoc! {"
3885 /**ˇ/
3886 "});
3887 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3888 cx.assert_editor_state(indoc! {"
3889 /**
3890 ˇ/
3891 "});
3892 // Ensure that if suffix exists on same line before
3893 // cursor it does not add comment prefix.
3894 cx.set_state(indoc! {"
3895 /** */ˇ
3896 "});
3897 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3898 cx.assert_editor_state(indoc! {"
3899 /** */
3900 ˇ
3901 "});
3902 // Ensure that if suffix exists on same line before
3903 // cursor it does not add comment prefix.
3904 cx.set_state(indoc! {"
3905 /**
3906 *
3907 */ˇ
3908 "});
3909 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3910 cx.assert_editor_state(indoc! {"
3911 /**
3912 *
3913 */
3914 ˇ
3915 "});
3916
3917 // Ensure that inline comment followed by code
3918 // doesn't add comment prefix on newline
3919 cx.set_state(indoc! {"
3920 /** */ textˇ
3921 "});
3922 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3923 cx.assert_editor_state(indoc! {"
3924 /** */ text
3925 ˇ
3926 "});
3927
3928 // Ensure that text after comment end tag
3929 // doesn't add comment prefix on newline
3930 cx.set_state(indoc! {"
3931 /**
3932 *
3933 */ˇtext
3934 "});
3935 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3936 cx.assert_editor_state(indoc! {"
3937 /**
3938 *
3939 */
3940 ˇtext
3941 "});
3942
3943 // Ensure if not comment block it doesn't
3944 // add comment prefix on newline
3945 cx.set_state(indoc! {"
3946 * textˇ
3947 "});
3948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3949 cx.assert_editor_state(indoc! {"
3950 * text
3951 ˇ
3952 "});
3953 }
3954 // Ensure that comment continuations can be disabled.
3955 update_test_language_settings(cx, |settings| {
3956 settings.defaults.extend_comment_on_newline = Some(false);
3957 });
3958 let mut cx = EditorTestContext::new(cx).await;
3959 cx.set_state(indoc! {"
3960 /**ˇ
3961 "});
3962 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3963 cx.assert_editor_state(indoc! {"
3964 /**
3965 ˇ
3966 "});
3967}
3968
3969#[gpui::test]
3970async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3971 init_test(cx, |settings| {
3972 settings.defaults.tab_size = NonZeroU32::new(4)
3973 });
3974
3975 let lua_language = Arc::new(Language::new(
3976 LanguageConfig {
3977 line_comments: vec!["--".into()],
3978 block_comment: Some(language::BlockCommentConfig {
3979 start: "--[[".into(),
3980 prefix: "".into(),
3981 end: "]]".into(),
3982 tab_size: 0,
3983 }),
3984 ..LanguageConfig::default()
3985 },
3986 None,
3987 ));
3988
3989 let mut cx = EditorTestContext::new(cx).await;
3990 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3991
3992 // Line with line comment should extend
3993 cx.set_state(indoc! {"
3994 --ˇ
3995 "});
3996 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3997 cx.assert_editor_state(indoc! {"
3998 --
3999 --ˇ
4000 "});
4001
4002 // Line with block comment that matches line comment should not extend
4003 cx.set_state(indoc! {"
4004 --[[ˇ
4005 "});
4006 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
4007 cx.assert_editor_state(indoc! {"
4008 --[[
4009 ˇ
4010 "});
4011}
4012
4013#[gpui::test]
4014fn test_insert_with_old_selections(cx: &mut TestAppContext) {
4015 init_test(cx, |_| {});
4016
4017 let editor = cx.add_window(|window, cx| {
4018 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
4019 let mut editor = build_editor(buffer, window, cx);
4020 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4021 s.select_ranges([
4022 MultiBufferOffset(3)..MultiBufferOffset(4),
4023 MultiBufferOffset(11)..MultiBufferOffset(12),
4024 MultiBufferOffset(19)..MultiBufferOffset(20),
4025 ])
4026 });
4027 editor
4028 });
4029
4030 _ = editor.update(cx, |editor, window, cx| {
4031 // Edit the buffer directly, deleting ranges surrounding the editor's selections
4032 editor.buffer.update(cx, |buffer, cx| {
4033 buffer.edit(
4034 [
4035 (MultiBufferOffset(2)..MultiBufferOffset(5), ""),
4036 (MultiBufferOffset(10)..MultiBufferOffset(13), ""),
4037 (MultiBufferOffset(18)..MultiBufferOffset(21), ""),
4038 ],
4039 None,
4040 cx,
4041 );
4042 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
4043 });
4044 assert_eq!(
4045 editor.selections.ranges(&editor.display_snapshot(cx)),
4046 &[
4047 MultiBufferOffset(2)..MultiBufferOffset(2),
4048 MultiBufferOffset(7)..MultiBufferOffset(7),
4049 MultiBufferOffset(12)..MultiBufferOffset(12)
4050 ],
4051 );
4052
4053 editor.insert("Z", window, cx);
4054 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
4055
4056 // The selections are moved after the inserted characters
4057 assert_eq!(
4058 editor.selections.ranges(&editor.display_snapshot(cx)),
4059 &[
4060 MultiBufferOffset(3)..MultiBufferOffset(3),
4061 MultiBufferOffset(9)..MultiBufferOffset(9),
4062 MultiBufferOffset(15)..MultiBufferOffset(15)
4063 ],
4064 );
4065 });
4066}
4067
4068#[gpui::test]
4069async fn test_tab(cx: &mut TestAppContext) {
4070 init_test(cx, |settings| {
4071 settings.defaults.tab_size = NonZeroU32::new(3)
4072 });
4073
4074 let mut cx = EditorTestContext::new(cx).await;
4075 cx.set_state(indoc! {"
4076 ˇabˇc
4077 ˇ🏀ˇ🏀ˇefg
4078 dˇ
4079 "});
4080 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4081 cx.assert_editor_state(indoc! {"
4082 ˇab ˇc
4083 ˇ🏀 ˇ🏀 ˇefg
4084 d ˇ
4085 "});
4086
4087 cx.set_state(indoc! {"
4088 a
4089 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
4090 "});
4091 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4092 cx.assert_editor_state(indoc! {"
4093 a
4094 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
4095 "});
4096}
4097
4098#[gpui::test]
4099async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
4100 init_test(cx, |_| {});
4101
4102 let mut cx = EditorTestContext::new(cx).await;
4103 let language = Arc::new(
4104 Language::new(
4105 LanguageConfig::default(),
4106 Some(tree_sitter_rust::LANGUAGE.into()),
4107 )
4108 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
4109 .unwrap(),
4110 );
4111 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4112
4113 // test when all cursors are not at suggested indent
4114 // then simply move to their suggested indent location
4115 cx.set_state(indoc! {"
4116 const a: B = (
4117 c(
4118 ˇ
4119 ˇ )
4120 );
4121 "});
4122 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4123 cx.assert_editor_state(indoc! {"
4124 const a: B = (
4125 c(
4126 ˇ
4127 ˇ)
4128 );
4129 "});
4130
4131 // test cursor already at suggested indent not moving when
4132 // other cursors are yet to reach their suggested indents
4133 cx.set_state(indoc! {"
4134 ˇ
4135 const a: B = (
4136 c(
4137 d(
4138 ˇ
4139 )
4140 ˇ
4141 ˇ )
4142 );
4143 "});
4144 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4145 cx.assert_editor_state(indoc! {"
4146 ˇ
4147 const a: B = (
4148 c(
4149 d(
4150 ˇ
4151 )
4152 ˇ
4153 ˇ)
4154 );
4155 "});
4156 // test when all cursors are at suggested indent then tab is inserted
4157 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4158 cx.assert_editor_state(indoc! {"
4159 ˇ
4160 const a: B = (
4161 c(
4162 d(
4163 ˇ
4164 )
4165 ˇ
4166 ˇ)
4167 );
4168 "});
4169
4170 // test when current indent is less than suggested indent,
4171 // we adjust line to match suggested indent and move cursor to it
4172 //
4173 // when no other cursor is at word boundary, all of them should move
4174 cx.set_state(indoc! {"
4175 const a: B = (
4176 c(
4177 d(
4178 ˇ
4179 ˇ )
4180 ˇ )
4181 );
4182 "});
4183 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4184 cx.assert_editor_state(indoc! {"
4185 const a: B = (
4186 c(
4187 d(
4188 ˇ
4189 ˇ)
4190 ˇ)
4191 );
4192 "});
4193
4194 // test when current indent is less than suggested indent,
4195 // we adjust line to match suggested indent and move cursor to it
4196 //
4197 // when some other cursor is at word boundary, it should not move
4198 cx.set_state(indoc! {"
4199 const a: B = (
4200 c(
4201 d(
4202 ˇ
4203 ˇ )
4204 ˇ)
4205 );
4206 "});
4207 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4208 cx.assert_editor_state(indoc! {"
4209 const a: B = (
4210 c(
4211 d(
4212 ˇ
4213 ˇ)
4214 ˇ)
4215 );
4216 "});
4217
4218 // test when current indent is more than suggested indent,
4219 // we just move cursor to current indent instead of suggested indent
4220 //
4221 // when no other cursor is at word boundary, all of them should move
4222 cx.set_state(indoc! {"
4223 const a: B = (
4224 c(
4225 d(
4226 ˇ
4227 ˇ )
4228 ˇ )
4229 );
4230 "});
4231 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4232 cx.assert_editor_state(indoc! {"
4233 const a: B = (
4234 c(
4235 d(
4236 ˇ
4237 ˇ)
4238 ˇ)
4239 );
4240 "});
4241 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4242 cx.assert_editor_state(indoc! {"
4243 const a: B = (
4244 c(
4245 d(
4246 ˇ
4247 ˇ)
4248 ˇ)
4249 );
4250 "});
4251
4252 // test when current indent is more than suggested indent,
4253 // we just move cursor to current indent instead of suggested indent
4254 //
4255 // when some other cursor is at word boundary, it doesn't move
4256 cx.set_state(indoc! {"
4257 const a: B = (
4258 c(
4259 d(
4260 ˇ
4261 ˇ )
4262 ˇ)
4263 );
4264 "});
4265 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4266 cx.assert_editor_state(indoc! {"
4267 const a: B = (
4268 c(
4269 d(
4270 ˇ
4271 ˇ)
4272 ˇ)
4273 );
4274 "});
4275
4276 // handle auto-indent when there are multiple cursors on the same line
4277 cx.set_state(indoc! {"
4278 const a: B = (
4279 c(
4280 ˇ ˇ
4281 ˇ )
4282 );
4283 "});
4284 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4285 cx.assert_editor_state(indoc! {"
4286 const a: B = (
4287 c(
4288 ˇ
4289 ˇ)
4290 );
4291 "});
4292}
4293
4294#[gpui::test]
4295async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
4296 init_test(cx, |settings| {
4297 settings.defaults.tab_size = NonZeroU32::new(3)
4298 });
4299
4300 let mut cx = EditorTestContext::new(cx).await;
4301 cx.set_state(indoc! {"
4302 ˇ
4303 \t ˇ
4304 \t ˇ
4305 \t ˇ
4306 \t \t\t \t \t\t \t\t \t \t ˇ
4307 "});
4308
4309 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4310 cx.assert_editor_state(indoc! {"
4311 ˇ
4312 \t ˇ
4313 \t ˇ
4314 \t ˇ
4315 \t \t\t \t \t\t \t\t \t \t ˇ
4316 "});
4317}
4318
4319#[gpui::test]
4320async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
4321 init_test(cx, |settings| {
4322 settings.defaults.tab_size = NonZeroU32::new(4)
4323 });
4324
4325 let language = Arc::new(
4326 Language::new(
4327 LanguageConfig::default(),
4328 Some(tree_sitter_rust::LANGUAGE.into()),
4329 )
4330 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
4331 .unwrap(),
4332 );
4333
4334 let mut cx = EditorTestContext::new(cx).await;
4335 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4336 cx.set_state(indoc! {"
4337 fn a() {
4338 if b {
4339 \t ˇc
4340 }
4341 }
4342 "});
4343
4344 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4345 cx.assert_editor_state(indoc! {"
4346 fn a() {
4347 if b {
4348 ˇc
4349 }
4350 }
4351 "});
4352}
4353
4354#[gpui::test]
4355async fn test_indent_outdent(cx: &mut TestAppContext) {
4356 init_test(cx, |settings| {
4357 settings.defaults.tab_size = NonZeroU32::new(4);
4358 });
4359
4360 let mut cx = EditorTestContext::new(cx).await;
4361
4362 cx.set_state(indoc! {"
4363 «oneˇ» «twoˇ»
4364 three
4365 four
4366 "});
4367 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4368 cx.assert_editor_state(indoc! {"
4369 «oneˇ» «twoˇ»
4370 three
4371 four
4372 "});
4373
4374 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4375 cx.assert_editor_state(indoc! {"
4376 «oneˇ» «twoˇ»
4377 three
4378 four
4379 "});
4380
4381 // select across line ending
4382 cx.set_state(indoc! {"
4383 one two
4384 t«hree
4385 ˇ» four
4386 "});
4387 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4388 cx.assert_editor_state(indoc! {"
4389 one two
4390 t«hree
4391 ˇ» four
4392 "});
4393
4394 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4395 cx.assert_editor_state(indoc! {"
4396 one two
4397 t«hree
4398 ˇ» four
4399 "});
4400
4401 // Ensure that indenting/outdenting works when the cursor is at column 0.
4402 cx.set_state(indoc! {"
4403 one two
4404 ˇthree
4405 four
4406 "});
4407 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4408 cx.assert_editor_state(indoc! {"
4409 one two
4410 ˇthree
4411 four
4412 "});
4413
4414 cx.set_state(indoc! {"
4415 one two
4416 ˇ three
4417 four
4418 "});
4419 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4420 cx.assert_editor_state(indoc! {"
4421 one two
4422 ˇthree
4423 four
4424 "});
4425}
4426
4427#[gpui::test]
4428async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4429 // This is a regression test for issue #33761
4430 init_test(cx, |_| {});
4431
4432 let mut cx = EditorTestContext::new(cx).await;
4433 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4434 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4435
4436 cx.set_state(
4437 r#"ˇ# ingress:
4438ˇ# api:
4439ˇ# enabled: false
4440ˇ# pathType: Prefix
4441ˇ# console:
4442ˇ# enabled: false
4443ˇ# pathType: Prefix
4444"#,
4445 );
4446
4447 // Press tab to indent all lines
4448 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4449
4450 cx.assert_editor_state(
4451 r#" ˇ# ingress:
4452 ˇ# api:
4453 ˇ# enabled: false
4454 ˇ# pathType: Prefix
4455 ˇ# console:
4456 ˇ# enabled: false
4457 ˇ# pathType: Prefix
4458"#,
4459 );
4460}
4461
4462#[gpui::test]
4463async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4464 // This is a test to make sure our fix for issue #33761 didn't break anything
4465 init_test(cx, |_| {});
4466
4467 let mut cx = EditorTestContext::new(cx).await;
4468 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4469 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4470
4471 cx.set_state(
4472 r#"ˇingress:
4473ˇ api:
4474ˇ enabled: false
4475ˇ pathType: Prefix
4476"#,
4477 );
4478
4479 // Press tab to indent all lines
4480 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4481
4482 cx.assert_editor_state(
4483 r#"ˇingress:
4484 ˇapi:
4485 ˇenabled: false
4486 ˇpathType: Prefix
4487"#,
4488 );
4489}
4490
4491#[gpui::test]
4492async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4493 init_test(cx, |settings| {
4494 settings.defaults.hard_tabs = Some(true);
4495 });
4496
4497 let mut cx = EditorTestContext::new(cx).await;
4498
4499 // select two ranges on one line
4500 cx.set_state(indoc! {"
4501 «oneˇ» «twoˇ»
4502 three
4503 four
4504 "});
4505 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4506 cx.assert_editor_state(indoc! {"
4507 \t«oneˇ» «twoˇ»
4508 three
4509 four
4510 "});
4511 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4512 cx.assert_editor_state(indoc! {"
4513 \t\t«oneˇ» «twoˇ»
4514 three
4515 four
4516 "});
4517 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4518 cx.assert_editor_state(indoc! {"
4519 \t«oneˇ» «twoˇ»
4520 three
4521 four
4522 "});
4523 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4524 cx.assert_editor_state(indoc! {"
4525 «oneˇ» «twoˇ»
4526 three
4527 four
4528 "});
4529
4530 // select across a line ending
4531 cx.set_state(indoc! {"
4532 one two
4533 t«hree
4534 ˇ»four
4535 "});
4536 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4537 cx.assert_editor_state(indoc! {"
4538 one two
4539 \tt«hree
4540 ˇ»four
4541 "});
4542 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4543 cx.assert_editor_state(indoc! {"
4544 one two
4545 \t\tt«hree
4546 ˇ»four
4547 "});
4548 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4549 cx.assert_editor_state(indoc! {"
4550 one two
4551 \tt«hree
4552 ˇ»four
4553 "});
4554 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4555 cx.assert_editor_state(indoc! {"
4556 one two
4557 t«hree
4558 ˇ»four
4559 "});
4560
4561 // Ensure that indenting/outdenting works when the cursor is at column 0.
4562 cx.set_state(indoc! {"
4563 one two
4564 ˇthree
4565 four
4566 "});
4567 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4568 cx.assert_editor_state(indoc! {"
4569 one two
4570 ˇthree
4571 four
4572 "});
4573 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4574 cx.assert_editor_state(indoc! {"
4575 one two
4576 \tˇthree
4577 four
4578 "});
4579 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4580 cx.assert_editor_state(indoc! {"
4581 one two
4582 ˇthree
4583 four
4584 "});
4585}
4586
4587#[gpui::test]
4588fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4589 init_test(cx, |settings| {
4590 settings.languages.0.extend([
4591 (
4592 "TOML".into(),
4593 LanguageSettingsContent {
4594 tab_size: NonZeroU32::new(2),
4595 ..Default::default()
4596 },
4597 ),
4598 (
4599 "Rust".into(),
4600 LanguageSettingsContent {
4601 tab_size: NonZeroU32::new(4),
4602 ..Default::default()
4603 },
4604 ),
4605 ]);
4606 });
4607
4608 let toml_language = Arc::new(Language::new(
4609 LanguageConfig {
4610 name: "TOML".into(),
4611 ..Default::default()
4612 },
4613 None,
4614 ));
4615 let rust_language = Arc::new(Language::new(
4616 LanguageConfig {
4617 name: "Rust".into(),
4618 ..Default::default()
4619 },
4620 None,
4621 ));
4622
4623 let toml_buffer =
4624 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4625 let rust_buffer =
4626 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4627 let multibuffer = cx.new(|cx| {
4628 let mut multibuffer = MultiBuffer::new(ReadWrite);
4629 multibuffer.push_excerpts(
4630 toml_buffer.clone(),
4631 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4632 cx,
4633 );
4634 multibuffer.push_excerpts(
4635 rust_buffer.clone(),
4636 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4637 cx,
4638 );
4639 multibuffer
4640 });
4641
4642 cx.add_window(|window, cx| {
4643 let mut editor = build_editor(multibuffer, window, cx);
4644
4645 assert_eq!(
4646 editor.text(cx),
4647 indoc! {"
4648 a = 1
4649 b = 2
4650
4651 const c: usize = 3;
4652 "}
4653 );
4654
4655 select_ranges(
4656 &mut editor,
4657 indoc! {"
4658 «aˇ» = 1
4659 b = 2
4660
4661 «const c:ˇ» usize = 3;
4662 "},
4663 window,
4664 cx,
4665 );
4666
4667 editor.tab(&Tab, window, cx);
4668 assert_text_with_selections(
4669 &mut editor,
4670 indoc! {"
4671 «aˇ» = 1
4672 b = 2
4673
4674 «const c:ˇ» usize = 3;
4675 "},
4676 cx,
4677 );
4678 editor.backtab(&Backtab, window, cx);
4679 assert_text_with_selections(
4680 &mut editor,
4681 indoc! {"
4682 «aˇ» = 1
4683 b = 2
4684
4685 «const c:ˇ» usize = 3;
4686 "},
4687 cx,
4688 );
4689
4690 editor
4691 });
4692}
4693
4694#[gpui::test]
4695async fn test_backspace(cx: &mut TestAppContext) {
4696 init_test(cx, |_| {});
4697
4698 let mut cx = EditorTestContext::new(cx).await;
4699
4700 // Basic backspace
4701 cx.set_state(indoc! {"
4702 onˇe two three
4703 fou«rˇ» five six
4704 seven «ˇeight nine
4705 »ten
4706 "});
4707 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4708 cx.assert_editor_state(indoc! {"
4709 oˇe two three
4710 fouˇ five six
4711 seven ˇten
4712 "});
4713
4714 // Test backspace inside and around indents
4715 cx.set_state(indoc! {"
4716 zero
4717 ˇone
4718 ˇtwo
4719 ˇ ˇ ˇ three
4720 ˇ ˇ four
4721 "});
4722 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4723 cx.assert_editor_state(indoc! {"
4724 zero
4725 ˇone
4726 ˇtwo
4727 ˇ threeˇ four
4728 "});
4729}
4730
4731#[gpui::test]
4732async fn test_delete(cx: &mut TestAppContext) {
4733 init_test(cx, |_| {});
4734
4735 let mut cx = EditorTestContext::new(cx).await;
4736 cx.set_state(indoc! {"
4737 onˇe two three
4738 fou«rˇ» five six
4739 seven «ˇeight nine
4740 »ten
4741 "});
4742 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4743 cx.assert_editor_state(indoc! {"
4744 onˇ two three
4745 fouˇ five six
4746 seven ˇten
4747 "});
4748}
4749
4750#[gpui::test]
4751fn test_delete_line(cx: &mut TestAppContext) {
4752 init_test(cx, |_| {});
4753
4754 let editor = cx.add_window(|window, cx| {
4755 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4756 build_editor(buffer, window, cx)
4757 });
4758 _ = editor.update(cx, |editor, window, cx| {
4759 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4760 s.select_display_ranges([
4761 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4762 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4763 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4764 ])
4765 });
4766 editor.delete_line(&DeleteLine, window, cx);
4767 assert_eq!(editor.display_text(cx), "ghi");
4768 assert_eq!(
4769 display_ranges(editor, cx),
4770 vec![
4771 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4772 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4773 ]
4774 );
4775 });
4776
4777 let editor = cx.add_window(|window, cx| {
4778 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4779 build_editor(buffer, window, cx)
4780 });
4781 _ = editor.update(cx, |editor, window, cx| {
4782 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4783 s.select_display_ranges([
4784 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4785 ])
4786 });
4787 editor.delete_line(&DeleteLine, window, cx);
4788 assert_eq!(editor.display_text(cx), "ghi\n");
4789 assert_eq!(
4790 display_ranges(editor, cx),
4791 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4792 );
4793 });
4794
4795 let editor = cx.add_window(|window, cx| {
4796 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4797 build_editor(buffer, window, cx)
4798 });
4799 _ = editor.update(cx, |editor, window, cx| {
4800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4801 s.select_display_ranges([
4802 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4803 ])
4804 });
4805 editor.delete_line(&DeleteLine, window, cx);
4806 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4807 assert_eq!(
4808 display_ranges(editor, cx),
4809 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4810 );
4811 });
4812}
4813
4814#[gpui::test]
4815fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4816 init_test(cx, |_| {});
4817
4818 cx.add_window(|window, cx| {
4819 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4820 let mut editor = build_editor(buffer.clone(), window, cx);
4821 let buffer = buffer.read(cx).as_singleton().unwrap();
4822
4823 assert_eq!(
4824 editor
4825 .selections
4826 .ranges::<Point>(&editor.display_snapshot(cx)),
4827 &[Point::new(0, 0)..Point::new(0, 0)]
4828 );
4829
4830 // When on single line, replace newline at end by space
4831 editor.join_lines(&JoinLines, window, cx);
4832 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4833 assert_eq!(
4834 editor
4835 .selections
4836 .ranges::<Point>(&editor.display_snapshot(cx)),
4837 &[Point::new(0, 3)..Point::new(0, 3)]
4838 );
4839
4840 // When multiple lines are selected, remove newlines that are spanned by the selection
4841 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4842 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4843 });
4844 editor.join_lines(&JoinLines, window, cx);
4845 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4846 assert_eq!(
4847 editor
4848 .selections
4849 .ranges::<Point>(&editor.display_snapshot(cx)),
4850 &[Point::new(0, 11)..Point::new(0, 11)]
4851 );
4852
4853 // Undo should be transactional
4854 editor.undo(&Undo, window, cx);
4855 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4856 assert_eq!(
4857 editor
4858 .selections
4859 .ranges::<Point>(&editor.display_snapshot(cx)),
4860 &[Point::new(0, 5)..Point::new(2, 2)]
4861 );
4862
4863 // When joining an empty line don't insert a space
4864 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4865 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4866 });
4867 editor.join_lines(&JoinLines, window, cx);
4868 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4869 assert_eq!(
4870 editor
4871 .selections
4872 .ranges::<Point>(&editor.display_snapshot(cx)),
4873 [Point::new(2, 3)..Point::new(2, 3)]
4874 );
4875
4876 // We can remove trailing newlines
4877 editor.join_lines(&JoinLines, window, cx);
4878 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4879 assert_eq!(
4880 editor
4881 .selections
4882 .ranges::<Point>(&editor.display_snapshot(cx)),
4883 [Point::new(2, 3)..Point::new(2, 3)]
4884 );
4885
4886 // We don't blow up on the last line
4887 editor.join_lines(&JoinLines, window, cx);
4888 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4889 assert_eq!(
4890 editor
4891 .selections
4892 .ranges::<Point>(&editor.display_snapshot(cx)),
4893 [Point::new(2, 3)..Point::new(2, 3)]
4894 );
4895
4896 // reset to test indentation
4897 editor.buffer.update(cx, |buffer, cx| {
4898 buffer.edit(
4899 [
4900 (Point::new(1, 0)..Point::new(1, 2), " "),
4901 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4902 ],
4903 None,
4904 cx,
4905 )
4906 });
4907
4908 // We remove any leading spaces
4909 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4910 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4911 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4912 });
4913 editor.join_lines(&JoinLines, window, cx);
4914 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4915
4916 // We don't insert a space for a line containing only spaces
4917 editor.join_lines(&JoinLines, window, cx);
4918 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4919
4920 // We ignore any leading tabs
4921 editor.join_lines(&JoinLines, window, cx);
4922 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4923
4924 editor
4925 });
4926}
4927
4928#[gpui::test]
4929fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4930 init_test(cx, |_| {});
4931
4932 cx.add_window(|window, cx| {
4933 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4934 let mut editor = build_editor(buffer.clone(), window, cx);
4935 let buffer = buffer.read(cx).as_singleton().unwrap();
4936
4937 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4938 s.select_ranges([
4939 Point::new(0, 2)..Point::new(1, 1),
4940 Point::new(1, 2)..Point::new(1, 2),
4941 Point::new(3, 1)..Point::new(3, 2),
4942 ])
4943 });
4944
4945 editor.join_lines(&JoinLines, window, cx);
4946 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4947
4948 assert_eq!(
4949 editor
4950 .selections
4951 .ranges::<Point>(&editor.display_snapshot(cx)),
4952 [
4953 Point::new(0, 7)..Point::new(0, 7),
4954 Point::new(1, 3)..Point::new(1, 3)
4955 ]
4956 );
4957 editor
4958 });
4959}
4960
4961#[gpui::test]
4962async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4963 init_test(cx, |_| {});
4964
4965 let mut cx = EditorTestContext::new(cx).await;
4966
4967 let diff_base = r#"
4968 Line 0
4969 Line 1
4970 Line 2
4971 Line 3
4972 "#
4973 .unindent();
4974
4975 cx.set_state(
4976 &r#"
4977 ˇLine 0
4978 Line 1
4979 Line 2
4980 Line 3
4981 "#
4982 .unindent(),
4983 );
4984
4985 cx.set_head_text(&diff_base);
4986 executor.run_until_parked();
4987
4988 // Join lines
4989 cx.update_editor(|editor, window, cx| {
4990 editor.join_lines(&JoinLines, window, cx);
4991 });
4992 executor.run_until_parked();
4993
4994 cx.assert_editor_state(
4995 &r#"
4996 Line 0ˇ Line 1
4997 Line 2
4998 Line 3
4999 "#
5000 .unindent(),
5001 );
5002 // Join again
5003 cx.update_editor(|editor, window, cx| {
5004 editor.join_lines(&JoinLines, window, cx);
5005 });
5006 executor.run_until_parked();
5007
5008 cx.assert_editor_state(
5009 &r#"
5010 Line 0 Line 1ˇ Line 2
5011 Line 3
5012 "#
5013 .unindent(),
5014 );
5015}
5016
5017#[gpui::test]
5018async fn test_join_lines_strips_comment_prefix(cx: &mut TestAppContext) {
5019 init_test(cx, |_| {});
5020
5021 {
5022 let language = Arc::new(Language::new(
5023 LanguageConfig {
5024 line_comments: vec!["// ".into(), "/// ".into()],
5025 documentation_comment: Some(BlockCommentConfig {
5026 start: "/*".into(),
5027 end: "*/".into(),
5028 prefix: "* ".into(),
5029 tab_size: 1,
5030 }),
5031 ..LanguageConfig::default()
5032 },
5033 None,
5034 ));
5035
5036 let mut cx = EditorTestContext::new(cx).await;
5037 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5038
5039 // Strips the comment prefix (with trailing space) from the joined-in line.
5040 cx.set_state(indoc! {"
5041 // ˇfoo
5042 // bar
5043 "});
5044 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5045 cx.assert_editor_state(indoc! {"
5046 // fooˇ bar
5047 "});
5048
5049 // Strips the longer doc-comment prefix when both `//` and `///` match.
5050 cx.set_state(indoc! {"
5051 /// ˇfoo
5052 /// bar
5053 "});
5054 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5055 cx.assert_editor_state(indoc! {"
5056 /// fooˇ bar
5057 "});
5058
5059 // Does not strip when the second line is a regular line (no comment prefix).
5060 cx.set_state(indoc! {"
5061 // ˇfoo
5062 bar
5063 "});
5064 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5065 cx.assert_editor_state(indoc! {"
5066 // fooˇ bar
5067 "});
5068
5069 // No-whitespace join also strips the comment prefix.
5070 cx.set_state(indoc! {"
5071 // ˇfoo
5072 // bar
5073 "});
5074 cx.update_editor(|e, window, cx| e.join_lines_impl(false, window, cx));
5075 cx.assert_editor_state(indoc! {"
5076 // fooˇbar
5077 "});
5078
5079 // Strips even when the joined-in line is just the bare prefix (no trailing space).
5080 cx.set_state(indoc! {"
5081 // ˇfoo
5082 //
5083 "});
5084 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5085 cx.assert_editor_state(indoc! {"
5086 // fooˇ
5087 "});
5088
5089 // Mixed line comment prefix types: the longer matching prefix is stripped.
5090 cx.set_state(indoc! {"
5091 // ˇfoo
5092 /// bar
5093 "});
5094 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5095 cx.assert_editor_state(indoc! {"
5096 // fooˇ bar
5097 "});
5098
5099 // Strips block comment body prefix (`* `) from the joined-in line.
5100 cx.set_state(indoc! {"
5101 * ˇfoo
5102 * bar
5103 "});
5104 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5105 cx.assert_editor_state(indoc! {"
5106 * fooˇ bar
5107 "});
5108
5109 // Strips bare block comment body prefix (`*` without trailing space).
5110 cx.set_state(indoc! {"
5111 * ˇfoo
5112 *
5113 "});
5114 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5115 cx.assert_editor_state(indoc! {"
5116 * fooˇ
5117 "});
5118 }
5119
5120 {
5121 let markdown_language = Arc::new(Language::new(
5122 LanguageConfig {
5123 unordered_list: vec!["- ".into(), "* ".into(), "+ ".into()],
5124 ..LanguageConfig::default()
5125 },
5126 None,
5127 ));
5128
5129 let mut cx = EditorTestContext::new(cx).await;
5130 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
5131
5132 // Strips the `- ` list marker from the joined-in line.
5133 cx.set_state(indoc! {"
5134 - ˇfoo
5135 - bar
5136 "});
5137 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5138 cx.assert_editor_state(indoc! {"
5139 - fooˇ bar
5140 "});
5141
5142 // Strips the `* ` list marker from the joined-in line.
5143 cx.set_state(indoc! {"
5144 * ˇfoo
5145 * bar
5146 "});
5147 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5148 cx.assert_editor_state(indoc! {"
5149 * fooˇ bar
5150 "});
5151
5152 // Strips the `+ ` list marker from the joined-in line.
5153 cx.set_state(indoc! {"
5154 + ˇfoo
5155 + bar
5156 "});
5157 cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
5158 cx.assert_editor_state(indoc! {"
5159 + fooˇ bar
5160 "});
5161
5162 // No-whitespace join also strips the list marker.
5163 cx.set_state(indoc! {"
5164 - ˇfoo
5165 - bar
5166 "});
5167 cx.update_editor(|e, window, cx| e.join_lines_impl(false, window, cx));
5168 cx.assert_editor_state(indoc! {"
5169 - fooˇbar
5170 "});
5171 }
5172}
5173
5174#[gpui::test]
5175async fn test_custom_newlines_cause_no_false_positive_diffs(
5176 executor: BackgroundExecutor,
5177 cx: &mut TestAppContext,
5178) {
5179 init_test(cx, |_| {});
5180 let mut cx = EditorTestContext::new(cx).await;
5181 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
5182 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
5183 executor.run_until_parked();
5184
5185 cx.update_editor(|editor, window, cx| {
5186 let snapshot = editor.snapshot(window, cx);
5187 assert_eq!(
5188 snapshot
5189 .buffer_snapshot()
5190 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
5191 .collect::<Vec<_>>(),
5192 Vec::new(),
5193 "Should not have any diffs for files with custom newlines"
5194 );
5195 });
5196}
5197
5198#[gpui::test]
5199async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
5200 init_test(cx, |_| {});
5201
5202 let mut cx = EditorTestContext::new(cx).await;
5203
5204 // Test sort_lines_case_insensitive()
5205 cx.set_state(indoc! {"
5206 «z
5207 y
5208 x
5209 Z
5210 Y
5211 Xˇ»
5212 "});
5213 cx.update_editor(|e, window, cx| {
5214 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
5215 });
5216 cx.assert_editor_state(indoc! {"
5217 «x
5218 X
5219 y
5220 Y
5221 z
5222 Zˇ»
5223 "});
5224
5225 // Test sort_lines_by_length()
5226 //
5227 // Demonstrates:
5228 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
5229 // - sort is stable
5230 cx.set_state(indoc! {"
5231 «123
5232 æ
5233 12
5234 ∞
5235 1
5236 æˇ»
5237 "});
5238 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
5239 cx.assert_editor_state(indoc! {"
5240 «æ
5241 ∞
5242 1
5243 æ
5244 12
5245 123ˇ»
5246 "});
5247
5248 // Test reverse_lines()
5249 cx.set_state(indoc! {"
5250 «5
5251 4
5252 3
5253 2
5254 1ˇ»
5255 "});
5256 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
5257 cx.assert_editor_state(indoc! {"
5258 «1
5259 2
5260 3
5261 4
5262 5ˇ»
5263 "});
5264
5265 // Skip testing shuffle_line()
5266
5267 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
5268 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
5269
5270 // Don't manipulate when cursor is on single line, but expand the selection
5271 cx.set_state(indoc! {"
5272 ddˇdd
5273 ccc
5274 bb
5275 a
5276 "});
5277 cx.update_editor(|e, window, cx| {
5278 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5279 });
5280 cx.assert_editor_state(indoc! {"
5281 «ddddˇ»
5282 ccc
5283 bb
5284 a
5285 "});
5286
5287 // Basic manipulate case
5288 // Start selection moves to column 0
5289 // End of selection shrinks to fit shorter line
5290 cx.set_state(indoc! {"
5291 dd«d
5292 ccc
5293 bb
5294 aaaaaˇ»
5295 "});
5296 cx.update_editor(|e, window, cx| {
5297 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5298 });
5299 cx.assert_editor_state(indoc! {"
5300 «aaaaa
5301 bb
5302 ccc
5303 dddˇ»
5304 "});
5305
5306 // Manipulate case with newlines
5307 cx.set_state(indoc! {"
5308 dd«d
5309 ccc
5310
5311 bb
5312 aaaaa
5313
5314 ˇ»
5315 "});
5316 cx.update_editor(|e, window, cx| {
5317 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5318 });
5319 cx.assert_editor_state(indoc! {"
5320 «
5321
5322 aaaaa
5323 bb
5324 ccc
5325 dddˇ»
5326
5327 "});
5328
5329 // Adding new line
5330 cx.set_state(indoc! {"
5331 aa«a
5332 bbˇ»b
5333 "});
5334 cx.update_editor(|e, window, cx| {
5335 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
5336 });
5337 cx.assert_editor_state(indoc! {"
5338 «aaa
5339 bbb
5340 added_lineˇ»
5341 "});
5342
5343 // Removing line
5344 cx.set_state(indoc! {"
5345 aa«a
5346 bbbˇ»
5347 "});
5348 cx.update_editor(|e, window, cx| {
5349 e.manipulate_immutable_lines(window, cx, |lines| {
5350 lines.pop();
5351 })
5352 });
5353 cx.assert_editor_state(indoc! {"
5354 «aaaˇ»
5355 "});
5356
5357 // Removing all lines
5358 cx.set_state(indoc! {"
5359 aa«a
5360 bbbˇ»
5361 "});
5362 cx.update_editor(|e, window, cx| {
5363 e.manipulate_immutable_lines(window, cx, |lines| {
5364 lines.drain(..);
5365 })
5366 });
5367 cx.assert_editor_state(indoc! {"
5368 ˇ
5369 "});
5370}
5371
5372#[gpui::test]
5373async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
5374 init_test(cx, |_| {});
5375
5376 let mut cx = EditorTestContext::new(cx).await;
5377
5378 // Consider continuous selection as single selection
5379 cx.set_state(indoc! {"
5380 Aaa«aa
5381 cˇ»c«c
5382 bb
5383 aaaˇ»aa
5384 "});
5385 cx.update_editor(|e, window, cx| {
5386 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5387 });
5388 cx.assert_editor_state(indoc! {"
5389 «Aaaaa
5390 ccc
5391 bb
5392 aaaaaˇ»
5393 "});
5394
5395 cx.set_state(indoc! {"
5396 Aaa«aa
5397 cˇ»c«c
5398 bb
5399 aaaˇ»aa
5400 "});
5401 cx.update_editor(|e, window, cx| {
5402 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5403 });
5404 cx.assert_editor_state(indoc! {"
5405 «Aaaaa
5406 ccc
5407 bbˇ»
5408 "});
5409
5410 // Consider non continuous selection as distinct dedup operations
5411 cx.set_state(indoc! {"
5412 «aaaaa
5413 bb
5414 aaaaa
5415 aaaaaˇ»
5416
5417 aaa«aaˇ»
5418 "});
5419 cx.update_editor(|e, window, cx| {
5420 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5421 });
5422 cx.assert_editor_state(indoc! {"
5423 «aaaaa
5424 bbˇ»
5425
5426 «aaaaaˇ»
5427 "});
5428}
5429
5430#[gpui::test]
5431async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
5432 init_test(cx, |_| {});
5433
5434 let mut cx = EditorTestContext::new(cx).await;
5435
5436 cx.set_state(indoc! {"
5437 «Aaa
5438 aAa
5439 Aaaˇ»
5440 "});
5441 cx.update_editor(|e, window, cx| {
5442 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5443 });
5444 cx.assert_editor_state(indoc! {"
5445 «Aaa
5446 aAaˇ»
5447 "});
5448
5449 cx.set_state(indoc! {"
5450 «Aaa
5451 aAa
5452 aaAˇ»
5453 "});
5454 cx.update_editor(|e, window, cx| {
5455 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5456 });
5457 cx.assert_editor_state(indoc! {"
5458 «Aaaˇ»
5459 "});
5460}
5461
5462#[gpui::test]
5463async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
5464 init_test(cx, |_| {});
5465
5466 let mut cx = EditorTestContext::new(cx).await;
5467
5468 let js_language = Arc::new(Language::new(
5469 LanguageConfig {
5470 name: "JavaScript".into(),
5471 wrap_characters: Some(language::WrapCharactersConfig {
5472 start_prefix: "<".into(),
5473 start_suffix: ">".into(),
5474 end_prefix: "</".into(),
5475 end_suffix: ">".into(),
5476 }),
5477 ..LanguageConfig::default()
5478 },
5479 None,
5480 ));
5481
5482 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5483
5484 cx.set_state(indoc! {"
5485 «testˇ»
5486 "});
5487 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5488 cx.assert_editor_state(indoc! {"
5489 <«ˇ»>test</«ˇ»>
5490 "});
5491
5492 cx.set_state(indoc! {"
5493 «test
5494 testˇ»
5495 "});
5496 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5497 cx.assert_editor_state(indoc! {"
5498 <«ˇ»>test
5499 test</«ˇ»>
5500 "});
5501
5502 cx.set_state(indoc! {"
5503 teˇst
5504 "});
5505 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5506 cx.assert_editor_state(indoc! {"
5507 te<«ˇ»></«ˇ»>st
5508 "});
5509}
5510
5511#[gpui::test]
5512async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5513 init_test(cx, |_| {});
5514
5515 let mut cx = EditorTestContext::new(cx).await;
5516
5517 let js_language = Arc::new(Language::new(
5518 LanguageConfig {
5519 name: "JavaScript".into(),
5520 wrap_characters: Some(language::WrapCharactersConfig {
5521 start_prefix: "<".into(),
5522 start_suffix: ">".into(),
5523 end_prefix: "</".into(),
5524 end_suffix: ">".into(),
5525 }),
5526 ..LanguageConfig::default()
5527 },
5528 None,
5529 ));
5530
5531 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5532
5533 cx.set_state(indoc! {"
5534 «testˇ»
5535 «testˇ» «testˇ»
5536 «testˇ»
5537 "});
5538 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5539 cx.assert_editor_state(indoc! {"
5540 <«ˇ»>test</«ˇ»>
5541 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5542 <«ˇ»>test</«ˇ»>
5543 "});
5544
5545 cx.set_state(indoc! {"
5546 «test
5547 testˇ»
5548 «test
5549 testˇ»
5550 "});
5551 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5552 cx.assert_editor_state(indoc! {"
5553 <«ˇ»>test
5554 test</«ˇ»>
5555 <«ˇ»>test
5556 test</«ˇ»>
5557 "});
5558}
5559
5560#[gpui::test]
5561async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5562 init_test(cx, |_| {});
5563
5564 let mut cx = EditorTestContext::new(cx).await;
5565
5566 let plaintext_language = Arc::new(Language::new(
5567 LanguageConfig {
5568 name: "Plain Text".into(),
5569 ..LanguageConfig::default()
5570 },
5571 None,
5572 ));
5573
5574 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5575
5576 cx.set_state(indoc! {"
5577 «testˇ»
5578 "});
5579 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5580 cx.assert_editor_state(indoc! {"
5581 «testˇ»
5582 "});
5583}
5584
5585#[gpui::test]
5586async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5587 init_test(cx, |_| {});
5588
5589 let mut cx = EditorTestContext::new(cx).await;
5590
5591 // Manipulate with multiple selections on a single line
5592 cx.set_state(indoc! {"
5593 dd«dd
5594 cˇ»c«c
5595 bb
5596 aaaˇ»aa
5597 "});
5598 cx.update_editor(|e, window, cx| {
5599 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5600 });
5601 cx.assert_editor_state(indoc! {"
5602 «aaaaa
5603 bb
5604 ccc
5605 ddddˇ»
5606 "});
5607
5608 // Manipulate with multiple disjoin selections
5609 cx.set_state(indoc! {"
5610 5«
5611 4
5612 3
5613 2
5614 1ˇ»
5615
5616 dd«dd
5617 ccc
5618 bb
5619 aaaˇ»aa
5620 "});
5621 cx.update_editor(|e, window, cx| {
5622 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5623 });
5624 cx.assert_editor_state(indoc! {"
5625 «1
5626 2
5627 3
5628 4
5629 5ˇ»
5630
5631 «aaaaa
5632 bb
5633 ccc
5634 ddddˇ»
5635 "});
5636
5637 // Adding lines on each selection
5638 cx.set_state(indoc! {"
5639 2«
5640 1ˇ»
5641
5642 bb«bb
5643 aaaˇ»aa
5644 "});
5645 cx.update_editor(|e, window, cx| {
5646 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5647 });
5648 cx.assert_editor_state(indoc! {"
5649 «2
5650 1
5651 added lineˇ»
5652
5653 «bbbb
5654 aaaaa
5655 added lineˇ»
5656 "});
5657
5658 // Removing lines on each selection
5659 cx.set_state(indoc! {"
5660 2«
5661 1ˇ»
5662
5663 bb«bb
5664 aaaˇ»aa
5665 "});
5666 cx.update_editor(|e, window, cx| {
5667 e.manipulate_immutable_lines(window, cx, |lines| {
5668 lines.pop();
5669 })
5670 });
5671 cx.assert_editor_state(indoc! {"
5672 «2ˇ»
5673
5674 «bbbbˇ»
5675 "});
5676}
5677
5678#[gpui::test]
5679async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5680 init_test(cx, |settings| {
5681 settings.defaults.tab_size = NonZeroU32::new(3)
5682 });
5683
5684 let mut cx = EditorTestContext::new(cx).await;
5685
5686 // MULTI SELECTION
5687 // Ln.1 "«" tests empty lines
5688 // Ln.9 tests just leading whitespace
5689 cx.set_state(indoc! {"
5690 «
5691 abc // No indentationˇ»
5692 «\tabc // 1 tabˇ»
5693 \t\tabc « ˇ» // 2 tabs
5694 \t ab«c // Tab followed by space
5695 \tabc // Space followed by tab (3 spaces should be the result)
5696 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5697 abˇ»ˇc ˇ ˇ // Already space indented«
5698 \t
5699 \tabc\tdef // Only the leading tab is manipulatedˇ»
5700 "});
5701 cx.update_editor(|e, window, cx| {
5702 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5703 });
5704 cx.assert_editor_state(
5705 indoc! {"
5706 «
5707 abc // No indentation
5708 abc // 1 tab
5709 abc // 2 tabs
5710 abc // Tab followed by space
5711 abc // Space followed by tab (3 spaces should be the result)
5712 abc // Mixed indentation (tab conversion depends on the column)
5713 abc // Already space indented
5714 ·
5715 abc\tdef // Only the leading tab is manipulatedˇ»
5716 "}
5717 .replace("·", "")
5718 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5719 );
5720
5721 // Test on just a few lines, the others should remain unchanged
5722 // Only lines (3, 5, 10, 11) should change
5723 cx.set_state(
5724 indoc! {"
5725 ·
5726 abc // No indentation
5727 \tabcˇ // 1 tab
5728 \t\tabc // 2 tabs
5729 \t abcˇ // Tab followed by space
5730 \tabc // Space followed by tab (3 spaces should be the result)
5731 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5732 abc // Already space indented
5733 «\t
5734 \tabc\tdef // Only the leading tab is manipulatedˇ»
5735 "}
5736 .replace("·", "")
5737 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5738 );
5739 cx.update_editor(|e, window, cx| {
5740 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5741 });
5742 cx.assert_editor_state(
5743 indoc! {"
5744 ·
5745 abc // No indentation
5746 « abc // 1 tabˇ»
5747 \t\tabc // 2 tabs
5748 « abc // Tab followed by spaceˇ»
5749 \tabc // Space followed by tab (3 spaces should be the result)
5750 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5751 abc // Already space indented
5752 « ·
5753 abc\tdef // Only the leading tab is manipulatedˇ»
5754 "}
5755 .replace("·", "")
5756 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5757 );
5758
5759 // SINGLE SELECTION
5760 // Ln.1 "«" tests empty lines
5761 // Ln.9 tests just leading whitespace
5762 cx.set_state(indoc! {"
5763 «
5764 abc // No indentation
5765 \tabc // 1 tab
5766 \t\tabc // 2 tabs
5767 \t abc // Tab followed by space
5768 \tabc // Space followed by tab (3 spaces should be the result)
5769 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5770 abc // Already space indented
5771 \t
5772 \tabc\tdef // Only the leading tab is manipulatedˇ»
5773 "});
5774 cx.update_editor(|e, window, cx| {
5775 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5776 });
5777 cx.assert_editor_state(
5778 indoc! {"
5779 «
5780 abc // No indentation
5781 abc // 1 tab
5782 abc // 2 tabs
5783 abc // Tab followed by space
5784 abc // Space followed by tab (3 spaces should be the result)
5785 abc // Mixed indentation (tab conversion depends on the column)
5786 abc // Already space indented
5787 ·
5788 abc\tdef // Only the leading tab is manipulatedˇ»
5789 "}
5790 .replace("·", "")
5791 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5792 );
5793}
5794
5795#[gpui::test]
5796async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5797 init_test(cx, |settings| {
5798 settings.defaults.tab_size = NonZeroU32::new(3)
5799 });
5800
5801 let mut cx = EditorTestContext::new(cx).await;
5802
5803 // MULTI SELECTION
5804 // Ln.1 "«" tests empty lines
5805 // Ln.11 tests just leading whitespace
5806 cx.set_state(indoc! {"
5807 «
5808 abˇ»ˇc // No indentation
5809 abc ˇ ˇ // 1 space (< 3 so dont convert)
5810 abc « // 2 spaces (< 3 so dont convert)
5811 abc // 3 spaces (convert)
5812 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5813 «\tˇ»\t«\tˇ»abc // Already tab indented
5814 «\t abc // Tab followed by space
5815 \tabc // Space followed by tab (should be consumed due to tab)
5816 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5817 \tˇ» «\t
5818 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5819 "});
5820 cx.update_editor(|e, window, cx| {
5821 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5822 });
5823 cx.assert_editor_state(indoc! {"
5824 «
5825 abc // No indentation
5826 abc // 1 space (< 3 so dont convert)
5827 abc // 2 spaces (< 3 so dont convert)
5828 \tabc // 3 spaces (convert)
5829 \t abc // 5 spaces (1 tab + 2 spaces)
5830 \t\t\tabc // Already tab indented
5831 \t abc // Tab followed by space
5832 \tabc // Space followed by tab (should be consumed due to tab)
5833 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5834 \t\t\t
5835 \tabc \t // Only the leading spaces should be convertedˇ»
5836 "});
5837
5838 // Test on just a few lines, the other should remain unchanged
5839 // Only lines (4, 8, 11, 12) should change
5840 cx.set_state(
5841 indoc! {"
5842 ·
5843 abc // No indentation
5844 abc // 1 space (< 3 so dont convert)
5845 abc // 2 spaces (< 3 so dont convert)
5846 « abc // 3 spaces (convert)ˇ»
5847 abc // 5 spaces (1 tab + 2 spaces)
5848 \t\t\tabc // Already tab indented
5849 \t abc // Tab followed by space
5850 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5851 \t\t \tabc // Mixed indentation
5852 \t \t \t \tabc // Mixed indentation
5853 \t \tˇ
5854 « abc \t // Only the leading spaces should be convertedˇ»
5855 "}
5856 .replace("·", "")
5857 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5858 );
5859 cx.update_editor(|e, window, cx| {
5860 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5861 });
5862 cx.assert_editor_state(
5863 indoc! {"
5864 ·
5865 abc // No indentation
5866 abc // 1 space (< 3 so dont convert)
5867 abc // 2 spaces (< 3 so dont convert)
5868 «\tabc // 3 spaces (convert)ˇ»
5869 abc // 5 spaces (1 tab + 2 spaces)
5870 \t\t\tabc // Already tab indented
5871 \t abc // Tab followed by space
5872 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5873 \t\t \tabc // Mixed indentation
5874 \t \t \t \tabc // Mixed indentation
5875 «\t\t\t
5876 \tabc \t // Only the leading spaces should be convertedˇ»
5877 "}
5878 .replace("·", "")
5879 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5880 );
5881
5882 // SINGLE SELECTION
5883 // Ln.1 "«" tests empty lines
5884 // Ln.11 tests just leading whitespace
5885 cx.set_state(indoc! {"
5886 «
5887 abc // No indentation
5888 abc // 1 space (< 3 so dont convert)
5889 abc // 2 spaces (< 3 so dont convert)
5890 abc // 3 spaces (convert)
5891 abc // 5 spaces (1 tab + 2 spaces)
5892 \t\t\tabc // Already tab indented
5893 \t abc // Tab followed by space
5894 \tabc // Space followed by tab (should be consumed due to tab)
5895 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5896 \t \t
5897 abc \t // Only the leading spaces should be convertedˇ»
5898 "});
5899 cx.update_editor(|e, window, cx| {
5900 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5901 });
5902 cx.assert_editor_state(indoc! {"
5903 «
5904 abc // No indentation
5905 abc // 1 space (< 3 so dont convert)
5906 abc // 2 spaces (< 3 so dont convert)
5907 \tabc // 3 spaces (convert)
5908 \t abc // 5 spaces (1 tab + 2 spaces)
5909 \t\t\tabc // Already tab indented
5910 \t abc // Tab followed by space
5911 \tabc // Space followed by tab (should be consumed due to tab)
5912 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5913 \t\t\t
5914 \tabc \t // Only the leading spaces should be convertedˇ»
5915 "});
5916}
5917
5918#[gpui::test]
5919async fn test_toggle_case(cx: &mut TestAppContext) {
5920 init_test(cx, |_| {});
5921
5922 let mut cx = EditorTestContext::new(cx).await;
5923
5924 // If all lower case -> upper case
5925 cx.set_state(indoc! {"
5926 «hello worldˇ»
5927 "});
5928 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5929 cx.assert_editor_state(indoc! {"
5930 «HELLO WORLDˇ»
5931 "});
5932
5933 // If all upper case -> lower case
5934 cx.set_state(indoc! {"
5935 «HELLO WORLDˇ»
5936 "});
5937 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5938 cx.assert_editor_state(indoc! {"
5939 «hello worldˇ»
5940 "});
5941
5942 // If any upper case characters are identified -> lower case
5943 // This matches JetBrains IDEs
5944 cx.set_state(indoc! {"
5945 «hEllo worldˇ»
5946 "});
5947 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5948 cx.assert_editor_state(indoc! {"
5949 «hello worldˇ»
5950 "});
5951}
5952
5953#[gpui::test]
5954async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5955 init_test(cx, |_| {});
5956
5957 let mut cx = EditorTestContext::new(cx).await;
5958
5959 cx.set_state(indoc! {"
5960 «implement-windows-supportˇ»
5961 "});
5962 cx.update_editor(|e, window, cx| {
5963 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5964 });
5965 cx.assert_editor_state(indoc! {"
5966 «Implement windows supportˇ»
5967 "});
5968}
5969
5970#[gpui::test]
5971async fn test_manipulate_text(cx: &mut TestAppContext) {
5972 init_test(cx, |_| {});
5973
5974 let mut cx = EditorTestContext::new(cx).await;
5975
5976 // Test convert_to_upper_case()
5977 cx.set_state(indoc! {"
5978 «hello worldˇ»
5979 "});
5980 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5981 cx.assert_editor_state(indoc! {"
5982 «HELLO WORLDˇ»
5983 "});
5984
5985 // Test convert_to_lower_case()
5986 cx.set_state(indoc! {"
5987 «HELLO WORLDˇ»
5988 "});
5989 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5990 cx.assert_editor_state(indoc! {"
5991 «hello worldˇ»
5992 "});
5993
5994 // Test multiple line, single selection case
5995 cx.set_state(indoc! {"
5996 «The quick brown
5997 fox jumps over
5998 the lazy dogˇ»
5999 "});
6000 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
6001 cx.assert_editor_state(indoc! {"
6002 «The Quick Brown
6003 Fox Jumps Over
6004 The Lazy Dogˇ»
6005 "});
6006
6007 // Test multiple line, single selection case
6008 cx.set_state(indoc! {"
6009 «The quick brown
6010 fox jumps over
6011 the lazy dogˇ»
6012 "});
6013 cx.update_editor(|e, window, cx| {
6014 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
6015 });
6016 cx.assert_editor_state(indoc! {"
6017 «TheQuickBrown
6018 FoxJumpsOver
6019 TheLazyDogˇ»
6020 "});
6021
6022 // From here on out, test more complex cases of manipulate_text()
6023
6024 // Test no selection case - should affect words cursors are in
6025 // Cursor at beginning, middle, and end of word
6026 cx.set_state(indoc! {"
6027 ˇhello big beauˇtiful worldˇ
6028 "});
6029 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
6030 cx.assert_editor_state(indoc! {"
6031 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
6032 "});
6033
6034 // Test multiple selections on a single line and across multiple lines
6035 cx.set_state(indoc! {"
6036 «Theˇ» quick «brown
6037 foxˇ» jumps «overˇ»
6038 the «lazyˇ» dog
6039 "});
6040 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
6041 cx.assert_editor_state(indoc! {"
6042 «THEˇ» quick «BROWN
6043 FOXˇ» jumps «OVERˇ»
6044 the «LAZYˇ» dog
6045 "});
6046
6047 // Test case where text length grows
6048 cx.set_state(indoc! {"
6049 «tschüߡ»
6050 "});
6051 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
6052 cx.assert_editor_state(indoc! {"
6053 «TSCHÜSSˇ»
6054 "});
6055
6056 // Test to make sure we don't crash when text shrinks
6057 cx.set_state(indoc! {"
6058 aaa_bbbˇ
6059 "});
6060 cx.update_editor(|e, window, cx| {
6061 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
6062 });
6063 cx.assert_editor_state(indoc! {"
6064 «aaaBbbˇ»
6065 "});
6066
6067 // Test to make sure we all aware of the fact that each word can grow and shrink
6068 // Final selections should be aware of this fact
6069 cx.set_state(indoc! {"
6070 aaa_bˇbb bbˇb_ccc ˇccc_ddd
6071 "});
6072 cx.update_editor(|e, window, cx| {
6073 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
6074 });
6075 cx.assert_editor_state(indoc! {"
6076 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
6077 "});
6078
6079 cx.set_state(indoc! {"
6080 «hElLo, WoRld!ˇ»
6081 "});
6082 cx.update_editor(|e, window, cx| {
6083 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
6084 });
6085 cx.assert_editor_state(indoc! {"
6086 «HeLlO, wOrLD!ˇ»
6087 "});
6088
6089 // Test selections with `line_mode() = true`.
6090 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
6091 cx.set_state(indoc! {"
6092 «The quick brown
6093 fox jumps over
6094 tˇ»he lazy dog
6095 "});
6096 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
6097 cx.assert_editor_state(indoc! {"
6098 «THE QUICK BROWN
6099 FOX JUMPS OVER
6100 THE LAZY DOGˇ»
6101 "});
6102}
6103
6104#[gpui::test]
6105fn test_duplicate_line(cx: &mut TestAppContext) {
6106 init_test(cx, |_| {});
6107
6108 let editor = cx.add_window(|window, cx| {
6109 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
6110 build_editor(buffer, window, cx)
6111 });
6112 _ = editor.update(cx, |editor, window, cx| {
6113 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6114 s.select_display_ranges([
6115 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6116 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6117 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6118 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
6119 ])
6120 });
6121 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
6122 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
6123 assert_eq!(
6124 display_ranges(editor, cx),
6125 vec![
6126 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6127 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
6128 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
6129 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
6130 ]
6131 );
6132 });
6133
6134 let editor = cx.add_window(|window, cx| {
6135 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
6136 build_editor(buffer, window, cx)
6137 });
6138 _ = editor.update(cx, |editor, window, cx| {
6139 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6140 s.select_display_ranges([
6141 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
6142 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
6143 ])
6144 });
6145 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
6146 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
6147 assert_eq!(
6148 display_ranges(editor, cx),
6149 vec![
6150 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
6151 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
6152 ]
6153 );
6154 });
6155
6156 // With `duplicate_line_up` the selections move to the duplicated lines,
6157 // which are inserted above the original lines
6158 let editor = cx.add_window(|window, cx| {
6159 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
6160 build_editor(buffer, window, cx)
6161 });
6162 _ = editor.update(cx, |editor, window, cx| {
6163 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6164 s.select_display_ranges([
6165 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6166 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6167 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6168 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
6169 ])
6170 });
6171 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
6172 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
6173 assert_eq!(
6174 display_ranges(editor, cx),
6175 vec![
6176 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6177 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6178 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
6179 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
6180 ]
6181 );
6182 });
6183
6184 let editor = cx.add_window(|window, cx| {
6185 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
6186 build_editor(buffer, window, cx)
6187 });
6188 _ = editor.update(cx, |editor, window, cx| {
6189 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6190 s.select_display_ranges([
6191 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
6192 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
6193 ])
6194 });
6195 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
6196 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
6197 assert_eq!(
6198 display_ranges(editor, cx),
6199 vec![
6200 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
6201 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
6202 ]
6203 );
6204 });
6205
6206 let editor = cx.add_window(|window, cx| {
6207 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
6208 build_editor(buffer, window, cx)
6209 });
6210 _ = editor.update(cx, |editor, window, cx| {
6211 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6212 s.select_display_ranges([
6213 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
6214 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
6215 ])
6216 });
6217 editor.duplicate_selection(&DuplicateSelection, window, cx);
6218 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
6219 assert_eq!(
6220 display_ranges(editor, cx),
6221 vec![
6222 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
6223 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
6224 ]
6225 );
6226 });
6227}
6228
6229#[gpui::test]
6230async fn test_rotate_selections(cx: &mut TestAppContext) {
6231 init_test(cx, |_| {});
6232
6233 let mut cx = EditorTestContext::new(cx).await;
6234
6235 // Rotate text selections (horizontal)
6236 cx.set_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
6237 cx.update_editor(|e, window, cx| {
6238 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
6239 });
6240 cx.assert_editor_state("x=«3ˇ», y=«1ˇ», z=«2ˇ»");
6241 cx.update_editor(|e, window, cx| {
6242 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
6243 });
6244 cx.assert_editor_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
6245
6246 // Rotate text selections (vertical)
6247 cx.set_state(indoc! {"
6248 x=«1ˇ»
6249 y=«2ˇ»
6250 z=«3ˇ»
6251 "});
6252 cx.update_editor(|e, window, cx| {
6253 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
6254 });
6255 cx.assert_editor_state(indoc! {"
6256 x=«3ˇ»
6257 y=«1ˇ»
6258 z=«2ˇ»
6259 "});
6260 cx.update_editor(|e, window, cx| {
6261 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
6262 });
6263 cx.assert_editor_state(indoc! {"
6264 x=«1ˇ»
6265 y=«2ˇ»
6266 z=«3ˇ»
6267 "});
6268
6269 // Rotate text selections (vertical, different lengths)
6270 cx.set_state(indoc! {"
6271 x=\"«ˇ»\"
6272 y=\"«aˇ»\"
6273 z=\"«aaˇ»\"
6274 "});
6275 cx.update_editor(|e, window, cx| {
6276 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
6277 });
6278 cx.assert_editor_state(indoc! {"
6279 x=\"«aaˇ»\"
6280 y=\"«ˇ»\"
6281 z=\"«aˇ»\"
6282 "});
6283 cx.update_editor(|e, window, cx| {
6284 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
6285 });
6286 cx.assert_editor_state(indoc! {"
6287 x=\"«ˇ»\"
6288 y=\"«aˇ»\"
6289 z=\"«aaˇ»\"
6290 "});
6291
6292 // Rotate whole lines (cursor positions preserved)
6293 cx.set_state(indoc! {"
6294 ˇline123
6295 liˇne23
6296 line3ˇ
6297 "});
6298 cx.update_editor(|e, window, cx| {
6299 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
6300 });
6301 cx.assert_editor_state(indoc! {"
6302 line3ˇ
6303 ˇline123
6304 liˇne23
6305 "});
6306 cx.update_editor(|e, window, cx| {
6307 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
6308 });
6309 cx.assert_editor_state(indoc! {"
6310 ˇline123
6311 liˇne23
6312 line3ˇ
6313 "});
6314
6315 // Rotate whole lines, multiple cursors per line (positions preserved)
6316 cx.set_state(indoc! {"
6317 ˇliˇne123
6318 ˇline23
6319 ˇline3
6320 "});
6321 cx.update_editor(|e, window, cx| {
6322 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
6323 });
6324 cx.assert_editor_state(indoc! {"
6325 ˇline3
6326 ˇliˇne123
6327 ˇline23
6328 "});
6329 cx.update_editor(|e, window, cx| {
6330 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
6331 });
6332 cx.assert_editor_state(indoc! {"
6333 ˇliˇne123
6334 ˇline23
6335 ˇline3
6336 "});
6337}
6338
6339#[gpui::test]
6340fn test_move_line_up_down(cx: &mut TestAppContext) {
6341 init_test(cx, |_| {});
6342
6343 let editor = cx.add_window(|window, cx| {
6344 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
6345 build_editor(buffer, window, cx)
6346 });
6347 _ = editor.update(cx, |editor, window, cx| {
6348 editor.fold_creases(
6349 vec![
6350 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6351 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6352 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6353 ],
6354 true,
6355 window,
6356 cx,
6357 );
6358 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6359 s.select_display_ranges([
6360 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6361 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
6362 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
6363 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
6364 ])
6365 });
6366 assert_eq!(
6367 editor.display_text(cx),
6368 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
6369 );
6370
6371 editor.move_line_up(&MoveLineUp, window, cx);
6372 assert_eq!(
6373 editor.display_text(cx),
6374 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
6375 );
6376 assert_eq!(
6377 display_ranges(editor, cx),
6378 vec![
6379 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6380 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6381 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
6382 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
6383 ]
6384 );
6385 });
6386
6387 _ = editor.update(cx, |editor, window, cx| {
6388 editor.move_line_down(&MoveLineDown, window, cx);
6389 assert_eq!(
6390 editor.display_text(cx),
6391 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
6392 );
6393 assert_eq!(
6394 display_ranges(editor, cx),
6395 vec![
6396 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6397 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
6398 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
6399 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
6400 ]
6401 );
6402 });
6403
6404 _ = editor.update(cx, |editor, window, cx| {
6405 editor.move_line_down(&MoveLineDown, window, cx);
6406 assert_eq!(
6407 editor.display_text(cx),
6408 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
6409 );
6410 assert_eq!(
6411 display_ranges(editor, cx),
6412 vec![
6413 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6414 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
6415 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
6416 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
6417 ]
6418 );
6419 });
6420
6421 _ = editor.update(cx, |editor, window, cx| {
6422 editor.move_line_up(&MoveLineUp, window, cx);
6423 assert_eq!(
6424 editor.display_text(cx),
6425 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
6426 );
6427 assert_eq!(
6428 display_ranges(editor, cx),
6429 vec![
6430 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6431 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6432 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
6433 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
6434 ]
6435 );
6436 });
6437}
6438
6439#[gpui::test]
6440fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
6441 init_test(cx, |_| {});
6442 let editor = cx.add_window(|window, cx| {
6443 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
6444 build_editor(buffer, window, cx)
6445 });
6446 _ = editor.update(cx, |editor, window, cx| {
6447 editor.fold_creases(
6448 vec![Crease::simple(
6449 Point::new(6, 4)..Point::new(7, 4),
6450 FoldPlaceholder::test(),
6451 )],
6452 true,
6453 window,
6454 cx,
6455 );
6456 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6457 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
6458 });
6459 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
6460 editor.move_line_up(&MoveLineUp, window, cx);
6461 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
6462 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
6463 });
6464}
6465
6466#[gpui::test]
6467fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
6468 init_test(cx, |_| {});
6469
6470 let editor = cx.add_window(|window, cx| {
6471 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
6472 build_editor(buffer, window, cx)
6473 });
6474 _ = editor.update(cx, |editor, window, cx| {
6475 let snapshot = editor.buffer.read(cx).snapshot(cx);
6476 editor.insert_blocks(
6477 [BlockProperties {
6478 style: BlockStyle::Fixed,
6479 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
6480 height: Some(1),
6481 render: Arc::new(|_| div().into_any()),
6482 priority: 0,
6483 }],
6484 Some(Autoscroll::fit()),
6485 cx,
6486 );
6487 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6488 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
6489 });
6490 editor.move_line_down(&MoveLineDown, window, cx);
6491 });
6492}
6493
6494#[gpui::test]
6495async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
6496 init_test(cx, |_| {});
6497
6498 let mut cx = EditorTestContext::new(cx).await;
6499 cx.set_state(
6500 &"
6501 ˇzero
6502 one
6503 two
6504 three
6505 four
6506 five
6507 "
6508 .unindent(),
6509 );
6510
6511 // Create a four-line block that replaces three lines of text.
6512 cx.update_editor(|editor, window, cx| {
6513 let snapshot = editor.snapshot(window, cx);
6514 let snapshot = &snapshot.buffer_snapshot();
6515 let placement = BlockPlacement::Replace(
6516 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
6517 );
6518 editor.insert_blocks(
6519 [BlockProperties {
6520 placement,
6521 height: Some(4),
6522 style: BlockStyle::Sticky,
6523 render: Arc::new(|_| gpui::div().into_any_element()),
6524 priority: 0,
6525 }],
6526 None,
6527 cx,
6528 );
6529 });
6530
6531 // Move down so that the cursor touches the block.
6532 cx.update_editor(|editor, window, cx| {
6533 editor.move_down(&Default::default(), window, cx);
6534 });
6535 cx.assert_editor_state(
6536 &"
6537 zero
6538 «one
6539 two
6540 threeˇ»
6541 four
6542 five
6543 "
6544 .unindent(),
6545 );
6546
6547 // Move down past the block.
6548 cx.update_editor(|editor, window, cx| {
6549 editor.move_down(&Default::default(), window, cx);
6550 });
6551 cx.assert_editor_state(
6552 &"
6553 zero
6554 one
6555 two
6556 three
6557 ˇfour
6558 five
6559 "
6560 .unindent(),
6561 );
6562}
6563
6564#[gpui::test]
6565fn test_transpose(cx: &mut TestAppContext) {
6566 init_test(cx, |_| {});
6567
6568 _ = cx.add_window(|window, cx| {
6569 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
6570 editor.set_style(EditorStyle::default(), window, cx);
6571 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6572 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
6573 });
6574 editor.transpose(&Default::default(), window, cx);
6575 assert_eq!(editor.text(cx), "bac");
6576 assert_eq!(
6577 editor.selections.ranges(&editor.display_snapshot(cx)),
6578 [MultiBufferOffset(2)..MultiBufferOffset(2)]
6579 );
6580
6581 editor.transpose(&Default::default(), window, cx);
6582 assert_eq!(editor.text(cx), "bca");
6583 assert_eq!(
6584 editor.selections.ranges(&editor.display_snapshot(cx)),
6585 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6586 );
6587
6588 editor.transpose(&Default::default(), window, cx);
6589 assert_eq!(editor.text(cx), "bac");
6590 assert_eq!(
6591 editor.selections.ranges(&editor.display_snapshot(cx)),
6592 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6593 );
6594
6595 editor
6596 });
6597
6598 _ = cx.add_window(|window, cx| {
6599 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6600 editor.set_style(EditorStyle::default(), window, cx);
6601 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6602 s.select_ranges([MultiBufferOffset(3)..MultiBufferOffset(3)])
6603 });
6604 editor.transpose(&Default::default(), window, cx);
6605 assert_eq!(editor.text(cx), "acb\nde");
6606 assert_eq!(
6607 editor.selections.ranges(&editor.display_snapshot(cx)),
6608 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6609 );
6610
6611 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6612 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6613 });
6614 editor.transpose(&Default::default(), window, cx);
6615 assert_eq!(editor.text(cx), "acbd\ne");
6616 assert_eq!(
6617 editor.selections.ranges(&editor.display_snapshot(cx)),
6618 [MultiBufferOffset(5)..MultiBufferOffset(5)]
6619 );
6620
6621 editor.transpose(&Default::default(), window, cx);
6622 assert_eq!(editor.text(cx), "acbde\n");
6623 assert_eq!(
6624 editor.selections.ranges(&editor.display_snapshot(cx)),
6625 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6626 );
6627
6628 editor.transpose(&Default::default(), window, cx);
6629 assert_eq!(editor.text(cx), "acbd\ne");
6630 assert_eq!(
6631 editor.selections.ranges(&editor.display_snapshot(cx)),
6632 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6633 );
6634
6635 editor
6636 });
6637
6638 _ = cx.add_window(|window, cx| {
6639 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6640 editor.set_style(EditorStyle::default(), window, cx);
6641 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6642 s.select_ranges([
6643 MultiBufferOffset(1)..MultiBufferOffset(1),
6644 MultiBufferOffset(2)..MultiBufferOffset(2),
6645 MultiBufferOffset(4)..MultiBufferOffset(4),
6646 ])
6647 });
6648 editor.transpose(&Default::default(), window, cx);
6649 assert_eq!(editor.text(cx), "bacd\ne");
6650 assert_eq!(
6651 editor.selections.ranges(&editor.display_snapshot(cx)),
6652 [
6653 MultiBufferOffset(2)..MultiBufferOffset(2),
6654 MultiBufferOffset(3)..MultiBufferOffset(3),
6655 MultiBufferOffset(5)..MultiBufferOffset(5)
6656 ]
6657 );
6658
6659 editor.transpose(&Default::default(), window, cx);
6660 assert_eq!(editor.text(cx), "bcade\n");
6661 assert_eq!(
6662 editor.selections.ranges(&editor.display_snapshot(cx)),
6663 [
6664 MultiBufferOffset(3)..MultiBufferOffset(3),
6665 MultiBufferOffset(4)..MultiBufferOffset(4),
6666 MultiBufferOffset(6)..MultiBufferOffset(6)
6667 ]
6668 );
6669
6670 editor.transpose(&Default::default(), window, cx);
6671 assert_eq!(editor.text(cx), "bcda\ne");
6672 assert_eq!(
6673 editor.selections.ranges(&editor.display_snapshot(cx)),
6674 [
6675 MultiBufferOffset(4)..MultiBufferOffset(4),
6676 MultiBufferOffset(6)..MultiBufferOffset(6)
6677 ]
6678 );
6679
6680 editor.transpose(&Default::default(), window, cx);
6681 assert_eq!(editor.text(cx), "bcade\n");
6682 assert_eq!(
6683 editor.selections.ranges(&editor.display_snapshot(cx)),
6684 [
6685 MultiBufferOffset(4)..MultiBufferOffset(4),
6686 MultiBufferOffset(6)..MultiBufferOffset(6)
6687 ]
6688 );
6689
6690 editor.transpose(&Default::default(), window, cx);
6691 assert_eq!(editor.text(cx), "bcaed\n");
6692 assert_eq!(
6693 editor.selections.ranges(&editor.display_snapshot(cx)),
6694 [
6695 MultiBufferOffset(5)..MultiBufferOffset(5),
6696 MultiBufferOffset(6)..MultiBufferOffset(6)
6697 ]
6698 );
6699
6700 editor
6701 });
6702
6703 _ = cx.add_window(|window, cx| {
6704 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6705 editor.set_style(EditorStyle::default(), window, cx);
6706 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6707 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6708 });
6709 editor.transpose(&Default::default(), window, cx);
6710 assert_eq!(editor.text(cx), "🏀🍐✋");
6711 assert_eq!(
6712 editor.selections.ranges(&editor.display_snapshot(cx)),
6713 [MultiBufferOffset(8)..MultiBufferOffset(8)]
6714 );
6715
6716 editor.transpose(&Default::default(), window, cx);
6717 assert_eq!(editor.text(cx), "🏀✋🍐");
6718 assert_eq!(
6719 editor.selections.ranges(&editor.display_snapshot(cx)),
6720 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6721 );
6722
6723 editor.transpose(&Default::default(), window, cx);
6724 assert_eq!(editor.text(cx), "🏀🍐✋");
6725 assert_eq!(
6726 editor.selections.ranges(&editor.display_snapshot(cx)),
6727 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6728 );
6729
6730 editor
6731 });
6732}
6733
6734#[gpui::test]
6735async fn test_rewrap(cx: &mut TestAppContext) {
6736 init_test(cx, |settings| {
6737 settings.languages.0.extend([
6738 (
6739 "Markdown".into(),
6740 LanguageSettingsContent {
6741 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6742 preferred_line_length: Some(40),
6743 ..Default::default()
6744 },
6745 ),
6746 (
6747 "Plain Text".into(),
6748 LanguageSettingsContent {
6749 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6750 preferred_line_length: Some(40),
6751 ..Default::default()
6752 },
6753 ),
6754 (
6755 "C++".into(),
6756 LanguageSettingsContent {
6757 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6758 preferred_line_length: Some(40),
6759 ..Default::default()
6760 },
6761 ),
6762 (
6763 "Python".into(),
6764 LanguageSettingsContent {
6765 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6766 preferred_line_length: Some(40),
6767 ..Default::default()
6768 },
6769 ),
6770 (
6771 "Rust".into(),
6772 LanguageSettingsContent {
6773 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6774 preferred_line_length: Some(40),
6775 ..Default::default()
6776 },
6777 ),
6778 ])
6779 });
6780
6781 let mut cx = EditorTestContext::new(cx).await;
6782
6783 let cpp_language = Arc::new(Language::new(
6784 LanguageConfig {
6785 name: "C++".into(),
6786 line_comments: vec!["// ".into()],
6787 ..LanguageConfig::default()
6788 },
6789 None,
6790 ));
6791 let python_language = Arc::new(Language::new(
6792 LanguageConfig {
6793 name: "Python".into(),
6794 line_comments: vec!["# ".into()],
6795 ..LanguageConfig::default()
6796 },
6797 None,
6798 ));
6799 let markdown_language = Arc::new(Language::new(
6800 LanguageConfig {
6801 name: "Markdown".into(),
6802 rewrap_prefixes: vec![
6803 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6804 regex::Regex::new("[-*+]\\s+").unwrap(),
6805 ],
6806 ..LanguageConfig::default()
6807 },
6808 None,
6809 ));
6810 let rust_language = Arc::new(
6811 Language::new(
6812 LanguageConfig {
6813 name: "Rust".into(),
6814 line_comments: vec!["// ".into(), "/// ".into()],
6815 ..LanguageConfig::default()
6816 },
6817 Some(tree_sitter_rust::LANGUAGE.into()),
6818 )
6819 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6820 .unwrap(),
6821 );
6822
6823 let plaintext_language = Arc::new(Language::new(
6824 LanguageConfig {
6825 name: "Plain Text".into(),
6826 ..LanguageConfig::default()
6827 },
6828 None,
6829 ));
6830
6831 // Test basic rewrapping of a long line with a cursor
6832 assert_rewrap(
6833 indoc! {"
6834 // ˇThis is a long comment that needs to be wrapped.
6835 "},
6836 indoc! {"
6837 // ˇThis is a long comment that needs to
6838 // be wrapped.
6839 "},
6840 cpp_language.clone(),
6841 &mut cx,
6842 );
6843
6844 // Test rewrapping a full selection
6845 assert_rewrap(
6846 indoc! {"
6847 «// This selected long comment needs to be wrapped.ˇ»"
6848 },
6849 indoc! {"
6850 «// This selected long comment needs to
6851 // be wrapped.ˇ»"
6852 },
6853 cpp_language.clone(),
6854 &mut cx,
6855 );
6856
6857 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6858 assert_rewrap(
6859 indoc! {"
6860 // ˇThis is the first line.
6861 // Thisˇ is the second line.
6862 // This is the thirdˇ line, all part of one paragraph.
6863 "},
6864 indoc! {"
6865 // ˇThis is the first line. Thisˇ is the
6866 // second line. This is the thirdˇ line,
6867 // all part of one paragraph.
6868 "},
6869 cpp_language.clone(),
6870 &mut cx,
6871 );
6872
6873 // Test multiple cursors in different paragraphs trigger separate rewraps
6874 assert_rewrap(
6875 indoc! {"
6876 // ˇThis is the first paragraph, first line.
6877 // ˇThis is the first paragraph, second line.
6878
6879 // ˇThis is the second paragraph, first line.
6880 // ˇThis is the second paragraph, second line.
6881 "},
6882 indoc! {"
6883 // ˇThis is the first paragraph, first
6884 // line. ˇThis is the first paragraph,
6885 // second line.
6886
6887 // ˇThis is the second paragraph, first
6888 // line. ˇThis is the second paragraph,
6889 // second line.
6890 "},
6891 cpp_language.clone(),
6892 &mut cx,
6893 );
6894
6895 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6896 assert_rewrap(
6897 indoc! {"
6898 «// A regular long long comment to be wrapped.
6899 /// A documentation long comment to be wrapped.ˇ»
6900 "},
6901 indoc! {"
6902 «// A regular long long comment to be
6903 // wrapped.
6904 /// A documentation long comment to be
6905 /// wrapped.ˇ»
6906 "},
6907 rust_language.clone(),
6908 &mut cx,
6909 );
6910
6911 // Test that change in indentation level trigger seperate rewraps
6912 assert_rewrap(
6913 indoc! {"
6914 fn foo() {
6915 «// This is a long comment at the base indent.
6916 // This is a long comment at the next indent.ˇ»
6917 }
6918 "},
6919 indoc! {"
6920 fn foo() {
6921 «// This is a long comment at the
6922 // base indent.
6923 // This is a long comment at the
6924 // next indent.ˇ»
6925 }
6926 "},
6927 rust_language.clone(),
6928 &mut cx,
6929 );
6930
6931 // Test that different comment prefix characters (e.g., '#') are handled correctly
6932 assert_rewrap(
6933 indoc! {"
6934 # ˇThis is a long comment using a pound sign.
6935 "},
6936 indoc! {"
6937 # ˇThis is a long comment using a pound
6938 # sign.
6939 "},
6940 python_language,
6941 &mut cx,
6942 );
6943
6944 // Test rewrapping only affects comments, not code even when selected
6945 assert_rewrap(
6946 indoc! {"
6947 «/// This doc comment is long and should be wrapped.
6948 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6949 "},
6950 indoc! {"
6951 «/// This doc comment is long and should
6952 /// be wrapped.
6953 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6954 "},
6955 rust_language.clone(),
6956 &mut cx,
6957 );
6958
6959 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6960 assert_rewrap(
6961 indoc! {"
6962 # Header
6963
6964 A long long long line of markdown text to wrap.ˇ
6965 "},
6966 indoc! {"
6967 # Header
6968
6969 A long long long line of markdown text
6970 to wrap.ˇ
6971 "},
6972 markdown_language.clone(),
6973 &mut cx,
6974 );
6975
6976 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6977 assert_rewrap(
6978 indoc! {"
6979 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6980 2. This is a numbered list item that is very long and needs to be wrapped properly.
6981 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6982 "},
6983 indoc! {"
6984 «1. This is a numbered list item that is
6985 very long and needs to be wrapped
6986 properly.
6987 2. This is a numbered list item that is
6988 very long and needs to be wrapped
6989 properly.
6990 - This is an unordered list item that is
6991 also very long and should not merge
6992 with the numbered item.ˇ»
6993 "},
6994 markdown_language.clone(),
6995 &mut cx,
6996 );
6997
6998 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6999 assert_rewrap(
7000 indoc! {"
7001 «1. This is a numbered list item that is
7002 very long and needs to be wrapped
7003 properly.
7004 2. This is a numbered list item that is
7005 very long and needs to be wrapped
7006 properly.
7007 - This is an unordered list item that is
7008 also very long and should not merge with
7009 the numbered item.ˇ»
7010 "},
7011 indoc! {"
7012 «1. This is a numbered list item that is
7013 very long and needs to be wrapped
7014 properly.
7015 2. This is a numbered list item that is
7016 very long and needs to be wrapped
7017 properly.
7018 - This is an unordered list item that is
7019 also very long and should not merge
7020 with the numbered item.ˇ»
7021 "},
7022 markdown_language.clone(),
7023 &mut cx,
7024 );
7025
7026 // Test that rewrapping maintain indents even when they already exists.
7027 assert_rewrap(
7028 indoc! {"
7029 «1. This is a numbered list
7030 item that is very long and needs to be wrapped properly.
7031 2. This is a numbered list
7032 item that is very long and needs to be wrapped properly.
7033 - This is an unordered list item that is also very long and
7034 should not merge with the numbered item.ˇ»
7035 "},
7036 indoc! {"
7037 «1. This is a numbered list item that is
7038 very long and needs to be wrapped
7039 properly.
7040 2. This is a numbered list item that is
7041 very long and needs to be wrapped
7042 properly.
7043 - This is an unordered list item that is
7044 also very long and should not merge
7045 with the numbered item.ˇ»
7046 "},
7047 markdown_language,
7048 &mut cx,
7049 );
7050
7051 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
7052 assert_rewrap(
7053 indoc! {"
7054 ˇThis is a very long line of plain text that will be wrapped.
7055 "},
7056 indoc! {"
7057 ˇThis is a very long line of plain text
7058 that will be wrapped.
7059 "},
7060 plaintext_language.clone(),
7061 &mut cx,
7062 );
7063
7064 // Test that non-commented code acts as a paragraph boundary within a selection
7065 assert_rewrap(
7066 indoc! {"
7067 «// This is the first long comment block to be wrapped.
7068 fn my_func(a: u32);
7069 // This is the second long comment block to be wrapped.ˇ»
7070 "},
7071 indoc! {"
7072 «// This is the first long comment block
7073 // to be wrapped.
7074 fn my_func(a: u32);
7075 // This is the second long comment block
7076 // to be wrapped.ˇ»
7077 "},
7078 rust_language,
7079 &mut cx,
7080 );
7081
7082 // Test rewrapping multiple selections, including ones with blank lines or tabs
7083 assert_rewrap(
7084 indoc! {"
7085 «ˇThis is a very long line that will be wrapped.
7086
7087 This is another paragraph in the same selection.»
7088
7089 «\tThis is a very long indented line that will be wrapped.ˇ»
7090 "},
7091 indoc! {"
7092 «ˇThis is a very long line that will be
7093 wrapped.
7094
7095 This is another paragraph in the same
7096 selection.»
7097
7098 «\tThis is a very long indented line
7099 \tthat will be wrapped.ˇ»
7100 "},
7101 plaintext_language,
7102 &mut cx,
7103 );
7104
7105 // Test that an empty comment line acts as a paragraph boundary
7106 assert_rewrap(
7107 indoc! {"
7108 // ˇThis is a long comment that will be wrapped.
7109 //
7110 // And this is another long comment that will also be wrapped.ˇ
7111 "},
7112 indoc! {"
7113 // ˇThis is a long comment that will be
7114 // wrapped.
7115 //
7116 // And this is another long comment that
7117 // will also be wrapped.ˇ
7118 "},
7119 cpp_language,
7120 &mut cx,
7121 );
7122
7123 #[track_caller]
7124 fn assert_rewrap(
7125 unwrapped_text: &str,
7126 wrapped_text: &str,
7127 language: Arc<Language>,
7128 cx: &mut EditorTestContext,
7129 ) {
7130 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
7131 cx.set_state(unwrapped_text);
7132 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
7133 cx.assert_editor_state(wrapped_text);
7134 }
7135}
7136
7137#[gpui::test]
7138async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
7139 init_test(cx, |settings| {
7140 settings.languages.0.extend([(
7141 "Rust".into(),
7142 LanguageSettingsContent {
7143 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
7144 preferred_line_length: Some(40),
7145 ..Default::default()
7146 },
7147 )])
7148 });
7149
7150 let mut cx = EditorTestContext::new(cx).await;
7151
7152 let rust_lang = Arc::new(
7153 Language::new(
7154 LanguageConfig {
7155 name: "Rust".into(),
7156 line_comments: vec!["// ".into()],
7157 block_comment: Some(BlockCommentConfig {
7158 start: "/*".into(),
7159 end: "*/".into(),
7160 prefix: "* ".into(),
7161 tab_size: 1,
7162 }),
7163 documentation_comment: Some(BlockCommentConfig {
7164 start: "/**".into(),
7165 end: "*/".into(),
7166 prefix: "* ".into(),
7167 tab_size: 1,
7168 }),
7169
7170 ..LanguageConfig::default()
7171 },
7172 Some(tree_sitter_rust::LANGUAGE.into()),
7173 )
7174 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
7175 .unwrap(),
7176 );
7177
7178 // regular block comment
7179 assert_rewrap(
7180 indoc! {"
7181 /*
7182 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7183 */
7184 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7185 "},
7186 indoc! {"
7187 /*
7188 *ˇ Lorem ipsum dolor sit amet,
7189 * consectetur adipiscing elit.
7190 */
7191 /*
7192 *ˇ Lorem ipsum dolor sit amet,
7193 * consectetur adipiscing elit.
7194 */
7195 "},
7196 rust_lang.clone(),
7197 &mut cx,
7198 );
7199
7200 // indent is respected
7201 assert_rewrap(
7202 indoc! {"
7203 {}
7204 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7205 "},
7206 indoc! {"
7207 {}
7208 /*
7209 *ˇ Lorem ipsum dolor sit amet,
7210 * consectetur adipiscing elit.
7211 */
7212 "},
7213 rust_lang.clone(),
7214 &mut cx,
7215 );
7216
7217 // short block comments with inline delimiters
7218 assert_rewrap(
7219 indoc! {"
7220 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7221 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7222 */
7223 /*
7224 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7225 "},
7226 indoc! {"
7227 /*
7228 *ˇ Lorem ipsum dolor sit amet,
7229 * consectetur adipiscing elit.
7230 */
7231 /*
7232 *ˇ Lorem ipsum dolor sit amet,
7233 * consectetur adipiscing elit.
7234 */
7235 /*
7236 *ˇ Lorem ipsum dolor sit amet,
7237 * consectetur adipiscing elit.
7238 */
7239 "},
7240 rust_lang.clone(),
7241 &mut cx,
7242 );
7243
7244 // multiline block comment with inline start/end delimiters
7245 assert_rewrap(
7246 indoc! {"
7247 /*ˇ Lorem ipsum dolor sit amet,
7248 * consectetur adipiscing elit. */
7249 "},
7250 indoc! {"
7251 /*
7252 *ˇ Lorem ipsum dolor sit amet,
7253 * consectetur adipiscing elit.
7254 */
7255 "},
7256 rust_lang.clone(),
7257 &mut cx,
7258 );
7259
7260 // block comment rewrap still respects paragraph bounds
7261 assert_rewrap(
7262 indoc! {"
7263 /*
7264 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7265 *
7266 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7267 */
7268 "},
7269 indoc! {"
7270 /*
7271 *ˇ Lorem ipsum dolor sit amet,
7272 * consectetur adipiscing elit.
7273 *
7274 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7275 */
7276 "},
7277 rust_lang.clone(),
7278 &mut cx,
7279 );
7280
7281 // documentation comments
7282 assert_rewrap(
7283 indoc! {"
7284 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7285 /**
7286 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7287 */
7288 "},
7289 indoc! {"
7290 /**
7291 *ˇ Lorem ipsum dolor sit amet,
7292 * consectetur adipiscing elit.
7293 */
7294 /**
7295 *ˇ Lorem ipsum dolor sit amet,
7296 * consectetur adipiscing elit.
7297 */
7298 "},
7299 rust_lang.clone(),
7300 &mut cx,
7301 );
7302
7303 // different, adjacent comments
7304 assert_rewrap(
7305 indoc! {"
7306 /**
7307 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7308 */
7309 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7310 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7311 "},
7312 indoc! {"
7313 /**
7314 *ˇ Lorem ipsum dolor sit amet,
7315 * consectetur adipiscing elit.
7316 */
7317 /*
7318 *ˇ Lorem ipsum dolor sit amet,
7319 * consectetur adipiscing elit.
7320 */
7321 //ˇ Lorem ipsum dolor sit amet,
7322 // consectetur adipiscing elit.
7323 "},
7324 rust_lang.clone(),
7325 &mut cx,
7326 );
7327
7328 // selection w/ single short block comment
7329 assert_rewrap(
7330 indoc! {"
7331 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7332 "},
7333 indoc! {"
7334 «/*
7335 * Lorem ipsum dolor sit amet,
7336 * consectetur adipiscing elit.
7337 */ˇ»
7338 "},
7339 rust_lang.clone(),
7340 &mut cx,
7341 );
7342
7343 // rewrapping a single comment w/ abutting comments
7344 assert_rewrap(
7345 indoc! {"
7346 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
7347 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7348 "},
7349 indoc! {"
7350 /*
7351 * ˇLorem ipsum dolor sit amet,
7352 * consectetur adipiscing elit.
7353 */
7354 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7355 "},
7356 rust_lang.clone(),
7357 &mut cx,
7358 );
7359
7360 // selection w/ non-abutting short block comments
7361 assert_rewrap(
7362 indoc! {"
7363 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7364
7365 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7366 "},
7367 indoc! {"
7368 «/*
7369 * Lorem ipsum dolor sit amet,
7370 * consectetur adipiscing elit.
7371 */
7372
7373 /*
7374 * Lorem ipsum dolor sit amet,
7375 * consectetur adipiscing elit.
7376 */ˇ»
7377 "},
7378 rust_lang.clone(),
7379 &mut cx,
7380 );
7381
7382 // selection of multiline block comments
7383 assert_rewrap(
7384 indoc! {"
7385 «/* Lorem ipsum dolor sit amet,
7386 * consectetur adipiscing elit. */ˇ»
7387 "},
7388 indoc! {"
7389 «/*
7390 * Lorem ipsum dolor sit amet,
7391 * consectetur adipiscing elit.
7392 */ˇ»
7393 "},
7394 rust_lang.clone(),
7395 &mut cx,
7396 );
7397
7398 // partial selection of multiline block comments
7399 assert_rewrap(
7400 indoc! {"
7401 «/* Lorem ipsum dolor sit amet,ˇ»
7402 * consectetur adipiscing elit. */
7403 /* Lorem ipsum dolor sit amet,
7404 «* consectetur adipiscing elit. */ˇ»
7405 "},
7406 indoc! {"
7407 «/*
7408 * Lorem ipsum dolor sit amet,ˇ»
7409 * consectetur adipiscing elit. */
7410 /* Lorem ipsum dolor sit amet,
7411 «* consectetur adipiscing elit.
7412 */ˇ»
7413 "},
7414 rust_lang.clone(),
7415 &mut cx,
7416 );
7417
7418 // selection w/ abutting short block comments
7419 // TODO: should not be combined; should rewrap as 2 comments
7420 assert_rewrap(
7421 indoc! {"
7422 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7423 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7424 "},
7425 // desired behavior:
7426 // indoc! {"
7427 // «/*
7428 // * Lorem ipsum dolor sit amet,
7429 // * consectetur adipiscing elit.
7430 // */
7431 // /*
7432 // * Lorem ipsum dolor sit amet,
7433 // * consectetur adipiscing elit.
7434 // */ˇ»
7435 // "},
7436 // actual behaviour:
7437 indoc! {"
7438 «/*
7439 * Lorem ipsum dolor sit amet,
7440 * consectetur adipiscing elit. Lorem
7441 * ipsum dolor sit amet, consectetur
7442 * adipiscing elit.
7443 */ˇ»
7444 "},
7445 rust_lang.clone(),
7446 &mut cx,
7447 );
7448
7449 // TODO: same as above, but with delimiters on separate line
7450 // assert_rewrap(
7451 // indoc! {"
7452 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7453 // */
7454 // /*
7455 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7456 // "},
7457 // // desired:
7458 // // indoc! {"
7459 // // «/*
7460 // // * Lorem ipsum dolor sit amet,
7461 // // * consectetur adipiscing elit.
7462 // // */
7463 // // /*
7464 // // * Lorem ipsum dolor sit amet,
7465 // // * consectetur adipiscing elit.
7466 // // */ˇ»
7467 // // "},
7468 // // actual: (but with trailing w/s on the empty lines)
7469 // indoc! {"
7470 // «/*
7471 // * Lorem ipsum dolor sit amet,
7472 // * consectetur adipiscing elit.
7473 // *
7474 // */
7475 // /*
7476 // *
7477 // * Lorem ipsum dolor sit amet,
7478 // * consectetur adipiscing elit.
7479 // */ˇ»
7480 // "},
7481 // rust_lang.clone(),
7482 // &mut cx,
7483 // );
7484
7485 // TODO these are unhandled edge cases; not correct, just documenting known issues
7486 assert_rewrap(
7487 indoc! {"
7488 /*
7489 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7490 */
7491 /*
7492 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7493 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
7494 "},
7495 // desired:
7496 // indoc! {"
7497 // /*
7498 // *ˇ Lorem ipsum dolor sit amet,
7499 // * consectetur adipiscing elit.
7500 // */
7501 // /*
7502 // *ˇ Lorem ipsum dolor sit amet,
7503 // * consectetur adipiscing elit.
7504 // */
7505 // /*
7506 // *ˇ Lorem ipsum dolor sit amet
7507 // */ /* consectetur adipiscing elit. */
7508 // "},
7509 // actual:
7510 indoc! {"
7511 /*
7512 //ˇ Lorem ipsum dolor sit amet,
7513 // consectetur adipiscing elit.
7514 */
7515 /*
7516 * //ˇ Lorem ipsum dolor sit amet,
7517 * consectetur adipiscing elit.
7518 */
7519 /*
7520 *ˇ Lorem ipsum dolor sit amet */ /*
7521 * consectetur adipiscing elit.
7522 */
7523 "},
7524 rust_lang,
7525 &mut cx,
7526 );
7527
7528 #[track_caller]
7529 fn assert_rewrap(
7530 unwrapped_text: &str,
7531 wrapped_text: &str,
7532 language: Arc<Language>,
7533 cx: &mut EditorTestContext,
7534 ) {
7535 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
7536 cx.set_state(unwrapped_text);
7537 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
7538 cx.assert_editor_state(wrapped_text);
7539 }
7540}
7541
7542#[gpui::test]
7543async fn test_hard_wrap(cx: &mut TestAppContext) {
7544 init_test(cx, |_| {});
7545 let mut cx = EditorTestContext::new(cx).await;
7546
7547 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
7548 cx.update_editor(|editor, _, cx| {
7549 editor.set_hard_wrap(Some(14), cx);
7550 });
7551
7552 cx.set_state(indoc!(
7553 "
7554 one two three ˇ
7555 "
7556 ));
7557 cx.simulate_input("four");
7558 cx.run_until_parked();
7559
7560 cx.assert_editor_state(indoc!(
7561 "
7562 one two three
7563 fourˇ
7564 "
7565 ));
7566
7567 cx.update_editor(|editor, window, cx| {
7568 editor.newline(&Default::default(), window, cx);
7569 });
7570 cx.run_until_parked();
7571 cx.assert_editor_state(indoc!(
7572 "
7573 one two three
7574 four
7575 ˇ
7576 "
7577 ));
7578
7579 cx.simulate_input("five");
7580 cx.run_until_parked();
7581 cx.assert_editor_state(indoc!(
7582 "
7583 one two three
7584 four
7585 fiveˇ
7586 "
7587 ));
7588
7589 cx.update_editor(|editor, window, cx| {
7590 editor.newline(&Default::default(), window, cx);
7591 });
7592 cx.run_until_parked();
7593 cx.simulate_input("# ");
7594 cx.run_until_parked();
7595 cx.assert_editor_state(indoc!(
7596 "
7597 one two three
7598 four
7599 five
7600 # ˇ
7601 "
7602 ));
7603
7604 cx.update_editor(|editor, window, cx| {
7605 editor.newline(&Default::default(), window, cx);
7606 });
7607 cx.run_until_parked();
7608 cx.assert_editor_state(indoc!(
7609 "
7610 one two three
7611 four
7612 five
7613 #\x20
7614 #ˇ
7615 "
7616 ));
7617
7618 cx.simulate_input(" 6");
7619 cx.run_until_parked();
7620 cx.assert_editor_state(indoc!(
7621 "
7622 one two three
7623 four
7624 five
7625 #
7626 # 6ˇ
7627 "
7628 ));
7629}
7630
7631#[gpui::test]
7632async fn test_cut_line_ends(cx: &mut TestAppContext) {
7633 init_test(cx, |_| {});
7634
7635 let mut cx = EditorTestContext::new(cx).await;
7636
7637 cx.set_state(indoc! {"The quick brownˇ"});
7638 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7639 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7640
7641 cx.set_state(indoc! {"The emacs foxˇ"});
7642 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7643 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7644
7645 cx.set_state(indoc! {"
7646 The quick« brownˇ»
7647 fox jumps overˇ
7648 the lazy dog"});
7649 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7650 cx.assert_editor_state(indoc! {"
7651 The quickˇ
7652 ˇthe lazy dog"});
7653
7654 cx.set_state(indoc! {"
7655 The quick« brownˇ»
7656 fox jumps overˇ
7657 the lazy dog"});
7658 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7659 cx.assert_editor_state(indoc! {"
7660 The quickˇ
7661 fox jumps overˇthe lazy dog"});
7662
7663 cx.set_state(indoc! {"
7664 The quick« brownˇ»
7665 fox jumps overˇ
7666 the lazy dog"});
7667 cx.update_editor(|e, window, cx| {
7668 e.cut_to_end_of_line(
7669 &CutToEndOfLine {
7670 stop_at_newlines: true,
7671 },
7672 window,
7673 cx,
7674 )
7675 });
7676 cx.assert_editor_state(indoc! {"
7677 The quickˇ
7678 fox jumps overˇ
7679 the lazy dog"});
7680
7681 cx.set_state(indoc! {"
7682 The quick« brownˇ»
7683 fox jumps overˇ
7684 the lazy dog"});
7685 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7686 cx.assert_editor_state(indoc! {"
7687 The quickˇ
7688 fox jumps overˇthe lazy dog"});
7689}
7690
7691#[gpui::test]
7692async fn test_clipboard(cx: &mut TestAppContext) {
7693 init_test(cx, |_| {});
7694
7695 let mut cx = EditorTestContext::new(cx).await;
7696
7697 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7698 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7699 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7700
7701 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7702 cx.set_state("two ˇfour ˇsix ˇ");
7703 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7704 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7705
7706 // Paste again but with only two cursors. Since the number of cursors doesn't
7707 // match the number of slices in the clipboard, the entire clipboard text
7708 // is pasted at each cursor.
7709 cx.set_state("ˇtwo one✅ four three six five ˇ");
7710 cx.update_editor(|e, window, cx| {
7711 e.handle_input("( ", window, cx);
7712 e.paste(&Paste, window, cx);
7713 e.handle_input(") ", window, cx);
7714 });
7715 cx.assert_editor_state(
7716 &([
7717 "( one✅ ",
7718 "three ",
7719 "five ) ˇtwo one✅ four three six five ( one✅ ",
7720 "three ",
7721 "five ) ˇ",
7722 ]
7723 .join("\n")),
7724 );
7725
7726 // Cut with three selections, one of which is full-line.
7727 cx.set_state(indoc! {"
7728 1«2ˇ»3
7729 4ˇ567
7730 «8ˇ»9"});
7731 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7732 cx.assert_editor_state(indoc! {"
7733 1ˇ3
7734 ˇ9"});
7735
7736 // Paste with three selections, noticing how the copied selection that was full-line
7737 // gets inserted before the second cursor.
7738 cx.set_state(indoc! {"
7739 1ˇ3
7740 9ˇ
7741 «oˇ»ne"});
7742 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7743 cx.assert_editor_state(indoc! {"
7744 12ˇ3
7745 4567
7746 9ˇ
7747 8ˇne"});
7748
7749 // Copy with a single cursor only, which writes the whole line into the clipboard.
7750 cx.set_state(indoc! {"
7751 The quick brown
7752 fox juˇmps over
7753 the lazy dog"});
7754 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7755 assert_eq!(
7756 cx.read_from_clipboard()
7757 .and_then(|item| item.text().as_deref().map(str::to_string)),
7758 Some("fox jumps over\n".to_string())
7759 );
7760
7761 // Paste with three selections, noticing how the copied full-line selection is inserted
7762 // before the empty selections but replaces the selection that is non-empty.
7763 cx.set_state(indoc! {"
7764 Tˇhe quick brown
7765 «foˇ»x jumps over
7766 tˇhe lazy dog"});
7767 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7768 cx.assert_editor_state(indoc! {"
7769 fox jumps over
7770 Tˇhe quick brown
7771 fox jumps over
7772 ˇx jumps over
7773 fox jumps over
7774 tˇhe lazy dog"});
7775}
7776
7777#[gpui::test]
7778async fn test_copy_trim(cx: &mut TestAppContext) {
7779 init_test(cx, |_| {});
7780
7781 let mut cx = EditorTestContext::new(cx).await;
7782 cx.set_state(
7783 r#" «for selection in selections.iter() {
7784 let mut start = selection.start;
7785 let mut end = selection.end;
7786 let is_entire_line = selection.is_empty();
7787 if is_entire_line {
7788 start = Point::new(start.row, 0);ˇ»
7789 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7790 }
7791 "#,
7792 );
7793 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7794 assert_eq!(
7795 cx.read_from_clipboard()
7796 .and_then(|item| item.text().as_deref().map(str::to_string)),
7797 Some(
7798 "for selection in selections.iter() {
7799 let mut start = selection.start;
7800 let mut end = selection.end;
7801 let is_entire_line = selection.is_empty();
7802 if is_entire_line {
7803 start = Point::new(start.row, 0);"
7804 .to_string()
7805 ),
7806 "Regular copying preserves all indentation selected",
7807 );
7808 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7809 assert_eq!(
7810 cx.read_from_clipboard()
7811 .and_then(|item| item.text().as_deref().map(str::to_string)),
7812 Some(
7813 "for selection in selections.iter() {
7814let mut start = selection.start;
7815let mut end = selection.end;
7816let is_entire_line = selection.is_empty();
7817if is_entire_line {
7818 start = Point::new(start.row, 0);"
7819 .to_string()
7820 ),
7821 "Copying with stripping should strip all leading whitespaces"
7822 );
7823
7824 cx.set_state(
7825 r#" « for selection in selections.iter() {
7826 let mut start = selection.start;
7827 let mut end = selection.end;
7828 let is_entire_line = selection.is_empty();
7829 if is_entire_line {
7830 start = Point::new(start.row, 0);ˇ»
7831 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7832 }
7833 "#,
7834 );
7835 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7836 assert_eq!(
7837 cx.read_from_clipboard()
7838 .and_then(|item| item.text().as_deref().map(str::to_string)),
7839 Some(
7840 " for selection in selections.iter() {
7841 let mut start = selection.start;
7842 let mut end = selection.end;
7843 let is_entire_line = selection.is_empty();
7844 if is_entire_line {
7845 start = Point::new(start.row, 0);"
7846 .to_string()
7847 ),
7848 "Regular copying preserves all indentation selected",
7849 );
7850 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7851 assert_eq!(
7852 cx.read_from_clipboard()
7853 .and_then(|item| item.text().as_deref().map(str::to_string)),
7854 Some(
7855 "for selection in selections.iter() {
7856let mut start = selection.start;
7857let mut end = selection.end;
7858let is_entire_line = selection.is_empty();
7859if is_entire_line {
7860 start = Point::new(start.row, 0);"
7861 .to_string()
7862 ),
7863 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7864 );
7865
7866 cx.set_state(
7867 r#" «ˇ for selection in selections.iter() {
7868 let mut start = selection.start;
7869 let mut end = selection.end;
7870 let is_entire_line = selection.is_empty();
7871 if is_entire_line {
7872 start = Point::new(start.row, 0);»
7873 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7874 }
7875 "#,
7876 );
7877 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7878 assert_eq!(
7879 cx.read_from_clipboard()
7880 .and_then(|item| item.text().as_deref().map(str::to_string)),
7881 Some(
7882 " for selection in selections.iter() {
7883 let mut start = selection.start;
7884 let mut end = selection.end;
7885 let is_entire_line = selection.is_empty();
7886 if is_entire_line {
7887 start = Point::new(start.row, 0);"
7888 .to_string()
7889 ),
7890 "Regular copying for reverse selection works the same",
7891 );
7892 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7893 assert_eq!(
7894 cx.read_from_clipboard()
7895 .and_then(|item| item.text().as_deref().map(str::to_string)),
7896 Some(
7897 "for selection in selections.iter() {
7898let mut start = selection.start;
7899let mut end = selection.end;
7900let is_entire_line = selection.is_empty();
7901if is_entire_line {
7902 start = Point::new(start.row, 0);"
7903 .to_string()
7904 ),
7905 "Copying with stripping for reverse selection works the same"
7906 );
7907
7908 cx.set_state(
7909 r#" for selection «in selections.iter() {
7910 let mut start = selection.start;
7911 let mut end = selection.end;
7912 let is_entire_line = selection.is_empty();
7913 if is_entire_line {
7914 start = Point::new(start.row, 0);ˇ»
7915 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7916 }
7917 "#,
7918 );
7919 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7920 assert_eq!(
7921 cx.read_from_clipboard()
7922 .and_then(|item| item.text().as_deref().map(str::to_string)),
7923 Some(
7924 "in selections.iter() {
7925 let mut start = selection.start;
7926 let mut end = selection.end;
7927 let is_entire_line = selection.is_empty();
7928 if is_entire_line {
7929 start = Point::new(start.row, 0);"
7930 .to_string()
7931 ),
7932 "When selecting past the indent, the copying works as usual",
7933 );
7934 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7935 assert_eq!(
7936 cx.read_from_clipboard()
7937 .and_then(|item| item.text().as_deref().map(str::to_string)),
7938 Some(
7939 "in selections.iter() {
7940 let mut start = selection.start;
7941 let mut end = selection.end;
7942 let is_entire_line = selection.is_empty();
7943 if is_entire_line {
7944 start = Point::new(start.row, 0);"
7945 .to_string()
7946 ),
7947 "When selecting past the indent, nothing is trimmed"
7948 );
7949
7950 cx.set_state(
7951 r#" «for selection in selections.iter() {
7952 let mut start = selection.start;
7953
7954 let mut end = selection.end;
7955 let is_entire_line = selection.is_empty();
7956 if is_entire_line {
7957 start = Point::new(start.row, 0);
7958ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7959 }
7960 "#,
7961 );
7962 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7963 assert_eq!(
7964 cx.read_from_clipboard()
7965 .and_then(|item| item.text().as_deref().map(str::to_string)),
7966 Some(
7967 "for selection in selections.iter() {
7968let mut start = selection.start;
7969
7970let mut end = selection.end;
7971let is_entire_line = selection.is_empty();
7972if is_entire_line {
7973 start = Point::new(start.row, 0);
7974"
7975 .to_string()
7976 ),
7977 "Copying with stripping should ignore empty lines"
7978 );
7979}
7980
7981#[gpui::test]
7982async fn test_copy_trim_line_mode(cx: &mut TestAppContext) {
7983 init_test(cx, |_| {});
7984
7985 let mut cx = EditorTestContext::new(cx).await;
7986
7987 cx.set_state(indoc! {"
7988 « a
7989 bˇ»
7990 "});
7991 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
7992 cx.update_editor(|editor, window, cx| editor.copy_and_trim(&CopyAndTrim, window, cx));
7993
7994 assert_eq!(
7995 cx.read_from_clipboard().and_then(|item| item.text()),
7996 Some("a\nb\n".to_string())
7997 );
7998}
7999
8000#[gpui::test]
8001async fn test_clipboard_line_numbers_from_multibuffer(cx: &mut TestAppContext) {
8002 init_test(cx, |_| {});
8003
8004 let fs = FakeFs::new(cx.executor());
8005 fs.insert_file(
8006 path!("/file.txt"),
8007 "first line\nsecond line\nthird line\nfourth line\nfifth line\n".into(),
8008 )
8009 .await;
8010
8011 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
8012
8013 let buffer = project
8014 .update(cx, |project, cx| {
8015 project.open_local_buffer(path!("/file.txt"), cx)
8016 })
8017 .await
8018 .unwrap();
8019
8020 let multibuffer = cx.new(|cx| {
8021 let mut multibuffer = MultiBuffer::new(ReadWrite);
8022 multibuffer.push_excerpts(
8023 buffer.clone(),
8024 [ExcerptRange::new(Point::new(2, 0)..Point::new(5, 0))],
8025 cx,
8026 );
8027 multibuffer
8028 });
8029
8030 let (editor, cx) = cx.add_window_view(|window, cx| {
8031 build_editor_with_project(project.clone(), multibuffer, window, cx)
8032 });
8033
8034 editor.update_in(cx, |editor, window, cx| {
8035 assert_eq!(editor.text(cx), "third line\nfourth line\nfifth line\n");
8036
8037 editor.select_all(&SelectAll, window, cx);
8038 editor.copy(&Copy, window, cx);
8039 });
8040
8041 let clipboard_selections: Option<Vec<ClipboardSelection>> = cx
8042 .read_from_clipboard()
8043 .and_then(|item| item.entries().first().cloned())
8044 .and_then(|entry| match entry {
8045 gpui::ClipboardEntry::String(text) => text.metadata_json(),
8046 _ => None,
8047 });
8048
8049 let selections = clipboard_selections.expect("should have clipboard selections");
8050 assert_eq!(selections.len(), 1);
8051 let selection = &selections[0];
8052 assert_eq!(
8053 selection.line_range,
8054 Some(2..=5),
8055 "line range should be from original file (rows 2-5), not multibuffer rows (0-2)"
8056 );
8057}
8058
8059#[gpui::test]
8060async fn test_paste_multiline(cx: &mut TestAppContext) {
8061 init_test(cx, |_| {});
8062
8063 let mut cx = EditorTestContext::new(cx).await;
8064 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8065
8066 // Cut an indented block, without the leading whitespace.
8067 cx.set_state(indoc! {"
8068 const a: B = (
8069 c(),
8070 «d(
8071 e,
8072 f
8073 )ˇ»
8074 );
8075 "});
8076 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
8077 cx.assert_editor_state(indoc! {"
8078 const a: B = (
8079 c(),
8080 ˇ
8081 );
8082 "});
8083
8084 // Paste it at the same position.
8085 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8086 cx.assert_editor_state(indoc! {"
8087 const a: B = (
8088 c(),
8089 d(
8090 e,
8091 f
8092 )ˇ
8093 );
8094 "});
8095
8096 // Paste it at a line with a lower indent level.
8097 cx.set_state(indoc! {"
8098 ˇ
8099 const a: B = (
8100 c(),
8101 );
8102 "});
8103 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8104 cx.assert_editor_state(indoc! {"
8105 d(
8106 e,
8107 f
8108 )ˇ
8109 const a: B = (
8110 c(),
8111 );
8112 "});
8113
8114 // Cut an indented block, with the leading whitespace.
8115 cx.set_state(indoc! {"
8116 const a: B = (
8117 c(),
8118 « d(
8119 e,
8120 f
8121 )
8122 ˇ»);
8123 "});
8124 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
8125 cx.assert_editor_state(indoc! {"
8126 const a: B = (
8127 c(),
8128 ˇ);
8129 "});
8130
8131 // Paste it at the same position.
8132 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8133 cx.assert_editor_state(indoc! {"
8134 const a: B = (
8135 c(),
8136 d(
8137 e,
8138 f
8139 )
8140 ˇ);
8141 "});
8142
8143 // Paste it at a line with a higher indent level.
8144 cx.set_state(indoc! {"
8145 const a: B = (
8146 c(),
8147 d(
8148 e,
8149 fˇ
8150 )
8151 );
8152 "});
8153 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8154 cx.assert_editor_state(indoc! {"
8155 const a: B = (
8156 c(),
8157 d(
8158 e,
8159 f d(
8160 e,
8161 f
8162 )
8163 ˇ
8164 )
8165 );
8166 "});
8167
8168 // Copy an indented block, starting mid-line
8169 cx.set_state(indoc! {"
8170 const a: B = (
8171 c(),
8172 somethin«g(
8173 e,
8174 f
8175 )ˇ»
8176 );
8177 "});
8178 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
8179
8180 // Paste it on a line with a lower indent level
8181 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
8182 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8183 cx.assert_editor_state(indoc! {"
8184 const a: B = (
8185 c(),
8186 something(
8187 e,
8188 f
8189 )
8190 );
8191 g(
8192 e,
8193 f
8194 )ˇ"});
8195}
8196
8197#[gpui::test]
8198async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
8199 init_test(cx, |_| {});
8200
8201 cx.write_to_clipboard(ClipboardItem::new_string(
8202 " d(\n e\n );\n".into(),
8203 ));
8204
8205 let mut cx = EditorTestContext::new(cx).await;
8206 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8207 cx.run_until_parked();
8208
8209 cx.set_state(indoc! {"
8210 fn a() {
8211 b();
8212 if c() {
8213 ˇ
8214 }
8215 }
8216 "});
8217
8218 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8219 cx.assert_editor_state(indoc! {"
8220 fn a() {
8221 b();
8222 if c() {
8223 d(
8224 e
8225 );
8226 ˇ
8227 }
8228 }
8229 "});
8230
8231 cx.set_state(indoc! {"
8232 fn a() {
8233 b();
8234 ˇ
8235 }
8236 "});
8237
8238 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
8239 cx.assert_editor_state(indoc! {"
8240 fn a() {
8241 b();
8242 d(
8243 e
8244 );
8245 ˇ
8246 }
8247 "});
8248}
8249
8250#[gpui::test]
8251fn test_select_all(cx: &mut TestAppContext) {
8252 init_test(cx, |_| {});
8253
8254 let editor = cx.add_window(|window, cx| {
8255 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
8256 build_editor(buffer, window, cx)
8257 });
8258 _ = editor.update(cx, |editor, window, cx| {
8259 editor.select_all(&SelectAll, window, cx);
8260 assert_eq!(
8261 display_ranges(editor, cx),
8262 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
8263 );
8264 });
8265}
8266
8267#[gpui::test]
8268fn test_select_line(cx: &mut TestAppContext) {
8269 init_test(cx, |_| {});
8270
8271 let editor = cx.add_window(|window, cx| {
8272 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
8273 build_editor(buffer, window, cx)
8274 });
8275 _ = editor.update(cx, |editor, window, cx| {
8276 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8277 s.select_display_ranges([
8278 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8279 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
8280 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
8281 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
8282 ])
8283 });
8284 editor.select_line(&SelectLine, window, cx);
8285 // Adjacent line selections should NOT merge (only overlapping ones do)
8286 assert_eq!(
8287 display_ranges(editor, cx),
8288 vec![
8289 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0),
8290 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(2), 0),
8291 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
8292 ]
8293 );
8294 });
8295
8296 _ = editor.update(cx, |editor, window, cx| {
8297 editor.select_line(&SelectLine, window, cx);
8298 assert_eq!(
8299 display_ranges(editor, cx),
8300 vec![
8301 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
8302 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
8303 ]
8304 );
8305 });
8306
8307 _ = editor.update(cx, |editor, window, cx| {
8308 editor.select_line(&SelectLine, window, cx);
8309 // Adjacent but not overlapping, so they stay separate
8310 assert_eq!(
8311 display_ranges(editor, cx),
8312 vec![
8313 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(4), 0),
8314 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
8315 ]
8316 );
8317 });
8318}
8319
8320#[gpui::test]
8321async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
8322 init_test(cx, |_| {});
8323 let mut cx = EditorTestContext::new(cx).await;
8324
8325 #[track_caller]
8326 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
8327 cx.set_state(initial_state);
8328 cx.update_editor(|e, window, cx| {
8329 e.split_selection_into_lines(&Default::default(), window, cx)
8330 });
8331 cx.assert_editor_state(expected_state);
8332 }
8333
8334 // Selection starts and ends at the middle of lines, left-to-right
8335 test(
8336 &mut cx,
8337 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
8338 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
8339 );
8340 // Same thing, right-to-left
8341 test(
8342 &mut cx,
8343 "aa\nb«b\ncc\ndd\neˇ»e\nff",
8344 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
8345 );
8346
8347 // Whole buffer, left-to-right, last line *doesn't* end with newline
8348 test(
8349 &mut cx,
8350 "«ˇaa\nbb\ncc\ndd\nee\nff»",
8351 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
8352 );
8353 // Same thing, right-to-left
8354 test(
8355 &mut cx,
8356 "«aa\nbb\ncc\ndd\nee\nffˇ»",
8357 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
8358 );
8359
8360 // Whole buffer, left-to-right, last line ends with newline
8361 test(
8362 &mut cx,
8363 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
8364 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
8365 );
8366 // Same thing, right-to-left
8367 test(
8368 &mut cx,
8369 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
8370 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
8371 );
8372
8373 // Starts at the end of a line, ends at the start of another
8374 test(
8375 &mut cx,
8376 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
8377 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
8378 );
8379}
8380
8381#[gpui::test]
8382async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
8383 init_test(cx, |_| {});
8384
8385 let editor = cx.add_window(|window, cx| {
8386 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
8387 build_editor(buffer, window, cx)
8388 });
8389
8390 // setup
8391 _ = editor.update(cx, |editor, window, cx| {
8392 editor.fold_creases(
8393 vec![
8394 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
8395 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
8396 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
8397 ],
8398 true,
8399 window,
8400 cx,
8401 );
8402 assert_eq!(
8403 editor.display_text(cx),
8404 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
8405 );
8406 });
8407
8408 _ = editor.update(cx, |editor, window, cx| {
8409 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8410 s.select_display_ranges([
8411 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8412 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
8413 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
8414 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
8415 ])
8416 });
8417 editor.split_selection_into_lines(&Default::default(), window, cx);
8418 assert_eq!(
8419 editor.display_text(cx),
8420 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
8421 );
8422 });
8423 EditorTestContext::for_editor(editor, cx)
8424 .await
8425 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
8426
8427 _ = editor.update(cx, |editor, window, cx| {
8428 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8429 s.select_display_ranges([
8430 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
8431 ])
8432 });
8433 editor.split_selection_into_lines(&Default::default(), window, cx);
8434 assert_eq!(
8435 editor.display_text(cx),
8436 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
8437 );
8438 assert_eq!(
8439 display_ranges(editor, cx),
8440 [
8441 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
8442 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
8443 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
8444 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
8445 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
8446 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
8447 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
8448 ]
8449 );
8450 });
8451 EditorTestContext::for_editor(editor, cx)
8452 .await
8453 .assert_editor_state(
8454 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
8455 );
8456}
8457
8458#[gpui::test]
8459async fn test_add_selection_above_below(cx: &mut TestAppContext) {
8460 init_test(cx, |_| {});
8461
8462 let mut cx = EditorTestContext::new(cx).await;
8463
8464 cx.set_state(indoc!(
8465 r#"abc
8466 defˇghi
8467
8468 jk
8469 nlmo
8470 "#
8471 ));
8472
8473 cx.update_editor(|editor, window, cx| {
8474 editor.add_selection_above(&Default::default(), window, cx);
8475 });
8476
8477 cx.assert_editor_state(indoc!(
8478 r#"abcˇ
8479 defˇghi
8480
8481 jk
8482 nlmo
8483 "#
8484 ));
8485
8486 cx.update_editor(|editor, window, cx| {
8487 editor.add_selection_above(&Default::default(), window, cx);
8488 });
8489
8490 cx.assert_editor_state(indoc!(
8491 r#"abcˇ
8492 defˇghi
8493
8494 jk
8495 nlmo
8496 "#
8497 ));
8498
8499 cx.update_editor(|editor, window, cx| {
8500 editor.add_selection_below(&Default::default(), window, cx);
8501 });
8502
8503 cx.assert_editor_state(indoc!(
8504 r#"abc
8505 defˇghi
8506
8507 jk
8508 nlmo
8509 "#
8510 ));
8511
8512 cx.update_editor(|editor, window, cx| {
8513 editor.undo_selection(&Default::default(), window, cx);
8514 });
8515
8516 cx.assert_editor_state(indoc!(
8517 r#"abcˇ
8518 defˇghi
8519
8520 jk
8521 nlmo
8522 "#
8523 ));
8524
8525 cx.update_editor(|editor, window, cx| {
8526 editor.redo_selection(&Default::default(), window, cx);
8527 });
8528
8529 cx.assert_editor_state(indoc!(
8530 r#"abc
8531 defˇghi
8532
8533 jk
8534 nlmo
8535 "#
8536 ));
8537
8538 cx.update_editor(|editor, window, cx| {
8539 editor.add_selection_below(&Default::default(), window, cx);
8540 });
8541
8542 cx.assert_editor_state(indoc!(
8543 r#"abc
8544 defˇghi
8545 ˇ
8546 jk
8547 nlmo
8548 "#
8549 ));
8550
8551 cx.update_editor(|editor, window, cx| {
8552 editor.add_selection_below(&Default::default(), window, cx);
8553 });
8554
8555 cx.assert_editor_state(indoc!(
8556 r#"abc
8557 defˇghi
8558 ˇ
8559 jkˇ
8560 nlmo
8561 "#
8562 ));
8563
8564 cx.update_editor(|editor, window, cx| {
8565 editor.add_selection_below(&Default::default(), window, cx);
8566 });
8567
8568 cx.assert_editor_state(indoc!(
8569 r#"abc
8570 defˇghi
8571 ˇ
8572 jkˇ
8573 nlmˇo
8574 "#
8575 ));
8576
8577 cx.update_editor(|editor, window, cx| {
8578 editor.add_selection_below(&Default::default(), window, cx);
8579 });
8580
8581 cx.assert_editor_state(indoc!(
8582 r#"abc
8583 defˇghi
8584 ˇ
8585 jkˇ
8586 nlmˇo
8587 ˇ"#
8588 ));
8589
8590 // change selections
8591 cx.set_state(indoc!(
8592 r#"abc
8593 def«ˇg»hi
8594
8595 jk
8596 nlmo
8597 "#
8598 ));
8599
8600 cx.update_editor(|editor, window, cx| {
8601 editor.add_selection_below(&Default::default(), window, cx);
8602 });
8603
8604 cx.assert_editor_state(indoc!(
8605 r#"abc
8606 def«ˇg»hi
8607
8608 jk
8609 nlm«ˇo»
8610 "#
8611 ));
8612
8613 cx.update_editor(|editor, window, cx| {
8614 editor.add_selection_below(&Default::default(), window, cx);
8615 });
8616
8617 cx.assert_editor_state(indoc!(
8618 r#"abc
8619 def«ˇg»hi
8620
8621 jk
8622 nlm«ˇo»
8623 "#
8624 ));
8625
8626 cx.update_editor(|editor, window, cx| {
8627 editor.add_selection_above(&Default::default(), window, cx);
8628 });
8629
8630 cx.assert_editor_state(indoc!(
8631 r#"abc
8632 def«ˇg»hi
8633
8634 jk
8635 nlmo
8636 "#
8637 ));
8638
8639 cx.update_editor(|editor, window, cx| {
8640 editor.add_selection_above(&Default::default(), window, cx);
8641 });
8642
8643 cx.assert_editor_state(indoc!(
8644 r#"abc
8645 def«ˇg»hi
8646
8647 jk
8648 nlmo
8649 "#
8650 ));
8651
8652 // Change selections again
8653 cx.set_state(indoc!(
8654 r#"a«bc
8655 defgˇ»hi
8656
8657 jk
8658 nlmo
8659 "#
8660 ));
8661
8662 cx.update_editor(|editor, window, cx| {
8663 editor.add_selection_below(&Default::default(), window, cx);
8664 });
8665
8666 cx.assert_editor_state(indoc!(
8667 r#"a«bcˇ»
8668 d«efgˇ»hi
8669
8670 j«kˇ»
8671 nlmo
8672 "#
8673 ));
8674
8675 cx.update_editor(|editor, window, cx| {
8676 editor.add_selection_below(&Default::default(), window, cx);
8677 });
8678 cx.assert_editor_state(indoc!(
8679 r#"a«bcˇ»
8680 d«efgˇ»hi
8681
8682 j«kˇ»
8683 n«lmoˇ»
8684 "#
8685 ));
8686 cx.update_editor(|editor, window, cx| {
8687 editor.add_selection_above(&Default::default(), window, cx);
8688 });
8689
8690 cx.assert_editor_state(indoc!(
8691 r#"a«bcˇ»
8692 d«efgˇ»hi
8693
8694 j«kˇ»
8695 nlmo
8696 "#
8697 ));
8698
8699 // Change selections again
8700 cx.set_state(indoc!(
8701 r#"abc
8702 d«ˇefghi
8703
8704 jk
8705 nlm»o
8706 "#
8707 ));
8708
8709 cx.update_editor(|editor, window, cx| {
8710 editor.add_selection_above(&Default::default(), window, cx);
8711 });
8712
8713 cx.assert_editor_state(indoc!(
8714 r#"a«ˇbc»
8715 d«ˇef»ghi
8716
8717 j«ˇk»
8718 n«ˇlm»o
8719 "#
8720 ));
8721
8722 cx.update_editor(|editor, window, cx| {
8723 editor.add_selection_below(&Default::default(), window, cx);
8724 });
8725
8726 cx.assert_editor_state(indoc!(
8727 r#"abc
8728 d«ˇef»ghi
8729
8730 j«ˇk»
8731 n«ˇlm»o
8732 "#
8733 ));
8734
8735 // Assert that the oldest selection's goal column is used when adding more
8736 // selections, not the most recently added selection's actual column.
8737 cx.set_state(indoc! {"
8738 foo bar bazˇ
8739 foo
8740 foo bar
8741 "});
8742
8743 cx.update_editor(|editor, window, cx| {
8744 editor.add_selection_below(
8745 &AddSelectionBelow {
8746 skip_soft_wrap: true,
8747 },
8748 window,
8749 cx,
8750 );
8751 });
8752
8753 cx.assert_editor_state(indoc! {"
8754 foo bar bazˇ
8755 fooˇ
8756 foo bar
8757 "});
8758
8759 cx.update_editor(|editor, window, cx| {
8760 editor.add_selection_below(
8761 &AddSelectionBelow {
8762 skip_soft_wrap: true,
8763 },
8764 window,
8765 cx,
8766 );
8767 });
8768
8769 cx.assert_editor_state(indoc! {"
8770 foo bar bazˇ
8771 fooˇ
8772 foo barˇ
8773 "});
8774
8775 cx.set_state(indoc! {"
8776 foo bar baz
8777 foo
8778 foo barˇ
8779 "});
8780
8781 cx.update_editor(|editor, window, cx| {
8782 editor.add_selection_above(
8783 &AddSelectionAbove {
8784 skip_soft_wrap: true,
8785 },
8786 window,
8787 cx,
8788 );
8789 });
8790
8791 cx.assert_editor_state(indoc! {"
8792 foo bar baz
8793 fooˇ
8794 foo barˇ
8795 "});
8796
8797 cx.update_editor(|editor, window, cx| {
8798 editor.add_selection_above(
8799 &AddSelectionAbove {
8800 skip_soft_wrap: true,
8801 },
8802 window,
8803 cx,
8804 );
8805 });
8806
8807 cx.assert_editor_state(indoc! {"
8808 foo barˇ baz
8809 fooˇ
8810 foo barˇ
8811 "});
8812}
8813
8814#[gpui::test]
8815async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8816 init_test(cx, |_| {});
8817 let mut cx = EditorTestContext::new(cx).await;
8818
8819 cx.set_state(indoc!(
8820 r#"line onˇe
8821 liˇne two
8822 line three
8823 line four"#
8824 ));
8825
8826 cx.update_editor(|editor, window, cx| {
8827 editor.add_selection_below(&Default::default(), window, cx);
8828 });
8829
8830 // test multiple cursors expand in the same direction
8831 cx.assert_editor_state(indoc!(
8832 r#"line onˇe
8833 liˇne twˇo
8834 liˇne three
8835 line four"#
8836 ));
8837
8838 cx.update_editor(|editor, window, cx| {
8839 editor.add_selection_below(&Default::default(), window, cx);
8840 });
8841
8842 cx.update_editor(|editor, window, cx| {
8843 editor.add_selection_below(&Default::default(), window, cx);
8844 });
8845
8846 // test multiple cursors expand below overflow
8847 cx.assert_editor_state(indoc!(
8848 r#"line onˇe
8849 liˇne twˇo
8850 liˇne thˇree
8851 liˇne foˇur"#
8852 ));
8853
8854 cx.update_editor(|editor, window, cx| {
8855 editor.add_selection_above(&Default::default(), window, cx);
8856 });
8857
8858 // test multiple cursors retrieves back correctly
8859 cx.assert_editor_state(indoc!(
8860 r#"line onˇe
8861 liˇne twˇo
8862 liˇne thˇree
8863 line four"#
8864 ));
8865
8866 cx.update_editor(|editor, window, cx| {
8867 editor.add_selection_above(&Default::default(), window, cx);
8868 });
8869
8870 cx.update_editor(|editor, window, cx| {
8871 editor.add_selection_above(&Default::default(), window, cx);
8872 });
8873
8874 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8875 cx.assert_editor_state(indoc!(
8876 r#"liˇne onˇe
8877 liˇne two
8878 line three
8879 line four"#
8880 ));
8881
8882 cx.update_editor(|editor, window, cx| {
8883 editor.undo_selection(&Default::default(), window, cx);
8884 });
8885
8886 // test undo
8887 cx.assert_editor_state(indoc!(
8888 r#"line onˇe
8889 liˇne twˇo
8890 line three
8891 line four"#
8892 ));
8893
8894 cx.update_editor(|editor, window, cx| {
8895 editor.redo_selection(&Default::default(), window, cx);
8896 });
8897
8898 // test redo
8899 cx.assert_editor_state(indoc!(
8900 r#"liˇne onˇe
8901 liˇne two
8902 line three
8903 line four"#
8904 ));
8905
8906 cx.set_state(indoc!(
8907 r#"abcd
8908 ef«ghˇ»
8909 ijkl
8910 «mˇ»nop"#
8911 ));
8912
8913 cx.update_editor(|editor, window, cx| {
8914 editor.add_selection_above(&Default::default(), window, cx);
8915 });
8916
8917 // test multiple selections expand in the same direction
8918 cx.assert_editor_state(indoc!(
8919 r#"ab«cdˇ»
8920 ef«ghˇ»
8921 «iˇ»jkl
8922 «mˇ»nop"#
8923 ));
8924
8925 cx.update_editor(|editor, window, cx| {
8926 editor.add_selection_above(&Default::default(), window, cx);
8927 });
8928
8929 // test multiple selection upward overflow
8930 cx.assert_editor_state(indoc!(
8931 r#"ab«cdˇ»
8932 «eˇ»f«ghˇ»
8933 «iˇ»jkl
8934 «mˇ»nop"#
8935 ));
8936
8937 cx.update_editor(|editor, window, cx| {
8938 editor.add_selection_below(&Default::default(), window, cx);
8939 });
8940
8941 // test multiple selection retrieves back correctly
8942 cx.assert_editor_state(indoc!(
8943 r#"abcd
8944 ef«ghˇ»
8945 «iˇ»jkl
8946 «mˇ»nop"#
8947 ));
8948
8949 cx.update_editor(|editor, window, cx| {
8950 editor.add_selection_below(&Default::default(), window, cx);
8951 });
8952
8953 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8954 cx.assert_editor_state(indoc!(
8955 r#"abcd
8956 ef«ghˇ»
8957 ij«klˇ»
8958 «mˇ»nop"#
8959 ));
8960
8961 cx.update_editor(|editor, window, cx| {
8962 editor.undo_selection(&Default::default(), window, cx);
8963 });
8964
8965 // test undo
8966 cx.assert_editor_state(indoc!(
8967 r#"abcd
8968 ef«ghˇ»
8969 «iˇ»jkl
8970 «mˇ»nop"#
8971 ));
8972
8973 cx.update_editor(|editor, window, cx| {
8974 editor.redo_selection(&Default::default(), window, cx);
8975 });
8976
8977 // test redo
8978 cx.assert_editor_state(indoc!(
8979 r#"abcd
8980 ef«ghˇ»
8981 ij«klˇ»
8982 «mˇ»nop"#
8983 ));
8984}
8985
8986#[gpui::test]
8987async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8988 init_test(cx, |_| {});
8989 let mut cx = EditorTestContext::new(cx).await;
8990
8991 cx.set_state(indoc!(
8992 r#"line onˇe
8993 liˇne two
8994 line three
8995 line four"#
8996 ));
8997
8998 cx.update_editor(|editor, window, cx| {
8999 editor.add_selection_below(&Default::default(), window, cx);
9000 editor.add_selection_below(&Default::default(), window, cx);
9001 editor.add_selection_below(&Default::default(), window, cx);
9002 });
9003
9004 // initial state with two multi cursor groups
9005 cx.assert_editor_state(indoc!(
9006 r#"line onˇe
9007 liˇne twˇo
9008 liˇne thˇree
9009 liˇne foˇur"#
9010 ));
9011
9012 // add single cursor in middle - simulate opt click
9013 cx.update_editor(|editor, window, cx| {
9014 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
9015 editor.begin_selection(new_cursor_point, true, 1, window, cx);
9016 editor.end_selection(window, cx);
9017 });
9018
9019 cx.assert_editor_state(indoc!(
9020 r#"line onˇe
9021 liˇne twˇo
9022 liˇneˇ thˇree
9023 liˇne foˇur"#
9024 ));
9025
9026 cx.update_editor(|editor, window, cx| {
9027 editor.add_selection_above(&Default::default(), window, cx);
9028 });
9029
9030 // test new added selection expands above and existing selection shrinks
9031 cx.assert_editor_state(indoc!(
9032 r#"line onˇe
9033 liˇneˇ twˇo
9034 liˇneˇ thˇree
9035 line four"#
9036 ));
9037
9038 cx.update_editor(|editor, window, cx| {
9039 editor.add_selection_above(&Default::default(), window, cx);
9040 });
9041
9042 // test new added selection expands above and existing selection shrinks
9043 cx.assert_editor_state(indoc!(
9044 r#"lineˇ onˇe
9045 liˇneˇ twˇo
9046 lineˇ three
9047 line four"#
9048 ));
9049
9050 // intial state with two selection groups
9051 cx.set_state(indoc!(
9052 r#"abcd
9053 ef«ghˇ»
9054 ijkl
9055 «mˇ»nop"#
9056 ));
9057
9058 cx.update_editor(|editor, window, cx| {
9059 editor.add_selection_above(&Default::default(), window, cx);
9060 editor.add_selection_above(&Default::default(), window, cx);
9061 });
9062
9063 cx.assert_editor_state(indoc!(
9064 r#"ab«cdˇ»
9065 «eˇ»f«ghˇ»
9066 «iˇ»jkl
9067 «mˇ»nop"#
9068 ));
9069
9070 // add single selection in middle - simulate opt drag
9071 cx.update_editor(|editor, window, cx| {
9072 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
9073 editor.begin_selection(new_cursor_point, true, 1, window, cx);
9074 editor.update_selection(
9075 DisplayPoint::new(DisplayRow(2), 4),
9076 0,
9077 gpui::Point::<f32>::default(),
9078 window,
9079 cx,
9080 );
9081 editor.end_selection(window, cx);
9082 });
9083
9084 cx.assert_editor_state(indoc!(
9085 r#"ab«cdˇ»
9086 «eˇ»f«ghˇ»
9087 «iˇ»jk«lˇ»
9088 «mˇ»nop"#
9089 ));
9090
9091 cx.update_editor(|editor, window, cx| {
9092 editor.add_selection_below(&Default::default(), window, cx);
9093 });
9094
9095 // test new added selection expands below, others shrinks from above
9096 cx.assert_editor_state(indoc!(
9097 r#"abcd
9098 ef«ghˇ»
9099 «iˇ»jk«lˇ»
9100 «mˇ»no«pˇ»"#
9101 ));
9102}
9103
9104#[gpui::test]
9105async fn test_select_next(cx: &mut TestAppContext) {
9106 init_test(cx, |_| {});
9107 let mut cx = EditorTestContext::new(cx).await;
9108
9109 // Enable case sensitive search.
9110 update_test_editor_settings(&mut cx, |settings| {
9111 let mut search_settings = SearchSettingsContent::default();
9112 search_settings.case_sensitive = Some(true);
9113 settings.search = Some(search_settings);
9114 });
9115
9116 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
9117
9118 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9119 .unwrap();
9120 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9121
9122 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9123 .unwrap();
9124 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
9125
9126 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9127 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9128
9129 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9130 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
9131
9132 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9133 .unwrap();
9134 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
9135
9136 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9137 .unwrap();
9138 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
9139
9140 // Test selection direction should be preserved
9141 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
9142
9143 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9144 .unwrap();
9145 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
9146
9147 // Test case sensitivity
9148 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
9149 cx.update_editor(|e, window, cx| {
9150 e.select_next(&SelectNext::default(), window, cx).unwrap();
9151 });
9152 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
9153
9154 // Disable case sensitive search.
9155 update_test_editor_settings(&mut cx, |settings| {
9156 let mut search_settings = SearchSettingsContent::default();
9157 search_settings.case_sensitive = Some(false);
9158 settings.search = Some(search_settings);
9159 });
9160
9161 cx.set_state("«ˇfoo»\nFOO\nFoo");
9162 cx.update_editor(|e, window, cx| {
9163 e.select_next(&SelectNext::default(), window, cx).unwrap();
9164 e.select_next(&SelectNext::default(), window, cx).unwrap();
9165 });
9166 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
9167}
9168
9169#[gpui::test]
9170async fn test_select_all_matches(cx: &mut TestAppContext) {
9171 init_test(cx, |_| {});
9172 let mut cx = EditorTestContext::new(cx).await;
9173
9174 // Enable case sensitive search.
9175 update_test_editor_settings(&mut cx, |settings| {
9176 let mut search_settings = SearchSettingsContent::default();
9177 search_settings.case_sensitive = Some(true);
9178 settings.search = Some(search_settings);
9179 });
9180
9181 // Test caret-only selections
9182 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
9183 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9184 .unwrap();
9185 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
9186
9187 // Test left-to-right selections
9188 cx.set_state("abc\n«abcˇ»\nabc");
9189 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9190 .unwrap();
9191 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
9192
9193 // Test right-to-left selections
9194 cx.set_state("abc\n«ˇabc»\nabc");
9195 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9196 .unwrap();
9197 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
9198
9199 // Test selecting whitespace with caret selection
9200 cx.set_state("abc\nˇ abc\nabc");
9201 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9202 .unwrap();
9203 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
9204
9205 // Test selecting whitespace with left-to-right selection
9206 cx.set_state("abc\n«ˇ »abc\nabc");
9207 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9208 .unwrap();
9209 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
9210
9211 // Test no matches with right-to-left selection
9212 cx.set_state("abc\n« ˇ»abc\nabc");
9213 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9214 .unwrap();
9215 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
9216
9217 // Test with a single word and clip_at_line_ends=true (#29823)
9218 cx.set_state("aˇbc");
9219 cx.update_editor(|e, window, cx| {
9220 e.set_clip_at_line_ends(true, cx);
9221 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
9222 e.set_clip_at_line_ends(false, cx);
9223 });
9224 cx.assert_editor_state("«abcˇ»");
9225
9226 // Test case sensitivity
9227 cx.set_state("fˇoo\nFOO\nFoo");
9228 cx.update_editor(|e, window, cx| {
9229 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
9230 });
9231 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
9232
9233 // Disable case sensitive search.
9234 update_test_editor_settings(&mut cx, |settings| {
9235 let mut search_settings = SearchSettingsContent::default();
9236 search_settings.case_sensitive = Some(false);
9237 settings.search = Some(search_settings);
9238 });
9239
9240 cx.set_state("fˇoo\nFOO\nFoo");
9241 cx.update_editor(|e, window, cx| {
9242 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
9243 });
9244 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
9245}
9246
9247#[gpui::test]
9248async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
9249 init_test(cx, |_| {});
9250
9251 let mut cx = EditorTestContext::new(cx).await;
9252
9253 let large_body_1 = "\nd".repeat(200);
9254 let large_body_2 = "\ne".repeat(200);
9255
9256 cx.set_state(&format!(
9257 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
9258 ));
9259 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
9260 let scroll_position = editor.scroll_position(cx);
9261 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
9262 scroll_position
9263 });
9264
9265 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
9266 .unwrap();
9267 cx.assert_editor_state(&format!(
9268 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
9269 ));
9270 let scroll_position_after_selection =
9271 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
9272 assert_eq!(
9273 initial_scroll_position, scroll_position_after_selection,
9274 "Scroll position should not change after selecting all matches"
9275 );
9276}
9277
9278#[gpui::test]
9279async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
9280 init_test(cx, |_| {});
9281
9282 let mut cx = EditorLspTestContext::new_rust(
9283 lsp::ServerCapabilities {
9284 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9285 ..Default::default()
9286 },
9287 cx,
9288 )
9289 .await;
9290
9291 cx.set_state(indoc! {"
9292 line 1
9293 line 2
9294 linˇe 3
9295 line 4
9296 line 5
9297 "});
9298
9299 // Make an edit
9300 cx.update_editor(|editor, window, cx| {
9301 editor.handle_input("X", window, cx);
9302 });
9303
9304 // Move cursor to a different position
9305 cx.update_editor(|editor, window, cx| {
9306 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9307 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
9308 });
9309 });
9310
9311 cx.assert_editor_state(indoc! {"
9312 line 1
9313 line 2
9314 linXe 3
9315 line 4
9316 liˇne 5
9317 "});
9318
9319 cx.lsp
9320 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
9321 Ok(Some(vec![lsp::TextEdit::new(
9322 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9323 "PREFIX ".to_string(),
9324 )]))
9325 });
9326
9327 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
9328 .unwrap()
9329 .await
9330 .unwrap();
9331
9332 cx.assert_editor_state(indoc! {"
9333 PREFIX line 1
9334 line 2
9335 linXe 3
9336 line 4
9337 liˇne 5
9338 "});
9339
9340 // Undo formatting
9341 cx.update_editor(|editor, window, cx| {
9342 editor.undo(&Default::default(), window, cx);
9343 });
9344
9345 // Verify cursor moved back to position after edit
9346 cx.assert_editor_state(indoc! {"
9347 line 1
9348 line 2
9349 linXˇe 3
9350 line 4
9351 line 5
9352 "});
9353}
9354
9355#[gpui::test]
9356async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
9357 init_test(cx, |_| {});
9358
9359 let mut cx = EditorTestContext::new(cx).await;
9360
9361 let provider = cx.new(|_| FakeEditPredictionDelegate::default());
9362 cx.update_editor(|editor, window, cx| {
9363 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
9364 });
9365
9366 cx.set_state(indoc! {"
9367 line 1
9368 line 2
9369 linˇe 3
9370 line 4
9371 line 5
9372 line 6
9373 line 7
9374 line 8
9375 line 9
9376 line 10
9377 "});
9378
9379 let snapshot = cx.buffer_snapshot();
9380 let edit_position = snapshot.anchor_after(Point::new(2, 4));
9381
9382 cx.update(|_, cx| {
9383 provider.update(cx, |provider, _| {
9384 provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
9385 id: None,
9386 edits: vec![(edit_position..edit_position, "X".into())],
9387 cursor_position: None,
9388 edit_preview: None,
9389 }))
9390 })
9391 });
9392
9393 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
9394 cx.update_editor(|editor, window, cx| {
9395 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
9396 });
9397
9398 cx.assert_editor_state(indoc! {"
9399 line 1
9400 line 2
9401 lineXˇ 3
9402 line 4
9403 line 5
9404 line 6
9405 line 7
9406 line 8
9407 line 9
9408 line 10
9409 "});
9410
9411 cx.update_editor(|editor, window, cx| {
9412 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9413 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
9414 });
9415 });
9416
9417 cx.assert_editor_state(indoc! {"
9418 line 1
9419 line 2
9420 lineX 3
9421 line 4
9422 line 5
9423 line 6
9424 line 7
9425 line 8
9426 line 9
9427 liˇne 10
9428 "});
9429
9430 cx.update_editor(|editor, window, cx| {
9431 editor.undo(&Default::default(), window, cx);
9432 });
9433
9434 cx.assert_editor_state(indoc! {"
9435 line 1
9436 line 2
9437 lineˇ 3
9438 line 4
9439 line 5
9440 line 6
9441 line 7
9442 line 8
9443 line 9
9444 line 10
9445 "});
9446}
9447
9448#[gpui::test]
9449async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
9450 init_test(cx, |_| {});
9451
9452 let mut cx = EditorTestContext::new(cx).await;
9453 cx.set_state(
9454 r#"let foo = 2;
9455lˇet foo = 2;
9456let fooˇ = 2;
9457let foo = 2;
9458let foo = ˇ2;"#,
9459 );
9460
9461 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9462 .unwrap();
9463 cx.assert_editor_state(
9464 r#"let foo = 2;
9465«letˇ» foo = 2;
9466let «fooˇ» = 2;
9467let foo = 2;
9468let foo = «2ˇ»;"#,
9469 );
9470
9471 // noop for multiple selections with different contents
9472 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9473 .unwrap();
9474 cx.assert_editor_state(
9475 r#"let foo = 2;
9476«letˇ» foo = 2;
9477let «fooˇ» = 2;
9478let foo = 2;
9479let foo = «2ˇ»;"#,
9480 );
9481
9482 // Test last selection direction should be preserved
9483 cx.set_state(
9484 r#"let foo = 2;
9485let foo = 2;
9486let «fooˇ» = 2;
9487let «ˇfoo» = 2;
9488let foo = 2;"#,
9489 );
9490
9491 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9492 .unwrap();
9493 cx.assert_editor_state(
9494 r#"let foo = 2;
9495let foo = 2;
9496let «fooˇ» = 2;
9497let «ˇfoo» = 2;
9498let «ˇfoo» = 2;"#,
9499 );
9500}
9501
9502#[gpui::test]
9503async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
9504 init_test(cx, |_| {});
9505
9506 let mut cx =
9507 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
9508
9509 cx.assert_editor_state(indoc! {"
9510 ˇbbb
9511 ccc
9512
9513 bbb
9514 ccc
9515 "});
9516 cx.dispatch_action(SelectPrevious::default());
9517 cx.assert_editor_state(indoc! {"
9518 «bbbˇ»
9519 ccc
9520
9521 bbb
9522 ccc
9523 "});
9524 cx.dispatch_action(SelectPrevious::default());
9525 cx.assert_editor_state(indoc! {"
9526 «bbbˇ»
9527 ccc
9528
9529 «bbbˇ»
9530 ccc
9531 "});
9532}
9533
9534#[gpui::test]
9535async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
9536 init_test(cx, |_| {});
9537
9538 let mut cx = EditorTestContext::new(cx).await;
9539 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
9540
9541 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9542 .unwrap();
9543 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9544
9545 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9546 .unwrap();
9547 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
9548
9549 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9550 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9551
9552 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9553 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
9554
9555 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9556 .unwrap();
9557 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
9558
9559 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9560 .unwrap();
9561 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
9562}
9563
9564#[gpui::test]
9565async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
9566 init_test(cx, |_| {});
9567
9568 let mut cx = EditorTestContext::new(cx).await;
9569 cx.set_state("aˇ");
9570
9571 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9572 .unwrap();
9573 cx.assert_editor_state("«aˇ»");
9574 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9575 .unwrap();
9576 cx.assert_editor_state("«aˇ»");
9577}
9578
9579#[gpui::test]
9580async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
9581 init_test(cx, |_| {});
9582
9583 let mut cx = EditorTestContext::new(cx).await;
9584 cx.set_state(
9585 r#"let foo = 2;
9586lˇet foo = 2;
9587let fooˇ = 2;
9588let foo = 2;
9589let foo = ˇ2;"#,
9590 );
9591
9592 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9593 .unwrap();
9594 cx.assert_editor_state(
9595 r#"let foo = 2;
9596«letˇ» foo = 2;
9597let «fooˇ» = 2;
9598let foo = 2;
9599let foo = «2ˇ»;"#,
9600 );
9601
9602 // noop for multiple selections with different contents
9603 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9604 .unwrap();
9605 cx.assert_editor_state(
9606 r#"let foo = 2;
9607«letˇ» foo = 2;
9608let «fooˇ» = 2;
9609let foo = 2;
9610let foo = «2ˇ»;"#,
9611 );
9612}
9613
9614#[gpui::test]
9615async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
9616 init_test(cx, |_| {});
9617 let mut cx = EditorTestContext::new(cx).await;
9618
9619 // Enable case sensitive search.
9620 update_test_editor_settings(&mut cx, |settings| {
9621 let mut search_settings = SearchSettingsContent::default();
9622 search_settings.case_sensitive = Some(true);
9623 settings.search = Some(search_settings);
9624 });
9625
9626 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
9627
9628 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9629 .unwrap();
9630 // selection direction is preserved
9631 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9632
9633 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9634 .unwrap();
9635 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9636
9637 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9638 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9639
9640 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9641 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9642
9643 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9644 .unwrap();
9645 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
9646
9647 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9648 .unwrap();
9649 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
9650
9651 // Test case sensitivity
9652 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
9653 cx.update_editor(|e, window, cx| {
9654 e.select_previous(&SelectPrevious::default(), window, cx)
9655 .unwrap();
9656 e.select_previous(&SelectPrevious::default(), window, cx)
9657 .unwrap();
9658 });
9659 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
9660
9661 // Disable case sensitive search.
9662 update_test_editor_settings(&mut cx, |settings| {
9663 let mut search_settings = SearchSettingsContent::default();
9664 search_settings.case_sensitive = Some(false);
9665 settings.search = Some(search_settings);
9666 });
9667
9668 cx.set_state("foo\nFOO\n«ˇFoo»");
9669 cx.update_editor(|e, window, cx| {
9670 e.select_previous(&SelectPrevious::default(), window, cx)
9671 .unwrap();
9672 e.select_previous(&SelectPrevious::default(), window, cx)
9673 .unwrap();
9674 });
9675 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
9676}
9677
9678#[gpui::test]
9679async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
9680 init_test(cx, |_| {});
9681
9682 let language = Arc::new(Language::new(
9683 LanguageConfig::default(),
9684 Some(tree_sitter_rust::LANGUAGE.into()),
9685 ));
9686
9687 let text = r#"
9688 use mod1::mod2::{mod3, mod4};
9689
9690 fn fn_1(param1: bool, param2: &str) {
9691 let var1 = "text";
9692 }
9693 "#
9694 .unindent();
9695
9696 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9697 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9698 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9699
9700 editor
9701 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9702 .await;
9703
9704 editor.update_in(cx, |editor, window, cx| {
9705 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9706 s.select_display_ranges([
9707 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
9708 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
9709 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
9710 ]);
9711 });
9712 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9713 });
9714 editor.update(cx, |editor, cx| {
9715 assert_text_with_selections(
9716 editor,
9717 indoc! {r#"
9718 use mod1::mod2::{mod3, «mod4ˇ»};
9719
9720 fn fn_1«ˇ(param1: bool, param2: &str)» {
9721 let var1 = "«ˇtext»";
9722 }
9723 "#},
9724 cx,
9725 );
9726 });
9727
9728 editor.update_in(cx, |editor, window, cx| {
9729 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9730 });
9731 editor.update(cx, |editor, cx| {
9732 assert_text_with_selections(
9733 editor,
9734 indoc! {r#"
9735 use mod1::mod2::«{mod3, mod4}ˇ»;
9736
9737 «ˇfn fn_1(param1: bool, param2: &str) {
9738 let var1 = "text";
9739 }»
9740 "#},
9741 cx,
9742 );
9743 });
9744
9745 editor.update_in(cx, |editor, window, cx| {
9746 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9747 });
9748 assert_eq!(
9749 editor.update(cx, |editor, cx| editor
9750 .selections
9751 .display_ranges(&editor.display_snapshot(cx))),
9752 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9753 );
9754
9755 // Trying to expand the selected syntax node one more time has no effect.
9756 editor.update_in(cx, |editor, window, cx| {
9757 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9758 });
9759 assert_eq!(
9760 editor.update(cx, |editor, cx| editor
9761 .selections
9762 .display_ranges(&editor.display_snapshot(cx))),
9763 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9764 );
9765
9766 editor.update_in(cx, |editor, window, cx| {
9767 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9768 });
9769 editor.update(cx, |editor, cx| {
9770 assert_text_with_selections(
9771 editor,
9772 indoc! {r#"
9773 use mod1::mod2::«{mod3, mod4}ˇ»;
9774
9775 «ˇfn fn_1(param1: bool, param2: &str) {
9776 let var1 = "text";
9777 }»
9778 "#},
9779 cx,
9780 );
9781 });
9782
9783 editor.update_in(cx, |editor, window, cx| {
9784 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9785 });
9786 editor.update(cx, |editor, cx| {
9787 assert_text_with_selections(
9788 editor,
9789 indoc! {r#"
9790 use mod1::mod2::{mod3, «mod4ˇ»};
9791
9792 fn fn_1«ˇ(param1: bool, param2: &str)» {
9793 let var1 = "«ˇtext»";
9794 }
9795 "#},
9796 cx,
9797 );
9798 });
9799
9800 editor.update_in(cx, |editor, window, cx| {
9801 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9802 });
9803 editor.update(cx, |editor, cx| {
9804 assert_text_with_selections(
9805 editor,
9806 indoc! {r#"
9807 use mod1::mod2::{mod3, moˇd4};
9808
9809 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9810 let var1 = "teˇxt";
9811 }
9812 "#},
9813 cx,
9814 );
9815 });
9816
9817 // Trying to shrink the selected syntax node one more time has no effect.
9818 editor.update_in(cx, |editor, window, cx| {
9819 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9820 });
9821 editor.update_in(cx, |editor, _, cx| {
9822 assert_text_with_selections(
9823 editor,
9824 indoc! {r#"
9825 use mod1::mod2::{mod3, moˇd4};
9826
9827 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9828 let var1 = "teˇxt";
9829 }
9830 "#},
9831 cx,
9832 );
9833 });
9834
9835 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9836 // a fold.
9837 editor.update_in(cx, |editor, window, cx| {
9838 editor.fold_creases(
9839 vec![
9840 Crease::simple(
9841 Point::new(0, 21)..Point::new(0, 24),
9842 FoldPlaceholder::test(),
9843 ),
9844 Crease::simple(
9845 Point::new(3, 20)..Point::new(3, 22),
9846 FoldPlaceholder::test(),
9847 ),
9848 ],
9849 true,
9850 window,
9851 cx,
9852 );
9853 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9854 });
9855 editor.update(cx, |editor, cx| {
9856 assert_text_with_selections(
9857 editor,
9858 indoc! {r#"
9859 use mod1::mod2::«{mod3, mod4}ˇ»;
9860
9861 fn fn_1«ˇ(param1: bool, param2: &str)» {
9862 let var1 = "«ˇtext»";
9863 }
9864 "#},
9865 cx,
9866 );
9867 });
9868}
9869
9870#[gpui::test]
9871async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9872 init_test(cx, |_| {});
9873
9874 let language = Arc::new(Language::new(
9875 LanguageConfig::default(),
9876 Some(tree_sitter_rust::LANGUAGE.into()),
9877 ));
9878
9879 let text = "let a = 2;";
9880
9881 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9882 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9883 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9884
9885 editor
9886 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9887 .await;
9888
9889 // Test case 1: Cursor at end of word
9890 editor.update_in(cx, |editor, window, cx| {
9891 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9892 s.select_display_ranges([
9893 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9894 ]);
9895 });
9896 });
9897 editor.update(cx, |editor, cx| {
9898 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9899 });
9900 editor.update_in(cx, |editor, window, cx| {
9901 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9902 });
9903 editor.update(cx, |editor, cx| {
9904 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9905 });
9906 editor.update_in(cx, |editor, window, cx| {
9907 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9908 });
9909 editor.update(cx, |editor, cx| {
9910 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9911 });
9912
9913 // Test case 2: Cursor at end of statement
9914 editor.update_in(cx, |editor, window, cx| {
9915 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9916 s.select_display_ranges([
9917 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9918 ]);
9919 });
9920 });
9921 editor.update(cx, |editor, cx| {
9922 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9923 });
9924 editor.update_in(cx, |editor, window, cx| {
9925 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9926 });
9927 editor.update(cx, |editor, cx| {
9928 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9929 });
9930}
9931
9932#[gpui::test]
9933async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9934 init_test(cx, |_| {});
9935
9936 let language = Arc::new(Language::new(
9937 LanguageConfig {
9938 name: "JavaScript".into(),
9939 ..Default::default()
9940 },
9941 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9942 ));
9943
9944 let text = r#"
9945 let a = {
9946 key: "value",
9947 };
9948 "#
9949 .unindent();
9950
9951 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9952 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9953 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9954
9955 editor
9956 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9957 .await;
9958
9959 // Test case 1: Cursor after '{'
9960 editor.update_in(cx, |editor, window, cx| {
9961 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9962 s.select_display_ranges([
9963 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9964 ]);
9965 });
9966 });
9967 editor.update(cx, |editor, cx| {
9968 assert_text_with_selections(
9969 editor,
9970 indoc! {r#"
9971 let a = {ˇ
9972 key: "value",
9973 };
9974 "#},
9975 cx,
9976 );
9977 });
9978 editor.update_in(cx, |editor, window, cx| {
9979 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9980 });
9981 editor.update(cx, |editor, cx| {
9982 assert_text_with_selections(
9983 editor,
9984 indoc! {r#"
9985 let a = «ˇ{
9986 key: "value",
9987 }»;
9988 "#},
9989 cx,
9990 );
9991 });
9992
9993 // Test case 2: Cursor after ':'
9994 editor.update_in(cx, |editor, window, cx| {
9995 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9996 s.select_display_ranges([
9997 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9998 ]);
9999 });
10000 });
10001 editor.update(cx, |editor, cx| {
10002 assert_text_with_selections(
10003 editor,
10004 indoc! {r#"
10005 let a = {
10006 key:ˇ "value",
10007 };
10008 "#},
10009 cx,
10010 );
10011 });
10012 editor.update_in(cx, |editor, window, cx| {
10013 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10014 });
10015 editor.update(cx, |editor, cx| {
10016 assert_text_with_selections(
10017 editor,
10018 indoc! {r#"
10019 let a = {
10020 «ˇkey: "value"»,
10021 };
10022 "#},
10023 cx,
10024 );
10025 });
10026 editor.update_in(cx, |editor, window, cx| {
10027 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10028 });
10029 editor.update(cx, |editor, cx| {
10030 assert_text_with_selections(
10031 editor,
10032 indoc! {r#"
10033 let a = «ˇ{
10034 key: "value",
10035 }»;
10036 "#},
10037 cx,
10038 );
10039 });
10040
10041 // Test case 3: Cursor after ','
10042 editor.update_in(cx, |editor, window, cx| {
10043 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10044 s.select_display_ranges([
10045 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
10046 ]);
10047 });
10048 });
10049 editor.update(cx, |editor, cx| {
10050 assert_text_with_selections(
10051 editor,
10052 indoc! {r#"
10053 let a = {
10054 key: "value",ˇ
10055 };
10056 "#},
10057 cx,
10058 );
10059 });
10060 editor.update_in(cx, |editor, window, cx| {
10061 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10062 });
10063 editor.update(cx, |editor, cx| {
10064 assert_text_with_selections(
10065 editor,
10066 indoc! {r#"
10067 let a = «ˇ{
10068 key: "value",
10069 }»;
10070 "#},
10071 cx,
10072 );
10073 });
10074
10075 // Test case 4: Cursor after ';'
10076 editor.update_in(cx, |editor, window, cx| {
10077 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10078 s.select_display_ranges([
10079 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
10080 ]);
10081 });
10082 });
10083 editor.update(cx, |editor, cx| {
10084 assert_text_with_selections(
10085 editor,
10086 indoc! {r#"
10087 let a = {
10088 key: "value",
10089 };ˇ
10090 "#},
10091 cx,
10092 );
10093 });
10094 editor.update_in(cx, |editor, window, cx| {
10095 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10096 });
10097 editor.update(cx, |editor, cx| {
10098 assert_text_with_selections(
10099 editor,
10100 indoc! {r#"
10101 «ˇlet a = {
10102 key: "value",
10103 };
10104 »"#},
10105 cx,
10106 );
10107 });
10108}
10109
10110#[gpui::test]
10111async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
10112 init_test(cx, |_| {});
10113
10114 let language = Arc::new(Language::new(
10115 LanguageConfig::default(),
10116 Some(tree_sitter_rust::LANGUAGE.into()),
10117 ));
10118
10119 let text = r#"
10120 use mod1::mod2::{mod3, mod4};
10121
10122 fn fn_1(param1: bool, param2: &str) {
10123 let var1 = "hello world";
10124 }
10125 "#
10126 .unindent();
10127
10128 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10129 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10130 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10131
10132 editor
10133 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10134 .await;
10135
10136 // Test 1: Cursor on a letter of a string word
10137 editor.update_in(cx, |editor, window, cx| {
10138 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10139 s.select_display_ranges([
10140 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
10141 ]);
10142 });
10143 });
10144 editor.update_in(cx, |editor, window, cx| {
10145 assert_text_with_selections(
10146 editor,
10147 indoc! {r#"
10148 use mod1::mod2::{mod3, mod4};
10149
10150 fn fn_1(param1: bool, param2: &str) {
10151 let var1 = "hˇello world";
10152 }
10153 "#},
10154 cx,
10155 );
10156 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10157 assert_text_with_selections(
10158 editor,
10159 indoc! {r#"
10160 use mod1::mod2::{mod3, mod4};
10161
10162 fn fn_1(param1: bool, param2: &str) {
10163 let var1 = "«ˇhello» world";
10164 }
10165 "#},
10166 cx,
10167 );
10168 });
10169
10170 // Test 2: Partial selection within a word
10171 editor.update_in(cx, |editor, window, cx| {
10172 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10173 s.select_display_ranges([
10174 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
10175 ]);
10176 });
10177 });
10178 editor.update_in(cx, |editor, window, cx| {
10179 assert_text_with_selections(
10180 editor,
10181 indoc! {r#"
10182 use mod1::mod2::{mod3, mod4};
10183
10184 fn fn_1(param1: bool, param2: &str) {
10185 let var1 = "h«elˇ»lo world";
10186 }
10187 "#},
10188 cx,
10189 );
10190 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10191 assert_text_with_selections(
10192 editor,
10193 indoc! {r#"
10194 use mod1::mod2::{mod3, mod4};
10195
10196 fn fn_1(param1: bool, param2: &str) {
10197 let var1 = "«ˇhello» world";
10198 }
10199 "#},
10200 cx,
10201 );
10202 });
10203
10204 // Test 3: Complete word already selected
10205 editor.update_in(cx, |editor, window, cx| {
10206 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10207 s.select_display_ranges([
10208 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
10209 ]);
10210 });
10211 });
10212 editor.update_in(cx, |editor, window, cx| {
10213 assert_text_with_selections(
10214 editor,
10215 indoc! {r#"
10216 use mod1::mod2::{mod3, mod4};
10217
10218 fn fn_1(param1: bool, param2: &str) {
10219 let var1 = "«helloˇ» world";
10220 }
10221 "#},
10222 cx,
10223 );
10224 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10225 assert_text_with_selections(
10226 editor,
10227 indoc! {r#"
10228 use mod1::mod2::{mod3, mod4};
10229
10230 fn fn_1(param1: bool, param2: &str) {
10231 let var1 = "«hello worldˇ»";
10232 }
10233 "#},
10234 cx,
10235 );
10236 });
10237
10238 // Test 4: Selection spanning across words
10239 editor.update_in(cx, |editor, window, cx| {
10240 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10241 s.select_display_ranges([
10242 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
10243 ]);
10244 });
10245 });
10246 editor.update_in(cx, |editor, window, cx| {
10247 assert_text_with_selections(
10248 editor,
10249 indoc! {r#"
10250 use mod1::mod2::{mod3, mod4};
10251
10252 fn fn_1(param1: bool, param2: &str) {
10253 let var1 = "hel«lo woˇ»rld";
10254 }
10255 "#},
10256 cx,
10257 );
10258 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10259 assert_text_with_selections(
10260 editor,
10261 indoc! {r#"
10262 use mod1::mod2::{mod3, mod4};
10263
10264 fn fn_1(param1: bool, param2: &str) {
10265 let var1 = "«ˇhello world»";
10266 }
10267 "#},
10268 cx,
10269 );
10270 });
10271
10272 // Test 5: Expansion beyond string
10273 editor.update_in(cx, |editor, window, cx| {
10274 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10275 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
10276 assert_text_with_selections(
10277 editor,
10278 indoc! {r#"
10279 use mod1::mod2::{mod3, mod4};
10280
10281 fn fn_1(param1: bool, param2: &str) {
10282 «ˇlet var1 = "hello world";»
10283 }
10284 "#},
10285 cx,
10286 );
10287 });
10288}
10289
10290#[gpui::test]
10291async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
10292 init_test(cx, |_| {});
10293
10294 let mut cx = EditorTestContext::new(cx).await;
10295
10296 let language = Arc::new(Language::new(
10297 LanguageConfig::default(),
10298 Some(tree_sitter_rust::LANGUAGE.into()),
10299 ));
10300
10301 cx.update_buffer(|buffer, cx| {
10302 buffer.set_language(Some(language), cx);
10303 });
10304
10305 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
10306 cx.update_editor(|editor, window, cx| {
10307 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
10308 });
10309
10310 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
10311
10312 cx.set_state(indoc! { r#"fn a() {
10313 // what
10314 // a
10315 // ˇlong
10316 // method
10317 // I
10318 // sure
10319 // hope
10320 // it
10321 // works
10322 }"# });
10323
10324 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
10325 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
10326 cx.update(|_, cx| {
10327 multi_buffer.update(cx, |multi_buffer, cx| {
10328 multi_buffer.set_excerpts_for_path(
10329 PathKey::for_buffer(&buffer, cx),
10330 buffer,
10331 [Point::new(1, 0)..Point::new(1, 0)],
10332 3,
10333 cx,
10334 );
10335 });
10336 });
10337
10338 let editor2 = cx.new_window_entity(|window, cx| {
10339 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
10340 });
10341
10342 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
10343 cx.update_editor(|editor, window, cx| {
10344 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
10345 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
10346 })
10347 });
10348
10349 cx.assert_editor_state(indoc! { "
10350 fn a() {
10351 // what
10352 // a
10353 ˇ // long
10354 // method"});
10355
10356 cx.update_editor(|editor, window, cx| {
10357 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
10358 });
10359
10360 // Although we could potentially make the action work when the syntax node
10361 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
10362 // did. Maybe we could also expand the excerpt to contain the range?
10363 cx.assert_editor_state(indoc! { "
10364 fn a() {
10365 // what
10366 // a
10367 ˇ // long
10368 // method"});
10369}
10370
10371#[gpui::test]
10372async fn test_fold_function_bodies(cx: &mut TestAppContext) {
10373 init_test(cx, |_| {});
10374
10375 let base_text = r#"
10376 impl A {
10377 // this is an uncommitted comment
10378
10379 fn b() {
10380 c();
10381 }
10382
10383 // this is another uncommitted comment
10384
10385 fn d() {
10386 // e
10387 // f
10388 }
10389 }
10390
10391 fn g() {
10392 // h
10393 }
10394 "#
10395 .unindent();
10396
10397 let text = r#"
10398 ˇimpl A {
10399
10400 fn b() {
10401 c();
10402 }
10403
10404 fn d() {
10405 // e
10406 // f
10407 }
10408 }
10409
10410 fn g() {
10411 // h
10412 }
10413 "#
10414 .unindent();
10415
10416 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10417 cx.set_state(&text);
10418 cx.set_head_text(&base_text);
10419 cx.update_editor(|editor, window, cx| {
10420 editor.expand_all_diff_hunks(&Default::default(), window, cx);
10421 });
10422
10423 cx.assert_state_with_diff(
10424 "
10425 ˇimpl A {
10426 - // this is an uncommitted comment
10427
10428 fn b() {
10429 c();
10430 }
10431
10432 - // this is another uncommitted comment
10433 -
10434 fn d() {
10435 // e
10436 // f
10437 }
10438 }
10439
10440 fn g() {
10441 // h
10442 }
10443 "
10444 .unindent(),
10445 );
10446
10447 let expected_display_text = "
10448 impl A {
10449 // this is an uncommitted comment
10450
10451 fn b() {
10452 ⋯
10453 }
10454
10455 // this is another uncommitted comment
10456
10457 fn d() {
10458 ⋯
10459 }
10460 }
10461
10462 fn g() {
10463 ⋯
10464 }
10465 "
10466 .unindent();
10467
10468 cx.update_editor(|editor, window, cx| {
10469 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
10470 assert_eq!(editor.display_text(cx), expected_display_text);
10471 });
10472}
10473
10474#[gpui::test]
10475async fn test_autoindent(cx: &mut TestAppContext) {
10476 init_test(cx, |_| {});
10477
10478 let language = Arc::new(
10479 Language::new(
10480 LanguageConfig {
10481 brackets: BracketPairConfig {
10482 pairs: vec![
10483 BracketPair {
10484 start: "{".to_string(),
10485 end: "}".to_string(),
10486 close: false,
10487 surround: false,
10488 newline: true,
10489 },
10490 BracketPair {
10491 start: "(".to_string(),
10492 end: ")".to_string(),
10493 close: false,
10494 surround: false,
10495 newline: true,
10496 },
10497 ],
10498 ..Default::default()
10499 },
10500 ..Default::default()
10501 },
10502 Some(tree_sitter_rust::LANGUAGE.into()),
10503 )
10504 .with_indents_query(
10505 r#"
10506 (_ "(" ")" @end) @indent
10507 (_ "{" "}" @end) @indent
10508 "#,
10509 )
10510 .unwrap(),
10511 );
10512
10513 let text = "fn a() {}";
10514
10515 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10516 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10517 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10518 editor
10519 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10520 .await;
10521
10522 editor.update_in(cx, |editor, window, cx| {
10523 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10524 s.select_ranges([
10525 MultiBufferOffset(5)..MultiBufferOffset(5),
10526 MultiBufferOffset(8)..MultiBufferOffset(8),
10527 MultiBufferOffset(9)..MultiBufferOffset(9),
10528 ])
10529 });
10530 editor.newline(&Newline, window, cx);
10531 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
10532 assert_eq!(
10533 editor.selections.ranges(&editor.display_snapshot(cx)),
10534 &[
10535 Point::new(1, 4)..Point::new(1, 4),
10536 Point::new(3, 4)..Point::new(3, 4),
10537 Point::new(5, 0)..Point::new(5, 0)
10538 ]
10539 );
10540 });
10541}
10542
10543#[gpui::test]
10544async fn test_autoindent_disabled(cx: &mut TestAppContext) {
10545 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
10546
10547 let language = Arc::new(
10548 Language::new(
10549 LanguageConfig {
10550 brackets: BracketPairConfig {
10551 pairs: vec![
10552 BracketPair {
10553 start: "{".to_string(),
10554 end: "}".to_string(),
10555 close: false,
10556 surround: false,
10557 newline: true,
10558 },
10559 BracketPair {
10560 start: "(".to_string(),
10561 end: ")".to_string(),
10562 close: false,
10563 surround: false,
10564 newline: true,
10565 },
10566 ],
10567 ..Default::default()
10568 },
10569 ..Default::default()
10570 },
10571 Some(tree_sitter_rust::LANGUAGE.into()),
10572 )
10573 .with_indents_query(
10574 r#"
10575 (_ "(" ")" @end) @indent
10576 (_ "{" "}" @end) @indent
10577 "#,
10578 )
10579 .unwrap(),
10580 );
10581
10582 let text = "fn a() {}";
10583
10584 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10585 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10586 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10587 editor
10588 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10589 .await;
10590
10591 editor.update_in(cx, |editor, window, cx| {
10592 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10593 s.select_ranges([
10594 MultiBufferOffset(5)..MultiBufferOffset(5),
10595 MultiBufferOffset(8)..MultiBufferOffset(8),
10596 MultiBufferOffset(9)..MultiBufferOffset(9),
10597 ])
10598 });
10599 editor.newline(&Newline, window, cx);
10600 assert_eq!(
10601 editor.text(cx),
10602 indoc!(
10603 "
10604 fn a(
10605
10606 ) {
10607
10608 }
10609 "
10610 )
10611 );
10612 assert_eq!(
10613 editor.selections.ranges(&editor.display_snapshot(cx)),
10614 &[
10615 Point::new(1, 0)..Point::new(1, 0),
10616 Point::new(3, 0)..Point::new(3, 0),
10617 Point::new(5, 0)..Point::new(5, 0)
10618 ]
10619 );
10620 });
10621}
10622
10623#[gpui::test]
10624async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
10625 init_test(cx, |settings| {
10626 settings.defaults.auto_indent = Some(true);
10627 settings.languages.0.insert(
10628 "python".into(),
10629 LanguageSettingsContent {
10630 auto_indent: Some(false),
10631 ..Default::default()
10632 },
10633 );
10634 });
10635
10636 let mut cx = EditorTestContext::new(cx).await;
10637
10638 let injected_language = Arc::new(
10639 Language::new(
10640 LanguageConfig {
10641 brackets: BracketPairConfig {
10642 pairs: vec![
10643 BracketPair {
10644 start: "{".to_string(),
10645 end: "}".to_string(),
10646 close: false,
10647 surround: false,
10648 newline: true,
10649 },
10650 BracketPair {
10651 start: "(".to_string(),
10652 end: ")".to_string(),
10653 close: true,
10654 surround: false,
10655 newline: true,
10656 },
10657 ],
10658 ..Default::default()
10659 },
10660 name: "python".into(),
10661 ..Default::default()
10662 },
10663 Some(tree_sitter_python::LANGUAGE.into()),
10664 )
10665 .with_indents_query(
10666 r#"
10667 (_ "(" ")" @end) @indent
10668 (_ "{" "}" @end) @indent
10669 "#,
10670 )
10671 .unwrap(),
10672 );
10673
10674 let language = Arc::new(
10675 Language::new(
10676 LanguageConfig {
10677 brackets: BracketPairConfig {
10678 pairs: vec![
10679 BracketPair {
10680 start: "{".to_string(),
10681 end: "}".to_string(),
10682 close: false,
10683 surround: false,
10684 newline: true,
10685 },
10686 BracketPair {
10687 start: "(".to_string(),
10688 end: ")".to_string(),
10689 close: true,
10690 surround: false,
10691 newline: true,
10692 },
10693 ],
10694 ..Default::default()
10695 },
10696 name: LanguageName::new_static("rust"),
10697 ..Default::default()
10698 },
10699 Some(tree_sitter_rust::LANGUAGE.into()),
10700 )
10701 .with_indents_query(
10702 r#"
10703 (_ "(" ")" @end) @indent
10704 (_ "{" "}" @end) @indent
10705 "#,
10706 )
10707 .unwrap()
10708 .with_injection_query(
10709 r#"
10710 (macro_invocation
10711 macro: (identifier) @_macro_name
10712 (token_tree) @injection.content
10713 (#set! injection.language "python"))
10714 "#,
10715 )
10716 .unwrap(),
10717 );
10718
10719 cx.language_registry().add(injected_language);
10720 cx.language_registry().add(language.clone());
10721
10722 cx.update_buffer(|buffer, cx| {
10723 buffer.set_language(Some(language), cx);
10724 });
10725
10726 cx.set_state(r#"struct A {ˇ}"#);
10727
10728 cx.update_editor(|editor, window, cx| {
10729 editor.newline(&Default::default(), window, cx);
10730 });
10731
10732 cx.assert_editor_state(indoc!(
10733 "struct A {
10734 ˇ
10735 }"
10736 ));
10737
10738 cx.set_state(r#"select_biased!(ˇ)"#);
10739
10740 cx.update_editor(|editor, window, cx| {
10741 editor.newline(&Default::default(), window, cx);
10742 editor.handle_input("def ", window, cx);
10743 editor.handle_input("(", window, cx);
10744 editor.newline(&Default::default(), window, cx);
10745 editor.handle_input("a", window, cx);
10746 });
10747
10748 cx.assert_editor_state(indoc!(
10749 "select_biased!(
10750 def (
10751 aˇ
10752 )
10753 )"
10754 ));
10755}
10756
10757#[gpui::test]
10758async fn test_autoindent_selections(cx: &mut TestAppContext) {
10759 init_test(cx, |_| {});
10760
10761 {
10762 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10763 cx.set_state(indoc! {"
10764 impl A {
10765
10766 fn b() {}
10767
10768 «fn c() {
10769
10770 }ˇ»
10771 }
10772 "});
10773
10774 cx.update_editor(|editor, window, cx| {
10775 editor.autoindent(&Default::default(), window, cx);
10776 });
10777 cx.wait_for_autoindent_applied().await;
10778
10779 cx.assert_editor_state(indoc! {"
10780 impl A {
10781
10782 fn b() {}
10783
10784 «fn c() {
10785
10786 }ˇ»
10787 }
10788 "});
10789 }
10790
10791 {
10792 let mut cx = EditorTestContext::new_multibuffer(
10793 cx,
10794 [indoc! { "
10795 impl A {
10796 «
10797 // a
10798 fn b(){}
10799 »
10800 «
10801 }
10802 fn c(){}
10803 »
10804 "}],
10805 );
10806
10807 let buffer = cx.update_editor(|editor, _, cx| {
10808 let buffer = editor.buffer().update(cx, |buffer, _| {
10809 buffer.all_buffers().iter().next().unwrap().clone()
10810 });
10811 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10812 buffer
10813 });
10814
10815 cx.run_until_parked();
10816 cx.update_editor(|editor, window, cx| {
10817 editor.select_all(&Default::default(), window, cx);
10818 editor.autoindent(&Default::default(), window, cx)
10819 });
10820 cx.run_until_parked();
10821
10822 cx.update(|_, cx| {
10823 assert_eq!(
10824 buffer.read(cx).text(),
10825 indoc! { "
10826 impl A {
10827
10828 // a
10829 fn b(){}
10830
10831
10832 }
10833 fn c(){}
10834
10835 " }
10836 )
10837 });
10838 }
10839}
10840
10841#[gpui::test]
10842async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10843 init_test(cx, |_| {});
10844
10845 let mut cx = EditorTestContext::new(cx).await;
10846
10847 let language = Arc::new(Language::new(
10848 LanguageConfig {
10849 brackets: BracketPairConfig {
10850 pairs: vec![
10851 BracketPair {
10852 start: "{".to_string(),
10853 end: "}".to_string(),
10854 close: true,
10855 surround: true,
10856 newline: true,
10857 },
10858 BracketPair {
10859 start: "(".to_string(),
10860 end: ")".to_string(),
10861 close: true,
10862 surround: true,
10863 newline: true,
10864 },
10865 BracketPair {
10866 start: "/*".to_string(),
10867 end: " */".to_string(),
10868 close: true,
10869 surround: true,
10870 newline: true,
10871 },
10872 BracketPair {
10873 start: "[".to_string(),
10874 end: "]".to_string(),
10875 close: false,
10876 surround: false,
10877 newline: true,
10878 },
10879 BracketPair {
10880 start: "\"".to_string(),
10881 end: "\"".to_string(),
10882 close: true,
10883 surround: true,
10884 newline: false,
10885 },
10886 BracketPair {
10887 start: "<".to_string(),
10888 end: ">".to_string(),
10889 close: false,
10890 surround: true,
10891 newline: true,
10892 },
10893 ],
10894 ..Default::default()
10895 },
10896 autoclose_before: "})]".to_string(),
10897 ..Default::default()
10898 },
10899 Some(tree_sitter_rust::LANGUAGE.into()),
10900 ));
10901
10902 cx.language_registry().add(language.clone());
10903 cx.update_buffer(|buffer, cx| {
10904 buffer.set_language(Some(language), cx);
10905 });
10906
10907 cx.set_state(
10908 &r#"
10909 🏀ˇ
10910 εˇ
10911 ❤️ˇ
10912 "#
10913 .unindent(),
10914 );
10915
10916 // autoclose multiple nested brackets at multiple cursors
10917 cx.update_editor(|editor, window, cx| {
10918 editor.handle_input("{", window, cx);
10919 editor.handle_input("{", window, cx);
10920 editor.handle_input("{", window, cx);
10921 });
10922 cx.assert_editor_state(
10923 &"
10924 🏀{{{ˇ}}}
10925 ε{{{ˇ}}}
10926 ❤️{{{ˇ}}}
10927 "
10928 .unindent(),
10929 );
10930
10931 // insert a different closing bracket
10932 cx.update_editor(|editor, window, cx| {
10933 editor.handle_input(")", window, cx);
10934 });
10935 cx.assert_editor_state(
10936 &"
10937 🏀{{{)ˇ}}}
10938 ε{{{)ˇ}}}
10939 ❤️{{{)ˇ}}}
10940 "
10941 .unindent(),
10942 );
10943
10944 // skip over the auto-closed brackets when typing a closing bracket
10945 cx.update_editor(|editor, window, cx| {
10946 editor.move_right(&MoveRight, window, cx);
10947 editor.handle_input("}", window, cx);
10948 editor.handle_input("}", window, cx);
10949 editor.handle_input("}", window, cx);
10950 });
10951 cx.assert_editor_state(
10952 &"
10953 🏀{{{)}}}}ˇ
10954 ε{{{)}}}}ˇ
10955 ❤️{{{)}}}}ˇ
10956 "
10957 .unindent(),
10958 );
10959
10960 // autoclose multi-character pairs
10961 cx.set_state(
10962 &"
10963 ˇ
10964 ˇ
10965 "
10966 .unindent(),
10967 );
10968 cx.update_editor(|editor, window, cx| {
10969 editor.handle_input("/", window, cx);
10970 editor.handle_input("*", window, cx);
10971 });
10972 cx.assert_editor_state(
10973 &"
10974 /*ˇ */
10975 /*ˇ */
10976 "
10977 .unindent(),
10978 );
10979
10980 // one cursor autocloses a multi-character pair, one cursor
10981 // does not autoclose.
10982 cx.set_state(
10983 &"
10984 /ˇ
10985 ˇ
10986 "
10987 .unindent(),
10988 );
10989 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10990 cx.assert_editor_state(
10991 &"
10992 /*ˇ */
10993 *ˇ
10994 "
10995 .unindent(),
10996 );
10997
10998 // Don't autoclose if the next character isn't whitespace and isn't
10999 // listed in the language's "autoclose_before" section.
11000 cx.set_state("ˇa b");
11001 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
11002 cx.assert_editor_state("{ˇa b");
11003
11004 // Don't autoclose if `close` is false for the bracket pair
11005 cx.set_state("ˇ");
11006 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
11007 cx.assert_editor_state("[ˇ");
11008
11009 // Surround with brackets if text is selected
11010 cx.set_state("«aˇ» b");
11011 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
11012 cx.assert_editor_state("{«aˇ»} b");
11013
11014 // Autoclose when not immediately after a word character
11015 cx.set_state("a ˇ");
11016 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
11017 cx.assert_editor_state("a \"ˇ\"");
11018
11019 // Autoclose pair where the start and end characters are the same
11020 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
11021 cx.assert_editor_state("a \"\"ˇ");
11022
11023 // Don't autoclose when immediately after a word character
11024 cx.set_state("aˇ");
11025 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
11026 cx.assert_editor_state("a\"ˇ");
11027
11028 // Do autoclose when after a non-word character
11029 cx.set_state("{ˇ");
11030 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
11031 cx.assert_editor_state("{\"ˇ\"");
11032
11033 // Non identical pairs autoclose regardless of preceding character
11034 cx.set_state("aˇ");
11035 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
11036 cx.assert_editor_state("a{ˇ}");
11037
11038 // Don't autoclose pair if autoclose is disabled
11039 cx.set_state("ˇ");
11040 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
11041 cx.assert_editor_state("<ˇ");
11042
11043 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
11044 cx.set_state("«aˇ» b");
11045 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
11046 cx.assert_editor_state("<«aˇ»> b");
11047}
11048
11049#[gpui::test]
11050async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
11051 init_test(cx, |settings| {
11052 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11053 });
11054
11055 let mut cx = EditorTestContext::new(cx).await;
11056
11057 let language = Arc::new(Language::new(
11058 LanguageConfig {
11059 brackets: BracketPairConfig {
11060 pairs: vec![
11061 BracketPair {
11062 start: "{".to_string(),
11063 end: "}".to_string(),
11064 close: true,
11065 surround: true,
11066 newline: true,
11067 },
11068 BracketPair {
11069 start: "(".to_string(),
11070 end: ")".to_string(),
11071 close: true,
11072 surround: true,
11073 newline: true,
11074 },
11075 BracketPair {
11076 start: "[".to_string(),
11077 end: "]".to_string(),
11078 close: false,
11079 surround: false,
11080 newline: true,
11081 },
11082 ],
11083 ..Default::default()
11084 },
11085 autoclose_before: "})]".to_string(),
11086 ..Default::default()
11087 },
11088 Some(tree_sitter_rust::LANGUAGE.into()),
11089 ));
11090
11091 cx.language_registry().add(language.clone());
11092 cx.update_buffer(|buffer, cx| {
11093 buffer.set_language(Some(language), cx);
11094 });
11095
11096 cx.set_state(
11097 &"
11098 ˇ
11099 ˇ
11100 ˇ
11101 "
11102 .unindent(),
11103 );
11104
11105 // ensure only matching closing brackets are skipped over
11106 cx.update_editor(|editor, window, cx| {
11107 editor.handle_input("}", window, cx);
11108 editor.move_left(&MoveLeft, window, cx);
11109 editor.handle_input(")", window, cx);
11110 editor.move_left(&MoveLeft, window, cx);
11111 });
11112 cx.assert_editor_state(
11113 &"
11114 ˇ)}
11115 ˇ)}
11116 ˇ)}
11117 "
11118 .unindent(),
11119 );
11120
11121 // skip-over closing brackets at multiple cursors
11122 cx.update_editor(|editor, window, cx| {
11123 editor.handle_input(")", window, cx);
11124 editor.handle_input("}", window, cx);
11125 });
11126 cx.assert_editor_state(
11127 &"
11128 )}ˇ
11129 )}ˇ
11130 )}ˇ
11131 "
11132 .unindent(),
11133 );
11134
11135 // ignore non-close brackets
11136 cx.update_editor(|editor, window, cx| {
11137 editor.handle_input("]", window, cx);
11138 editor.move_left(&MoveLeft, window, cx);
11139 editor.handle_input("]", window, cx);
11140 });
11141 cx.assert_editor_state(
11142 &"
11143 )}]ˇ]
11144 )}]ˇ]
11145 )}]ˇ]
11146 "
11147 .unindent(),
11148 );
11149}
11150
11151#[gpui::test]
11152async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
11153 init_test(cx, |_| {});
11154
11155 let mut cx = EditorTestContext::new(cx).await;
11156
11157 let html_language = Arc::new(
11158 Language::new(
11159 LanguageConfig {
11160 name: "HTML".into(),
11161 brackets: BracketPairConfig {
11162 pairs: vec![
11163 BracketPair {
11164 start: "<".into(),
11165 end: ">".into(),
11166 close: true,
11167 ..Default::default()
11168 },
11169 BracketPair {
11170 start: "{".into(),
11171 end: "}".into(),
11172 close: true,
11173 ..Default::default()
11174 },
11175 BracketPair {
11176 start: "(".into(),
11177 end: ")".into(),
11178 close: true,
11179 ..Default::default()
11180 },
11181 ],
11182 ..Default::default()
11183 },
11184 autoclose_before: "})]>".into(),
11185 ..Default::default()
11186 },
11187 Some(tree_sitter_html::LANGUAGE.into()),
11188 )
11189 .with_injection_query(
11190 r#"
11191 (script_element
11192 (raw_text) @injection.content
11193 (#set! injection.language "javascript"))
11194 "#,
11195 )
11196 .unwrap(),
11197 );
11198
11199 let javascript_language = Arc::new(Language::new(
11200 LanguageConfig {
11201 name: "JavaScript".into(),
11202 brackets: BracketPairConfig {
11203 pairs: vec![
11204 BracketPair {
11205 start: "/*".into(),
11206 end: " */".into(),
11207 close: true,
11208 ..Default::default()
11209 },
11210 BracketPair {
11211 start: "{".into(),
11212 end: "}".into(),
11213 close: true,
11214 ..Default::default()
11215 },
11216 BracketPair {
11217 start: "(".into(),
11218 end: ")".into(),
11219 close: true,
11220 ..Default::default()
11221 },
11222 ],
11223 ..Default::default()
11224 },
11225 autoclose_before: "})]>".into(),
11226 ..Default::default()
11227 },
11228 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11229 ));
11230
11231 cx.language_registry().add(html_language.clone());
11232 cx.language_registry().add(javascript_language);
11233 cx.executor().run_until_parked();
11234
11235 cx.update_buffer(|buffer, cx| {
11236 buffer.set_language(Some(html_language), cx);
11237 });
11238
11239 cx.set_state(
11240 &r#"
11241 <body>ˇ
11242 <script>
11243 var x = 1;ˇ
11244 </script>
11245 </body>ˇ
11246 "#
11247 .unindent(),
11248 );
11249
11250 // Precondition: different languages are active at different locations.
11251 cx.update_editor(|editor, window, cx| {
11252 let snapshot = editor.snapshot(window, cx);
11253 let cursors = editor
11254 .selections
11255 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
11256 let languages = cursors
11257 .iter()
11258 .map(|c| snapshot.language_at(c.start).unwrap().name())
11259 .collect::<Vec<_>>();
11260 assert_eq!(
11261 languages,
11262 &["HTML".into(), "JavaScript".into(), "HTML".into()]
11263 );
11264 });
11265
11266 // Angle brackets autoclose in HTML, but not JavaScript.
11267 cx.update_editor(|editor, window, cx| {
11268 editor.handle_input("<", window, cx);
11269 editor.handle_input("a", window, cx);
11270 });
11271 cx.assert_editor_state(
11272 &r#"
11273 <body><aˇ>
11274 <script>
11275 var x = 1;<aˇ
11276 </script>
11277 </body><aˇ>
11278 "#
11279 .unindent(),
11280 );
11281
11282 // Curly braces and parens autoclose in both HTML and JavaScript.
11283 cx.update_editor(|editor, window, cx| {
11284 editor.handle_input(" b=", window, cx);
11285 editor.handle_input("{", window, cx);
11286 editor.handle_input("c", window, cx);
11287 editor.handle_input("(", window, cx);
11288 });
11289 cx.assert_editor_state(
11290 &r#"
11291 <body><a b={c(ˇ)}>
11292 <script>
11293 var x = 1;<a b={c(ˇ)}
11294 </script>
11295 </body><a b={c(ˇ)}>
11296 "#
11297 .unindent(),
11298 );
11299
11300 // Brackets that were already autoclosed are skipped.
11301 cx.update_editor(|editor, window, cx| {
11302 editor.handle_input(")", window, cx);
11303 editor.handle_input("d", window, cx);
11304 editor.handle_input("}", window, cx);
11305 });
11306 cx.assert_editor_state(
11307 &r#"
11308 <body><a b={c()d}ˇ>
11309 <script>
11310 var x = 1;<a b={c()d}ˇ
11311 </script>
11312 </body><a b={c()d}ˇ>
11313 "#
11314 .unindent(),
11315 );
11316 cx.update_editor(|editor, window, cx| {
11317 editor.handle_input(">", window, cx);
11318 });
11319 cx.assert_editor_state(
11320 &r#"
11321 <body><a b={c()d}>ˇ
11322 <script>
11323 var x = 1;<a b={c()d}>ˇ
11324 </script>
11325 </body><a b={c()d}>ˇ
11326 "#
11327 .unindent(),
11328 );
11329
11330 // Reset
11331 cx.set_state(
11332 &r#"
11333 <body>ˇ
11334 <script>
11335 var x = 1;ˇ
11336 </script>
11337 </body>ˇ
11338 "#
11339 .unindent(),
11340 );
11341
11342 cx.update_editor(|editor, window, cx| {
11343 editor.handle_input("<", window, cx);
11344 });
11345 cx.assert_editor_state(
11346 &r#"
11347 <body><ˇ>
11348 <script>
11349 var x = 1;<ˇ
11350 </script>
11351 </body><ˇ>
11352 "#
11353 .unindent(),
11354 );
11355
11356 // When backspacing, the closing angle brackets are removed.
11357 cx.update_editor(|editor, window, cx| {
11358 editor.backspace(&Backspace, window, cx);
11359 });
11360 cx.assert_editor_state(
11361 &r#"
11362 <body>ˇ
11363 <script>
11364 var x = 1;ˇ
11365 </script>
11366 </body>ˇ
11367 "#
11368 .unindent(),
11369 );
11370
11371 // Block comments autoclose in JavaScript, but not HTML.
11372 cx.update_editor(|editor, window, cx| {
11373 editor.handle_input("/", window, cx);
11374 editor.handle_input("*", window, cx);
11375 });
11376 cx.assert_editor_state(
11377 &r#"
11378 <body>/*ˇ
11379 <script>
11380 var x = 1;/*ˇ */
11381 </script>
11382 </body>/*ˇ
11383 "#
11384 .unindent(),
11385 );
11386}
11387
11388#[gpui::test]
11389async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
11390 init_test(cx, |_| {});
11391
11392 let mut cx = EditorTestContext::new(cx).await;
11393
11394 let rust_language = Arc::new(
11395 Language::new(
11396 LanguageConfig {
11397 name: "Rust".into(),
11398 brackets: serde_json::from_value(json!([
11399 { "start": "{", "end": "}", "close": true, "newline": true },
11400 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
11401 ]))
11402 .unwrap(),
11403 autoclose_before: "})]>".into(),
11404 ..Default::default()
11405 },
11406 Some(tree_sitter_rust::LANGUAGE.into()),
11407 )
11408 .with_override_query("(string_literal) @string")
11409 .unwrap(),
11410 );
11411
11412 cx.language_registry().add(rust_language.clone());
11413 cx.update_buffer(|buffer, cx| {
11414 buffer.set_language(Some(rust_language), cx);
11415 });
11416
11417 cx.set_state(
11418 &r#"
11419 let x = ˇ
11420 "#
11421 .unindent(),
11422 );
11423
11424 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
11425 cx.update_editor(|editor, window, cx| {
11426 editor.handle_input("\"", window, cx);
11427 });
11428 cx.assert_editor_state(
11429 &r#"
11430 let x = "ˇ"
11431 "#
11432 .unindent(),
11433 );
11434
11435 // Inserting another quotation mark. The cursor moves across the existing
11436 // automatically-inserted quotation mark.
11437 cx.update_editor(|editor, window, cx| {
11438 editor.handle_input("\"", window, cx);
11439 });
11440 cx.assert_editor_state(
11441 &r#"
11442 let x = ""ˇ
11443 "#
11444 .unindent(),
11445 );
11446
11447 // Reset
11448 cx.set_state(
11449 &r#"
11450 let x = ˇ
11451 "#
11452 .unindent(),
11453 );
11454
11455 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
11456 cx.update_editor(|editor, window, cx| {
11457 editor.handle_input("\"", window, cx);
11458 editor.handle_input(" ", window, cx);
11459 editor.move_left(&Default::default(), window, cx);
11460 editor.handle_input("\\", window, cx);
11461 editor.handle_input("\"", window, cx);
11462 });
11463 cx.assert_editor_state(
11464 &r#"
11465 let x = "\"ˇ "
11466 "#
11467 .unindent(),
11468 );
11469
11470 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
11471 // mark. Nothing is inserted.
11472 cx.update_editor(|editor, window, cx| {
11473 editor.move_right(&Default::default(), window, cx);
11474 editor.handle_input("\"", window, cx);
11475 });
11476 cx.assert_editor_state(
11477 &r#"
11478 let x = "\" "ˇ
11479 "#
11480 .unindent(),
11481 );
11482}
11483
11484#[gpui::test]
11485async fn test_autoclose_quotes_with_scope_awareness(cx: &mut TestAppContext) {
11486 init_test(cx, |_| {});
11487
11488 let mut cx = EditorTestContext::new(cx).await;
11489 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
11490
11491 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11492
11493 // Double quote inside single-quoted string
11494 cx.set_state(indoc! {r#"
11495 def main():
11496 items = ['"', ˇ]
11497 "#});
11498 cx.update_editor(|editor, window, cx| {
11499 editor.handle_input("\"", window, cx);
11500 });
11501 cx.assert_editor_state(indoc! {r#"
11502 def main():
11503 items = ['"', "ˇ"]
11504 "#});
11505
11506 // Two double quotes inside single-quoted string
11507 cx.set_state(indoc! {r#"
11508 def main():
11509 items = ['""', ˇ]
11510 "#});
11511 cx.update_editor(|editor, window, cx| {
11512 editor.handle_input("\"", window, cx);
11513 });
11514 cx.assert_editor_state(indoc! {r#"
11515 def main():
11516 items = ['""', "ˇ"]
11517 "#});
11518
11519 // Single quote inside double-quoted string
11520 cx.set_state(indoc! {r#"
11521 def main():
11522 items = ["'", ˇ]
11523 "#});
11524 cx.update_editor(|editor, window, cx| {
11525 editor.handle_input("'", window, cx);
11526 });
11527 cx.assert_editor_state(indoc! {r#"
11528 def main():
11529 items = ["'", 'ˇ']
11530 "#});
11531
11532 // Two single quotes inside double-quoted string
11533 cx.set_state(indoc! {r#"
11534 def main():
11535 items = ["''", ˇ]
11536 "#});
11537 cx.update_editor(|editor, window, cx| {
11538 editor.handle_input("'", window, cx);
11539 });
11540 cx.assert_editor_state(indoc! {r#"
11541 def main():
11542 items = ["''", 'ˇ']
11543 "#});
11544
11545 // Mixed quotes on same line
11546 cx.set_state(indoc! {r#"
11547 def main():
11548 items = ['"""', "'''''", ˇ]
11549 "#});
11550 cx.update_editor(|editor, window, cx| {
11551 editor.handle_input("\"", window, cx);
11552 });
11553 cx.assert_editor_state(indoc! {r#"
11554 def main():
11555 items = ['"""', "'''''", "ˇ"]
11556 "#});
11557 cx.update_editor(|editor, window, cx| {
11558 editor.move_right(&MoveRight, window, cx);
11559 });
11560 cx.update_editor(|editor, window, cx| {
11561 editor.handle_input(", ", window, cx);
11562 });
11563 cx.update_editor(|editor, window, cx| {
11564 editor.handle_input("'", window, cx);
11565 });
11566 cx.assert_editor_state(indoc! {r#"
11567 def main():
11568 items = ['"""', "'''''", "", 'ˇ']
11569 "#});
11570}
11571
11572#[gpui::test]
11573async fn test_autoclose_quotes_with_multibyte_characters(cx: &mut TestAppContext) {
11574 init_test(cx, |_| {});
11575
11576 let mut cx = EditorTestContext::new(cx).await;
11577 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
11578 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11579
11580 cx.set_state(indoc! {r#"
11581 def main():
11582 items = ["🎉", ˇ]
11583 "#});
11584 cx.update_editor(|editor, window, cx| {
11585 editor.handle_input("\"", window, cx);
11586 });
11587 cx.assert_editor_state(indoc! {r#"
11588 def main():
11589 items = ["🎉", "ˇ"]
11590 "#});
11591}
11592
11593#[gpui::test]
11594async fn test_surround_with_pair(cx: &mut TestAppContext) {
11595 init_test(cx, |_| {});
11596
11597 let language = Arc::new(Language::new(
11598 LanguageConfig {
11599 brackets: BracketPairConfig {
11600 pairs: vec![
11601 BracketPair {
11602 start: "{".to_string(),
11603 end: "}".to_string(),
11604 close: true,
11605 surround: true,
11606 newline: true,
11607 },
11608 BracketPair {
11609 start: "/* ".to_string(),
11610 end: "*/".to_string(),
11611 close: true,
11612 surround: true,
11613 ..Default::default()
11614 },
11615 ],
11616 ..Default::default()
11617 },
11618 ..Default::default()
11619 },
11620 Some(tree_sitter_rust::LANGUAGE.into()),
11621 ));
11622
11623 let text = r#"
11624 a
11625 b
11626 c
11627 "#
11628 .unindent();
11629
11630 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11631 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11632 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11633 editor
11634 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11635 .await;
11636
11637 editor.update_in(cx, |editor, window, cx| {
11638 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11639 s.select_display_ranges([
11640 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11641 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11642 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
11643 ])
11644 });
11645
11646 editor.handle_input("{", window, cx);
11647 editor.handle_input("{", window, cx);
11648 editor.handle_input("{", window, cx);
11649 assert_eq!(
11650 editor.text(cx),
11651 "
11652 {{{a}}}
11653 {{{b}}}
11654 {{{c}}}
11655 "
11656 .unindent()
11657 );
11658 assert_eq!(
11659 display_ranges(editor, cx),
11660 [
11661 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
11662 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
11663 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
11664 ]
11665 );
11666
11667 editor.undo(&Undo, window, cx);
11668 editor.undo(&Undo, window, cx);
11669 editor.undo(&Undo, window, cx);
11670 assert_eq!(
11671 editor.text(cx),
11672 "
11673 a
11674 b
11675 c
11676 "
11677 .unindent()
11678 );
11679 assert_eq!(
11680 display_ranges(editor, cx),
11681 [
11682 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11683 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11684 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11685 ]
11686 );
11687
11688 // Ensure inserting the first character of a multi-byte bracket pair
11689 // doesn't surround the selections with the bracket.
11690 editor.handle_input("/", window, cx);
11691 assert_eq!(
11692 editor.text(cx),
11693 "
11694 /
11695 /
11696 /
11697 "
11698 .unindent()
11699 );
11700 assert_eq!(
11701 display_ranges(editor, cx),
11702 [
11703 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11704 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11705 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11706 ]
11707 );
11708
11709 editor.undo(&Undo, window, cx);
11710 assert_eq!(
11711 editor.text(cx),
11712 "
11713 a
11714 b
11715 c
11716 "
11717 .unindent()
11718 );
11719 assert_eq!(
11720 display_ranges(editor, cx),
11721 [
11722 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11723 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11724 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11725 ]
11726 );
11727
11728 // Ensure inserting the last character of a multi-byte bracket pair
11729 // doesn't surround the selections with the bracket.
11730 editor.handle_input("*", window, cx);
11731 assert_eq!(
11732 editor.text(cx),
11733 "
11734 *
11735 *
11736 *
11737 "
11738 .unindent()
11739 );
11740 assert_eq!(
11741 display_ranges(editor, cx),
11742 [
11743 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11744 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11745 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11746 ]
11747 );
11748 });
11749}
11750
11751#[gpui::test]
11752async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
11753 init_test(cx, |_| {});
11754
11755 let language = Arc::new(Language::new(
11756 LanguageConfig {
11757 brackets: BracketPairConfig {
11758 pairs: vec![BracketPair {
11759 start: "{".to_string(),
11760 end: "}".to_string(),
11761 close: true,
11762 surround: true,
11763 newline: true,
11764 }],
11765 ..Default::default()
11766 },
11767 autoclose_before: "}".to_string(),
11768 ..Default::default()
11769 },
11770 Some(tree_sitter_rust::LANGUAGE.into()),
11771 ));
11772
11773 let text = r#"
11774 a
11775 b
11776 c
11777 "#
11778 .unindent();
11779
11780 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11781 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11782 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11783 editor
11784 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11785 .await;
11786
11787 editor.update_in(cx, |editor, window, cx| {
11788 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11789 s.select_ranges([
11790 Point::new(0, 1)..Point::new(0, 1),
11791 Point::new(1, 1)..Point::new(1, 1),
11792 Point::new(2, 1)..Point::new(2, 1),
11793 ])
11794 });
11795
11796 editor.handle_input("{", window, cx);
11797 editor.handle_input("{", window, cx);
11798 editor.handle_input("_", window, cx);
11799 assert_eq!(
11800 editor.text(cx),
11801 "
11802 a{{_}}
11803 b{{_}}
11804 c{{_}}
11805 "
11806 .unindent()
11807 );
11808 assert_eq!(
11809 editor
11810 .selections
11811 .ranges::<Point>(&editor.display_snapshot(cx)),
11812 [
11813 Point::new(0, 4)..Point::new(0, 4),
11814 Point::new(1, 4)..Point::new(1, 4),
11815 Point::new(2, 4)..Point::new(2, 4)
11816 ]
11817 );
11818
11819 editor.backspace(&Default::default(), window, cx);
11820 editor.backspace(&Default::default(), window, cx);
11821 assert_eq!(
11822 editor.text(cx),
11823 "
11824 a{}
11825 b{}
11826 c{}
11827 "
11828 .unindent()
11829 );
11830 assert_eq!(
11831 editor
11832 .selections
11833 .ranges::<Point>(&editor.display_snapshot(cx)),
11834 [
11835 Point::new(0, 2)..Point::new(0, 2),
11836 Point::new(1, 2)..Point::new(1, 2),
11837 Point::new(2, 2)..Point::new(2, 2)
11838 ]
11839 );
11840
11841 editor.delete_to_previous_word_start(&Default::default(), window, cx);
11842 assert_eq!(
11843 editor.text(cx),
11844 "
11845 a
11846 b
11847 c
11848 "
11849 .unindent()
11850 );
11851 assert_eq!(
11852 editor
11853 .selections
11854 .ranges::<Point>(&editor.display_snapshot(cx)),
11855 [
11856 Point::new(0, 1)..Point::new(0, 1),
11857 Point::new(1, 1)..Point::new(1, 1),
11858 Point::new(2, 1)..Point::new(2, 1)
11859 ]
11860 );
11861 });
11862}
11863
11864#[gpui::test]
11865async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
11866 init_test(cx, |settings| {
11867 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11868 });
11869
11870 let mut cx = EditorTestContext::new(cx).await;
11871
11872 let language = Arc::new(Language::new(
11873 LanguageConfig {
11874 brackets: BracketPairConfig {
11875 pairs: vec![
11876 BracketPair {
11877 start: "{".to_string(),
11878 end: "}".to_string(),
11879 close: true,
11880 surround: true,
11881 newline: true,
11882 },
11883 BracketPair {
11884 start: "(".to_string(),
11885 end: ")".to_string(),
11886 close: true,
11887 surround: true,
11888 newline: true,
11889 },
11890 BracketPair {
11891 start: "[".to_string(),
11892 end: "]".to_string(),
11893 close: false,
11894 surround: true,
11895 newline: true,
11896 },
11897 ],
11898 ..Default::default()
11899 },
11900 autoclose_before: "})]".to_string(),
11901 ..Default::default()
11902 },
11903 Some(tree_sitter_rust::LANGUAGE.into()),
11904 ));
11905
11906 cx.language_registry().add(language.clone());
11907 cx.update_buffer(|buffer, cx| {
11908 buffer.set_language(Some(language), cx);
11909 });
11910
11911 cx.set_state(
11912 &"
11913 {(ˇ)}
11914 [[ˇ]]
11915 {(ˇ)}
11916 "
11917 .unindent(),
11918 );
11919
11920 cx.update_editor(|editor, window, cx| {
11921 editor.backspace(&Default::default(), window, cx);
11922 editor.backspace(&Default::default(), window, cx);
11923 });
11924
11925 cx.assert_editor_state(
11926 &"
11927 ˇ
11928 ˇ]]
11929 ˇ
11930 "
11931 .unindent(),
11932 );
11933
11934 cx.update_editor(|editor, window, cx| {
11935 editor.handle_input("{", window, cx);
11936 editor.handle_input("{", window, cx);
11937 editor.move_right(&MoveRight, window, cx);
11938 editor.move_right(&MoveRight, window, cx);
11939 editor.move_left(&MoveLeft, window, cx);
11940 editor.move_left(&MoveLeft, window, cx);
11941 editor.backspace(&Default::default(), window, cx);
11942 });
11943
11944 cx.assert_editor_state(
11945 &"
11946 {ˇ}
11947 {ˇ}]]
11948 {ˇ}
11949 "
11950 .unindent(),
11951 );
11952
11953 cx.update_editor(|editor, window, cx| {
11954 editor.backspace(&Default::default(), window, cx);
11955 });
11956
11957 cx.assert_editor_state(
11958 &"
11959 ˇ
11960 ˇ]]
11961 ˇ
11962 "
11963 .unindent(),
11964 );
11965}
11966
11967#[gpui::test]
11968async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11969 init_test(cx, |_| {});
11970
11971 let language = Arc::new(Language::new(
11972 LanguageConfig::default(),
11973 Some(tree_sitter_rust::LANGUAGE.into()),
11974 ));
11975
11976 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11977 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11978 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11979 editor
11980 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11981 .await;
11982
11983 editor.update_in(cx, |editor, window, cx| {
11984 editor.set_auto_replace_emoji_shortcode(true);
11985
11986 editor.handle_input("Hello ", window, cx);
11987 editor.handle_input(":wave", window, cx);
11988 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11989
11990 editor.handle_input(":", window, cx);
11991 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11992
11993 editor.handle_input(" :smile", window, cx);
11994 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11995
11996 editor.handle_input(":", window, cx);
11997 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11998
11999 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
12000 editor.handle_input(":wave", window, cx);
12001 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
12002
12003 editor.handle_input(":", window, cx);
12004 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
12005
12006 editor.handle_input(":1", window, cx);
12007 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
12008
12009 editor.handle_input(":", window, cx);
12010 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
12011
12012 // Ensure shortcode does not get replaced when it is part of a word
12013 editor.handle_input(" Test:wave", window, cx);
12014 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
12015
12016 editor.handle_input(":", window, cx);
12017 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
12018
12019 editor.set_auto_replace_emoji_shortcode(false);
12020
12021 // Ensure shortcode does not get replaced when auto replace is off
12022 editor.handle_input(" :wave", window, cx);
12023 assert_eq!(
12024 editor.text(cx),
12025 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
12026 );
12027
12028 editor.handle_input(":", window, cx);
12029 assert_eq!(
12030 editor.text(cx),
12031 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
12032 );
12033 });
12034}
12035
12036#[gpui::test]
12037async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
12038 init_test(cx, |_| {});
12039
12040 let (text, insertion_ranges) = marked_text_ranges(
12041 indoc! {"
12042 ˇ
12043 "},
12044 false,
12045 );
12046
12047 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
12048 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12049
12050 _ = editor.update_in(cx, |editor, window, cx| {
12051 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
12052
12053 editor
12054 .insert_snippet(
12055 &insertion_ranges
12056 .iter()
12057 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
12058 .collect::<Vec<_>>(),
12059 snippet,
12060 window,
12061 cx,
12062 )
12063 .unwrap();
12064
12065 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
12066 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
12067 assert_eq!(editor.text(cx), expected_text);
12068 assert_eq!(
12069 editor.selections.ranges(&editor.display_snapshot(cx)),
12070 selection_ranges
12071 .iter()
12072 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
12073 .collect::<Vec<_>>()
12074 );
12075 }
12076
12077 assert(
12078 editor,
12079 cx,
12080 indoc! {"
12081 type «» =•
12082 "},
12083 );
12084
12085 assert!(editor.context_menu_visible(), "There should be a matches");
12086 });
12087}
12088
12089#[gpui::test]
12090async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
12091 init_test(cx, |_| {});
12092
12093 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
12094 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
12095 assert_eq!(editor.text(cx), expected_text);
12096 assert_eq!(
12097 editor.selections.ranges(&editor.display_snapshot(cx)),
12098 selection_ranges
12099 .iter()
12100 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
12101 .collect::<Vec<_>>()
12102 );
12103 }
12104
12105 let (text, insertion_ranges) = marked_text_ranges(
12106 indoc! {"
12107 ˇ
12108 "},
12109 false,
12110 );
12111
12112 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
12113 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12114
12115 _ = editor.update_in(cx, |editor, window, cx| {
12116 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
12117
12118 editor
12119 .insert_snippet(
12120 &insertion_ranges
12121 .iter()
12122 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
12123 .collect::<Vec<_>>(),
12124 snippet,
12125 window,
12126 cx,
12127 )
12128 .unwrap();
12129
12130 assert_state(
12131 editor,
12132 cx,
12133 indoc! {"
12134 type «» = ;•
12135 "},
12136 );
12137
12138 assert!(
12139 editor.context_menu_visible(),
12140 "Context menu should be visible for placeholder choices"
12141 );
12142
12143 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
12144
12145 assert_state(
12146 editor,
12147 cx,
12148 indoc! {"
12149 type = «»;•
12150 "},
12151 );
12152
12153 assert!(
12154 !editor.context_menu_visible(),
12155 "Context menu should be hidden after moving to next tabstop"
12156 );
12157
12158 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
12159
12160 assert_state(
12161 editor,
12162 cx,
12163 indoc! {"
12164 type = ; ˇ
12165 "},
12166 );
12167
12168 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
12169
12170 assert_state(
12171 editor,
12172 cx,
12173 indoc! {"
12174 type = ; ˇ
12175 "},
12176 );
12177 });
12178
12179 _ = editor.update_in(cx, |editor, window, cx| {
12180 editor.select_all(&SelectAll, window, cx);
12181 editor.backspace(&Backspace, window, cx);
12182
12183 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
12184 let insertion_ranges = editor
12185 .selections
12186 .all(&editor.display_snapshot(cx))
12187 .iter()
12188 .map(|s| s.range())
12189 .collect::<Vec<_>>();
12190
12191 editor
12192 .insert_snippet(&insertion_ranges, snippet, window, cx)
12193 .unwrap();
12194
12195 assert_state(editor, cx, "fn «» = value;•");
12196
12197 assert!(
12198 editor.context_menu_visible(),
12199 "Context menu should be visible for placeholder choices"
12200 );
12201
12202 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
12203
12204 assert_state(editor, cx, "fn = «valueˇ»;•");
12205
12206 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
12207
12208 assert_state(editor, cx, "fn «» = value;•");
12209
12210 assert!(
12211 editor.context_menu_visible(),
12212 "Context menu should be visible again after returning to first tabstop"
12213 );
12214
12215 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
12216
12217 assert_state(editor, cx, "fn «» = value;•");
12218 });
12219}
12220
12221#[gpui::test]
12222async fn test_snippets(cx: &mut TestAppContext) {
12223 init_test(cx, |_| {});
12224
12225 let mut cx = EditorTestContext::new(cx).await;
12226
12227 cx.set_state(indoc! {"
12228 a.ˇ b
12229 a.ˇ b
12230 a.ˇ b
12231 "});
12232
12233 cx.update_editor(|editor, window, cx| {
12234 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
12235 let insertion_ranges = editor
12236 .selections
12237 .all(&editor.display_snapshot(cx))
12238 .iter()
12239 .map(|s| s.range())
12240 .collect::<Vec<_>>();
12241 editor
12242 .insert_snippet(&insertion_ranges, snippet, window, cx)
12243 .unwrap();
12244 });
12245
12246 cx.assert_editor_state(indoc! {"
12247 a.f(«oneˇ», two, «threeˇ») b
12248 a.f(«oneˇ», two, «threeˇ») b
12249 a.f(«oneˇ», two, «threeˇ») b
12250 "});
12251
12252 // Can't move earlier than the first tab stop
12253 cx.update_editor(|editor, window, cx| {
12254 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
12255 });
12256 cx.assert_editor_state(indoc! {"
12257 a.f(«oneˇ», two, «threeˇ») b
12258 a.f(«oneˇ», two, «threeˇ») b
12259 a.f(«oneˇ», two, «threeˇ») b
12260 "});
12261
12262 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
12263 cx.assert_editor_state(indoc! {"
12264 a.f(one, «twoˇ», three) b
12265 a.f(one, «twoˇ», three) b
12266 a.f(one, «twoˇ», three) b
12267 "});
12268
12269 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
12270 cx.assert_editor_state(indoc! {"
12271 a.f(«oneˇ», two, «threeˇ») b
12272 a.f(«oneˇ», two, «threeˇ») b
12273 a.f(«oneˇ», two, «threeˇ») b
12274 "});
12275
12276 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
12277 cx.assert_editor_state(indoc! {"
12278 a.f(one, «twoˇ», three) b
12279 a.f(one, «twoˇ», three) b
12280 a.f(one, «twoˇ», three) b
12281 "});
12282 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
12283 cx.assert_editor_state(indoc! {"
12284 a.f(one, two, three)ˇ b
12285 a.f(one, two, three)ˇ b
12286 a.f(one, two, three)ˇ b
12287 "});
12288
12289 // As soon as the last tab stop is reached, snippet state is gone
12290 cx.update_editor(|editor, window, cx| {
12291 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
12292 });
12293 cx.assert_editor_state(indoc! {"
12294 a.f(one, two, three)ˇ b
12295 a.f(one, two, three)ˇ b
12296 a.f(one, two, three)ˇ b
12297 "});
12298}
12299
12300#[gpui::test]
12301async fn test_snippet_indentation(cx: &mut TestAppContext) {
12302 init_test(cx, |_| {});
12303
12304 let mut cx = EditorTestContext::new(cx).await;
12305
12306 cx.update_editor(|editor, window, cx| {
12307 let snippet = Snippet::parse(indoc! {"
12308 /*
12309 * Multiline comment with leading indentation
12310 *
12311 * $1
12312 */
12313 $0"})
12314 .unwrap();
12315 let insertion_ranges = editor
12316 .selections
12317 .all(&editor.display_snapshot(cx))
12318 .iter()
12319 .map(|s| s.range())
12320 .collect::<Vec<_>>();
12321 editor
12322 .insert_snippet(&insertion_ranges, snippet, window, cx)
12323 .unwrap();
12324 });
12325
12326 cx.assert_editor_state(indoc! {"
12327 /*
12328 * Multiline comment with leading indentation
12329 *
12330 * ˇ
12331 */
12332 "});
12333
12334 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
12335 cx.assert_editor_state(indoc! {"
12336 /*
12337 * Multiline comment with leading indentation
12338 *
12339 *•
12340 */
12341 ˇ"});
12342}
12343
12344#[gpui::test]
12345async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
12346 init_test(cx, |_| {});
12347
12348 let mut cx = EditorTestContext::new(cx).await;
12349 cx.update_editor(|editor, _, cx| {
12350 editor.project().unwrap().update(cx, |project, cx| {
12351 project.snippets().update(cx, |snippets, _cx| {
12352 let snippet = project::snippet_provider::Snippet {
12353 prefix: vec!["multi word".to_string()],
12354 body: "this is many words".to_string(),
12355 description: Some("description".to_string()),
12356 name: "multi-word snippet test".to_string(),
12357 };
12358 snippets.add_snippet_for_test(
12359 None,
12360 PathBuf::from("test_snippets.json"),
12361 vec![Arc::new(snippet)],
12362 );
12363 });
12364 })
12365 });
12366
12367 for (input_to_simulate, should_match_snippet) in [
12368 ("m", true),
12369 ("m ", true),
12370 ("m w", true),
12371 ("aa m w", true),
12372 ("aa m g", false),
12373 ] {
12374 cx.set_state("ˇ");
12375 cx.simulate_input(input_to_simulate); // fails correctly
12376
12377 cx.update_editor(|editor, _, _| {
12378 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
12379 else {
12380 assert!(!should_match_snippet); // no completions! don't even show the menu
12381 return;
12382 };
12383 assert!(context_menu.visible());
12384 let completions = context_menu.completions.borrow();
12385
12386 assert_eq!(!completions.is_empty(), should_match_snippet);
12387 });
12388 }
12389}
12390
12391#[gpui::test]
12392async fn test_document_format_during_save(cx: &mut TestAppContext) {
12393 init_test(cx, |_| {});
12394
12395 let fs = FakeFs::new(cx.executor());
12396 fs.insert_file(path!("/file.rs"), Default::default()).await;
12397
12398 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
12399
12400 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12401 language_registry.add(rust_lang());
12402 let mut fake_servers = language_registry.register_fake_lsp(
12403 "Rust",
12404 FakeLspAdapter {
12405 capabilities: lsp::ServerCapabilities {
12406 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12407 ..Default::default()
12408 },
12409 ..Default::default()
12410 },
12411 );
12412
12413 let buffer = project
12414 .update(cx, |project, cx| {
12415 project.open_local_buffer(path!("/file.rs"), cx)
12416 })
12417 .await
12418 .unwrap();
12419
12420 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12421 let (editor, cx) = cx.add_window_view(|window, cx| {
12422 build_editor_with_project(project.clone(), buffer, window, cx)
12423 });
12424 editor.update_in(cx, |editor, window, cx| {
12425 editor.set_text("one\ntwo\nthree\n", window, cx)
12426 });
12427 assert!(cx.read(|cx| editor.is_dirty(cx)));
12428
12429 let fake_server = fake_servers.next().await.unwrap();
12430
12431 {
12432 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12433 move |params, _| async move {
12434 assert_eq!(
12435 params.text_document.uri,
12436 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12437 );
12438 assert_eq!(params.options.tab_size, 4);
12439 Ok(Some(vec![lsp::TextEdit::new(
12440 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12441 ", ".to_string(),
12442 )]))
12443 },
12444 );
12445 let save = editor
12446 .update_in(cx, |editor, window, cx| {
12447 editor.save(
12448 SaveOptions {
12449 format: true,
12450 autosave: false,
12451 },
12452 project.clone(),
12453 window,
12454 cx,
12455 )
12456 })
12457 .unwrap();
12458 save.await;
12459
12460 assert_eq!(
12461 editor.update(cx, |editor, cx| editor.text(cx)),
12462 "one, two\nthree\n"
12463 );
12464 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12465 }
12466
12467 {
12468 editor.update_in(cx, |editor, window, cx| {
12469 editor.set_text("one\ntwo\nthree\n", window, cx)
12470 });
12471 assert!(cx.read(|cx| editor.is_dirty(cx)));
12472
12473 // Ensure we can still save even if formatting hangs.
12474 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12475 move |params, _| async move {
12476 assert_eq!(
12477 params.text_document.uri,
12478 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12479 );
12480 futures::future::pending::<()>().await;
12481 unreachable!()
12482 },
12483 );
12484 let save = editor
12485 .update_in(cx, |editor, window, cx| {
12486 editor.save(
12487 SaveOptions {
12488 format: true,
12489 autosave: false,
12490 },
12491 project.clone(),
12492 window,
12493 cx,
12494 )
12495 })
12496 .unwrap();
12497 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12498 save.await;
12499 assert_eq!(
12500 editor.update(cx, |editor, cx| editor.text(cx)),
12501 "one\ntwo\nthree\n"
12502 );
12503 }
12504
12505 // Set rust language override and assert overridden tabsize is sent to language server
12506 update_test_language_settings(cx, |settings| {
12507 settings.languages.0.insert(
12508 "Rust".into(),
12509 LanguageSettingsContent {
12510 tab_size: NonZeroU32::new(8),
12511 ..Default::default()
12512 },
12513 );
12514 });
12515
12516 {
12517 editor.update_in(cx, |editor, window, cx| {
12518 editor.set_text("somehting_new\n", window, cx)
12519 });
12520 assert!(cx.read(|cx| editor.is_dirty(cx)));
12521 let _formatting_request_signal = fake_server
12522 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12523 assert_eq!(
12524 params.text_document.uri,
12525 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12526 );
12527 assert_eq!(params.options.tab_size, 8);
12528 Ok(Some(vec![]))
12529 });
12530 let save = editor
12531 .update_in(cx, |editor, window, cx| {
12532 editor.save(
12533 SaveOptions {
12534 format: true,
12535 autosave: false,
12536 },
12537 project.clone(),
12538 window,
12539 cx,
12540 )
12541 })
12542 .unwrap();
12543 save.await;
12544 }
12545}
12546
12547#[gpui::test]
12548async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
12549 init_test(cx, |settings| {
12550 settings.defaults.ensure_final_newline_on_save = Some(false);
12551 });
12552
12553 let fs = FakeFs::new(cx.executor());
12554 fs.insert_file(path!("/file.txt"), "foo".into()).await;
12555
12556 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
12557
12558 let buffer = project
12559 .update(cx, |project, cx| {
12560 project.open_local_buffer(path!("/file.txt"), cx)
12561 })
12562 .await
12563 .unwrap();
12564
12565 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12566 let (editor, cx) = cx.add_window_view(|window, cx| {
12567 build_editor_with_project(project.clone(), buffer, window, cx)
12568 });
12569 editor.update_in(cx, |editor, window, cx| {
12570 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
12571 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
12572 });
12573 });
12574 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12575
12576 editor.update_in(cx, |editor, window, cx| {
12577 editor.handle_input("\n", window, cx)
12578 });
12579 cx.run_until_parked();
12580 save(&editor, &project, cx).await;
12581 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
12582
12583 editor.update_in(cx, |editor, window, cx| {
12584 editor.undo(&Default::default(), window, cx);
12585 });
12586 save(&editor, &project, cx).await;
12587 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
12588
12589 editor.update_in(cx, |editor, window, cx| {
12590 editor.redo(&Default::default(), window, cx);
12591 });
12592 cx.run_until_parked();
12593 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
12594
12595 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
12596 let save = editor
12597 .update_in(cx, |editor, window, cx| {
12598 editor.save(
12599 SaveOptions {
12600 format: true,
12601 autosave: false,
12602 },
12603 project.clone(),
12604 window,
12605 cx,
12606 )
12607 })
12608 .unwrap();
12609 save.await;
12610 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12611 }
12612}
12613
12614#[gpui::test]
12615async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
12616 init_test(cx, |_| {});
12617
12618 let cols = 4;
12619 let rows = 10;
12620 let sample_text_1 = sample_text(rows, cols, 'a');
12621 assert_eq!(
12622 sample_text_1,
12623 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12624 );
12625 let sample_text_2 = sample_text(rows, cols, 'l');
12626 assert_eq!(
12627 sample_text_2,
12628 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12629 );
12630 let sample_text_3 = sample_text(rows, cols, 'v');
12631 assert_eq!(
12632 sample_text_3,
12633 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12634 );
12635
12636 let fs = FakeFs::new(cx.executor());
12637 fs.insert_tree(
12638 path!("/a"),
12639 json!({
12640 "main.rs": sample_text_1,
12641 "other.rs": sample_text_2,
12642 "lib.rs": sample_text_3,
12643 }),
12644 )
12645 .await;
12646
12647 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12648 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
12649 let cx = &mut VisualTestContext::from_window(*window, cx);
12650
12651 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12652 language_registry.add(rust_lang());
12653 let mut fake_servers = language_registry.register_fake_lsp(
12654 "Rust",
12655 FakeLspAdapter {
12656 capabilities: lsp::ServerCapabilities {
12657 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12658 ..Default::default()
12659 },
12660 ..Default::default()
12661 },
12662 );
12663
12664 let worktree = project.update(cx, |project, cx| {
12665 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
12666 assert_eq!(worktrees.len(), 1);
12667 worktrees.pop().unwrap()
12668 });
12669 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12670
12671 let buffer_1 = project
12672 .update(cx, |project, cx| {
12673 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
12674 })
12675 .await
12676 .unwrap();
12677 let buffer_2 = project
12678 .update(cx, |project, cx| {
12679 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
12680 })
12681 .await
12682 .unwrap();
12683 let buffer_3 = project
12684 .update(cx, |project, cx| {
12685 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
12686 })
12687 .await
12688 .unwrap();
12689
12690 let multi_buffer = cx.new(|cx| {
12691 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12692 multi_buffer.push_excerpts(
12693 buffer_1.clone(),
12694 [
12695 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12696 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12697 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12698 ],
12699 cx,
12700 );
12701 multi_buffer.push_excerpts(
12702 buffer_2.clone(),
12703 [
12704 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12705 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12706 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12707 ],
12708 cx,
12709 );
12710 multi_buffer.push_excerpts(
12711 buffer_3.clone(),
12712 [
12713 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12714 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12715 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12716 ],
12717 cx,
12718 );
12719 multi_buffer
12720 });
12721 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12722 Editor::new(
12723 EditorMode::full(),
12724 multi_buffer,
12725 Some(project.clone()),
12726 window,
12727 cx,
12728 )
12729 });
12730
12731 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12732 editor.change_selections(
12733 SelectionEffects::scroll(Autoscroll::Next),
12734 window,
12735 cx,
12736 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
12737 );
12738 editor.insert("|one|two|three|", window, cx);
12739 });
12740 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12741 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12742 editor.change_selections(
12743 SelectionEffects::scroll(Autoscroll::Next),
12744 window,
12745 cx,
12746 |s| s.select_ranges(Some(MultiBufferOffset(60)..MultiBufferOffset(70))),
12747 );
12748 editor.insert("|four|five|six|", window, cx);
12749 });
12750 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12751
12752 // First two buffers should be edited, but not the third one.
12753 assert_eq!(
12754 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12755 "a|one|two|three|aa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\nllll\nmmmm\nnnnn|four|five|six|\nr\n\nuuuu\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
12756 );
12757 buffer_1.update(cx, |buffer, _| {
12758 assert!(buffer.is_dirty());
12759 assert_eq!(
12760 buffer.text(),
12761 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
12762 )
12763 });
12764 buffer_2.update(cx, |buffer, _| {
12765 assert!(buffer.is_dirty());
12766 assert_eq!(
12767 buffer.text(),
12768 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
12769 )
12770 });
12771 buffer_3.update(cx, |buffer, _| {
12772 assert!(!buffer.is_dirty());
12773 assert_eq!(buffer.text(), sample_text_3,)
12774 });
12775 cx.executor().run_until_parked();
12776
12777 let save = multi_buffer_editor
12778 .update_in(cx, |editor, window, cx| {
12779 editor.save(
12780 SaveOptions {
12781 format: true,
12782 autosave: false,
12783 },
12784 project.clone(),
12785 window,
12786 cx,
12787 )
12788 })
12789 .unwrap();
12790
12791 let fake_server = fake_servers.next().await.unwrap();
12792 fake_server
12793 .server
12794 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
12795 Ok(Some(vec![lsp::TextEdit::new(
12796 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12797 format!("[{} formatted]", params.text_document.uri),
12798 )]))
12799 })
12800 .detach();
12801 save.await;
12802
12803 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
12804 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
12805 assert_eq!(
12806 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12807 uri!(
12808 "a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}"
12809 ),
12810 );
12811 buffer_1.update(cx, |buffer, _| {
12812 assert!(!buffer.is_dirty());
12813 assert_eq!(
12814 buffer.text(),
12815 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
12816 )
12817 });
12818 buffer_2.update(cx, |buffer, _| {
12819 assert!(!buffer.is_dirty());
12820 assert_eq!(
12821 buffer.text(),
12822 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
12823 )
12824 });
12825 buffer_3.update(cx, |buffer, _| {
12826 assert!(!buffer.is_dirty());
12827 assert_eq!(buffer.text(), sample_text_3,)
12828 });
12829}
12830
12831#[gpui::test]
12832async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
12833 init_test(cx, |_| {});
12834
12835 let fs = FakeFs::new(cx.executor());
12836 fs.insert_tree(
12837 path!("/dir"),
12838 json!({
12839 "file1.rs": "fn main() { println!(\"hello\"); }",
12840 "file2.rs": "fn test() { println!(\"test\"); }",
12841 "file3.rs": "fn other() { println!(\"other\"); }\n",
12842 }),
12843 )
12844 .await;
12845
12846 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
12847 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
12848 let cx = &mut VisualTestContext::from_window(*window, cx);
12849
12850 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12851 language_registry.add(rust_lang());
12852
12853 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
12854 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12855
12856 // Open three buffers
12857 let buffer_1 = project
12858 .update(cx, |project, cx| {
12859 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
12860 })
12861 .await
12862 .unwrap();
12863 let buffer_2 = project
12864 .update(cx, |project, cx| {
12865 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
12866 })
12867 .await
12868 .unwrap();
12869 let buffer_3 = project
12870 .update(cx, |project, cx| {
12871 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
12872 })
12873 .await
12874 .unwrap();
12875
12876 // Create a multi-buffer with all three buffers
12877 let multi_buffer = cx.new(|cx| {
12878 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12879 multi_buffer.push_excerpts(
12880 buffer_1.clone(),
12881 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12882 cx,
12883 );
12884 multi_buffer.push_excerpts(
12885 buffer_2.clone(),
12886 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12887 cx,
12888 );
12889 multi_buffer.push_excerpts(
12890 buffer_3.clone(),
12891 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12892 cx,
12893 );
12894 multi_buffer
12895 });
12896
12897 let editor = cx.new_window_entity(|window, cx| {
12898 Editor::new(
12899 EditorMode::full(),
12900 multi_buffer,
12901 Some(project.clone()),
12902 window,
12903 cx,
12904 )
12905 });
12906
12907 // Edit only the first buffer
12908 editor.update_in(cx, |editor, window, cx| {
12909 editor.change_selections(
12910 SelectionEffects::scroll(Autoscroll::Next),
12911 window,
12912 cx,
12913 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
12914 );
12915 editor.insert("// edited", window, cx);
12916 });
12917
12918 // Verify that only buffer 1 is dirty
12919 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
12920 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12921 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12922
12923 // Get write counts after file creation (files were created with initial content)
12924 // We expect each file to have been written once during creation
12925 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12926 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12927 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12928
12929 // Perform autosave
12930 let save_task = editor.update_in(cx, |editor, window, cx| {
12931 editor.save(
12932 SaveOptions {
12933 format: true,
12934 autosave: true,
12935 },
12936 project.clone(),
12937 window,
12938 cx,
12939 )
12940 });
12941 save_task.await.unwrap();
12942
12943 // Only the dirty buffer should have been saved
12944 assert_eq!(
12945 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12946 1,
12947 "Buffer 1 was dirty, so it should have been written once during autosave"
12948 );
12949 assert_eq!(
12950 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12951 0,
12952 "Buffer 2 was clean, so it should not have been written during autosave"
12953 );
12954 assert_eq!(
12955 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12956 0,
12957 "Buffer 3 was clean, so it should not have been written during autosave"
12958 );
12959
12960 // Verify buffer states after autosave
12961 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12962 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12963 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12964
12965 // Now perform a manual save (format = true)
12966 let save_task = editor.update_in(cx, |editor, window, cx| {
12967 editor.save(
12968 SaveOptions {
12969 format: true,
12970 autosave: false,
12971 },
12972 project.clone(),
12973 window,
12974 cx,
12975 )
12976 });
12977 save_task.await.unwrap();
12978
12979 // During manual save, clean buffers don't get written to disk
12980 // They just get did_save called for language server notifications
12981 assert_eq!(
12982 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12983 1,
12984 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12985 );
12986 assert_eq!(
12987 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12988 0,
12989 "Buffer 2 should not have been written at all"
12990 );
12991 assert_eq!(
12992 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12993 0,
12994 "Buffer 3 should not have been written at all"
12995 );
12996}
12997
12998async fn setup_range_format_test(
12999 cx: &mut TestAppContext,
13000) -> (
13001 Entity<Project>,
13002 Entity<Editor>,
13003 &mut gpui::VisualTestContext,
13004 lsp::FakeLanguageServer,
13005) {
13006 init_test(cx, |_| {});
13007
13008 let fs = FakeFs::new(cx.executor());
13009 fs.insert_file(path!("/file.rs"), Default::default()).await;
13010
13011 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13012
13013 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13014 language_registry.add(rust_lang());
13015 let mut fake_servers = language_registry.register_fake_lsp(
13016 "Rust",
13017 FakeLspAdapter {
13018 capabilities: lsp::ServerCapabilities {
13019 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
13020 ..lsp::ServerCapabilities::default()
13021 },
13022 ..FakeLspAdapter::default()
13023 },
13024 );
13025
13026 let buffer = project
13027 .update(cx, |project, cx| {
13028 project.open_local_buffer(path!("/file.rs"), cx)
13029 })
13030 .await
13031 .unwrap();
13032
13033 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13034 let (editor, cx) = cx.add_window_view(|window, cx| {
13035 build_editor_with_project(project.clone(), buffer, window, cx)
13036 });
13037
13038 let fake_server = fake_servers.next().await.unwrap();
13039
13040 (project, editor, cx, fake_server)
13041}
13042
13043#[gpui::test]
13044async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
13045 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
13046
13047 editor.update_in(cx, |editor, window, cx| {
13048 editor.set_text("one\ntwo\nthree\n", window, cx)
13049 });
13050 assert!(cx.read(|cx| editor.is_dirty(cx)));
13051
13052 let save = editor
13053 .update_in(cx, |editor, window, cx| {
13054 editor.save(
13055 SaveOptions {
13056 format: true,
13057 autosave: false,
13058 },
13059 project.clone(),
13060 window,
13061 cx,
13062 )
13063 })
13064 .unwrap();
13065 fake_server
13066 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
13067 assert_eq!(
13068 params.text_document.uri,
13069 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13070 );
13071 assert_eq!(params.options.tab_size, 4);
13072 Ok(Some(vec![lsp::TextEdit::new(
13073 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
13074 ", ".to_string(),
13075 )]))
13076 })
13077 .next()
13078 .await;
13079 save.await;
13080 assert_eq!(
13081 editor.update(cx, |editor, cx| editor.text(cx)),
13082 "one, two\nthree\n"
13083 );
13084 assert!(!cx.read(|cx| editor.is_dirty(cx)));
13085}
13086
13087#[gpui::test]
13088async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
13089 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
13090
13091 editor.update_in(cx, |editor, window, cx| {
13092 editor.set_text("one\ntwo\nthree\n", window, cx)
13093 });
13094 assert!(cx.read(|cx| editor.is_dirty(cx)));
13095
13096 // Test that save still works when formatting hangs
13097 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
13098 move |params, _| async move {
13099 assert_eq!(
13100 params.text_document.uri,
13101 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13102 );
13103 futures::future::pending::<()>().await;
13104 unreachable!()
13105 },
13106 );
13107 let save = editor
13108 .update_in(cx, |editor, window, cx| {
13109 editor.save(
13110 SaveOptions {
13111 format: true,
13112 autosave: false,
13113 },
13114 project.clone(),
13115 window,
13116 cx,
13117 )
13118 })
13119 .unwrap();
13120 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
13121 save.await;
13122 assert_eq!(
13123 editor.update(cx, |editor, cx| editor.text(cx)),
13124 "one\ntwo\nthree\n"
13125 );
13126 assert!(!cx.read(|cx| editor.is_dirty(cx)));
13127}
13128
13129#[gpui::test]
13130async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
13131 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
13132
13133 // Buffer starts clean, no formatting should be requested
13134 let save = editor
13135 .update_in(cx, |editor, window, cx| {
13136 editor.save(
13137 SaveOptions {
13138 format: false,
13139 autosave: false,
13140 },
13141 project.clone(),
13142 window,
13143 cx,
13144 )
13145 })
13146 .unwrap();
13147 let _pending_format_request = fake_server
13148 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
13149 panic!("Should not be invoked");
13150 })
13151 .next();
13152 save.await;
13153 cx.run_until_parked();
13154}
13155
13156#[gpui::test]
13157async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
13158 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
13159
13160 // Set Rust language override and assert overridden tabsize is sent to language server
13161 update_test_language_settings(cx, |settings| {
13162 settings.languages.0.insert(
13163 "Rust".into(),
13164 LanguageSettingsContent {
13165 tab_size: NonZeroU32::new(8),
13166 ..Default::default()
13167 },
13168 );
13169 });
13170
13171 editor.update_in(cx, |editor, window, cx| {
13172 editor.set_text("something_new\n", window, cx)
13173 });
13174 assert!(cx.read(|cx| editor.is_dirty(cx)));
13175 let save = editor
13176 .update_in(cx, |editor, window, cx| {
13177 editor.save(
13178 SaveOptions {
13179 format: true,
13180 autosave: false,
13181 },
13182 project.clone(),
13183 window,
13184 cx,
13185 )
13186 })
13187 .unwrap();
13188 fake_server
13189 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
13190 assert_eq!(
13191 params.text_document.uri,
13192 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13193 );
13194 assert_eq!(params.options.tab_size, 8);
13195 Ok(Some(Vec::new()))
13196 })
13197 .next()
13198 .await;
13199 save.await;
13200}
13201
13202#[gpui::test]
13203async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
13204 init_test(cx, |settings| {
13205 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
13206 settings::LanguageServerFormatterSpecifier::Current,
13207 )))
13208 });
13209
13210 let fs = FakeFs::new(cx.executor());
13211 fs.insert_file(path!("/file.rs"), Default::default()).await;
13212
13213 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13214
13215 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13216 language_registry.add(Arc::new(Language::new(
13217 LanguageConfig {
13218 name: "Rust".into(),
13219 matcher: LanguageMatcher {
13220 path_suffixes: vec!["rs".to_string()],
13221 ..Default::default()
13222 },
13223 ..LanguageConfig::default()
13224 },
13225 Some(tree_sitter_rust::LANGUAGE.into()),
13226 )));
13227 update_test_language_settings(cx, |settings| {
13228 // Enable Prettier formatting for the same buffer, and ensure
13229 // LSP is called instead of Prettier.
13230 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
13231 });
13232 let mut fake_servers = language_registry.register_fake_lsp(
13233 "Rust",
13234 FakeLspAdapter {
13235 capabilities: lsp::ServerCapabilities {
13236 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13237 ..Default::default()
13238 },
13239 ..Default::default()
13240 },
13241 );
13242
13243 let buffer = project
13244 .update(cx, |project, cx| {
13245 project.open_local_buffer(path!("/file.rs"), cx)
13246 })
13247 .await
13248 .unwrap();
13249
13250 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13251 let (editor, cx) = cx.add_window_view(|window, cx| {
13252 build_editor_with_project(project.clone(), buffer, window, cx)
13253 });
13254 editor.update_in(cx, |editor, window, cx| {
13255 editor.set_text("one\ntwo\nthree\n", window, cx)
13256 });
13257
13258 let fake_server = fake_servers.next().await.unwrap();
13259
13260 let format = editor
13261 .update_in(cx, |editor, window, cx| {
13262 editor.perform_format(
13263 project.clone(),
13264 FormatTrigger::Manual,
13265 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
13266 window,
13267 cx,
13268 )
13269 })
13270 .unwrap();
13271 fake_server
13272 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
13273 assert_eq!(
13274 params.text_document.uri,
13275 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13276 );
13277 assert_eq!(params.options.tab_size, 4);
13278 Ok(Some(vec![lsp::TextEdit::new(
13279 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
13280 ", ".to_string(),
13281 )]))
13282 })
13283 .next()
13284 .await;
13285 format.await;
13286 assert_eq!(
13287 editor.update(cx, |editor, cx| editor.text(cx)),
13288 "one, two\nthree\n"
13289 );
13290
13291 editor.update_in(cx, |editor, window, cx| {
13292 editor.set_text("one\ntwo\nthree\n", window, cx)
13293 });
13294 // Ensure we don't lock if formatting hangs.
13295 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
13296 move |params, _| async move {
13297 assert_eq!(
13298 params.text_document.uri,
13299 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
13300 );
13301 futures::future::pending::<()>().await;
13302 unreachable!()
13303 },
13304 );
13305 let format = editor
13306 .update_in(cx, |editor, window, cx| {
13307 editor.perform_format(
13308 project,
13309 FormatTrigger::Manual,
13310 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
13311 window,
13312 cx,
13313 )
13314 })
13315 .unwrap();
13316 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
13317 format.await;
13318 assert_eq!(
13319 editor.update(cx, |editor, cx| editor.text(cx)),
13320 "one\ntwo\nthree\n"
13321 );
13322}
13323
13324#[gpui::test]
13325async fn test_multiple_formatters(cx: &mut TestAppContext) {
13326 init_test(cx, |settings| {
13327 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
13328 settings.defaults.formatter = Some(FormatterList::Vec(vec![
13329 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
13330 Formatter::CodeAction("code-action-1".into()),
13331 Formatter::CodeAction("code-action-2".into()),
13332 ]))
13333 });
13334
13335 let fs = FakeFs::new(cx.executor());
13336 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
13337 .await;
13338
13339 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13340 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13341 language_registry.add(rust_lang());
13342
13343 let mut fake_servers = language_registry.register_fake_lsp(
13344 "Rust",
13345 FakeLspAdapter {
13346 capabilities: lsp::ServerCapabilities {
13347 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13348 execute_command_provider: Some(lsp::ExecuteCommandOptions {
13349 commands: vec!["the-command-for-code-action-1".into()],
13350 ..Default::default()
13351 }),
13352 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
13353 ..Default::default()
13354 },
13355 ..Default::default()
13356 },
13357 );
13358
13359 let buffer = project
13360 .update(cx, |project, cx| {
13361 project.open_local_buffer(path!("/file.rs"), cx)
13362 })
13363 .await
13364 .unwrap();
13365
13366 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13367 let (editor, cx) = cx.add_window_view(|window, cx| {
13368 build_editor_with_project(project.clone(), buffer, window, cx)
13369 });
13370
13371 let fake_server = fake_servers.next().await.unwrap();
13372 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
13373 move |_params, _| async move {
13374 Ok(Some(vec![lsp::TextEdit::new(
13375 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
13376 "applied-formatting\n".to_string(),
13377 )]))
13378 },
13379 );
13380 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
13381 move |params, _| async move {
13382 let requested_code_actions = params.context.only.expect("Expected code action request");
13383 assert_eq!(requested_code_actions.len(), 1);
13384
13385 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
13386 let code_action = match requested_code_actions[0].as_str() {
13387 "code-action-1" => lsp::CodeAction {
13388 kind: Some("code-action-1".into()),
13389 edit: Some(lsp::WorkspaceEdit::new(
13390 [(
13391 uri,
13392 vec![lsp::TextEdit::new(
13393 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
13394 "applied-code-action-1-edit\n".to_string(),
13395 )],
13396 )]
13397 .into_iter()
13398 .collect(),
13399 )),
13400 command: Some(lsp::Command {
13401 command: "the-command-for-code-action-1".into(),
13402 ..Default::default()
13403 }),
13404 ..Default::default()
13405 },
13406 "code-action-2" => lsp::CodeAction {
13407 kind: Some("code-action-2".into()),
13408 edit: Some(lsp::WorkspaceEdit::new(
13409 [(
13410 uri,
13411 vec![lsp::TextEdit::new(
13412 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
13413 "applied-code-action-2-edit\n".to_string(),
13414 )],
13415 )]
13416 .into_iter()
13417 .collect(),
13418 )),
13419 ..Default::default()
13420 },
13421 req => panic!("Unexpected code action request: {:?}", req),
13422 };
13423 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
13424 code_action,
13425 )]))
13426 },
13427 );
13428
13429 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
13430 move |params, _| async move { Ok(params) }
13431 });
13432
13433 let command_lock = Arc::new(futures::lock::Mutex::new(()));
13434 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
13435 let fake = fake_server.clone();
13436 let lock = command_lock.clone();
13437 move |params, _| {
13438 assert_eq!(params.command, "the-command-for-code-action-1");
13439 let fake = fake.clone();
13440 let lock = lock.clone();
13441 async move {
13442 lock.lock().await;
13443 fake.server
13444 .request::<lsp::request::ApplyWorkspaceEdit>(
13445 lsp::ApplyWorkspaceEditParams {
13446 label: None,
13447 edit: lsp::WorkspaceEdit {
13448 changes: Some(
13449 [(
13450 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
13451 vec![lsp::TextEdit {
13452 range: lsp::Range::new(
13453 lsp::Position::new(0, 0),
13454 lsp::Position::new(0, 0),
13455 ),
13456 new_text: "applied-code-action-1-command\n".into(),
13457 }],
13458 )]
13459 .into_iter()
13460 .collect(),
13461 ),
13462 ..Default::default()
13463 },
13464 },
13465 DEFAULT_LSP_REQUEST_TIMEOUT,
13466 )
13467 .await
13468 .into_response()
13469 .unwrap();
13470 Ok(Some(json!(null)))
13471 }
13472 }
13473 });
13474
13475 editor
13476 .update_in(cx, |editor, window, cx| {
13477 editor.perform_format(
13478 project.clone(),
13479 FormatTrigger::Manual,
13480 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
13481 window,
13482 cx,
13483 )
13484 })
13485 .unwrap()
13486 .await;
13487 editor.update(cx, |editor, cx| {
13488 assert_eq!(
13489 editor.text(cx),
13490 r#"
13491 applied-code-action-2-edit
13492 applied-code-action-1-command
13493 applied-code-action-1-edit
13494 applied-formatting
13495 one
13496 two
13497 three
13498 "#
13499 .unindent()
13500 );
13501 });
13502
13503 editor.update_in(cx, |editor, window, cx| {
13504 editor.undo(&Default::default(), window, cx);
13505 assert_eq!(editor.text(cx), "one \ntwo \nthree");
13506 });
13507
13508 // Perform a manual edit while waiting for an LSP command
13509 // that's being run as part of a formatting code action.
13510 let lock_guard = command_lock.lock().await;
13511 let format = editor
13512 .update_in(cx, |editor, window, cx| {
13513 editor.perform_format(
13514 project.clone(),
13515 FormatTrigger::Manual,
13516 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
13517 window,
13518 cx,
13519 )
13520 })
13521 .unwrap();
13522 cx.run_until_parked();
13523 editor.update(cx, |editor, cx| {
13524 assert_eq!(
13525 editor.text(cx),
13526 r#"
13527 applied-code-action-1-edit
13528 applied-formatting
13529 one
13530 two
13531 three
13532 "#
13533 .unindent()
13534 );
13535
13536 editor.buffer.update(cx, |buffer, cx| {
13537 let ix = buffer.len(cx);
13538 buffer.edit([(ix..ix, "edited\n")], None, cx);
13539 });
13540 });
13541
13542 // Allow the LSP command to proceed. Because the buffer was edited,
13543 // the second code action will not be run.
13544 drop(lock_guard);
13545 format.await;
13546 editor.update_in(cx, |editor, window, cx| {
13547 assert_eq!(
13548 editor.text(cx),
13549 r#"
13550 applied-code-action-1-command
13551 applied-code-action-1-edit
13552 applied-formatting
13553 one
13554 two
13555 three
13556 edited
13557 "#
13558 .unindent()
13559 );
13560
13561 // The manual edit is undone first, because it is the last thing the user did
13562 // (even though the command completed afterwards).
13563 editor.undo(&Default::default(), window, cx);
13564 assert_eq!(
13565 editor.text(cx),
13566 r#"
13567 applied-code-action-1-command
13568 applied-code-action-1-edit
13569 applied-formatting
13570 one
13571 two
13572 three
13573 "#
13574 .unindent()
13575 );
13576
13577 // All the formatting (including the command, which completed after the manual edit)
13578 // is undone together.
13579 editor.undo(&Default::default(), window, cx);
13580 assert_eq!(editor.text(cx), "one \ntwo \nthree");
13581 });
13582}
13583
13584#[gpui::test]
13585async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
13586 init_test(cx, |settings| {
13587 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
13588 settings::LanguageServerFormatterSpecifier::Current,
13589 )]))
13590 });
13591
13592 let fs = FakeFs::new(cx.executor());
13593 fs.insert_file(path!("/file.ts"), Default::default()).await;
13594
13595 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13596
13597 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13598 language_registry.add(Arc::new(Language::new(
13599 LanguageConfig {
13600 name: "TypeScript".into(),
13601 matcher: LanguageMatcher {
13602 path_suffixes: vec!["ts".to_string()],
13603 ..Default::default()
13604 },
13605 ..LanguageConfig::default()
13606 },
13607 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13608 )));
13609 update_test_language_settings(cx, |settings| {
13610 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
13611 });
13612 let mut fake_servers = language_registry.register_fake_lsp(
13613 "TypeScript",
13614 FakeLspAdapter {
13615 capabilities: lsp::ServerCapabilities {
13616 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
13617 ..Default::default()
13618 },
13619 ..Default::default()
13620 },
13621 );
13622
13623 let buffer = project
13624 .update(cx, |project, cx| {
13625 project.open_local_buffer(path!("/file.ts"), cx)
13626 })
13627 .await
13628 .unwrap();
13629
13630 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13631 let (editor, cx) = cx.add_window_view(|window, cx| {
13632 build_editor_with_project(project.clone(), buffer, window, cx)
13633 });
13634 editor.update_in(cx, |editor, window, cx| {
13635 editor.set_text(
13636 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13637 window,
13638 cx,
13639 )
13640 });
13641
13642 let fake_server = fake_servers.next().await.unwrap();
13643
13644 let format = editor
13645 .update_in(cx, |editor, window, cx| {
13646 editor.perform_code_action_kind(
13647 project.clone(),
13648 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13649 window,
13650 cx,
13651 )
13652 })
13653 .unwrap();
13654 fake_server
13655 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
13656 assert_eq!(
13657 params.text_document.uri,
13658 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13659 );
13660 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
13661 lsp::CodeAction {
13662 title: "Organize Imports".to_string(),
13663 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
13664 edit: Some(lsp::WorkspaceEdit {
13665 changes: Some(
13666 [(
13667 params.text_document.uri.clone(),
13668 vec![lsp::TextEdit::new(
13669 lsp::Range::new(
13670 lsp::Position::new(1, 0),
13671 lsp::Position::new(2, 0),
13672 ),
13673 "".to_string(),
13674 )],
13675 )]
13676 .into_iter()
13677 .collect(),
13678 ),
13679 ..Default::default()
13680 }),
13681 ..Default::default()
13682 },
13683 )]))
13684 })
13685 .next()
13686 .await;
13687 format.await;
13688 assert_eq!(
13689 editor.update(cx, |editor, cx| editor.text(cx)),
13690 "import { a } from 'module';\n\nconst x = a;\n"
13691 );
13692
13693 editor.update_in(cx, |editor, window, cx| {
13694 editor.set_text(
13695 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13696 window,
13697 cx,
13698 )
13699 });
13700 // Ensure we don't lock if code action hangs.
13701 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
13702 move |params, _| async move {
13703 assert_eq!(
13704 params.text_document.uri,
13705 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13706 );
13707 futures::future::pending::<()>().await;
13708 unreachable!()
13709 },
13710 );
13711 let format = editor
13712 .update_in(cx, |editor, window, cx| {
13713 editor.perform_code_action_kind(
13714 project,
13715 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13716 window,
13717 cx,
13718 )
13719 })
13720 .unwrap();
13721 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
13722 format.await;
13723 assert_eq!(
13724 editor.update(cx, |editor, cx| editor.text(cx)),
13725 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
13726 );
13727}
13728
13729#[gpui::test]
13730async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
13731 init_test(cx, |_| {});
13732
13733 let mut cx = EditorLspTestContext::new_rust(
13734 lsp::ServerCapabilities {
13735 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13736 ..Default::default()
13737 },
13738 cx,
13739 )
13740 .await;
13741
13742 cx.set_state(indoc! {"
13743 one.twoˇ
13744 "});
13745
13746 // The format request takes a long time. When it completes, it inserts
13747 // a newline and an indent before the `.`
13748 cx.lsp
13749 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
13750 let executor = cx.background_executor().clone();
13751 async move {
13752 executor.timer(Duration::from_millis(100)).await;
13753 Ok(Some(vec![lsp::TextEdit {
13754 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
13755 new_text: "\n ".into(),
13756 }]))
13757 }
13758 });
13759
13760 // Submit a format request.
13761 let format_1 = cx
13762 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13763 .unwrap();
13764 cx.executor().run_until_parked();
13765
13766 // Submit a second format request.
13767 let format_2 = cx
13768 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13769 .unwrap();
13770 cx.executor().run_until_parked();
13771
13772 // Wait for both format requests to complete
13773 cx.executor().advance_clock(Duration::from_millis(200));
13774 format_1.await.unwrap();
13775 format_2.await.unwrap();
13776
13777 // The formatting edits only happens once.
13778 cx.assert_editor_state(indoc! {"
13779 one
13780 .twoˇ
13781 "});
13782}
13783
13784#[gpui::test]
13785async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
13786 init_test(cx, |settings| {
13787 settings.defaults.formatter = Some(FormatterList::default())
13788 });
13789
13790 let mut cx = EditorLspTestContext::new_rust(
13791 lsp::ServerCapabilities {
13792 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13793 ..Default::default()
13794 },
13795 cx,
13796 )
13797 .await;
13798
13799 // Record which buffer changes have been sent to the language server
13800 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
13801 cx.lsp
13802 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
13803 let buffer_changes = buffer_changes.clone();
13804 move |params, _| {
13805 buffer_changes.lock().extend(
13806 params
13807 .content_changes
13808 .into_iter()
13809 .map(|e| (e.range.unwrap(), e.text)),
13810 );
13811 }
13812 });
13813 // Handle formatting requests to the language server.
13814 cx.lsp
13815 .set_request_handler::<lsp::request::Formatting, _, _>({
13816 move |_, _| {
13817 // Insert blank lines between each line of the buffer.
13818 async move {
13819 // TODO: this assertion is not reliably true. Currently nothing guarantees that we deliver
13820 // DidChangedTextDocument to the LSP before sending the formatting request.
13821 // assert_eq!(
13822 // &buffer_changes.lock()[1..],
13823 // &[
13824 // (
13825 // lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13826 // "".into()
13827 // ),
13828 // (
13829 // lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13830 // "".into()
13831 // ),
13832 // (
13833 // lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13834 // "\n".into()
13835 // ),
13836 // ]
13837 // );
13838
13839 Ok(Some(vec![
13840 lsp::TextEdit {
13841 range: lsp::Range::new(
13842 lsp::Position::new(1, 0),
13843 lsp::Position::new(1, 0),
13844 ),
13845 new_text: "\n".into(),
13846 },
13847 lsp::TextEdit {
13848 range: lsp::Range::new(
13849 lsp::Position::new(2, 0),
13850 lsp::Position::new(2, 0),
13851 ),
13852 new_text: "\n".into(),
13853 },
13854 ]))
13855 }
13856 }
13857 });
13858
13859 // Set up a buffer white some trailing whitespace and no trailing newline.
13860 cx.set_state(
13861 &[
13862 "one ", //
13863 "twoˇ", //
13864 "three ", //
13865 "four", //
13866 ]
13867 .join("\n"),
13868 );
13869
13870 // Submit a format request.
13871 let format = cx
13872 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13873 .unwrap();
13874
13875 cx.run_until_parked();
13876 // After formatting the buffer, the trailing whitespace is stripped,
13877 // a newline is appended, and the edits provided by the language server
13878 // have been applied.
13879 format.await.unwrap();
13880
13881 cx.assert_editor_state(
13882 &[
13883 "one", //
13884 "", //
13885 "twoˇ", //
13886 "", //
13887 "three", //
13888 "four", //
13889 "", //
13890 ]
13891 .join("\n"),
13892 );
13893
13894 // Undoing the formatting undoes the trailing whitespace removal, the
13895 // trailing newline, and the LSP edits.
13896 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13897 cx.assert_editor_state(
13898 &[
13899 "one ", //
13900 "twoˇ", //
13901 "three ", //
13902 "four", //
13903 ]
13904 .join("\n"),
13905 );
13906}
13907
13908#[gpui::test]
13909async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13910 cx: &mut TestAppContext,
13911) {
13912 init_test(cx, |_| {});
13913
13914 cx.update(|cx| {
13915 cx.update_global::<SettingsStore, _>(|settings, cx| {
13916 settings.update_user_settings(cx, |settings| {
13917 settings.editor.auto_signature_help = Some(true);
13918 settings.editor.hover_popover_delay = Some(DelayMs(300));
13919 });
13920 });
13921 });
13922
13923 let mut cx = EditorLspTestContext::new_rust(
13924 lsp::ServerCapabilities {
13925 signature_help_provider: Some(lsp::SignatureHelpOptions {
13926 ..Default::default()
13927 }),
13928 ..Default::default()
13929 },
13930 cx,
13931 )
13932 .await;
13933
13934 let language = Language::new(
13935 LanguageConfig {
13936 name: "Rust".into(),
13937 brackets: BracketPairConfig {
13938 pairs: vec![
13939 BracketPair {
13940 start: "{".to_string(),
13941 end: "}".to_string(),
13942 close: true,
13943 surround: true,
13944 newline: true,
13945 },
13946 BracketPair {
13947 start: "(".to_string(),
13948 end: ")".to_string(),
13949 close: true,
13950 surround: true,
13951 newline: true,
13952 },
13953 BracketPair {
13954 start: "/*".to_string(),
13955 end: " */".to_string(),
13956 close: true,
13957 surround: true,
13958 newline: true,
13959 },
13960 BracketPair {
13961 start: "[".to_string(),
13962 end: "]".to_string(),
13963 close: false,
13964 surround: false,
13965 newline: true,
13966 },
13967 BracketPair {
13968 start: "\"".to_string(),
13969 end: "\"".to_string(),
13970 close: true,
13971 surround: true,
13972 newline: false,
13973 },
13974 BracketPair {
13975 start: "<".to_string(),
13976 end: ">".to_string(),
13977 close: false,
13978 surround: true,
13979 newline: true,
13980 },
13981 ],
13982 ..Default::default()
13983 },
13984 autoclose_before: "})]".to_string(),
13985 ..Default::default()
13986 },
13987 Some(tree_sitter_rust::LANGUAGE.into()),
13988 );
13989 let language = Arc::new(language);
13990
13991 cx.language_registry().add(language.clone());
13992 cx.update_buffer(|buffer, cx| {
13993 buffer.set_language(Some(language), cx);
13994 });
13995
13996 cx.set_state(
13997 &r#"
13998 fn main() {
13999 sampleˇ
14000 }
14001 "#
14002 .unindent(),
14003 );
14004
14005 cx.update_editor(|editor, window, cx| {
14006 editor.handle_input("(", window, cx);
14007 });
14008 cx.assert_editor_state(
14009 &"
14010 fn main() {
14011 sample(ˇ)
14012 }
14013 "
14014 .unindent(),
14015 );
14016
14017 let mocked_response = lsp::SignatureHelp {
14018 signatures: vec![lsp::SignatureInformation {
14019 label: "fn sample(param1: u8, param2: u8)".to_string(),
14020 documentation: None,
14021 parameters: Some(vec![
14022 lsp::ParameterInformation {
14023 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14024 documentation: None,
14025 },
14026 lsp::ParameterInformation {
14027 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14028 documentation: None,
14029 },
14030 ]),
14031 active_parameter: None,
14032 }],
14033 active_signature: Some(0),
14034 active_parameter: Some(0),
14035 };
14036 handle_signature_help_request(&mut cx, mocked_response).await;
14037
14038 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14039 .await;
14040
14041 cx.editor(|editor, _, _| {
14042 let signature_help_state = editor.signature_help_state.popover().cloned();
14043 let signature = signature_help_state.unwrap();
14044 assert_eq!(
14045 signature.signatures[signature.current_signature].label,
14046 "fn sample(param1: u8, param2: u8)"
14047 );
14048 });
14049}
14050
14051#[gpui::test]
14052async fn test_signature_help_delay_only_for_auto(cx: &mut TestAppContext) {
14053 init_test(cx, |_| {});
14054
14055 let delay_ms = 500;
14056 cx.update(|cx| {
14057 cx.update_global::<SettingsStore, _>(|settings, cx| {
14058 settings.update_user_settings(cx, |settings| {
14059 settings.editor.auto_signature_help = Some(true);
14060 settings.editor.show_signature_help_after_edits = Some(false);
14061 settings.editor.hover_popover_delay = Some(DelayMs(delay_ms));
14062 });
14063 });
14064 });
14065
14066 let mut cx = EditorLspTestContext::new_rust(
14067 lsp::ServerCapabilities {
14068 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14069 ..lsp::ServerCapabilities::default()
14070 },
14071 cx,
14072 )
14073 .await;
14074
14075 let mocked_response = lsp::SignatureHelp {
14076 signatures: vec![lsp::SignatureInformation {
14077 label: "fn sample(param1: u8)".to_string(),
14078 documentation: None,
14079 parameters: Some(vec![lsp::ParameterInformation {
14080 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14081 documentation: None,
14082 }]),
14083 active_parameter: None,
14084 }],
14085 active_signature: Some(0),
14086 active_parameter: Some(0),
14087 };
14088
14089 cx.set_state(indoc! {"
14090 fn main() {
14091 sample(ˇ);
14092 }
14093
14094 fn sample(param1: u8) {}
14095 "});
14096
14097 // Manual trigger should show immediately without delay
14098 cx.update_editor(|editor, window, cx| {
14099 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14100 });
14101 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14102 cx.run_until_parked();
14103 cx.editor(|editor, _, _| {
14104 assert!(
14105 editor.signature_help_state.is_shown(),
14106 "Manual trigger should show signature help without delay"
14107 );
14108 });
14109
14110 cx.update_editor(|editor, _, cx| {
14111 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
14112 });
14113 cx.run_until_parked();
14114 cx.editor(|editor, _, _| {
14115 assert!(!editor.signature_help_state.is_shown());
14116 });
14117
14118 // Auto trigger (cursor movement into brackets) should respect delay
14119 cx.set_state(indoc! {"
14120 fn main() {
14121 sampleˇ();
14122 }
14123
14124 fn sample(param1: u8) {}
14125 "});
14126 cx.update_editor(|editor, window, cx| {
14127 editor.move_right(&MoveRight, window, cx);
14128 });
14129 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14130 cx.run_until_parked();
14131 cx.editor(|editor, _, _| {
14132 assert!(
14133 !editor.signature_help_state.is_shown(),
14134 "Auto trigger should wait for delay before showing signature help"
14135 );
14136 });
14137
14138 cx.executor()
14139 .advance_clock(Duration::from_millis(delay_ms + 50));
14140 cx.run_until_parked();
14141 cx.editor(|editor, _, _| {
14142 assert!(
14143 editor.signature_help_state.is_shown(),
14144 "Auto trigger should show signature help after delay elapsed"
14145 );
14146 });
14147}
14148
14149#[gpui::test]
14150async fn test_signature_help_after_edits_no_delay(cx: &mut TestAppContext) {
14151 init_test(cx, |_| {});
14152
14153 let delay_ms = 500;
14154 cx.update(|cx| {
14155 cx.update_global::<SettingsStore, _>(|settings, cx| {
14156 settings.update_user_settings(cx, |settings| {
14157 settings.editor.auto_signature_help = Some(false);
14158 settings.editor.show_signature_help_after_edits = Some(true);
14159 settings.editor.hover_popover_delay = Some(DelayMs(delay_ms));
14160 });
14161 });
14162 });
14163
14164 let mut cx = EditorLspTestContext::new_rust(
14165 lsp::ServerCapabilities {
14166 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14167 ..lsp::ServerCapabilities::default()
14168 },
14169 cx,
14170 )
14171 .await;
14172
14173 let language = Arc::new(Language::new(
14174 LanguageConfig {
14175 name: "Rust".into(),
14176 brackets: BracketPairConfig {
14177 pairs: vec![BracketPair {
14178 start: "(".to_string(),
14179 end: ")".to_string(),
14180 close: true,
14181 surround: true,
14182 newline: true,
14183 }],
14184 ..BracketPairConfig::default()
14185 },
14186 autoclose_before: "})".to_string(),
14187 ..LanguageConfig::default()
14188 },
14189 Some(tree_sitter_rust::LANGUAGE.into()),
14190 ));
14191 cx.language_registry().add(language.clone());
14192 cx.update_buffer(|buffer, cx| {
14193 buffer.set_language(Some(language), cx);
14194 });
14195
14196 let mocked_response = lsp::SignatureHelp {
14197 signatures: vec![lsp::SignatureInformation {
14198 label: "fn sample(param1: u8)".to_string(),
14199 documentation: None,
14200 parameters: Some(vec![lsp::ParameterInformation {
14201 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14202 documentation: None,
14203 }]),
14204 active_parameter: None,
14205 }],
14206 active_signature: Some(0),
14207 active_parameter: Some(0),
14208 };
14209
14210 cx.set_state(indoc! {"
14211 fn main() {
14212 sampleˇ
14213 }
14214 "});
14215
14216 // Typing bracket should show signature help immediately without delay
14217 cx.update_editor(|editor, window, cx| {
14218 editor.handle_input("(", window, cx);
14219 });
14220 handle_signature_help_request(&mut cx, mocked_response).await;
14221 cx.run_until_parked();
14222 cx.editor(|editor, _, _| {
14223 assert!(
14224 editor.signature_help_state.is_shown(),
14225 "show_signature_help_after_edits should show signature help without delay"
14226 );
14227 });
14228}
14229
14230#[gpui::test]
14231async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
14232 init_test(cx, |_| {});
14233
14234 cx.update(|cx| {
14235 cx.update_global::<SettingsStore, _>(|settings, cx| {
14236 settings.update_user_settings(cx, |settings| {
14237 settings.editor.auto_signature_help = Some(false);
14238 settings.editor.show_signature_help_after_edits = Some(false);
14239 });
14240 });
14241 });
14242
14243 let mut cx = EditorLspTestContext::new_rust(
14244 lsp::ServerCapabilities {
14245 signature_help_provider: Some(lsp::SignatureHelpOptions {
14246 ..Default::default()
14247 }),
14248 ..Default::default()
14249 },
14250 cx,
14251 )
14252 .await;
14253
14254 let language = Language::new(
14255 LanguageConfig {
14256 name: "Rust".into(),
14257 brackets: BracketPairConfig {
14258 pairs: vec![
14259 BracketPair {
14260 start: "{".to_string(),
14261 end: "}".to_string(),
14262 close: true,
14263 surround: true,
14264 newline: true,
14265 },
14266 BracketPair {
14267 start: "(".to_string(),
14268 end: ")".to_string(),
14269 close: true,
14270 surround: true,
14271 newline: true,
14272 },
14273 BracketPair {
14274 start: "/*".to_string(),
14275 end: " */".to_string(),
14276 close: true,
14277 surround: true,
14278 newline: true,
14279 },
14280 BracketPair {
14281 start: "[".to_string(),
14282 end: "]".to_string(),
14283 close: false,
14284 surround: false,
14285 newline: true,
14286 },
14287 BracketPair {
14288 start: "\"".to_string(),
14289 end: "\"".to_string(),
14290 close: true,
14291 surround: true,
14292 newline: false,
14293 },
14294 BracketPair {
14295 start: "<".to_string(),
14296 end: ">".to_string(),
14297 close: false,
14298 surround: true,
14299 newline: true,
14300 },
14301 ],
14302 ..Default::default()
14303 },
14304 autoclose_before: "})]".to_string(),
14305 ..Default::default()
14306 },
14307 Some(tree_sitter_rust::LANGUAGE.into()),
14308 );
14309 let language = Arc::new(language);
14310
14311 cx.language_registry().add(language.clone());
14312 cx.update_buffer(|buffer, cx| {
14313 buffer.set_language(Some(language), cx);
14314 });
14315
14316 // Ensure that signature_help is not called when no signature help is enabled.
14317 cx.set_state(
14318 &r#"
14319 fn main() {
14320 sampleˇ
14321 }
14322 "#
14323 .unindent(),
14324 );
14325 cx.update_editor(|editor, window, cx| {
14326 editor.handle_input("(", window, cx);
14327 });
14328 cx.assert_editor_state(
14329 &"
14330 fn main() {
14331 sample(ˇ)
14332 }
14333 "
14334 .unindent(),
14335 );
14336 cx.editor(|editor, _, _| {
14337 assert!(editor.signature_help_state.task().is_none());
14338 });
14339
14340 let mocked_response = lsp::SignatureHelp {
14341 signatures: vec![lsp::SignatureInformation {
14342 label: "fn sample(param1: u8, param2: u8)".to_string(),
14343 documentation: None,
14344 parameters: Some(vec![
14345 lsp::ParameterInformation {
14346 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14347 documentation: None,
14348 },
14349 lsp::ParameterInformation {
14350 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14351 documentation: None,
14352 },
14353 ]),
14354 active_parameter: None,
14355 }],
14356 active_signature: Some(0),
14357 active_parameter: Some(0),
14358 };
14359
14360 // Ensure that signature_help is called when enabled afte edits
14361 cx.update(|_, cx| {
14362 cx.update_global::<SettingsStore, _>(|settings, cx| {
14363 settings.update_user_settings(cx, |settings| {
14364 settings.editor.auto_signature_help = Some(false);
14365 settings.editor.show_signature_help_after_edits = Some(true);
14366 });
14367 });
14368 });
14369 cx.set_state(
14370 &r#"
14371 fn main() {
14372 sampleˇ
14373 }
14374 "#
14375 .unindent(),
14376 );
14377 cx.update_editor(|editor, window, cx| {
14378 editor.handle_input("(", window, cx);
14379 });
14380 cx.assert_editor_state(
14381 &"
14382 fn main() {
14383 sample(ˇ)
14384 }
14385 "
14386 .unindent(),
14387 );
14388 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14389 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14390 .await;
14391 cx.update_editor(|editor, _, _| {
14392 let signature_help_state = editor.signature_help_state.popover().cloned();
14393 assert!(signature_help_state.is_some());
14394 let signature = signature_help_state.unwrap();
14395 assert_eq!(
14396 signature.signatures[signature.current_signature].label,
14397 "fn sample(param1: u8, param2: u8)"
14398 );
14399 editor.signature_help_state = SignatureHelpState::default();
14400 });
14401
14402 // Ensure that signature_help is called when auto signature help override is enabled
14403 cx.update(|_, cx| {
14404 cx.update_global::<SettingsStore, _>(|settings, cx| {
14405 settings.update_user_settings(cx, |settings| {
14406 settings.editor.auto_signature_help = Some(true);
14407 settings.editor.show_signature_help_after_edits = Some(false);
14408 });
14409 });
14410 });
14411 cx.set_state(
14412 &r#"
14413 fn main() {
14414 sampleˇ
14415 }
14416 "#
14417 .unindent(),
14418 );
14419 cx.update_editor(|editor, window, cx| {
14420 editor.handle_input("(", window, cx);
14421 });
14422 cx.assert_editor_state(
14423 &"
14424 fn main() {
14425 sample(ˇ)
14426 }
14427 "
14428 .unindent(),
14429 );
14430 handle_signature_help_request(&mut cx, mocked_response).await;
14431 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14432 .await;
14433 cx.editor(|editor, _, _| {
14434 let signature_help_state = editor.signature_help_state.popover().cloned();
14435 assert!(signature_help_state.is_some());
14436 let signature = signature_help_state.unwrap();
14437 assert_eq!(
14438 signature.signatures[signature.current_signature].label,
14439 "fn sample(param1: u8, param2: u8)"
14440 );
14441 });
14442}
14443
14444#[gpui::test]
14445async fn test_signature_help(cx: &mut TestAppContext) {
14446 init_test(cx, |_| {});
14447 cx.update(|cx| {
14448 cx.update_global::<SettingsStore, _>(|settings, cx| {
14449 settings.update_user_settings(cx, |settings| {
14450 settings.editor.auto_signature_help = Some(true);
14451 });
14452 });
14453 });
14454
14455 let mut cx = EditorLspTestContext::new_rust(
14456 lsp::ServerCapabilities {
14457 signature_help_provider: Some(lsp::SignatureHelpOptions {
14458 ..Default::default()
14459 }),
14460 ..Default::default()
14461 },
14462 cx,
14463 )
14464 .await;
14465
14466 // A test that directly calls `show_signature_help`
14467 cx.update_editor(|editor, window, cx| {
14468 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14469 });
14470
14471 let mocked_response = lsp::SignatureHelp {
14472 signatures: vec![lsp::SignatureInformation {
14473 label: "fn sample(param1: u8, param2: u8)".to_string(),
14474 documentation: None,
14475 parameters: Some(vec![
14476 lsp::ParameterInformation {
14477 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14478 documentation: None,
14479 },
14480 lsp::ParameterInformation {
14481 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14482 documentation: None,
14483 },
14484 ]),
14485 active_parameter: None,
14486 }],
14487 active_signature: Some(0),
14488 active_parameter: Some(0),
14489 };
14490 handle_signature_help_request(&mut cx, mocked_response).await;
14491
14492 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14493 .await;
14494
14495 cx.editor(|editor, _, _| {
14496 let signature_help_state = editor.signature_help_state.popover().cloned();
14497 assert!(signature_help_state.is_some());
14498 let signature = signature_help_state.unwrap();
14499 assert_eq!(
14500 signature.signatures[signature.current_signature].label,
14501 "fn sample(param1: u8, param2: u8)"
14502 );
14503 });
14504
14505 // When exiting outside from inside the brackets, `signature_help` is closed.
14506 cx.set_state(indoc! {"
14507 fn main() {
14508 sample(ˇ);
14509 }
14510
14511 fn sample(param1: u8, param2: u8) {}
14512 "});
14513
14514 cx.update_editor(|editor, window, cx| {
14515 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14516 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
14517 });
14518 });
14519
14520 let mocked_response = lsp::SignatureHelp {
14521 signatures: Vec::new(),
14522 active_signature: None,
14523 active_parameter: None,
14524 };
14525 handle_signature_help_request(&mut cx, mocked_response).await;
14526
14527 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
14528 .await;
14529
14530 cx.editor(|editor, _, _| {
14531 assert!(!editor.signature_help_state.is_shown());
14532 });
14533
14534 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
14535 cx.set_state(indoc! {"
14536 fn main() {
14537 sample(ˇ);
14538 }
14539
14540 fn sample(param1: u8, param2: u8) {}
14541 "});
14542
14543 let mocked_response = lsp::SignatureHelp {
14544 signatures: vec![lsp::SignatureInformation {
14545 label: "fn sample(param1: u8, param2: u8)".to_string(),
14546 documentation: None,
14547 parameters: Some(vec![
14548 lsp::ParameterInformation {
14549 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14550 documentation: None,
14551 },
14552 lsp::ParameterInformation {
14553 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14554 documentation: None,
14555 },
14556 ]),
14557 active_parameter: None,
14558 }],
14559 active_signature: Some(0),
14560 active_parameter: Some(0),
14561 };
14562 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14563 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14564 .await;
14565 cx.editor(|editor, _, _| {
14566 assert!(editor.signature_help_state.is_shown());
14567 });
14568
14569 // Restore the popover with more parameter input
14570 cx.set_state(indoc! {"
14571 fn main() {
14572 sample(param1, param2ˇ);
14573 }
14574
14575 fn sample(param1: u8, param2: u8) {}
14576 "});
14577
14578 let mocked_response = lsp::SignatureHelp {
14579 signatures: vec![lsp::SignatureInformation {
14580 label: "fn sample(param1: u8, param2: u8)".to_string(),
14581 documentation: None,
14582 parameters: Some(vec![
14583 lsp::ParameterInformation {
14584 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14585 documentation: None,
14586 },
14587 lsp::ParameterInformation {
14588 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14589 documentation: None,
14590 },
14591 ]),
14592 active_parameter: None,
14593 }],
14594 active_signature: Some(0),
14595 active_parameter: Some(1),
14596 };
14597 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14598 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14599 .await;
14600
14601 // When selecting a range, the popover is gone.
14602 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
14603 cx.update_editor(|editor, window, cx| {
14604 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14605 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
14606 })
14607 });
14608 cx.assert_editor_state(indoc! {"
14609 fn main() {
14610 sample(param1, «ˇparam2»);
14611 }
14612
14613 fn sample(param1: u8, param2: u8) {}
14614 "});
14615 cx.editor(|editor, _, _| {
14616 assert!(!editor.signature_help_state.is_shown());
14617 });
14618
14619 // When unselecting again, the popover is back if within the brackets.
14620 cx.update_editor(|editor, window, cx| {
14621 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14622 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14623 })
14624 });
14625 cx.assert_editor_state(indoc! {"
14626 fn main() {
14627 sample(param1, ˇparam2);
14628 }
14629
14630 fn sample(param1: u8, param2: u8) {}
14631 "});
14632 handle_signature_help_request(&mut cx, mocked_response).await;
14633 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14634 .await;
14635 cx.editor(|editor, _, _| {
14636 assert!(editor.signature_help_state.is_shown());
14637 });
14638
14639 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
14640 cx.update_editor(|editor, window, cx| {
14641 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14642 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
14643 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14644 })
14645 });
14646 cx.assert_editor_state(indoc! {"
14647 fn main() {
14648 sample(param1, ˇparam2);
14649 }
14650
14651 fn sample(param1: u8, param2: u8) {}
14652 "});
14653
14654 let mocked_response = lsp::SignatureHelp {
14655 signatures: vec![lsp::SignatureInformation {
14656 label: "fn sample(param1: u8, param2: u8)".to_string(),
14657 documentation: None,
14658 parameters: Some(vec![
14659 lsp::ParameterInformation {
14660 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14661 documentation: None,
14662 },
14663 lsp::ParameterInformation {
14664 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14665 documentation: None,
14666 },
14667 ]),
14668 active_parameter: None,
14669 }],
14670 active_signature: Some(0),
14671 active_parameter: Some(1),
14672 };
14673 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14674 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14675 .await;
14676 cx.update_editor(|editor, _, cx| {
14677 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
14678 });
14679 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
14680 .await;
14681 cx.update_editor(|editor, window, cx| {
14682 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14683 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
14684 })
14685 });
14686 cx.assert_editor_state(indoc! {"
14687 fn main() {
14688 sample(param1, «ˇparam2»);
14689 }
14690
14691 fn sample(param1: u8, param2: u8) {}
14692 "});
14693 cx.update_editor(|editor, window, cx| {
14694 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14695 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14696 })
14697 });
14698 cx.assert_editor_state(indoc! {"
14699 fn main() {
14700 sample(param1, ˇparam2);
14701 }
14702
14703 fn sample(param1: u8, param2: u8) {}
14704 "});
14705 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
14706 .await;
14707}
14708
14709#[gpui::test]
14710async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
14711 init_test(cx, |_| {});
14712
14713 let mut cx = EditorLspTestContext::new_rust(
14714 lsp::ServerCapabilities {
14715 signature_help_provider: Some(lsp::SignatureHelpOptions {
14716 ..Default::default()
14717 }),
14718 ..Default::default()
14719 },
14720 cx,
14721 )
14722 .await;
14723
14724 cx.set_state(indoc! {"
14725 fn main() {
14726 overloadedˇ
14727 }
14728 "});
14729
14730 cx.update_editor(|editor, window, cx| {
14731 editor.handle_input("(", window, cx);
14732 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14733 });
14734
14735 // Mock response with 3 signatures
14736 let mocked_response = lsp::SignatureHelp {
14737 signatures: vec![
14738 lsp::SignatureInformation {
14739 label: "fn overloaded(x: i32)".to_string(),
14740 documentation: None,
14741 parameters: Some(vec![lsp::ParameterInformation {
14742 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14743 documentation: None,
14744 }]),
14745 active_parameter: None,
14746 },
14747 lsp::SignatureInformation {
14748 label: "fn overloaded(x: i32, y: i32)".to_string(),
14749 documentation: None,
14750 parameters: Some(vec![
14751 lsp::ParameterInformation {
14752 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14753 documentation: None,
14754 },
14755 lsp::ParameterInformation {
14756 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14757 documentation: None,
14758 },
14759 ]),
14760 active_parameter: None,
14761 },
14762 lsp::SignatureInformation {
14763 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
14764 documentation: None,
14765 parameters: Some(vec![
14766 lsp::ParameterInformation {
14767 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14768 documentation: None,
14769 },
14770 lsp::ParameterInformation {
14771 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14772 documentation: None,
14773 },
14774 lsp::ParameterInformation {
14775 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
14776 documentation: None,
14777 },
14778 ]),
14779 active_parameter: None,
14780 },
14781 ],
14782 active_signature: Some(1),
14783 active_parameter: Some(0),
14784 };
14785 handle_signature_help_request(&mut cx, mocked_response).await;
14786
14787 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14788 .await;
14789
14790 // Verify we have multiple signatures and the right one is selected
14791 cx.editor(|editor, _, _| {
14792 let popover = editor.signature_help_state.popover().cloned().unwrap();
14793 assert_eq!(popover.signatures.len(), 3);
14794 // active_signature was 1, so that should be the current
14795 assert_eq!(popover.current_signature, 1);
14796 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
14797 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
14798 assert_eq!(
14799 popover.signatures[2].label,
14800 "fn overloaded(x: i32, y: i32, z: i32)"
14801 );
14802 });
14803
14804 // Test navigation functionality
14805 cx.update_editor(|editor, window, cx| {
14806 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14807 });
14808
14809 cx.editor(|editor, _, _| {
14810 let popover = editor.signature_help_state.popover().cloned().unwrap();
14811 assert_eq!(popover.current_signature, 2);
14812 });
14813
14814 // Test wrap around
14815 cx.update_editor(|editor, window, cx| {
14816 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14817 });
14818
14819 cx.editor(|editor, _, _| {
14820 let popover = editor.signature_help_state.popover().cloned().unwrap();
14821 assert_eq!(popover.current_signature, 0);
14822 });
14823
14824 // Test previous navigation
14825 cx.update_editor(|editor, window, cx| {
14826 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
14827 });
14828
14829 cx.editor(|editor, _, _| {
14830 let popover = editor.signature_help_state.popover().cloned().unwrap();
14831 assert_eq!(popover.current_signature, 2);
14832 });
14833}
14834
14835#[gpui::test]
14836async fn test_completion_mode(cx: &mut TestAppContext) {
14837 init_test(cx, |_| {});
14838 let mut cx = EditorLspTestContext::new_rust(
14839 lsp::ServerCapabilities {
14840 completion_provider: Some(lsp::CompletionOptions {
14841 resolve_provider: Some(true),
14842 ..Default::default()
14843 }),
14844 ..Default::default()
14845 },
14846 cx,
14847 )
14848 .await;
14849
14850 struct Run {
14851 run_description: &'static str,
14852 initial_state: String,
14853 buffer_marked_text: String,
14854 completion_label: &'static str,
14855 completion_text: &'static str,
14856 expected_with_insert_mode: String,
14857 expected_with_replace_mode: String,
14858 expected_with_replace_subsequence_mode: String,
14859 expected_with_replace_suffix_mode: String,
14860 }
14861
14862 let runs = [
14863 Run {
14864 run_description: "Start of word matches completion text",
14865 initial_state: "before ediˇ after".into(),
14866 buffer_marked_text: "before <edi|> after".into(),
14867 completion_label: "editor",
14868 completion_text: "editor",
14869 expected_with_insert_mode: "before editorˇ after".into(),
14870 expected_with_replace_mode: "before editorˇ after".into(),
14871 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14872 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14873 },
14874 Run {
14875 run_description: "Accept same text at the middle of the word",
14876 initial_state: "before ediˇtor after".into(),
14877 buffer_marked_text: "before <edi|tor> after".into(),
14878 completion_label: "editor",
14879 completion_text: "editor",
14880 expected_with_insert_mode: "before editorˇtor after".into(),
14881 expected_with_replace_mode: "before editorˇ after".into(),
14882 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14883 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14884 },
14885 Run {
14886 run_description: "End of word matches completion text -- cursor at end",
14887 initial_state: "before torˇ after".into(),
14888 buffer_marked_text: "before <tor|> after".into(),
14889 completion_label: "editor",
14890 completion_text: "editor",
14891 expected_with_insert_mode: "before editorˇ after".into(),
14892 expected_with_replace_mode: "before editorˇ after".into(),
14893 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14894 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14895 },
14896 Run {
14897 run_description: "End of word matches completion text -- cursor at start",
14898 initial_state: "before ˇtor after".into(),
14899 buffer_marked_text: "before <|tor> after".into(),
14900 completion_label: "editor",
14901 completion_text: "editor",
14902 expected_with_insert_mode: "before editorˇtor after".into(),
14903 expected_with_replace_mode: "before editorˇ after".into(),
14904 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14905 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14906 },
14907 Run {
14908 run_description: "Prepend text containing whitespace",
14909 initial_state: "pˇfield: bool".into(),
14910 buffer_marked_text: "<p|field>: bool".into(),
14911 completion_label: "pub ",
14912 completion_text: "pub ",
14913 expected_with_insert_mode: "pub ˇfield: bool".into(),
14914 expected_with_replace_mode: "pub ˇ: bool".into(),
14915 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
14916 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
14917 },
14918 Run {
14919 run_description: "Add element to start of list",
14920 initial_state: "[element_ˇelement_2]".into(),
14921 buffer_marked_text: "[<element_|element_2>]".into(),
14922 completion_label: "element_1",
14923 completion_text: "element_1",
14924 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
14925 expected_with_replace_mode: "[element_1ˇ]".into(),
14926 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
14927 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
14928 },
14929 Run {
14930 run_description: "Add element to start of list -- first and second elements are equal",
14931 initial_state: "[elˇelement]".into(),
14932 buffer_marked_text: "[<el|element>]".into(),
14933 completion_label: "element",
14934 completion_text: "element",
14935 expected_with_insert_mode: "[elementˇelement]".into(),
14936 expected_with_replace_mode: "[elementˇ]".into(),
14937 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
14938 expected_with_replace_suffix_mode: "[elementˇ]".into(),
14939 },
14940 Run {
14941 run_description: "Ends with matching suffix",
14942 initial_state: "SubˇError".into(),
14943 buffer_marked_text: "<Sub|Error>".into(),
14944 completion_label: "SubscriptionError",
14945 completion_text: "SubscriptionError",
14946 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
14947 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14948 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14949 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
14950 },
14951 Run {
14952 run_description: "Suffix is a subsequence -- contiguous",
14953 initial_state: "SubˇErr".into(),
14954 buffer_marked_text: "<Sub|Err>".into(),
14955 completion_label: "SubscriptionError",
14956 completion_text: "SubscriptionError",
14957 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
14958 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14959 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14960 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
14961 },
14962 Run {
14963 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
14964 initial_state: "Suˇscrirr".into(),
14965 buffer_marked_text: "<Su|scrirr>".into(),
14966 completion_label: "SubscriptionError",
14967 completion_text: "SubscriptionError",
14968 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
14969 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14970 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14971 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
14972 },
14973 Run {
14974 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
14975 initial_state: "foo(indˇix)".into(),
14976 buffer_marked_text: "foo(<ind|ix>)".into(),
14977 completion_label: "node_index",
14978 completion_text: "node_index",
14979 expected_with_insert_mode: "foo(node_indexˇix)".into(),
14980 expected_with_replace_mode: "foo(node_indexˇ)".into(),
14981 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
14982 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
14983 },
14984 Run {
14985 run_description: "Replace range ends before cursor - should extend to cursor",
14986 initial_state: "before editˇo after".into(),
14987 buffer_marked_text: "before <{ed}>it|o after".into(),
14988 completion_label: "editor",
14989 completion_text: "editor",
14990 expected_with_insert_mode: "before editorˇo after".into(),
14991 expected_with_replace_mode: "before editorˇo after".into(),
14992 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
14993 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14994 },
14995 Run {
14996 run_description: "Uses label for suffix matching",
14997 initial_state: "before ediˇtor after".into(),
14998 buffer_marked_text: "before <edi|tor> after".into(),
14999 completion_label: "editor",
15000 completion_text: "editor()",
15001 expected_with_insert_mode: "before editor()ˇtor after".into(),
15002 expected_with_replace_mode: "before editor()ˇ after".into(),
15003 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
15004 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
15005 },
15006 Run {
15007 run_description: "Case insensitive subsequence and suffix matching",
15008 initial_state: "before EDiˇtoR after".into(),
15009 buffer_marked_text: "before <EDi|toR> after".into(),
15010 completion_label: "editor",
15011 completion_text: "editor",
15012 expected_with_insert_mode: "before editorˇtoR after".into(),
15013 expected_with_replace_mode: "before editorˇ after".into(),
15014 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
15015 expected_with_replace_suffix_mode: "before editorˇ after".into(),
15016 },
15017 ];
15018
15019 for run in runs {
15020 let run_variations = [
15021 (LspInsertMode::Insert, run.expected_with_insert_mode),
15022 (LspInsertMode::Replace, run.expected_with_replace_mode),
15023 (
15024 LspInsertMode::ReplaceSubsequence,
15025 run.expected_with_replace_subsequence_mode,
15026 ),
15027 (
15028 LspInsertMode::ReplaceSuffix,
15029 run.expected_with_replace_suffix_mode,
15030 ),
15031 ];
15032
15033 for (lsp_insert_mode, expected_text) in run_variations {
15034 eprintln!(
15035 "run = {:?}, mode = {lsp_insert_mode:.?}",
15036 run.run_description,
15037 );
15038
15039 update_test_language_settings(&mut cx, |settings| {
15040 settings.defaults.completions = Some(CompletionSettingsContent {
15041 lsp_insert_mode: Some(lsp_insert_mode),
15042 words: Some(WordsCompletionMode::Disabled),
15043 words_min_length: Some(0),
15044 ..Default::default()
15045 });
15046 });
15047
15048 cx.set_state(&run.initial_state);
15049
15050 // Set up resolve handler before showing completions, since resolve may be
15051 // triggered when menu becomes visible (for documentation), not just on confirm.
15052 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(
15053 move |_, _, _| async move {
15054 Ok(lsp::CompletionItem {
15055 additional_text_edits: None,
15056 ..Default::default()
15057 })
15058 },
15059 );
15060
15061 cx.update_editor(|editor, window, cx| {
15062 editor.show_completions(&ShowCompletions, window, cx);
15063 });
15064
15065 let counter = Arc::new(AtomicUsize::new(0));
15066 handle_completion_request_with_insert_and_replace(
15067 &mut cx,
15068 &run.buffer_marked_text,
15069 vec![(run.completion_label, run.completion_text)],
15070 counter.clone(),
15071 )
15072 .await;
15073 cx.condition(|editor, _| editor.context_menu_visible())
15074 .await;
15075 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15076
15077 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15078 editor
15079 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15080 .unwrap()
15081 });
15082 cx.assert_editor_state(&expected_text);
15083 apply_additional_edits.await.unwrap();
15084 }
15085 }
15086}
15087
15088#[gpui::test]
15089async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
15090 init_test(cx, |_| {});
15091 let mut cx = EditorLspTestContext::new_rust(
15092 lsp::ServerCapabilities {
15093 completion_provider: Some(lsp::CompletionOptions {
15094 resolve_provider: Some(true),
15095 ..Default::default()
15096 }),
15097 ..Default::default()
15098 },
15099 cx,
15100 )
15101 .await;
15102
15103 let initial_state = "SubˇError";
15104 let buffer_marked_text = "<Sub|Error>";
15105 let completion_text = "SubscriptionError";
15106 let expected_with_insert_mode = "SubscriptionErrorˇError";
15107 let expected_with_replace_mode = "SubscriptionErrorˇ";
15108
15109 update_test_language_settings(&mut cx, |settings| {
15110 settings.defaults.completions = Some(CompletionSettingsContent {
15111 words: Some(WordsCompletionMode::Disabled),
15112 words_min_length: Some(0),
15113 // set the opposite here to ensure that the action is overriding the default behavior
15114 lsp_insert_mode: Some(LspInsertMode::Insert),
15115 ..Default::default()
15116 });
15117 });
15118
15119 cx.set_state(initial_state);
15120 cx.update_editor(|editor, window, cx| {
15121 editor.show_completions(&ShowCompletions, window, cx);
15122 });
15123
15124 let counter = Arc::new(AtomicUsize::new(0));
15125 handle_completion_request_with_insert_and_replace(
15126 &mut cx,
15127 buffer_marked_text,
15128 vec![(completion_text, completion_text)],
15129 counter.clone(),
15130 )
15131 .await;
15132 cx.condition(|editor, _| editor.context_menu_visible())
15133 .await;
15134 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15135
15136 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15137 editor
15138 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
15139 .unwrap()
15140 });
15141 cx.assert_editor_state(expected_with_replace_mode);
15142 handle_resolve_completion_request(&mut cx, None).await;
15143 apply_additional_edits.await.unwrap();
15144
15145 update_test_language_settings(&mut cx, |settings| {
15146 settings.defaults.completions = Some(CompletionSettingsContent {
15147 words: Some(WordsCompletionMode::Disabled),
15148 words_min_length: Some(0),
15149 // set the opposite here to ensure that the action is overriding the default behavior
15150 lsp_insert_mode: Some(LspInsertMode::Replace),
15151 ..Default::default()
15152 });
15153 });
15154
15155 cx.set_state(initial_state);
15156 cx.update_editor(|editor, window, cx| {
15157 editor.show_completions(&ShowCompletions, window, cx);
15158 });
15159 handle_completion_request_with_insert_and_replace(
15160 &mut cx,
15161 buffer_marked_text,
15162 vec![(completion_text, completion_text)],
15163 counter.clone(),
15164 )
15165 .await;
15166 cx.condition(|editor, _| editor.context_menu_visible())
15167 .await;
15168 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15169
15170 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15171 editor
15172 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
15173 .unwrap()
15174 });
15175 cx.assert_editor_state(expected_with_insert_mode);
15176 handle_resolve_completion_request(&mut cx, None).await;
15177 apply_additional_edits.await.unwrap();
15178}
15179
15180#[gpui::test]
15181async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
15182 init_test(cx, |_| {});
15183 let mut cx = EditorLspTestContext::new_rust(
15184 lsp::ServerCapabilities {
15185 completion_provider: Some(lsp::CompletionOptions {
15186 resolve_provider: Some(true),
15187 ..Default::default()
15188 }),
15189 ..Default::default()
15190 },
15191 cx,
15192 )
15193 .await;
15194
15195 // scenario: surrounding text matches completion text
15196 let completion_text = "to_offset";
15197 let initial_state = indoc! {"
15198 1. buf.to_offˇsuffix
15199 2. buf.to_offˇsuf
15200 3. buf.to_offˇfix
15201 4. buf.to_offˇ
15202 5. into_offˇensive
15203 6. ˇsuffix
15204 7. let ˇ //
15205 8. aaˇzz
15206 9. buf.to_off«zzzzzˇ»suffix
15207 10. buf.«ˇzzzzz»suffix
15208 11. to_off«ˇzzzzz»
15209
15210 buf.to_offˇsuffix // newest cursor
15211 "};
15212 let completion_marked_buffer = indoc! {"
15213 1. buf.to_offsuffix
15214 2. buf.to_offsuf
15215 3. buf.to_offfix
15216 4. buf.to_off
15217 5. into_offensive
15218 6. suffix
15219 7. let //
15220 8. aazz
15221 9. buf.to_offzzzzzsuffix
15222 10. buf.zzzzzsuffix
15223 11. to_offzzzzz
15224
15225 buf.<to_off|suffix> // newest cursor
15226 "};
15227 let expected = indoc! {"
15228 1. buf.to_offsetˇ
15229 2. buf.to_offsetˇsuf
15230 3. buf.to_offsetˇfix
15231 4. buf.to_offsetˇ
15232 5. into_offsetˇensive
15233 6. to_offsetˇsuffix
15234 7. let to_offsetˇ //
15235 8. aato_offsetˇzz
15236 9. buf.to_offsetˇ
15237 10. buf.to_offsetˇsuffix
15238 11. to_offsetˇ
15239
15240 buf.to_offsetˇ // newest cursor
15241 "};
15242 cx.set_state(initial_state);
15243 cx.update_editor(|editor, window, cx| {
15244 editor.show_completions(&ShowCompletions, window, cx);
15245 });
15246 handle_completion_request_with_insert_and_replace(
15247 &mut cx,
15248 completion_marked_buffer,
15249 vec![(completion_text, completion_text)],
15250 Arc::new(AtomicUsize::new(0)),
15251 )
15252 .await;
15253 cx.condition(|editor, _| editor.context_menu_visible())
15254 .await;
15255 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15256 editor
15257 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
15258 .unwrap()
15259 });
15260 cx.assert_editor_state(expected);
15261 handle_resolve_completion_request(&mut cx, None).await;
15262 apply_additional_edits.await.unwrap();
15263
15264 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
15265 let completion_text = "foo_and_bar";
15266 let initial_state = indoc! {"
15267 1. ooanbˇ
15268 2. zooanbˇ
15269 3. ooanbˇz
15270 4. zooanbˇz
15271 5. ooanˇ
15272 6. oanbˇ
15273
15274 ooanbˇ
15275 "};
15276 let completion_marked_buffer = indoc! {"
15277 1. ooanb
15278 2. zooanb
15279 3. ooanbz
15280 4. zooanbz
15281 5. ooan
15282 6. oanb
15283
15284 <ooanb|>
15285 "};
15286 let expected = indoc! {"
15287 1. foo_and_barˇ
15288 2. zfoo_and_barˇ
15289 3. foo_and_barˇz
15290 4. zfoo_and_barˇz
15291 5. ooanfoo_and_barˇ
15292 6. oanbfoo_and_barˇ
15293
15294 foo_and_barˇ
15295 "};
15296 cx.set_state(initial_state);
15297 cx.update_editor(|editor, window, cx| {
15298 editor.show_completions(&ShowCompletions, window, cx);
15299 });
15300 handle_completion_request_with_insert_and_replace(
15301 &mut cx,
15302 completion_marked_buffer,
15303 vec![(completion_text, completion_text)],
15304 Arc::new(AtomicUsize::new(0)),
15305 )
15306 .await;
15307 cx.condition(|editor, _| editor.context_menu_visible())
15308 .await;
15309 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15310 editor
15311 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
15312 .unwrap()
15313 });
15314 cx.assert_editor_state(expected);
15315 handle_resolve_completion_request(&mut cx, None).await;
15316 apply_additional_edits.await.unwrap();
15317
15318 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
15319 // (expects the same as if it was inserted at the end)
15320 let completion_text = "foo_and_bar";
15321 let initial_state = indoc! {"
15322 1. ooˇanb
15323 2. zooˇanb
15324 3. ooˇanbz
15325 4. zooˇanbz
15326
15327 ooˇanb
15328 "};
15329 let completion_marked_buffer = indoc! {"
15330 1. ooanb
15331 2. zooanb
15332 3. ooanbz
15333 4. zooanbz
15334
15335 <oo|anb>
15336 "};
15337 let expected = indoc! {"
15338 1. foo_and_barˇ
15339 2. zfoo_and_barˇ
15340 3. foo_and_barˇz
15341 4. zfoo_and_barˇz
15342
15343 foo_and_barˇ
15344 "};
15345 cx.set_state(initial_state);
15346 cx.update_editor(|editor, window, cx| {
15347 editor.show_completions(&ShowCompletions, window, cx);
15348 });
15349 handle_completion_request_with_insert_and_replace(
15350 &mut cx,
15351 completion_marked_buffer,
15352 vec![(completion_text, completion_text)],
15353 Arc::new(AtomicUsize::new(0)),
15354 )
15355 .await;
15356 cx.condition(|editor, _| editor.context_menu_visible())
15357 .await;
15358 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15359 editor
15360 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
15361 .unwrap()
15362 });
15363 cx.assert_editor_state(expected);
15364 handle_resolve_completion_request(&mut cx, None).await;
15365 apply_additional_edits.await.unwrap();
15366}
15367
15368// This used to crash
15369#[gpui::test]
15370async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
15371 init_test(cx, |_| {});
15372
15373 let buffer_text = indoc! {"
15374 fn main() {
15375 10.satu;
15376
15377 //
15378 // separate cursors so they open in different excerpts (manually reproducible)
15379 //
15380
15381 10.satu20;
15382 }
15383 "};
15384 let multibuffer_text_with_selections = indoc! {"
15385 fn main() {
15386 10.satuˇ;
15387
15388 //
15389
15390 //
15391
15392 10.satuˇ20;
15393 }
15394 "};
15395 let expected_multibuffer = indoc! {"
15396 fn main() {
15397 10.saturating_sub()ˇ;
15398
15399 //
15400
15401 //
15402
15403 10.saturating_sub()ˇ;
15404 }
15405 "};
15406
15407 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
15408 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
15409
15410 let fs = FakeFs::new(cx.executor());
15411 fs.insert_tree(
15412 path!("/a"),
15413 json!({
15414 "main.rs": buffer_text,
15415 }),
15416 )
15417 .await;
15418
15419 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15420 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15421 language_registry.add(rust_lang());
15422 let mut fake_servers = language_registry.register_fake_lsp(
15423 "Rust",
15424 FakeLspAdapter {
15425 capabilities: lsp::ServerCapabilities {
15426 completion_provider: Some(lsp::CompletionOptions {
15427 resolve_provider: None,
15428 ..lsp::CompletionOptions::default()
15429 }),
15430 ..lsp::ServerCapabilities::default()
15431 },
15432 ..FakeLspAdapter::default()
15433 },
15434 );
15435 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
15436 let workspace = window
15437 .read_with(cx, |mw, _| mw.workspace().clone())
15438 .unwrap();
15439 let cx = &mut VisualTestContext::from_window(*window, cx);
15440 let buffer = project
15441 .update(cx, |project, cx| {
15442 project.open_local_buffer(path!("/a/main.rs"), cx)
15443 })
15444 .await
15445 .unwrap();
15446
15447 let multi_buffer = cx.new(|cx| {
15448 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
15449 multi_buffer.push_excerpts(
15450 buffer.clone(),
15451 [ExcerptRange::new(0..first_excerpt_end)],
15452 cx,
15453 );
15454 multi_buffer.push_excerpts(
15455 buffer.clone(),
15456 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
15457 cx,
15458 );
15459 multi_buffer
15460 });
15461
15462 let editor = workspace.update_in(cx, |_, window, cx| {
15463 cx.new(|cx| {
15464 Editor::new(
15465 EditorMode::Full {
15466 scale_ui_elements_with_buffer_font_size: false,
15467 show_active_line_background: false,
15468 sizing_behavior: SizingBehavior::Default,
15469 },
15470 multi_buffer.clone(),
15471 Some(project.clone()),
15472 window,
15473 cx,
15474 )
15475 })
15476 });
15477
15478 let pane = workspace.update_in(cx, |workspace, _, _| workspace.active_pane().clone());
15479 pane.update_in(cx, |pane, window, cx| {
15480 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
15481 });
15482
15483 let fake_server = fake_servers.next().await.unwrap();
15484 cx.run_until_parked();
15485
15486 editor.update_in(cx, |editor, window, cx| {
15487 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15488 s.select_ranges([
15489 Point::new(1, 11)..Point::new(1, 11),
15490 Point::new(7, 11)..Point::new(7, 11),
15491 ])
15492 });
15493
15494 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
15495 });
15496
15497 editor.update_in(cx, |editor, window, cx| {
15498 editor.show_completions(&ShowCompletions, window, cx);
15499 });
15500
15501 fake_server
15502 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15503 let completion_item = lsp::CompletionItem {
15504 label: "saturating_sub()".into(),
15505 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15506 lsp::InsertReplaceEdit {
15507 new_text: "saturating_sub()".to_owned(),
15508 insert: lsp::Range::new(
15509 lsp::Position::new(7, 7),
15510 lsp::Position::new(7, 11),
15511 ),
15512 replace: lsp::Range::new(
15513 lsp::Position::new(7, 7),
15514 lsp::Position::new(7, 13),
15515 ),
15516 },
15517 )),
15518 ..lsp::CompletionItem::default()
15519 };
15520
15521 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
15522 })
15523 .next()
15524 .await
15525 .unwrap();
15526
15527 cx.condition(&editor, |editor, _| editor.context_menu_visible())
15528 .await;
15529
15530 editor
15531 .update_in(cx, |editor, window, cx| {
15532 editor
15533 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
15534 .unwrap()
15535 })
15536 .await
15537 .unwrap();
15538
15539 editor.update(cx, |editor, cx| {
15540 assert_text_with_selections(editor, expected_multibuffer, cx);
15541 })
15542}
15543
15544#[gpui::test]
15545async fn test_completion(cx: &mut TestAppContext) {
15546 init_test(cx, |_| {});
15547
15548 let mut cx = EditorLspTestContext::new_rust(
15549 lsp::ServerCapabilities {
15550 completion_provider: Some(lsp::CompletionOptions {
15551 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15552 resolve_provider: Some(true),
15553 ..Default::default()
15554 }),
15555 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15556 ..Default::default()
15557 },
15558 cx,
15559 )
15560 .await;
15561 let counter = Arc::new(AtomicUsize::new(0));
15562
15563 cx.set_state(indoc! {"
15564 oneˇ
15565 two
15566 three
15567 "});
15568 cx.simulate_keystroke(".");
15569 handle_completion_request(
15570 indoc! {"
15571 one.|<>
15572 two
15573 three
15574 "},
15575 vec!["first_completion", "second_completion"],
15576 true,
15577 counter.clone(),
15578 &mut cx,
15579 )
15580 .await;
15581 cx.condition(|editor, _| editor.context_menu_visible())
15582 .await;
15583 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15584
15585 let _handler = handle_signature_help_request(
15586 &mut cx,
15587 lsp::SignatureHelp {
15588 signatures: vec![lsp::SignatureInformation {
15589 label: "test signature".to_string(),
15590 documentation: None,
15591 parameters: Some(vec![lsp::ParameterInformation {
15592 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
15593 documentation: None,
15594 }]),
15595 active_parameter: None,
15596 }],
15597 active_signature: None,
15598 active_parameter: None,
15599 },
15600 );
15601 cx.update_editor(|editor, window, cx| {
15602 assert!(
15603 !editor.signature_help_state.is_shown(),
15604 "No signature help was called for"
15605 );
15606 editor.show_signature_help(&ShowSignatureHelp, window, cx);
15607 });
15608 cx.run_until_parked();
15609 cx.update_editor(|editor, _, _| {
15610 assert!(
15611 !editor.signature_help_state.is_shown(),
15612 "No signature help should be shown when completions menu is open"
15613 );
15614 });
15615
15616 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15617 editor.context_menu_next(&Default::default(), window, cx);
15618 editor
15619 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15620 .unwrap()
15621 });
15622 cx.assert_editor_state(indoc! {"
15623 one.second_completionˇ
15624 two
15625 three
15626 "});
15627
15628 handle_resolve_completion_request(
15629 &mut cx,
15630 Some(vec![
15631 (
15632 //This overlaps with the primary completion edit which is
15633 //misbehavior from the LSP spec, test that we filter it out
15634 indoc! {"
15635 one.second_ˇcompletion
15636 two
15637 threeˇ
15638 "},
15639 "overlapping additional edit",
15640 ),
15641 (
15642 indoc! {"
15643 one.second_completion
15644 two
15645 threeˇ
15646 "},
15647 "\nadditional edit",
15648 ),
15649 ]),
15650 )
15651 .await;
15652 apply_additional_edits.await.unwrap();
15653 cx.assert_editor_state(indoc! {"
15654 one.second_completionˇ
15655 two
15656 three
15657 additional edit
15658 "});
15659
15660 cx.set_state(indoc! {"
15661 one.second_completion
15662 twoˇ
15663 threeˇ
15664 additional edit
15665 "});
15666 cx.simulate_keystroke(" ");
15667 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15668 cx.simulate_keystroke("s");
15669 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15670
15671 cx.assert_editor_state(indoc! {"
15672 one.second_completion
15673 two sˇ
15674 three sˇ
15675 additional edit
15676 "});
15677 handle_completion_request(
15678 indoc! {"
15679 one.second_completion
15680 two s
15681 three <s|>
15682 additional edit
15683 "},
15684 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
15685 true,
15686 counter.clone(),
15687 &mut cx,
15688 )
15689 .await;
15690 cx.condition(|editor, _| editor.context_menu_visible())
15691 .await;
15692 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15693
15694 cx.simulate_keystroke("i");
15695
15696 handle_completion_request(
15697 indoc! {"
15698 one.second_completion
15699 two si
15700 three <si|>
15701 additional edit
15702 "},
15703 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
15704 true,
15705 counter.clone(),
15706 &mut cx,
15707 )
15708 .await;
15709 cx.condition(|editor, _| editor.context_menu_visible())
15710 .await;
15711 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15712
15713 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15714 editor
15715 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15716 .unwrap()
15717 });
15718 cx.assert_editor_state(indoc! {"
15719 one.second_completion
15720 two sixth_completionˇ
15721 three sixth_completionˇ
15722 additional edit
15723 "});
15724
15725 apply_additional_edits.await.unwrap();
15726
15727 update_test_language_settings(&mut cx, |settings| {
15728 settings.defaults.show_completions_on_input = Some(false);
15729 });
15730 cx.set_state("editorˇ");
15731 cx.simulate_keystroke(".");
15732 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15733 cx.simulate_keystrokes("c l o");
15734 cx.assert_editor_state("editor.cloˇ");
15735 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15736 cx.update_editor(|editor, window, cx| {
15737 editor.show_completions(&ShowCompletions, window, cx);
15738 });
15739 handle_completion_request(
15740 "editor.<clo|>",
15741 vec!["close", "clobber"],
15742 true,
15743 counter.clone(),
15744 &mut cx,
15745 )
15746 .await;
15747 cx.condition(|editor, _| editor.context_menu_visible())
15748 .await;
15749 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
15750
15751 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15752 editor
15753 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15754 .unwrap()
15755 });
15756 cx.assert_editor_state("editor.clobberˇ");
15757 handle_resolve_completion_request(&mut cx, None).await;
15758 apply_additional_edits.await.unwrap();
15759}
15760
15761#[gpui::test]
15762async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
15763 init_test(cx, |_| {});
15764
15765 let fs = FakeFs::new(cx.executor());
15766 fs.insert_tree(
15767 path!("/a"),
15768 json!({
15769 "main.rs": "",
15770 }),
15771 )
15772 .await;
15773
15774 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15775 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15776 language_registry.add(rust_lang());
15777 let command_calls = Arc::new(AtomicUsize::new(0));
15778 let registered_command = "_the/command";
15779
15780 let closure_command_calls = command_calls.clone();
15781 let mut fake_servers = language_registry.register_fake_lsp(
15782 "Rust",
15783 FakeLspAdapter {
15784 capabilities: lsp::ServerCapabilities {
15785 completion_provider: Some(lsp::CompletionOptions {
15786 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15787 ..lsp::CompletionOptions::default()
15788 }),
15789 execute_command_provider: Some(lsp::ExecuteCommandOptions {
15790 commands: vec![registered_command.to_owned()],
15791 ..lsp::ExecuteCommandOptions::default()
15792 }),
15793 ..lsp::ServerCapabilities::default()
15794 },
15795 initializer: Some(Box::new(move |fake_server| {
15796 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15797 move |params, _| async move {
15798 Ok(Some(lsp::CompletionResponse::Array(vec![
15799 lsp::CompletionItem {
15800 label: "registered_command".to_owned(),
15801 text_edit: gen_text_edit(¶ms, ""),
15802 command: Some(lsp::Command {
15803 title: registered_command.to_owned(),
15804 command: "_the/command".to_owned(),
15805 arguments: Some(vec![serde_json::Value::Bool(true)]),
15806 }),
15807 ..lsp::CompletionItem::default()
15808 },
15809 lsp::CompletionItem {
15810 label: "unregistered_command".to_owned(),
15811 text_edit: gen_text_edit(¶ms, ""),
15812 command: Some(lsp::Command {
15813 title: "????????????".to_owned(),
15814 command: "????????????".to_owned(),
15815 arguments: Some(vec![serde_json::Value::Null]),
15816 }),
15817 ..lsp::CompletionItem::default()
15818 },
15819 ])))
15820 },
15821 );
15822 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
15823 let command_calls = closure_command_calls.clone();
15824 move |params, _| {
15825 assert_eq!(params.command, registered_command);
15826 let command_calls = command_calls.clone();
15827 async move {
15828 command_calls.fetch_add(1, atomic::Ordering::Release);
15829 Ok(Some(json!(null)))
15830 }
15831 }
15832 });
15833 })),
15834 ..FakeLspAdapter::default()
15835 },
15836 );
15837 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
15838 let workspace = window
15839 .read_with(cx, |mw, _| mw.workspace().clone())
15840 .unwrap();
15841 let cx = &mut VisualTestContext::from_window(*window, cx);
15842 let editor = workspace
15843 .update_in(cx, |workspace, window, cx| {
15844 workspace.open_abs_path(
15845 PathBuf::from(path!("/a/main.rs")),
15846 OpenOptions::default(),
15847 window,
15848 cx,
15849 )
15850 })
15851 .await
15852 .unwrap()
15853 .downcast::<Editor>()
15854 .unwrap();
15855 let _fake_server = fake_servers.next().await.unwrap();
15856 cx.run_until_parked();
15857
15858 editor.update_in(cx, |editor, window, cx| {
15859 cx.focus_self(window);
15860 editor.move_to_end(&MoveToEnd, window, cx);
15861 editor.handle_input(".", window, cx);
15862 });
15863 cx.run_until_parked();
15864 editor.update(cx, |editor, _| {
15865 assert!(editor.context_menu_visible());
15866 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15867 {
15868 let completion_labels = menu
15869 .completions
15870 .borrow()
15871 .iter()
15872 .map(|c| c.label.text.clone())
15873 .collect::<Vec<_>>();
15874 assert_eq!(
15875 completion_labels,
15876 &["registered_command", "unregistered_command",],
15877 );
15878 } else {
15879 panic!("expected completion menu to be open");
15880 }
15881 });
15882
15883 editor
15884 .update_in(cx, |editor, window, cx| {
15885 editor
15886 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15887 .unwrap()
15888 })
15889 .await
15890 .unwrap();
15891 cx.run_until_parked();
15892 assert_eq!(
15893 command_calls.load(atomic::Ordering::Acquire),
15894 1,
15895 "For completion with a registered command, Zed should send a command execution request",
15896 );
15897
15898 editor.update_in(cx, |editor, window, cx| {
15899 cx.focus_self(window);
15900 editor.handle_input(".", window, cx);
15901 });
15902 cx.run_until_parked();
15903 editor.update(cx, |editor, _| {
15904 assert!(editor.context_menu_visible());
15905 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15906 {
15907 let completion_labels = menu
15908 .completions
15909 .borrow()
15910 .iter()
15911 .map(|c| c.label.text.clone())
15912 .collect::<Vec<_>>();
15913 assert_eq!(
15914 completion_labels,
15915 &["registered_command", "unregistered_command",],
15916 );
15917 } else {
15918 panic!("expected completion menu to be open");
15919 }
15920 });
15921 editor
15922 .update_in(cx, |editor, window, cx| {
15923 editor.context_menu_next(&Default::default(), window, cx);
15924 editor
15925 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15926 .unwrap()
15927 })
15928 .await
15929 .unwrap();
15930 cx.run_until_parked();
15931 assert_eq!(
15932 command_calls.load(atomic::Ordering::Acquire),
15933 1,
15934 "For completion with an unregistered command, Zed should not send a command execution request",
15935 );
15936}
15937
15938#[gpui::test]
15939async fn test_completion_reuse(cx: &mut TestAppContext) {
15940 init_test(cx, |_| {});
15941
15942 let mut cx = EditorLspTestContext::new_rust(
15943 lsp::ServerCapabilities {
15944 completion_provider: Some(lsp::CompletionOptions {
15945 trigger_characters: Some(vec![".".to_string()]),
15946 ..Default::default()
15947 }),
15948 ..Default::default()
15949 },
15950 cx,
15951 )
15952 .await;
15953
15954 let counter = Arc::new(AtomicUsize::new(0));
15955 cx.set_state("objˇ");
15956 cx.simulate_keystroke(".");
15957
15958 // Initial completion request returns complete results
15959 let is_incomplete = false;
15960 handle_completion_request(
15961 "obj.|<>",
15962 vec!["a", "ab", "abc"],
15963 is_incomplete,
15964 counter.clone(),
15965 &mut cx,
15966 )
15967 .await;
15968 cx.run_until_parked();
15969 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15970 cx.assert_editor_state("obj.ˇ");
15971 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15972
15973 // Type "a" - filters existing completions
15974 cx.simulate_keystroke("a");
15975 cx.run_until_parked();
15976 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15977 cx.assert_editor_state("obj.aˇ");
15978 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15979
15980 // Type "b" - filters existing completions
15981 cx.simulate_keystroke("b");
15982 cx.run_until_parked();
15983 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15984 cx.assert_editor_state("obj.abˇ");
15985 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15986
15987 // Type "c" - filters existing completions
15988 cx.simulate_keystroke("c");
15989 cx.run_until_parked();
15990 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15991 cx.assert_editor_state("obj.abcˇ");
15992 check_displayed_completions(vec!["abc"], &mut cx);
15993
15994 // Backspace to delete "c" - filters existing completions
15995 cx.update_editor(|editor, window, cx| {
15996 editor.backspace(&Backspace, window, cx);
15997 });
15998 cx.run_until_parked();
15999 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16000 cx.assert_editor_state("obj.abˇ");
16001 check_displayed_completions(vec!["ab", "abc"], &mut cx);
16002
16003 // Moving cursor to the left dismisses menu.
16004 cx.update_editor(|editor, window, cx| {
16005 editor.move_left(&MoveLeft, window, cx);
16006 });
16007 cx.run_until_parked();
16008 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16009 cx.assert_editor_state("obj.aˇb");
16010 cx.update_editor(|editor, _, _| {
16011 assert_eq!(editor.context_menu_visible(), false);
16012 });
16013
16014 // Type "b" - new request
16015 cx.simulate_keystroke("b");
16016 let is_incomplete = false;
16017 handle_completion_request(
16018 "obj.<ab|>a",
16019 vec!["ab", "abc"],
16020 is_incomplete,
16021 counter.clone(),
16022 &mut cx,
16023 )
16024 .await;
16025 cx.run_until_parked();
16026 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
16027 cx.assert_editor_state("obj.abˇb");
16028 check_displayed_completions(vec!["ab", "abc"], &mut cx);
16029
16030 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
16031 cx.update_editor(|editor, window, cx| {
16032 editor.backspace(&Backspace, window, cx);
16033 });
16034 let is_incomplete = false;
16035 handle_completion_request(
16036 "obj.<a|>b",
16037 vec!["a", "ab", "abc"],
16038 is_incomplete,
16039 counter.clone(),
16040 &mut cx,
16041 )
16042 .await;
16043 cx.run_until_parked();
16044 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
16045 cx.assert_editor_state("obj.aˇb");
16046 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
16047
16048 // Backspace to delete "a" - dismisses menu.
16049 cx.update_editor(|editor, window, cx| {
16050 editor.backspace(&Backspace, window, cx);
16051 });
16052 cx.run_until_parked();
16053 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
16054 cx.assert_editor_state("obj.ˇb");
16055 cx.update_editor(|editor, _, _| {
16056 assert_eq!(editor.context_menu_visible(), false);
16057 });
16058}
16059
16060#[gpui::test]
16061async fn test_word_completion(cx: &mut TestAppContext) {
16062 let lsp_fetch_timeout_ms = 10;
16063 init_test(cx, |language_settings| {
16064 language_settings.defaults.completions = Some(CompletionSettingsContent {
16065 words_min_length: Some(0),
16066 lsp_fetch_timeout_ms: Some(10),
16067 lsp_insert_mode: Some(LspInsertMode::Insert),
16068 ..Default::default()
16069 });
16070 });
16071
16072 let mut cx = EditorLspTestContext::new_rust(
16073 lsp::ServerCapabilities {
16074 completion_provider: Some(lsp::CompletionOptions {
16075 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16076 ..lsp::CompletionOptions::default()
16077 }),
16078 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
16079 ..lsp::ServerCapabilities::default()
16080 },
16081 cx,
16082 )
16083 .await;
16084
16085 let throttle_completions = Arc::new(AtomicBool::new(false));
16086
16087 let lsp_throttle_completions = throttle_completions.clone();
16088 let _completion_requests_handler =
16089 cx.lsp
16090 .server
16091 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
16092 let lsp_throttle_completions = lsp_throttle_completions.clone();
16093 let cx = cx.clone();
16094 async move {
16095 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
16096 cx.background_executor()
16097 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
16098 .await;
16099 }
16100 Ok(Some(lsp::CompletionResponse::Array(vec![
16101 lsp::CompletionItem {
16102 label: "first".into(),
16103 ..lsp::CompletionItem::default()
16104 },
16105 lsp::CompletionItem {
16106 label: "last".into(),
16107 ..lsp::CompletionItem::default()
16108 },
16109 ])))
16110 }
16111 });
16112
16113 cx.set_state(indoc! {"
16114 oneˇ
16115 two
16116 three
16117 "});
16118 cx.simulate_keystroke(".");
16119 cx.executor().run_until_parked();
16120 cx.condition(|editor, _| editor.context_menu_visible())
16121 .await;
16122 cx.update_editor(|editor, window, cx| {
16123 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16124 {
16125 assert_eq!(
16126 completion_menu_entries(menu),
16127 &["first", "last"],
16128 "When LSP server is fast to reply, no fallback word completions are used"
16129 );
16130 } else {
16131 panic!("expected completion menu to be open");
16132 }
16133 editor.cancel(&Cancel, window, cx);
16134 });
16135 cx.executor().run_until_parked();
16136 cx.condition(|editor, _| !editor.context_menu_visible())
16137 .await;
16138
16139 throttle_completions.store(true, atomic::Ordering::Release);
16140 cx.simulate_keystroke(".");
16141 cx.executor()
16142 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
16143 cx.executor().run_until_parked();
16144 cx.condition(|editor, _| editor.context_menu_visible())
16145 .await;
16146 cx.update_editor(|editor, _, _| {
16147 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16148 {
16149 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
16150 "When LSP server is slow, document words can be shown instead, if configured accordingly");
16151 } else {
16152 panic!("expected completion menu to be open");
16153 }
16154 });
16155}
16156
16157#[gpui::test]
16158async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
16159 init_test(cx, |language_settings| {
16160 language_settings.defaults.completions = Some(CompletionSettingsContent {
16161 words: Some(WordsCompletionMode::Enabled),
16162 words_min_length: Some(0),
16163 lsp_insert_mode: Some(LspInsertMode::Insert),
16164 ..Default::default()
16165 });
16166 });
16167
16168 let mut cx = EditorLspTestContext::new_rust(
16169 lsp::ServerCapabilities {
16170 completion_provider: Some(lsp::CompletionOptions {
16171 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16172 ..lsp::CompletionOptions::default()
16173 }),
16174 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
16175 ..lsp::ServerCapabilities::default()
16176 },
16177 cx,
16178 )
16179 .await;
16180
16181 let _completion_requests_handler =
16182 cx.lsp
16183 .server
16184 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
16185 Ok(Some(lsp::CompletionResponse::Array(vec![
16186 lsp::CompletionItem {
16187 label: "first".into(),
16188 ..lsp::CompletionItem::default()
16189 },
16190 lsp::CompletionItem {
16191 label: "last".into(),
16192 ..lsp::CompletionItem::default()
16193 },
16194 ])))
16195 });
16196
16197 cx.set_state(indoc! {"ˇ
16198 first
16199 last
16200 second
16201 "});
16202 cx.simulate_keystroke(".");
16203 cx.executor().run_until_parked();
16204 cx.condition(|editor, _| editor.context_menu_visible())
16205 .await;
16206 cx.update_editor(|editor, _, _| {
16207 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16208 {
16209 assert_eq!(
16210 completion_menu_entries(menu),
16211 &["first", "last", "second"],
16212 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
16213 );
16214 } else {
16215 panic!("expected completion menu to be open");
16216 }
16217 });
16218}
16219
16220#[gpui::test]
16221async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
16222 init_test(cx, |language_settings| {
16223 language_settings.defaults.completions = Some(CompletionSettingsContent {
16224 words: Some(WordsCompletionMode::Disabled),
16225 words_min_length: Some(0),
16226 lsp_insert_mode: Some(LspInsertMode::Insert),
16227 ..Default::default()
16228 });
16229 });
16230
16231 let mut cx = EditorLspTestContext::new_rust(
16232 lsp::ServerCapabilities {
16233 completion_provider: Some(lsp::CompletionOptions {
16234 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16235 ..lsp::CompletionOptions::default()
16236 }),
16237 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
16238 ..lsp::ServerCapabilities::default()
16239 },
16240 cx,
16241 )
16242 .await;
16243
16244 let _completion_requests_handler =
16245 cx.lsp
16246 .server
16247 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
16248 panic!("LSP completions should not be queried when dealing with word completions")
16249 });
16250
16251 cx.set_state(indoc! {"ˇ
16252 first
16253 last
16254 second
16255 "});
16256 cx.update_editor(|editor, window, cx| {
16257 editor.show_word_completions(&ShowWordCompletions, window, cx);
16258 });
16259 cx.executor().run_until_parked();
16260 cx.condition(|editor, _| editor.context_menu_visible())
16261 .await;
16262 cx.update_editor(|editor, _, _| {
16263 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16264 {
16265 assert_eq!(
16266 completion_menu_entries(menu),
16267 &["first", "last", "second"],
16268 "`ShowWordCompletions` action should show word completions"
16269 );
16270 } else {
16271 panic!("expected completion menu to be open");
16272 }
16273 });
16274
16275 cx.simulate_keystroke("l");
16276 cx.executor().run_until_parked();
16277 cx.condition(|editor, _| editor.context_menu_visible())
16278 .await;
16279 cx.update_editor(|editor, _, _| {
16280 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16281 {
16282 assert_eq!(
16283 completion_menu_entries(menu),
16284 &["last"],
16285 "After showing word completions, further editing should filter them and not query the LSP"
16286 );
16287 } else {
16288 panic!("expected completion menu to be open");
16289 }
16290 });
16291}
16292
16293#[gpui::test]
16294async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
16295 init_test(cx, |language_settings| {
16296 language_settings.defaults.completions = Some(CompletionSettingsContent {
16297 words_min_length: Some(0),
16298 lsp: Some(false),
16299 lsp_insert_mode: Some(LspInsertMode::Insert),
16300 ..Default::default()
16301 });
16302 });
16303
16304 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16305
16306 cx.set_state(indoc! {"ˇ
16307 0_usize
16308 let
16309 33
16310 4.5f32
16311 "});
16312 cx.update_editor(|editor, window, cx| {
16313 editor.show_completions(&ShowCompletions, window, cx);
16314 });
16315 cx.executor().run_until_parked();
16316 cx.condition(|editor, _| editor.context_menu_visible())
16317 .await;
16318 cx.update_editor(|editor, window, cx| {
16319 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16320 {
16321 assert_eq!(
16322 completion_menu_entries(menu),
16323 &["let"],
16324 "With no digits in the completion query, no digits should be in the word completions"
16325 );
16326 } else {
16327 panic!("expected completion menu to be open");
16328 }
16329 editor.cancel(&Cancel, window, cx);
16330 });
16331
16332 cx.set_state(indoc! {"3ˇ
16333 0_usize
16334 let
16335 3
16336 33.35f32
16337 "});
16338 cx.update_editor(|editor, window, cx| {
16339 editor.show_completions(&ShowCompletions, window, cx);
16340 });
16341 cx.executor().run_until_parked();
16342 cx.condition(|editor, _| editor.context_menu_visible())
16343 .await;
16344 cx.update_editor(|editor, _, _| {
16345 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16346 {
16347 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
16348 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
16349 } else {
16350 panic!("expected completion menu to be open");
16351 }
16352 });
16353}
16354
16355#[gpui::test]
16356async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
16357 init_test(cx, |language_settings| {
16358 language_settings.defaults.completions = Some(CompletionSettingsContent {
16359 words: Some(WordsCompletionMode::Enabled),
16360 words_min_length: Some(3),
16361 lsp_insert_mode: Some(LspInsertMode::Insert),
16362 ..Default::default()
16363 });
16364 });
16365
16366 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16367 cx.set_state(indoc! {"ˇ
16368 wow
16369 wowen
16370 wowser
16371 "});
16372 cx.simulate_keystroke("w");
16373 cx.executor().run_until_parked();
16374 cx.update_editor(|editor, _, _| {
16375 if editor.context_menu.borrow_mut().is_some() {
16376 panic!(
16377 "expected completion menu to be hidden, as words completion threshold is not met"
16378 );
16379 }
16380 });
16381
16382 cx.update_editor(|editor, window, cx| {
16383 editor.show_word_completions(&ShowWordCompletions, window, cx);
16384 });
16385 cx.executor().run_until_parked();
16386 cx.update_editor(|editor, window, cx| {
16387 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16388 {
16389 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");
16390 } else {
16391 panic!("expected completion menu to be open after the word completions are called with an action");
16392 }
16393
16394 editor.cancel(&Cancel, window, cx);
16395 });
16396 cx.update_editor(|editor, _, _| {
16397 if editor.context_menu.borrow_mut().is_some() {
16398 panic!("expected completion menu to be hidden after canceling");
16399 }
16400 });
16401
16402 cx.simulate_keystroke("o");
16403 cx.executor().run_until_parked();
16404 cx.update_editor(|editor, _, _| {
16405 if editor.context_menu.borrow_mut().is_some() {
16406 panic!(
16407 "expected completion menu to be hidden, as words completion threshold is not met still"
16408 );
16409 }
16410 });
16411
16412 cx.simulate_keystroke("w");
16413 cx.executor().run_until_parked();
16414 cx.update_editor(|editor, _, _| {
16415 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16416 {
16417 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
16418 } else {
16419 panic!("expected completion menu to be open after the word completions threshold is met");
16420 }
16421 });
16422}
16423
16424#[gpui::test]
16425async fn test_word_completions_disabled(cx: &mut TestAppContext) {
16426 init_test(cx, |language_settings| {
16427 language_settings.defaults.completions = Some(CompletionSettingsContent {
16428 words: Some(WordsCompletionMode::Enabled),
16429 words_min_length: Some(0),
16430 lsp_insert_mode: Some(LspInsertMode::Insert),
16431 ..Default::default()
16432 });
16433 });
16434
16435 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16436 cx.update_editor(|editor, _, _| {
16437 editor.disable_word_completions();
16438 });
16439 cx.set_state(indoc! {"ˇ
16440 wow
16441 wowen
16442 wowser
16443 "});
16444 cx.simulate_keystroke("w");
16445 cx.executor().run_until_parked();
16446 cx.update_editor(|editor, _, _| {
16447 if editor.context_menu.borrow_mut().is_some() {
16448 panic!(
16449 "expected completion menu to be hidden, as words completion are disabled for this editor"
16450 );
16451 }
16452 });
16453
16454 cx.update_editor(|editor, window, cx| {
16455 editor.show_word_completions(&ShowWordCompletions, window, cx);
16456 });
16457 cx.executor().run_until_parked();
16458 cx.update_editor(|editor, _, _| {
16459 if editor.context_menu.borrow_mut().is_some() {
16460 panic!(
16461 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
16462 );
16463 }
16464 });
16465}
16466
16467#[gpui::test]
16468async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
16469 init_test(cx, |language_settings| {
16470 language_settings.defaults.completions = Some(CompletionSettingsContent {
16471 words: Some(WordsCompletionMode::Disabled),
16472 words_min_length: Some(0),
16473 lsp_insert_mode: Some(LspInsertMode::Insert),
16474 ..Default::default()
16475 });
16476 });
16477
16478 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16479 cx.update_editor(|editor, _, _| {
16480 editor.set_completion_provider(None);
16481 });
16482 cx.set_state(indoc! {"ˇ
16483 wow
16484 wowen
16485 wowser
16486 "});
16487 cx.simulate_keystroke("w");
16488 cx.executor().run_until_parked();
16489 cx.update_editor(|editor, _, _| {
16490 if editor.context_menu.borrow_mut().is_some() {
16491 panic!("expected completion menu to be hidden, as disabled in settings");
16492 }
16493 });
16494}
16495
16496fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
16497 let position = || lsp::Position {
16498 line: params.text_document_position.position.line,
16499 character: params.text_document_position.position.character,
16500 };
16501 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16502 range: lsp::Range {
16503 start: position(),
16504 end: position(),
16505 },
16506 new_text: text.to_string(),
16507 }))
16508}
16509
16510#[gpui::test]
16511async fn test_multiline_completion(cx: &mut TestAppContext) {
16512 init_test(cx, |_| {});
16513
16514 let fs = FakeFs::new(cx.executor());
16515 fs.insert_tree(
16516 path!("/a"),
16517 json!({
16518 "main.ts": "a",
16519 }),
16520 )
16521 .await;
16522
16523 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16524 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16525 let typescript_language = Arc::new(Language::new(
16526 LanguageConfig {
16527 name: "TypeScript".into(),
16528 matcher: LanguageMatcher {
16529 path_suffixes: vec!["ts".to_string()],
16530 ..LanguageMatcher::default()
16531 },
16532 line_comments: vec!["// ".into()],
16533 ..LanguageConfig::default()
16534 },
16535 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16536 ));
16537 language_registry.add(typescript_language.clone());
16538 let mut fake_servers = language_registry.register_fake_lsp(
16539 "TypeScript",
16540 FakeLspAdapter {
16541 capabilities: lsp::ServerCapabilities {
16542 completion_provider: Some(lsp::CompletionOptions {
16543 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16544 ..lsp::CompletionOptions::default()
16545 }),
16546 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
16547 ..lsp::ServerCapabilities::default()
16548 },
16549 // Emulate vtsls label generation
16550 label_for_completion: Some(Box::new(|item, _| {
16551 let text = if let Some(description) = item
16552 .label_details
16553 .as_ref()
16554 .and_then(|label_details| label_details.description.as_ref())
16555 {
16556 format!("{} {}", item.label, description)
16557 } else if let Some(detail) = &item.detail {
16558 format!("{} {}", item.label, detail)
16559 } else {
16560 item.label.clone()
16561 };
16562 Some(language::CodeLabel::plain(text, None))
16563 })),
16564 ..FakeLspAdapter::default()
16565 },
16566 );
16567 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
16568 let workspace = window
16569 .read_with(cx, |mw, _| mw.workspace().clone())
16570 .unwrap();
16571 let cx = &mut VisualTestContext::from_window(*window, cx);
16572 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
16573 workspace.project().update(cx, |project, cx| {
16574 project.worktrees(cx).next().unwrap().read(cx).id()
16575 })
16576 });
16577
16578 let _buffer = project
16579 .update(cx, |project, cx| {
16580 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
16581 })
16582 .await
16583 .unwrap();
16584 let editor = workspace
16585 .update_in(cx, |workspace, window, cx| {
16586 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
16587 })
16588 .await
16589 .unwrap()
16590 .downcast::<Editor>()
16591 .unwrap();
16592 let fake_server = fake_servers.next().await.unwrap();
16593 cx.run_until_parked();
16594
16595 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
16596 let multiline_label_2 = "a\nb\nc\n";
16597 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
16598 let multiline_description = "d\ne\nf\n";
16599 let multiline_detail_2 = "g\nh\ni\n";
16600
16601 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
16602 move |params, _| async move {
16603 Ok(Some(lsp::CompletionResponse::Array(vec![
16604 lsp::CompletionItem {
16605 label: multiline_label.to_string(),
16606 text_edit: gen_text_edit(¶ms, "new_text_1"),
16607 ..lsp::CompletionItem::default()
16608 },
16609 lsp::CompletionItem {
16610 label: "single line label 1".to_string(),
16611 detail: Some(multiline_detail.to_string()),
16612 text_edit: gen_text_edit(¶ms, "new_text_2"),
16613 ..lsp::CompletionItem::default()
16614 },
16615 lsp::CompletionItem {
16616 label: "single line label 2".to_string(),
16617 label_details: Some(lsp::CompletionItemLabelDetails {
16618 description: Some(multiline_description.to_string()),
16619 detail: None,
16620 }),
16621 text_edit: gen_text_edit(¶ms, "new_text_2"),
16622 ..lsp::CompletionItem::default()
16623 },
16624 lsp::CompletionItem {
16625 label: multiline_label_2.to_string(),
16626 detail: Some(multiline_detail_2.to_string()),
16627 text_edit: gen_text_edit(¶ms, "new_text_3"),
16628 ..lsp::CompletionItem::default()
16629 },
16630 lsp::CompletionItem {
16631 label: "Label with many spaces and \t but without newlines".to_string(),
16632 detail: Some(
16633 "Details with many spaces and \t but without newlines".to_string(),
16634 ),
16635 text_edit: gen_text_edit(¶ms, "new_text_4"),
16636 ..lsp::CompletionItem::default()
16637 },
16638 ])))
16639 },
16640 );
16641
16642 editor.update_in(cx, |editor, window, cx| {
16643 cx.focus_self(window);
16644 editor.move_to_end(&MoveToEnd, window, cx);
16645 editor.handle_input(".", window, cx);
16646 });
16647 cx.run_until_parked();
16648 completion_handle.next().await.unwrap();
16649
16650 editor.update(cx, |editor, _| {
16651 assert!(editor.context_menu_visible());
16652 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16653 {
16654 let completion_labels = menu
16655 .completions
16656 .borrow()
16657 .iter()
16658 .map(|c| c.label.text.clone())
16659 .collect::<Vec<_>>();
16660 assert_eq!(
16661 completion_labels,
16662 &[
16663 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
16664 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
16665 "single line label 2 d e f ",
16666 "a b c g h i ",
16667 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
16668 ],
16669 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
16670 );
16671
16672 for completion in menu
16673 .completions
16674 .borrow()
16675 .iter() {
16676 assert_eq!(
16677 completion.label.filter_range,
16678 0..completion.label.text.len(),
16679 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
16680 );
16681 }
16682 } else {
16683 panic!("expected completion menu to be open");
16684 }
16685 });
16686}
16687
16688#[gpui::test]
16689async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
16690 init_test(cx, |_| {});
16691 let mut cx = EditorLspTestContext::new_rust(
16692 lsp::ServerCapabilities {
16693 completion_provider: Some(lsp::CompletionOptions {
16694 trigger_characters: Some(vec![".".to_string()]),
16695 ..Default::default()
16696 }),
16697 ..Default::default()
16698 },
16699 cx,
16700 )
16701 .await;
16702 cx.lsp
16703 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16704 Ok(Some(lsp::CompletionResponse::Array(vec![
16705 lsp::CompletionItem {
16706 label: "first".into(),
16707 ..Default::default()
16708 },
16709 lsp::CompletionItem {
16710 label: "last".into(),
16711 ..Default::default()
16712 },
16713 ])))
16714 });
16715 cx.set_state("variableˇ");
16716 cx.simulate_keystroke(".");
16717 cx.executor().run_until_parked();
16718
16719 cx.update_editor(|editor, _, _| {
16720 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16721 {
16722 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
16723 } else {
16724 panic!("expected completion menu to be open");
16725 }
16726 });
16727
16728 cx.update_editor(|editor, window, cx| {
16729 editor.move_page_down(&MovePageDown::default(), window, cx);
16730 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16731 {
16732 assert!(
16733 menu.selected_item == 1,
16734 "expected PageDown to select the last item from the context menu"
16735 );
16736 } else {
16737 panic!("expected completion menu to stay open after PageDown");
16738 }
16739 });
16740
16741 cx.update_editor(|editor, window, cx| {
16742 editor.move_page_up(&MovePageUp::default(), window, cx);
16743 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16744 {
16745 assert!(
16746 menu.selected_item == 0,
16747 "expected PageUp to select the first item from the context menu"
16748 );
16749 } else {
16750 panic!("expected completion menu to stay open after PageUp");
16751 }
16752 });
16753}
16754
16755#[gpui::test]
16756async fn test_as_is_completions(cx: &mut TestAppContext) {
16757 init_test(cx, |_| {});
16758 let mut cx = EditorLspTestContext::new_rust(
16759 lsp::ServerCapabilities {
16760 completion_provider: Some(lsp::CompletionOptions {
16761 ..Default::default()
16762 }),
16763 ..Default::default()
16764 },
16765 cx,
16766 )
16767 .await;
16768 cx.lsp
16769 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16770 Ok(Some(lsp::CompletionResponse::Array(vec![
16771 lsp::CompletionItem {
16772 label: "unsafe".into(),
16773 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16774 range: lsp::Range {
16775 start: lsp::Position {
16776 line: 1,
16777 character: 2,
16778 },
16779 end: lsp::Position {
16780 line: 1,
16781 character: 3,
16782 },
16783 },
16784 new_text: "unsafe".to_string(),
16785 })),
16786 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
16787 ..Default::default()
16788 },
16789 ])))
16790 });
16791 cx.set_state("fn a() {}\n nˇ");
16792 cx.executor().run_until_parked();
16793 cx.update_editor(|editor, window, cx| {
16794 editor.trigger_completion_on_input("n", true, window, cx)
16795 });
16796 cx.executor().run_until_parked();
16797
16798 cx.update_editor(|editor, window, cx| {
16799 editor.confirm_completion(&Default::default(), window, cx)
16800 });
16801 cx.executor().run_until_parked();
16802 cx.assert_editor_state("fn a() {}\n unsafeˇ");
16803}
16804
16805#[gpui::test]
16806async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
16807 init_test(cx, |_| {});
16808 let language =
16809 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
16810 let mut cx = EditorLspTestContext::new(
16811 language,
16812 lsp::ServerCapabilities {
16813 completion_provider: Some(lsp::CompletionOptions {
16814 ..lsp::CompletionOptions::default()
16815 }),
16816 ..lsp::ServerCapabilities::default()
16817 },
16818 cx,
16819 )
16820 .await;
16821
16822 cx.set_state(
16823 "#ifndef BAR_H
16824#define BAR_H
16825
16826#include <stdbool.h>
16827
16828int fn_branch(bool do_branch1, bool do_branch2);
16829
16830#endif // BAR_H
16831ˇ",
16832 );
16833 cx.executor().run_until_parked();
16834 cx.update_editor(|editor, window, cx| {
16835 editor.handle_input("#", window, cx);
16836 });
16837 cx.executor().run_until_parked();
16838 cx.update_editor(|editor, window, cx| {
16839 editor.handle_input("i", window, cx);
16840 });
16841 cx.executor().run_until_parked();
16842 cx.update_editor(|editor, window, cx| {
16843 editor.handle_input("n", window, cx);
16844 });
16845 cx.executor().run_until_parked();
16846 cx.assert_editor_state(
16847 "#ifndef BAR_H
16848#define BAR_H
16849
16850#include <stdbool.h>
16851
16852int fn_branch(bool do_branch1, bool do_branch2);
16853
16854#endif // BAR_H
16855#inˇ",
16856 );
16857
16858 cx.lsp
16859 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16860 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16861 is_incomplete: false,
16862 item_defaults: None,
16863 items: vec![lsp::CompletionItem {
16864 kind: Some(lsp::CompletionItemKind::SNIPPET),
16865 label_details: Some(lsp::CompletionItemLabelDetails {
16866 detail: Some("header".to_string()),
16867 description: None,
16868 }),
16869 label: " include".to_string(),
16870 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16871 range: lsp::Range {
16872 start: lsp::Position {
16873 line: 8,
16874 character: 1,
16875 },
16876 end: lsp::Position {
16877 line: 8,
16878 character: 1,
16879 },
16880 },
16881 new_text: "include \"$0\"".to_string(),
16882 })),
16883 sort_text: Some("40b67681include".to_string()),
16884 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16885 filter_text: Some("include".to_string()),
16886 insert_text: Some("include \"$0\"".to_string()),
16887 ..lsp::CompletionItem::default()
16888 }],
16889 })))
16890 });
16891 cx.update_editor(|editor, window, cx| {
16892 editor.show_completions(&ShowCompletions, window, cx);
16893 });
16894 cx.executor().run_until_parked();
16895 cx.update_editor(|editor, window, cx| {
16896 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16897 });
16898 cx.executor().run_until_parked();
16899 cx.assert_editor_state(
16900 "#ifndef BAR_H
16901#define BAR_H
16902
16903#include <stdbool.h>
16904
16905int fn_branch(bool do_branch1, bool do_branch2);
16906
16907#endif // BAR_H
16908#include \"ˇ\"",
16909 );
16910
16911 cx.lsp
16912 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16913 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16914 is_incomplete: true,
16915 item_defaults: None,
16916 items: vec![lsp::CompletionItem {
16917 kind: Some(lsp::CompletionItemKind::FILE),
16918 label: "AGL/".to_string(),
16919 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16920 range: lsp::Range {
16921 start: lsp::Position {
16922 line: 8,
16923 character: 10,
16924 },
16925 end: lsp::Position {
16926 line: 8,
16927 character: 11,
16928 },
16929 },
16930 new_text: "AGL/".to_string(),
16931 })),
16932 sort_text: Some("40b67681AGL/".to_string()),
16933 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16934 filter_text: Some("AGL/".to_string()),
16935 insert_text: Some("AGL/".to_string()),
16936 ..lsp::CompletionItem::default()
16937 }],
16938 })))
16939 });
16940 cx.update_editor(|editor, window, cx| {
16941 editor.show_completions(&ShowCompletions, window, cx);
16942 });
16943 cx.executor().run_until_parked();
16944 cx.update_editor(|editor, window, cx| {
16945 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16946 });
16947 cx.executor().run_until_parked();
16948 cx.assert_editor_state(
16949 r##"#ifndef BAR_H
16950#define BAR_H
16951
16952#include <stdbool.h>
16953
16954int fn_branch(bool do_branch1, bool do_branch2);
16955
16956#endif // BAR_H
16957#include "AGL/ˇ"##,
16958 );
16959
16960 cx.update_editor(|editor, window, cx| {
16961 editor.handle_input("\"", window, cx);
16962 });
16963 cx.executor().run_until_parked();
16964 cx.assert_editor_state(
16965 r##"#ifndef BAR_H
16966#define BAR_H
16967
16968#include <stdbool.h>
16969
16970int fn_branch(bool do_branch1, bool do_branch2);
16971
16972#endif // BAR_H
16973#include "AGL/"ˇ"##,
16974 );
16975}
16976
16977#[gpui::test]
16978async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
16979 init_test(cx, |_| {});
16980
16981 let mut cx = EditorLspTestContext::new_rust(
16982 lsp::ServerCapabilities {
16983 completion_provider: Some(lsp::CompletionOptions {
16984 trigger_characters: Some(vec![".".to_string()]),
16985 resolve_provider: Some(false),
16986 ..lsp::CompletionOptions::default()
16987 }),
16988 ..lsp::ServerCapabilities::default()
16989 },
16990 cx,
16991 )
16992 .await;
16993
16994 cx.set_state("fn main() { let a = 2ˇ; }");
16995 cx.simulate_keystroke(".");
16996 let completion_item = lsp::CompletionItem {
16997 label: "Some".into(),
16998 kind: Some(lsp::CompletionItemKind::SNIPPET),
16999 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17000 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17001 kind: lsp::MarkupKind::Markdown,
17002 value: "```rust\nSome(2)\n```".to_string(),
17003 })),
17004 deprecated: Some(false),
17005 sort_text: Some("Some".to_string()),
17006 filter_text: Some("Some".to_string()),
17007 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17008 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17009 range: lsp::Range {
17010 start: lsp::Position {
17011 line: 0,
17012 character: 22,
17013 },
17014 end: lsp::Position {
17015 line: 0,
17016 character: 22,
17017 },
17018 },
17019 new_text: "Some(2)".to_string(),
17020 })),
17021 additional_text_edits: Some(vec![lsp::TextEdit {
17022 range: lsp::Range {
17023 start: lsp::Position {
17024 line: 0,
17025 character: 20,
17026 },
17027 end: lsp::Position {
17028 line: 0,
17029 character: 22,
17030 },
17031 },
17032 new_text: "".to_string(),
17033 }]),
17034 ..Default::default()
17035 };
17036
17037 let closure_completion_item = completion_item.clone();
17038 let counter = Arc::new(AtomicUsize::new(0));
17039 let counter_clone = counter.clone();
17040 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17041 let task_completion_item = closure_completion_item.clone();
17042 counter_clone.fetch_add(1, atomic::Ordering::Release);
17043 async move {
17044 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17045 is_incomplete: true,
17046 item_defaults: None,
17047 items: vec![task_completion_item],
17048 })))
17049 }
17050 });
17051
17052 cx.condition(|editor, _| editor.context_menu_visible())
17053 .await;
17054 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
17055 assert!(request.next().await.is_some());
17056 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
17057
17058 cx.simulate_keystrokes("S o m");
17059 cx.condition(|editor, _| editor.context_menu_visible())
17060 .await;
17061 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
17062 assert!(request.next().await.is_some());
17063 assert!(request.next().await.is_some());
17064 assert!(request.next().await.is_some());
17065 request.close();
17066 assert!(request.next().await.is_none());
17067 assert_eq!(
17068 counter.load(atomic::Ordering::Acquire),
17069 4,
17070 "With the completions menu open, only one LSP request should happen per input"
17071 );
17072}
17073
17074#[gpui::test]
17075async fn test_toggle_comment(cx: &mut TestAppContext) {
17076 init_test(cx, |_| {});
17077 let mut cx = EditorTestContext::new(cx).await;
17078 let language = Arc::new(Language::new(
17079 LanguageConfig {
17080 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
17081 ..Default::default()
17082 },
17083 Some(tree_sitter_rust::LANGUAGE.into()),
17084 ));
17085 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
17086
17087 // If multiple selections intersect a line, the line is only toggled once.
17088 cx.set_state(indoc! {"
17089 fn a() {
17090 «//b();
17091 ˇ»// «c();
17092 //ˇ» d();
17093 }
17094 "});
17095
17096 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17097
17098 cx.assert_editor_state(indoc! {"
17099 fn a() {
17100 «b();
17101 ˇ»«c();
17102 ˇ» d();
17103 }
17104 "});
17105
17106 // The comment prefix is inserted at the same column for every line in a
17107 // selection.
17108 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17109
17110 cx.assert_editor_state(indoc! {"
17111 fn a() {
17112 // «b();
17113 ˇ»// «c();
17114 ˇ» // d();
17115 }
17116 "});
17117
17118 // If a selection ends at the beginning of a line, that line is not toggled.
17119 cx.set_selections_state(indoc! {"
17120 fn a() {
17121 // b();
17122 «// c();
17123 ˇ» // d();
17124 }
17125 "});
17126
17127 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17128
17129 cx.assert_editor_state(indoc! {"
17130 fn a() {
17131 // b();
17132 «c();
17133 ˇ» // d();
17134 }
17135 "});
17136
17137 // If a selection span a single line and is empty, the line is toggled.
17138 cx.set_state(indoc! {"
17139 fn a() {
17140 a();
17141 b();
17142 ˇ
17143 }
17144 "});
17145
17146 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17147
17148 cx.assert_editor_state(indoc! {"
17149 fn a() {
17150 a();
17151 b();
17152 //•ˇ
17153 }
17154 "});
17155
17156 // If a selection span multiple lines, empty lines are not toggled.
17157 cx.set_state(indoc! {"
17158 fn a() {
17159 «a();
17160
17161 c();ˇ»
17162 }
17163 "});
17164
17165 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17166
17167 cx.assert_editor_state(indoc! {"
17168 fn a() {
17169 // «a();
17170
17171 // c();ˇ»
17172 }
17173 "});
17174
17175 // If a selection includes multiple comment prefixes, all lines are uncommented.
17176 cx.set_state(indoc! {"
17177 fn a() {
17178 «// a();
17179 /// b();
17180 //! c();ˇ»
17181 }
17182 "});
17183
17184 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
17185
17186 cx.assert_editor_state(indoc! {"
17187 fn a() {
17188 «a();
17189 b();
17190 c();ˇ»
17191 }
17192 "});
17193}
17194
17195#[gpui::test]
17196async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
17197 init_test(cx, |_| {});
17198 let mut cx = EditorTestContext::new(cx).await;
17199 let language = Arc::new(Language::new(
17200 LanguageConfig {
17201 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
17202 ..Default::default()
17203 },
17204 Some(tree_sitter_rust::LANGUAGE.into()),
17205 ));
17206 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
17207
17208 let toggle_comments = &ToggleComments {
17209 advance_downwards: false,
17210 ignore_indent: true,
17211 };
17212
17213 // If multiple selections intersect a line, the line is only toggled once.
17214 cx.set_state(indoc! {"
17215 fn a() {
17216 // «b();
17217 // c();
17218 // ˇ» d();
17219 }
17220 "});
17221
17222 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17223
17224 cx.assert_editor_state(indoc! {"
17225 fn a() {
17226 «b();
17227 c();
17228 ˇ» d();
17229 }
17230 "});
17231
17232 // The comment prefix is inserted at the beginning of each line
17233 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17234
17235 cx.assert_editor_state(indoc! {"
17236 fn a() {
17237 // «b();
17238 // c();
17239 // ˇ» d();
17240 }
17241 "});
17242
17243 // If a selection ends at the beginning of a line, that line is not toggled.
17244 cx.set_selections_state(indoc! {"
17245 fn a() {
17246 // b();
17247 // «c();
17248 ˇ»// d();
17249 }
17250 "});
17251
17252 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17253
17254 cx.assert_editor_state(indoc! {"
17255 fn a() {
17256 // b();
17257 «c();
17258 ˇ»// d();
17259 }
17260 "});
17261
17262 // If a selection span a single line and is empty, the line is toggled.
17263 cx.set_state(indoc! {"
17264 fn a() {
17265 a();
17266 b();
17267 ˇ
17268 }
17269 "});
17270
17271 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17272
17273 cx.assert_editor_state(indoc! {"
17274 fn a() {
17275 a();
17276 b();
17277 //ˇ
17278 }
17279 "});
17280
17281 // If a selection span multiple lines, empty lines are not toggled.
17282 cx.set_state(indoc! {"
17283 fn a() {
17284 «a();
17285
17286 c();ˇ»
17287 }
17288 "});
17289
17290 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17291
17292 cx.assert_editor_state(indoc! {"
17293 fn a() {
17294 // «a();
17295
17296 // c();ˇ»
17297 }
17298 "});
17299
17300 // If a selection includes multiple comment prefixes, all lines are uncommented.
17301 cx.set_state(indoc! {"
17302 fn a() {
17303 // «a();
17304 /// b();
17305 //! c();ˇ»
17306 }
17307 "});
17308
17309 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
17310
17311 cx.assert_editor_state(indoc! {"
17312 fn a() {
17313 «a();
17314 b();
17315 c();ˇ»
17316 }
17317 "});
17318}
17319
17320#[gpui::test]
17321async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
17322 init_test(cx, |_| {});
17323
17324 let language = Arc::new(Language::new(
17325 LanguageConfig {
17326 line_comments: vec!["// ".into()],
17327 ..Default::default()
17328 },
17329 Some(tree_sitter_rust::LANGUAGE.into()),
17330 ));
17331
17332 let mut cx = EditorTestContext::new(cx).await;
17333
17334 cx.language_registry().add(language.clone());
17335 cx.update_buffer(|buffer, cx| {
17336 buffer.set_language(Some(language), cx);
17337 });
17338
17339 let toggle_comments = &ToggleComments {
17340 advance_downwards: true,
17341 ignore_indent: false,
17342 };
17343
17344 // Single cursor on one line -> advance
17345 // Cursor moves horizontally 3 characters as well on non-blank line
17346 cx.set_state(indoc!(
17347 "fn a() {
17348 ˇdog();
17349 cat();
17350 }"
17351 ));
17352 cx.update_editor(|editor, window, cx| {
17353 editor.toggle_comments(toggle_comments, window, cx);
17354 });
17355 cx.assert_editor_state(indoc!(
17356 "fn a() {
17357 // dog();
17358 catˇ();
17359 }"
17360 ));
17361
17362 // Single selection on one line -> don't advance
17363 cx.set_state(indoc!(
17364 "fn a() {
17365 «dog()ˇ»;
17366 cat();
17367 }"
17368 ));
17369 cx.update_editor(|editor, window, cx| {
17370 editor.toggle_comments(toggle_comments, window, cx);
17371 });
17372 cx.assert_editor_state(indoc!(
17373 "fn a() {
17374 // «dog()ˇ»;
17375 cat();
17376 }"
17377 ));
17378
17379 // Multiple cursors on one line -> advance
17380 cx.set_state(indoc!(
17381 "fn a() {
17382 ˇdˇog();
17383 cat();
17384 }"
17385 ));
17386 cx.update_editor(|editor, window, cx| {
17387 editor.toggle_comments(toggle_comments, window, cx);
17388 });
17389 cx.assert_editor_state(indoc!(
17390 "fn a() {
17391 // dog();
17392 catˇ(ˇ);
17393 }"
17394 ));
17395
17396 // Multiple cursors on one line, with selection -> don't advance
17397 cx.set_state(indoc!(
17398 "fn a() {
17399 ˇdˇog«()ˇ»;
17400 cat();
17401 }"
17402 ));
17403 cx.update_editor(|editor, window, cx| {
17404 editor.toggle_comments(toggle_comments, window, cx);
17405 });
17406 cx.assert_editor_state(indoc!(
17407 "fn a() {
17408 // ˇdˇog«()ˇ»;
17409 cat();
17410 }"
17411 ));
17412
17413 // Single cursor on one line -> advance
17414 // Cursor moves to column 0 on blank line
17415 cx.set_state(indoc!(
17416 "fn a() {
17417 ˇdog();
17418
17419 cat();
17420 }"
17421 ));
17422 cx.update_editor(|editor, window, cx| {
17423 editor.toggle_comments(toggle_comments, window, cx);
17424 });
17425 cx.assert_editor_state(indoc!(
17426 "fn a() {
17427 // dog();
17428 ˇ
17429 cat();
17430 }"
17431 ));
17432
17433 // Single cursor on one line -> advance
17434 // Cursor starts and ends at column 0
17435 cx.set_state(indoc!(
17436 "fn a() {
17437 ˇ dog();
17438 cat();
17439 }"
17440 ));
17441 cx.update_editor(|editor, window, cx| {
17442 editor.toggle_comments(toggle_comments, window, cx);
17443 });
17444 cx.assert_editor_state(indoc!(
17445 "fn a() {
17446 // dog();
17447 ˇ cat();
17448 }"
17449 ));
17450}
17451
17452#[gpui::test]
17453async fn test_toggle_block_comment(cx: &mut TestAppContext) {
17454 init_test(cx, |_| {});
17455
17456 let mut cx = EditorTestContext::new(cx).await;
17457
17458 let html_language = Arc::new(
17459 Language::new(
17460 LanguageConfig {
17461 name: "HTML".into(),
17462 block_comment: Some(BlockCommentConfig {
17463 start: "<!-- ".into(),
17464 prefix: "".into(),
17465 end: " -->".into(),
17466 tab_size: 0,
17467 }),
17468 ..Default::default()
17469 },
17470 Some(tree_sitter_html::LANGUAGE.into()),
17471 )
17472 .with_injection_query(
17473 r#"
17474 (script_element
17475 (raw_text) @injection.content
17476 (#set! injection.language "javascript"))
17477 "#,
17478 )
17479 .unwrap(),
17480 );
17481
17482 let javascript_language = Arc::new(Language::new(
17483 LanguageConfig {
17484 name: "JavaScript".into(),
17485 line_comments: vec!["// ".into()],
17486 ..Default::default()
17487 },
17488 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17489 ));
17490
17491 cx.language_registry().add(html_language.clone());
17492 cx.language_registry().add(javascript_language);
17493 cx.update_buffer(|buffer, cx| {
17494 buffer.set_language(Some(html_language), cx);
17495 });
17496
17497 // Toggle comments for empty selections
17498 cx.set_state(
17499 &r#"
17500 <p>A</p>ˇ
17501 <p>B</p>ˇ
17502 <p>C</p>ˇ
17503 "#
17504 .unindent(),
17505 );
17506 cx.update_editor(|editor, window, cx| {
17507 editor.toggle_comments(&ToggleComments::default(), window, cx)
17508 });
17509 cx.assert_editor_state(
17510 &r#"
17511 <!-- <p>A</p>ˇ -->
17512 <!-- <p>B</p>ˇ -->
17513 <!-- <p>C</p>ˇ -->
17514 "#
17515 .unindent(),
17516 );
17517 cx.update_editor(|editor, window, cx| {
17518 editor.toggle_comments(&ToggleComments::default(), window, cx)
17519 });
17520 cx.assert_editor_state(
17521 &r#"
17522 <p>A</p>ˇ
17523 <p>B</p>ˇ
17524 <p>C</p>ˇ
17525 "#
17526 .unindent(),
17527 );
17528
17529 // Toggle comments for mixture of empty and non-empty selections, where
17530 // multiple selections occupy a given line.
17531 cx.set_state(
17532 &r#"
17533 <p>A«</p>
17534 <p>ˇ»B</p>ˇ
17535 <p>C«</p>
17536 <p>ˇ»D</p>ˇ
17537 "#
17538 .unindent(),
17539 );
17540
17541 cx.update_editor(|editor, window, cx| {
17542 editor.toggle_comments(&ToggleComments::default(), window, cx)
17543 });
17544 cx.assert_editor_state(
17545 &r#"
17546 <!-- <p>A«</p>
17547 <p>ˇ»B</p>ˇ -->
17548 <!-- <p>C«</p>
17549 <p>ˇ»D</p>ˇ -->
17550 "#
17551 .unindent(),
17552 );
17553 cx.update_editor(|editor, window, cx| {
17554 editor.toggle_comments(&ToggleComments::default(), window, cx)
17555 });
17556 cx.assert_editor_state(
17557 &r#"
17558 <p>A«</p>
17559 <p>ˇ»B</p>ˇ
17560 <p>C«</p>
17561 <p>ˇ»D</p>ˇ
17562 "#
17563 .unindent(),
17564 );
17565
17566 // Toggle comments when different languages are active for different
17567 // selections.
17568 cx.set_state(
17569 &r#"
17570 ˇ<script>
17571 ˇvar x = new Y();
17572 ˇ</script>
17573 "#
17574 .unindent(),
17575 );
17576 cx.executor().run_until_parked();
17577 cx.update_editor(|editor, window, cx| {
17578 editor.toggle_comments(&ToggleComments::default(), window, cx)
17579 });
17580 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
17581 // Uncommenting and commenting from this position brings in even more wrong artifacts.
17582 cx.assert_editor_state(
17583 &r#"
17584 <!-- ˇ<script> -->
17585 // ˇvar x = new Y();
17586 <!-- ˇ</script> -->
17587 "#
17588 .unindent(),
17589 );
17590}
17591
17592#[gpui::test]
17593fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
17594 init_test(cx, |_| {});
17595
17596 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17597 let multibuffer = cx.new(|cx| {
17598 let mut multibuffer = MultiBuffer::new(ReadWrite);
17599 multibuffer.push_excerpts(
17600 buffer.clone(),
17601 [
17602 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
17603 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
17604 ],
17605 cx,
17606 );
17607 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
17608 multibuffer
17609 });
17610
17611 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
17612 editor.update_in(cx, |editor, window, cx| {
17613 assert_eq!(editor.text(cx), "aaaa\nbbbb");
17614 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17615 s.select_ranges([
17616 Point::new(0, 0)..Point::new(0, 0),
17617 Point::new(1, 0)..Point::new(1, 0),
17618 ])
17619 });
17620
17621 editor.handle_input("X", window, cx);
17622 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
17623 assert_eq!(
17624 editor.selections.ranges(&editor.display_snapshot(cx)),
17625 [
17626 Point::new(0, 1)..Point::new(0, 1),
17627 Point::new(1, 1)..Point::new(1, 1),
17628 ]
17629 );
17630
17631 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
17632 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17633 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
17634 });
17635 editor.backspace(&Default::default(), window, cx);
17636 assert_eq!(editor.text(cx), "Xa\nbbb");
17637 assert_eq!(
17638 editor.selections.ranges(&editor.display_snapshot(cx)),
17639 [Point::new(1, 0)..Point::new(1, 0)]
17640 );
17641
17642 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17643 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
17644 });
17645 editor.backspace(&Default::default(), window, cx);
17646 assert_eq!(editor.text(cx), "X\nbb");
17647 assert_eq!(
17648 editor.selections.ranges(&editor.display_snapshot(cx)),
17649 [Point::new(0, 1)..Point::new(0, 1)]
17650 );
17651 });
17652}
17653
17654#[gpui::test]
17655fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
17656 init_test(cx, |_| {});
17657
17658 let markers = vec![('[', ']').into(), ('(', ')').into()];
17659 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
17660 indoc! {"
17661 [aaaa
17662 (bbbb]
17663 cccc)",
17664 },
17665 markers.clone(),
17666 );
17667 let excerpt_ranges = markers.into_iter().map(|marker| {
17668 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
17669 ExcerptRange::new(context)
17670 });
17671 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
17672 let multibuffer = cx.new(|cx| {
17673 let mut multibuffer = MultiBuffer::new(ReadWrite);
17674 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
17675 multibuffer
17676 });
17677
17678 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
17679 editor.update_in(cx, |editor, window, cx| {
17680 let (expected_text, selection_ranges) = marked_text_ranges(
17681 indoc! {"
17682 aaaa
17683 bˇbbb
17684 bˇbbˇb
17685 cccc"
17686 },
17687 true,
17688 );
17689 assert_eq!(editor.text(cx), expected_text);
17690 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17691 s.select_ranges(
17692 selection_ranges
17693 .iter()
17694 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
17695 )
17696 });
17697
17698 editor.handle_input("X", window, cx);
17699
17700 let (expected_text, expected_selections) = marked_text_ranges(
17701 indoc! {"
17702 aaaa
17703 bXˇbbXb
17704 bXˇbbXˇb
17705 cccc"
17706 },
17707 false,
17708 );
17709 assert_eq!(editor.text(cx), expected_text);
17710 assert_eq!(
17711 editor.selections.ranges(&editor.display_snapshot(cx)),
17712 expected_selections
17713 .iter()
17714 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
17715 .collect::<Vec<_>>()
17716 );
17717
17718 editor.newline(&Newline, window, cx);
17719 let (expected_text, expected_selections) = marked_text_ranges(
17720 indoc! {"
17721 aaaa
17722 bX
17723 ˇbbX
17724 b
17725 bX
17726 ˇbbX
17727 ˇb
17728 cccc"
17729 },
17730 false,
17731 );
17732 assert_eq!(editor.text(cx), expected_text);
17733 assert_eq!(
17734 editor.selections.ranges(&editor.display_snapshot(cx)),
17735 expected_selections
17736 .iter()
17737 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
17738 .collect::<Vec<_>>()
17739 );
17740 });
17741}
17742
17743#[gpui::test]
17744fn test_refresh_selections(cx: &mut TestAppContext) {
17745 init_test(cx, |_| {});
17746
17747 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17748 let mut excerpt1_id = None;
17749 let multibuffer = cx.new(|cx| {
17750 let mut multibuffer = MultiBuffer::new(ReadWrite);
17751 excerpt1_id = multibuffer
17752 .push_excerpts(
17753 buffer.clone(),
17754 [
17755 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17756 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17757 ],
17758 cx,
17759 )
17760 .into_iter()
17761 .next();
17762 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17763 multibuffer
17764 });
17765
17766 let editor = cx.add_window(|window, cx| {
17767 let mut editor = build_editor(multibuffer.clone(), window, cx);
17768 let snapshot = editor.snapshot(window, cx);
17769 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17770 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
17771 });
17772 editor.begin_selection(
17773 Point::new(2, 1).to_display_point(&snapshot),
17774 true,
17775 1,
17776 window,
17777 cx,
17778 );
17779 assert_eq!(
17780 editor.selections.ranges(&editor.display_snapshot(cx)),
17781 [
17782 Point::new(1, 3)..Point::new(1, 3),
17783 Point::new(2, 1)..Point::new(2, 1),
17784 ]
17785 );
17786 editor
17787 });
17788
17789 // Refreshing selections is a no-op when excerpts haven't changed.
17790 _ = editor.update(cx, |editor, window, cx| {
17791 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17792 assert_eq!(
17793 editor.selections.ranges(&editor.display_snapshot(cx)),
17794 [
17795 Point::new(1, 3)..Point::new(1, 3),
17796 Point::new(2, 1)..Point::new(2, 1),
17797 ]
17798 );
17799 });
17800
17801 multibuffer.update(cx, |multibuffer, cx| {
17802 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17803 });
17804 _ = editor.update(cx, |editor, window, cx| {
17805 // Removing an excerpt causes the first selection to become degenerate.
17806 assert_eq!(
17807 editor.selections.ranges(&editor.display_snapshot(cx)),
17808 [
17809 Point::new(0, 0)..Point::new(0, 0),
17810 Point::new(0, 1)..Point::new(0, 1)
17811 ]
17812 );
17813
17814 // Refreshing selections will relocate the first selection to the original buffer
17815 // location.
17816 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17817 assert_eq!(
17818 editor.selections.ranges(&editor.display_snapshot(cx)),
17819 [
17820 Point::new(0, 1)..Point::new(0, 1),
17821 Point::new(0, 3)..Point::new(0, 3)
17822 ]
17823 );
17824 assert!(editor.selections.pending_anchor().is_some());
17825 });
17826}
17827
17828#[gpui::test]
17829fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
17830 init_test(cx, |_| {});
17831
17832 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17833 let mut excerpt1_id = None;
17834 let multibuffer = cx.new(|cx| {
17835 let mut multibuffer = MultiBuffer::new(ReadWrite);
17836 excerpt1_id = multibuffer
17837 .push_excerpts(
17838 buffer.clone(),
17839 [
17840 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17841 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17842 ],
17843 cx,
17844 )
17845 .into_iter()
17846 .next();
17847 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17848 multibuffer
17849 });
17850
17851 let editor = cx.add_window(|window, cx| {
17852 let mut editor = build_editor(multibuffer.clone(), window, cx);
17853 let snapshot = editor.snapshot(window, cx);
17854 editor.begin_selection(
17855 Point::new(1, 3).to_display_point(&snapshot),
17856 false,
17857 1,
17858 window,
17859 cx,
17860 );
17861 assert_eq!(
17862 editor.selections.ranges(&editor.display_snapshot(cx)),
17863 [Point::new(1, 3)..Point::new(1, 3)]
17864 );
17865 editor
17866 });
17867
17868 multibuffer.update(cx, |multibuffer, cx| {
17869 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17870 });
17871 _ = editor.update(cx, |editor, window, cx| {
17872 assert_eq!(
17873 editor.selections.ranges(&editor.display_snapshot(cx)),
17874 [Point::new(0, 0)..Point::new(0, 0)]
17875 );
17876
17877 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
17878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17879 assert_eq!(
17880 editor.selections.ranges(&editor.display_snapshot(cx)),
17881 [Point::new(0, 3)..Point::new(0, 3)]
17882 );
17883 assert!(editor.selections.pending_anchor().is_some());
17884 });
17885}
17886
17887#[gpui::test]
17888async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
17889 init_test(cx, |_| {});
17890
17891 let language = Arc::new(
17892 Language::new(
17893 LanguageConfig {
17894 brackets: BracketPairConfig {
17895 pairs: vec![
17896 BracketPair {
17897 start: "{".to_string(),
17898 end: "}".to_string(),
17899 close: true,
17900 surround: true,
17901 newline: true,
17902 },
17903 BracketPair {
17904 start: "/* ".to_string(),
17905 end: " */".to_string(),
17906 close: true,
17907 surround: true,
17908 newline: true,
17909 },
17910 ],
17911 ..Default::default()
17912 },
17913 ..Default::default()
17914 },
17915 Some(tree_sitter_rust::LANGUAGE.into()),
17916 )
17917 .with_indents_query("")
17918 .unwrap(),
17919 );
17920
17921 let text = concat!(
17922 "{ }\n", //
17923 " x\n", //
17924 " /* */\n", //
17925 "x\n", //
17926 "{{} }\n", //
17927 );
17928
17929 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17930 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17931 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17932 editor
17933 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
17934 .await;
17935
17936 editor.update_in(cx, |editor, window, cx| {
17937 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17938 s.select_display_ranges([
17939 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
17940 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
17941 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
17942 ])
17943 });
17944 editor.newline(&Newline, window, cx);
17945
17946 assert_eq!(
17947 editor.buffer().read(cx).read(cx).text(),
17948 concat!(
17949 "{ \n", // Suppress rustfmt
17950 "\n", //
17951 "}\n", //
17952 " x\n", //
17953 " /* \n", //
17954 " \n", //
17955 " */\n", //
17956 "x\n", //
17957 "{{} \n", //
17958 "}\n", //
17959 )
17960 );
17961 });
17962}
17963
17964#[gpui::test]
17965fn test_highlighted_ranges(cx: &mut TestAppContext) {
17966 init_test(cx, |_| {});
17967
17968 let editor = cx.add_window(|window, cx| {
17969 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
17970 build_editor(buffer, window, cx)
17971 });
17972
17973 _ = editor.update(cx, |editor, window, cx| {
17974 let buffer = editor.buffer.read(cx).snapshot(cx);
17975
17976 let anchor_range =
17977 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
17978
17979 editor.highlight_background(
17980 HighlightKey::ColorizeBracket(0),
17981 &[
17982 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
17983 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
17984 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
17985 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
17986 ],
17987 |_, _| Hsla::red(),
17988 cx,
17989 );
17990 editor.highlight_background(
17991 HighlightKey::ColorizeBracket(1),
17992 &[
17993 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
17994 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
17995 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
17996 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
17997 ],
17998 |_, _| Hsla::green(),
17999 cx,
18000 );
18001
18002 let snapshot = editor.snapshot(window, cx);
18003 let highlighted_ranges = editor.sorted_background_highlights_in_range(
18004 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
18005 &snapshot,
18006 cx.theme(),
18007 );
18008 assert_eq!(
18009 highlighted_ranges,
18010 &[
18011 (
18012 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
18013 Hsla::green(),
18014 ),
18015 (
18016 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
18017 Hsla::red(),
18018 ),
18019 (
18020 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
18021 Hsla::green(),
18022 ),
18023 (
18024 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
18025 Hsla::red(),
18026 ),
18027 ]
18028 );
18029 assert_eq!(
18030 editor.sorted_background_highlights_in_range(
18031 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
18032 &snapshot,
18033 cx.theme(),
18034 ),
18035 &[(
18036 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
18037 Hsla::red(),
18038 )]
18039 );
18040 });
18041}
18042
18043#[gpui::test]
18044async fn test_following(cx: &mut TestAppContext) {
18045 init_test(cx, |_| {});
18046
18047 let fs = FakeFs::new(cx.executor());
18048 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
18049
18050 let buffer = project.update(cx, |project, cx| {
18051 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
18052 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
18053 });
18054 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
18055 let follower = cx.update(|cx| {
18056 cx.open_window(
18057 WindowOptions {
18058 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
18059 gpui::Point::new(px(0.), px(0.)),
18060 gpui::Point::new(px(10.), px(80.)),
18061 ))),
18062 ..Default::default()
18063 },
18064 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
18065 )
18066 .unwrap()
18067 });
18068
18069 let is_still_following = Rc::new(RefCell::new(true));
18070 let follower_edit_event_count = Rc::new(RefCell::new(0));
18071 let pending_update = Rc::new(RefCell::new(None));
18072 let leader_entity = leader.root(cx).unwrap();
18073 let follower_entity = follower.root(cx).unwrap();
18074 _ = follower.update(cx, {
18075 let update = pending_update.clone();
18076 let is_still_following = is_still_following.clone();
18077 let follower_edit_event_count = follower_edit_event_count.clone();
18078 |_, window, cx| {
18079 cx.subscribe_in(
18080 &leader_entity,
18081 window,
18082 move |_, leader, event, window, cx| {
18083 leader.update(cx, |leader, cx| {
18084 leader.add_event_to_update_proto(
18085 event,
18086 &mut update.borrow_mut(),
18087 window,
18088 cx,
18089 );
18090 });
18091 },
18092 )
18093 .detach();
18094
18095 cx.subscribe_in(
18096 &follower_entity,
18097 window,
18098 move |_, _, event: &EditorEvent, _window, _cx| {
18099 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
18100 *is_still_following.borrow_mut() = false;
18101 }
18102
18103 if let EditorEvent::BufferEdited = event {
18104 *follower_edit_event_count.borrow_mut() += 1;
18105 }
18106 },
18107 )
18108 .detach();
18109 }
18110 });
18111
18112 // Update the selections only
18113 _ = leader.update(cx, |leader, window, cx| {
18114 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18115 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
18116 });
18117 });
18118 follower
18119 .update(cx, |follower, window, cx| {
18120 follower.apply_update_proto(
18121 &project,
18122 pending_update.borrow_mut().take().unwrap(),
18123 window,
18124 cx,
18125 )
18126 })
18127 .unwrap()
18128 .await
18129 .unwrap();
18130 _ = follower.update(cx, |follower, _, cx| {
18131 assert_eq!(
18132 follower.selections.ranges(&follower.display_snapshot(cx)),
18133 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
18134 );
18135 });
18136 assert!(*is_still_following.borrow());
18137 assert_eq!(*follower_edit_event_count.borrow(), 0);
18138
18139 // Update the scroll position only
18140 _ = leader.update(cx, |leader, window, cx| {
18141 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
18142 });
18143 follower
18144 .update(cx, |follower, window, cx| {
18145 follower.apply_update_proto(
18146 &project,
18147 pending_update.borrow_mut().take().unwrap(),
18148 window,
18149 cx,
18150 )
18151 })
18152 .unwrap()
18153 .await
18154 .unwrap();
18155 assert_eq!(
18156 follower
18157 .update(cx, |follower, _, cx| follower.scroll_position(cx))
18158 .unwrap(),
18159 gpui::Point::new(1.5, 3.5)
18160 );
18161 assert!(*is_still_following.borrow());
18162 assert_eq!(*follower_edit_event_count.borrow(), 0);
18163
18164 // Update the selections and scroll position. The follower's scroll position is updated
18165 // via autoscroll, not via the leader's exact scroll position.
18166 _ = leader.update(cx, |leader, window, cx| {
18167 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18168 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
18169 });
18170 leader.request_autoscroll(Autoscroll::newest(), cx);
18171 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
18172 });
18173 follower
18174 .update(cx, |follower, window, cx| {
18175 follower.apply_update_proto(
18176 &project,
18177 pending_update.borrow_mut().take().unwrap(),
18178 window,
18179 cx,
18180 )
18181 })
18182 .unwrap()
18183 .await
18184 .unwrap();
18185 _ = follower.update(cx, |follower, _, cx| {
18186 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
18187 assert_eq!(
18188 follower.selections.ranges(&follower.display_snapshot(cx)),
18189 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
18190 );
18191 });
18192 assert!(*is_still_following.borrow());
18193
18194 // Creating a pending selection that precedes another selection
18195 _ = leader.update(cx, |leader, window, cx| {
18196 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18197 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
18198 });
18199 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
18200 });
18201 follower
18202 .update(cx, |follower, window, cx| {
18203 follower.apply_update_proto(
18204 &project,
18205 pending_update.borrow_mut().take().unwrap(),
18206 window,
18207 cx,
18208 )
18209 })
18210 .unwrap()
18211 .await
18212 .unwrap();
18213 _ = follower.update(cx, |follower, _, cx| {
18214 assert_eq!(
18215 follower.selections.ranges(&follower.display_snapshot(cx)),
18216 vec![
18217 MultiBufferOffset(0)..MultiBufferOffset(0),
18218 MultiBufferOffset(1)..MultiBufferOffset(1)
18219 ]
18220 );
18221 });
18222 assert!(*is_still_following.borrow());
18223
18224 // Extend the pending selection so that it surrounds another selection
18225 _ = leader.update(cx, |leader, window, cx| {
18226 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
18227 });
18228 follower
18229 .update(cx, |follower, window, cx| {
18230 follower.apply_update_proto(
18231 &project,
18232 pending_update.borrow_mut().take().unwrap(),
18233 window,
18234 cx,
18235 )
18236 })
18237 .unwrap()
18238 .await
18239 .unwrap();
18240 _ = follower.update(cx, |follower, _, cx| {
18241 assert_eq!(
18242 follower.selections.ranges(&follower.display_snapshot(cx)),
18243 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
18244 );
18245 });
18246
18247 // Scrolling locally breaks the follow
18248 _ = follower.update(cx, |follower, window, cx| {
18249 let top_anchor = follower
18250 .buffer()
18251 .read(cx)
18252 .read(cx)
18253 .anchor_after(MultiBufferOffset(0));
18254 follower.set_scroll_anchor(
18255 ScrollAnchor {
18256 anchor: top_anchor,
18257 offset: gpui::Point::new(0.0, 0.5),
18258 },
18259 window,
18260 cx,
18261 );
18262 });
18263 assert!(!(*is_still_following.borrow()));
18264}
18265
18266#[gpui::test]
18267async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
18268 init_test(cx, |_| {});
18269
18270 let fs = FakeFs::new(cx.executor());
18271 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
18272 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
18273 let workspace = window
18274 .read_with(cx, |mw, _| mw.workspace().clone())
18275 .unwrap();
18276 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
18277
18278 let cx = &mut VisualTestContext::from_window(*window, cx);
18279
18280 let leader = pane.update_in(cx, |_, window, cx| {
18281 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
18282 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
18283 });
18284
18285 // Start following the editor when it has no excerpts.
18286 let mut state_message =
18287 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
18288 let workspace_entity = workspace.clone();
18289 let follower_1 = cx
18290 .update_window(*window, |_, window, cx| {
18291 Editor::from_state_proto(
18292 workspace_entity,
18293 ViewId {
18294 creator: CollaboratorId::PeerId(PeerId::default()),
18295 id: 0,
18296 },
18297 &mut state_message,
18298 window,
18299 cx,
18300 )
18301 })
18302 .unwrap()
18303 .unwrap()
18304 .await
18305 .unwrap();
18306
18307 let update_message = Rc::new(RefCell::new(None));
18308 follower_1.update_in(cx, {
18309 let update = update_message.clone();
18310 |_, window, cx| {
18311 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
18312 leader.update(cx, |leader, cx| {
18313 leader.add_event_to_update_proto(event, &mut update.borrow_mut(), window, cx);
18314 });
18315 })
18316 .detach();
18317 }
18318 });
18319
18320 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
18321 (
18322 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
18323 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
18324 )
18325 });
18326
18327 // Insert some excerpts.
18328 leader.update(cx, |leader, cx| {
18329 leader.buffer.update(cx, |multibuffer, cx| {
18330 multibuffer.set_excerpts_for_path(
18331 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
18332 buffer_1.clone(),
18333 vec![
18334 Point::row_range(0..3),
18335 Point::row_range(1..6),
18336 Point::row_range(12..15),
18337 ],
18338 0,
18339 cx,
18340 );
18341 multibuffer.set_excerpts_for_path(
18342 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
18343 buffer_2.clone(),
18344 vec![Point::row_range(0..6), Point::row_range(8..12)],
18345 0,
18346 cx,
18347 );
18348 });
18349 });
18350
18351 // Apply the update of adding the excerpts.
18352 follower_1
18353 .update_in(cx, |follower, window, cx| {
18354 follower.apply_update_proto(
18355 &project,
18356 update_message.borrow().clone().unwrap(),
18357 window,
18358 cx,
18359 )
18360 })
18361 .await
18362 .unwrap();
18363 assert_eq!(
18364 follower_1.update(cx, |editor, cx| editor.text(cx)),
18365 leader.update(cx, |editor, cx| editor.text(cx))
18366 );
18367 update_message.borrow_mut().take();
18368
18369 // Start following separately after it already has excerpts.
18370 let mut state_message =
18371 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
18372 let workspace_entity = workspace.clone();
18373 let follower_2 = cx
18374 .update_window(*window, |_, window, cx| {
18375 Editor::from_state_proto(
18376 workspace_entity,
18377 ViewId {
18378 creator: CollaboratorId::PeerId(PeerId::default()),
18379 id: 0,
18380 },
18381 &mut state_message,
18382 window,
18383 cx,
18384 )
18385 })
18386 .unwrap()
18387 .unwrap()
18388 .await
18389 .unwrap();
18390 assert_eq!(
18391 follower_2.update(cx, |editor, cx| editor.text(cx)),
18392 leader.update(cx, |editor, cx| editor.text(cx))
18393 );
18394
18395 // Remove some excerpts.
18396 leader.update(cx, |leader, cx| {
18397 leader.buffer.update(cx, |multibuffer, cx| {
18398 let excerpt_ids = multibuffer.excerpt_ids();
18399 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
18400 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
18401 });
18402 });
18403
18404 // Apply the update of removing the excerpts.
18405 follower_1
18406 .update_in(cx, |follower, window, cx| {
18407 follower.apply_update_proto(
18408 &project,
18409 update_message.borrow().clone().unwrap(),
18410 window,
18411 cx,
18412 )
18413 })
18414 .await
18415 .unwrap();
18416 follower_2
18417 .update_in(cx, |follower, window, cx| {
18418 follower.apply_update_proto(
18419 &project,
18420 update_message.borrow().clone().unwrap(),
18421 window,
18422 cx,
18423 )
18424 })
18425 .await
18426 .unwrap();
18427 update_message.borrow_mut().take();
18428 assert_eq!(
18429 follower_1.update(cx, |editor, cx| editor.text(cx)),
18430 leader.update(cx, |editor, cx| editor.text(cx))
18431 );
18432}
18433
18434#[gpui::test]
18435async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18436 init_test(cx, |_| {});
18437
18438 let mut cx = EditorTestContext::new(cx).await;
18439 let lsp_store =
18440 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
18441
18442 cx.set_state(indoc! {"
18443 ˇfn func(abc def: i32) -> u32 {
18444 }
18445 "});
18446
18447 cx.update(|_, cx| {
18448 lsp_store.update(cx, |lsp_store, cx| {
18449 lsp_store
18450 .update_diagnostics(
18451 LanguageServerId(0),
18452 lsp::PublishDiagnosticsParams {
18453 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
18454 version: None,
18455 diagnostics: vec![
18456 lsp::Diagnostic {
18457 range: lsp::Range::new(
18458 lsp::Position::new(0, 11),
18459 lsp::Position::new(0, 12),
18460 ),
18461 severity: Some(lsp::DiagnosticSeverity::ERROR),
18462 ..Default::default()
18463 },
18464 lsp::Diagnostic {
18465 range: lsp::Range::new(
18466 lsp::Position::new(0, 12),
18467 lsp::Position::new(0, 15),
18468 ),
18469 severity: Some(lsp::DiagnosticSeverity::ERROR),
18470 ..Default::default()
18471 },
18472 lsp::Diagnostic {
18473 range: lsp::Range::new(
18474 lsp::Position::new(0, 25),
18475 lsp::Position::new(0, 28),
18476 ),
18477 severity: Some(lsp::DiagnosticSeverity::ERROR),
18478 ..Default::default()
18479 },
18480 ],
18481 },
18482 None,
18483 DiagnosticSourceKind::Pushed,
18484 &[],
18485 cx,
18486 )
18487 .unwrap()
18488 });
18489 });
18490
18491 executor.run_until_parked();
18492
18493 cx.update_editor(|editor, window, cx| {
18494 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
18495 });
18496
18497 cx.assert_editor_state(indoc! {"
18498 fn func(abc def: i32) -> ˇu32 {
18499 }
18500 "});
18501
18502 cx.update_editor(|editor, window, cx| {
18503 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
18504 });
18505
18506 cx.assert_editor_state(indoc! {"
18507 fn func(abc ˇdef: i32) -> u32 {
18508 }
18509 "});
18510
18511 cx.update_editor(|editor, window, cx| {
18512 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
18513 });
18514
18515 cx.assert_editor_state(indoc! {"
18516 fn func(abcˇ def: i32) -> u32 {
18517 }
18518 "});
18519
18520 cx.update_editor(|editor, window, cx| {
18521 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
18522 });
18523
18524 cx.assert_editor_state(indoc! {"
18525 fn func(abc def: i32) -> ˇu32 {
18526 }
18527 "});
18528}
18529
18530#[gpui::test]
18531async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18532 init_test(cx, |_| {});
18533
18534 let mut cx = EditorTestContext::new(cx).await;
18535
18536 let diff_base = r#"
18537 use some::mod;
18538
18539 const A: u32 = 42;
18540
18541 fn main() {
18542 println!("hello");
18543
18544 println!("world");
18545 }
18546 "#
18547 .unindent();
18548
18549 // Edits are modified, removed, modified, added
18550 cx.set_state(
18551 &r#"
18552 use some::modified;
18553
18554 ˇ
18555 fn main() {
18556 println!("hello there");
18557
18558 println!("around the");
18559 println!("world");
18560 }
18561 "#
18562 .unindent(),
18563 );
18564
18565 cx.set_head_text(&diff_base);
18566 executor.run_until_parked();
18567
18568 cx.update_editor(|editor, window, cx| {
18569 //Wrap around the bottom of the buffer
18570 for _ in 0..3 {
18571 editor.go_to_next_hunk(&GoToHunk, window, cx);
18572 }
18573 });
18574
18575 cx.assert_editor_state(
18576 &r#"
18577 ˇuse some::modified;
18578
18579
18580 fn main() {
18581 println!("hello there");
18582
18583 println!("around the");
18584 println!("world");
18585 }
18586 "#
18587 .unindent(),
18588 );
18589
18590 cx.update_editor(|editor, window, cx| {
18591 //Wrap around the top of the buffer
18592 for _ in 0..2 {
18593 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18594 }
18595 });
18596
18597 cx.assert_editor_state(
18598 &r#"
18599 use some::modified;
18600
18601
18602 fn main() {
18603 ˇ println!("hello there");
18604
18605 println!("around the");
18606 println!("world");
18607 }
18608 "#
18609 .unindent(),
18610 );
18611
18612 cx.update_editor(|editor, window, cx| {
18613 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18614 });
18615
18616 cx.assert_editor_state(
18617 &r#"
18618 use some::modified;
18619
18620 ˇ
18621 fn main() {
18622 println!("hello there");
18623
18624 println!("around the");
18625 println!("world");
18626 }
18627 "#
18628 .unindent(),
18629 );
18630
18631 cx.update_editor(|editor, window, cx| {
18632 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18633 });
18634
18635 cx.assert_editor_state(
18636 &r#"
18637 ˇuse some::modified;
18638
18639
18640 fn main() {
18641 println!("hello there");
18642
18643 println!("around the");
18644 println!("world");
18645 }
18646 "#
18647 .unindent(),
18648 );
18649
18650 cx.update_editor(|editor, window, cx| {
18651 for _ in 0..2 {
18652 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18653 }
18654 });
18655
18656 cx.assert_editor_state(
18657 &r#"
18658 use some::modified;
18659
18660
18661 fn main() {
18662 ˇ println!("hello there");
18663
18664 println!("around the");
18665 println!("world");
18666 }
18667 "#
18668 .unindent(),
18669 );
18670
18671 cx.update_editor(|editor, window, cx| {
18672 editor.fold(&Fold, window, cx);
18673 });
18674
18675 cx.update_editor(|editor, window, cx| {
18676 editor.go_to_next_hunk(&GoToHunk, window, cx);
18677 });
18678
18679 cx.assert_editor_state(
18680 &r#"
18681 ˇuse some::modified;
18682
18683
18684 fn main() {
18685 println!("hello there");
18686
18687 println!("around the");
18688 println!("world");
18689 }
18690 "#
18691 .unindent(),
18692 );
18693}
18694
18695#[test]
18696fn test_split_words() {
18697 fn split(text: &str) -> Vec<&str> {
18698 split_words(text).collect()
18699 }
18700
18701 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
18702 assert_eq!(split("hello_world"), &["hello_", "world"]);
18703 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
18704 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
18705 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
18706 assert_eq!(split("helloworld"), &["helloworld"]);
18707
18708 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
18709}
18710
18711#[test]
18712fn test_split_words_for_snippet_prefix() {
18713 fn split(text: &str) -> Vec<&str> {
18714 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
18715 }
18716
18717 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
18718 assert_eq!(split("hello_world"), &["hello_world"]);
18719 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
18720 assert_eq!(split("Hello_World"), &["Hello_World"]);
18721 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
18722 assert_eq!(split("helloworld"), &["helloworld"]);
18723 assert_eq!(
18724 split("this@is!@#$^many . symbols"),
18725 &[
18726 "symbols",
18727 " symbols",
18728 ". symbols",
18729 " . symbols",
18730 " . symbols",
18731 " . symbols",
18732 "many . symbols",
18733 "^many . symbols",
18734 "$^many . symbols",
18735 "#$^many . symbols",
18736 "@#$^many . symbols",
18737 "!@#$^many . symbols",
18738 "is!@#$^many . symbols",
18739 "@is!@#$^many . symbols",
18740 "this@is!@#$^many . symbols",
18741 ],
18742 );
18743 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
18744}
18745
18746#[gpui::test]
18747async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
18748 init_test(cx, |_| {});
18749
18750 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
18751
18752 #[track_caller]
18753 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
18754 let _state_context = cx.set_state(before);
18755 cx.run_until_parked();
18756 cx.update_editor(|editor, window, cx| {
18757 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
18758 });
18759 cx.run_until_parked();
18760 cx.assert_editor_state(after);
18761 }
18762
18763 // Outside bracket jumps to outside of matching bracket
18764 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
18765 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
18766
18767 // Inside bracket jumps to inside of matching bracket
18768 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
18769 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
18770
18771 // When outside a bracket and inside, favor jumping to the inside bracket
18772 assert(
18773 "console.log('foo', [1, 2, 3]ˇ);",
18774 "console.log('foo', ˇ[1, 2, 3]);",
18775 &mut cx,
18776 );
18777 assert(
18778 "console.log(ˇ'foo', [1, 2, 3]);",
18779 "console.log('foo'ˇ, [1, 2, 3]);",
18780 &mut cx,
18781 );
18782
18783 // Bias forward if two options are equally likely
18784 assert(
18785 "let result = curried_fun()ˇ();",
18786 "let result = curried_fun()()ˇ;",
18787 &mut cx,
18788 );
18789
18790 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
18791 assert(
18792 indoc! {"
18793 function test() {
18794 console.log('test')ˇ
18795 }"},
18796 indoc! {"
18797 function test() {
18798 console.logˇ('test')
18799 }"},
18800 &mut cx,
18801 );
18802}
18803
18804#[gpui::test]
18805async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
18806 init_test(cx, |_| {});
18807 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
18808 language_registry.add(markdown_lang());
18809 language_registry.add(rust_lang());
18810 let buffer = cx.new(|cx| {
18811 let mut buffer = language::Buffer::local(
18812 indoc! {"
18813 ```rs
18814 impl Worktree {
18815 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18816 }
18817 }
18818 ```
18819 "},
18820 cx,
18821 );
18822 buffer.set_language_registry(language_registry.clone());
18823 buffer.set_language(Some(markdown_lang()), cx);
18824 buffer
18825 });
18826 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18827 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
18828 cx.executor().run_until_parked();
18829 _ = editor.update(cx, |editor, window, cx| {
18830 // Case 1: Test outer enclosing brackets
18831 select_ranges(
18832 editor,
18833 &indoc! {"
18834 ```rs
18835 impl Worktree {
18836 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18837 }
18838 }ˇ
18839 ```
18840 "},
18841 window,
18842 cx,
18843 );
18844 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18845 assert_text_with_selections(
18846 editor,
18847 &indoc! {"
18848 ```rs
18849 impl Worktree ˇ{
18850 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18851 }
18852 }
18853 ```
18854 "},
18855 cx,
18856 );
18857 // Case 2: Test inner enclosing brackets
18858 select_ranges(
18859 editor,
18860 &indoc! {"
18861 ```rs
18862 impl Worktree {
18863 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18864 }ˇ
18865 }
18866 ```
18867 "},
18868 window,
18869 cx,
18870 );
18871 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18872 assert_text_with_selections(
18873 editor,
18874 &indoc! {"
18875 ```rs
18876 impl Worktree {
18877 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
18878 }
18879 }
18880 ```
18881 "},
18882 cx,
18883 );
18884 });
18885}
18886
18887#[gpui::test]
18888async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
18889 init_test(cx, |_| {});
18890
18891 let fs = FakeFs::new(cx.executor());
18892 fs.insert_tree(
18893 path!("/a"),
18894 json!({
18895 "main.rs": "fn main() { let a = 5; }",
18896 "other.rs": "// Test file",
18897 }),
18898 )
18899 .await;
18900 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18901
18902 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18903 language_registry.add(Arc::new(Language::new(
18904 LanguageConfig {
18905 name: "Rust".into(),
18906 matcher: LanguageMatcher {
18907 path_suffixes: vec!["rs".to_string()],
18908 ..Default::default()
18909 },
18910 brackets: BracketPairConfig {
18911 pairs: vec![BracketPair {
18912 start: "{".to_string(),
18913 end: "}".to_string(),
18914 close: true,
18915 surround: true,
18916 newline: true,
18917 }],
18918 disabled_scopes_by_bracket_ix: Vec::new(),
18919 },
18920 ..Default::default()
18921 },
18922 Some(tree_sitter_rust::LANGUAGE.into()),
18923 )));
18924 let mut fake_servers = language_registry.register_fake_lsp(
18925 "Rust",
18926 FakeLspAdapter {
18927 capabilities: lsp::ServerCapabilities {
18928 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18929 first_trigger_character: "{".to_string(),
18930 more_trigger_character: None,
18931 }),
18932 ..Default::default()
18933 },
18934 ..Default::default()
18935 },
18936 );
18937
18938 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
18939 let workspace = window
18940 .read_with(cx, |mw, _| mw.workspace().clone())
18941 .unwrap();
18942
18943 let cx = &mut VisualTestContext::from_window(*window, cx);
18944
18945 let worktree_id = workspace.update_in(cx, |workspace, _, cx| {
18946 workspace.project().update(cx, |project, cx| {
18947 project.worktrees(cx).next().unwrap().read(cx).id()
18948 })
18949 });
18950
18951 let buffer = project
18952 .update(cx, |project, cx| {
18953 project.open_local_buffer(path!("/a/main.rs"), cx)
18954 })
18955 .await
18956 .unwrap();
18957 let editor_handle = workspace
18958 .update_in(cx, |workspace, window, cx| {
18959 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
18960 })
18961 .await
18962 .unwrap()
18963 .downcast::<Editor>()
18964 .unwrap();
18965
18966 let fake_server = fake_servers.next().await.unwrap();
18967
18968 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
18969 |params, _| async move {
18970 assert_eq!(
18971 params.text_document_position.text_document.uri,
18972 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
18973 );
18974 assert_eq!(
18975 params.text_document_position.position,
18976 lsp::Position::new(0, 21),
18977 );
18978
18979 Ok(Some(vec![lsp::TextEdit {
18980 new_text: "]".to_string(),
18981 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18982 }]))
18983 },
18984 );
18985
18986 editor_handle.update_in(cx, |editor, window, cx| {
18987 window.focus(&editor.focus_handle(cx), cx);
18988 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18989 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
18990 });
18991 editor.handle_input("{", window, cx);
18992 });
18993
18994 cx.executor().run_until_parked();
18995
18996 buffer.update(cx, |buffer, _| {
18997 assert_eq!(
18998 buffer.text(),
18999 "fn main() { let a = {5}; }",
19000 "No extra braces from on type formatting should appear in the buffer"
19001 )
19002 });
19003}
19004
19005#[gpui::test(iterations = 20, seeds(31))]
19006async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
19007 init_test(cx, |_| {});
19008
19009 let mut cx = EditorLspTestContext::new_rust(
19010 lsp::ServerCapabilities {
19011 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
19012 first_trigger_character: ".".to_string(),
19013 more_trigger_character: None,
19014 }),
19015 ..Default::default()
19016 },
19017 cx,
19018 )
19019 .await;
19020
19021 cx.update_buffer(|buffer, _| {
19022 // This causes autoindent to be async.
19023 buffer.set_sync_parse_timeout(None)
19024 });
19025
19026 cx.set_state("fn c() {\n d()ˇ\n}\n");
19027 cx.simulate_keystroke("\n");
19028 cx.run_until_parked();
19029
19030 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
19031 let mut request =
19032 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
19033 let buffer_cloned = buffer_cloned.clone();
19034 async move {
19035 buffer_cloned.update(&mut cx, |buffer, _| {
19036 assert_eq!(
19037 buffer.text(),
19038 "fn c() {\n d()\n .\n}\n",
19039 "OnTypeFormatting should triggered after autoindent applied"
19040 )
19041 });
19042
19043 Ok(Some(vec![]))
19044 }
19045 });
19046
19047 cx.simulate_keystroke(".");
19048 cx.run_until_parked();
19049
19050 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
19051 assert!(request.next().await.is_some());
19052 request.close();
19053 assert!(request.next().await.is_none());
19054}
19055
19056#[gpui::test]
19057async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
19058 init_test(cx, |_| {});
19059
19060 let fs = FakeFs::new(cx.executor());
19061 fs.insert_tree(
19062 path!("/a"),
19063 json!({
19064 "main.rs": "fn main() { let a = 5; }",
19065 "other.rs": "// Test file",
19066 }),
19067 )
19068 .await;
19069
19070 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19071
19072 let server_restarts = Arc::new(AtomicUsize::new(0));
19073 let closure_restarts = Arc::clone(&server_restarts);
19074 let language_server_name = "test language server";
19075 let language_name: LanguageName = "Rust".into();
19076
19077 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19078 language_registry.add(Arc::new(Language::new(
19079 LanguageConfig {
19080 name: language_name.clone(),
19081 matcher: LanguageMatcher {
19082 path_suffixes: vec!["rs".to_string()],
19083 ..Default::default()
19084 },
19085 ..Default::default()
19086 },
19087 Some(tree_sitter_rust::LANGUAGE.into()),
19088 )));
19089 let mut fake_servers = language_registry.register_fake_lsp(
19090 "Rust",
19091 FakeLspAdapter {
19092 name: language_server_name,
19093 initialization_options: Some(json!({
19094 "testOptionValue": true
19095 })),
19096 initializer: Some(Box::new(move |fake_server| {
19097 let task_restarts = Arc::clone(&closure_restarts);
19098 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
19099 task_restarts.fetch_add(1, atomic::Ordering::Release);
19100 futures::future::ready(Ok(()))
19101 });
19102 })),
19103 ..Default::default()
19104 },
19105 );
19106
19107 let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
19108 let _buffer = project
19109 .update(cx, |project, cx| {
19110 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
19111 })
19112 .await
19113 .unwrap();
19114 let _fake_server = fake_servers.next().await.unwrap();
19115 update_test_language_settings(cx, |language_settings| {
19116 language_settings.languages.0.insert(
19117 language_name.clone().0.to_string(),
19118 LanguageSettingsContent {
19119 tab_size: NonZeroU32::new(8),
19120 ..Default::default()
19121 },
19122 );
19123 });
19124 cx.executor().run_until_parked();
19125 assert_eq!(
19126 server_restarts.load(atomic::Ordering::Acquire),
19127 0,
19128 "Should not restart LSP server on an unrelated change"
19129 );
19130
19131 update_test_project_settings(cx, |project_settings| {
19132 project_settings.lsp.0.insert(
19133 "Some other server name".into(),
19134 LspSettings {
19135 binary: None,
19136 settings: None,
19137 initialization_options: Some(json!({
19138 "some other init value": false
19139 })),
19140 enable_lsp_tasks: false,
19141 fetch: None,
19142 },
19143 );
19144 });
19145 cx.executor().run_until_parked();
19146 assert_eq!(
19147 server_restarts.load(atomic::Ordering::Acquire),
19148 0,
19149 "Should not restart LSP server on an unrelated LSP settings change"
19150 );
19151
19152 update_test_project_settings(cx, |project_settings| {
19153 project_settings.lsp.0.insert(
19154 language_server_name.into(),
19155 LspSettings {
19156 binary: None,
19157 settings: None,
19158 initialization_options: Some(json!({
19159 "anotherInitValue": false
19160 })),
19161 enable_lsp_tasks: false,
19162 fetch: None,
19163 },
19164 );
19165 });
19166 cx.executor().run_until_parked();
19167 assert_eq!(
19168 server_restarts.load(atomic::Ordering::Acquire),
19169 1,
19170 "Should restart LSP server on a related LSP settings change"
19171 );
19172
19173 update_test_project_settings(cx, |project_settings| {
19174 project_settings.lsp.0.insert(
19175 language_server_name.into(),
19176 LspSettings {
19177 binary: None,
19178 settings: None,
19179 initialization_options: Some(json!({
19180 "anotherInitValue": false
19181 })),
19182 enable_lsp_tasks: false,
19183 fetch: None,
19184 },
19185 );
19186 });
19187 cx.executor().run_until_parked();
19188 assert_eq!(
19189 server_restarts.load(atomic::Ordering::Acquire),
19190 1,
19191 "Should not restart LSP server on a related LSP settings change that is the same"
19192 );
19193
19194 update_test_project_settings(cx, |project_settings| {
19195 project_settings.lsp.0.insert(
19196 language_server_name.into(),
19197 LspSettings {
19198 binary: None,
19199 settings: None,
19200 initialization_options: None,
19201 enable_lsp_tasks: false,
19202 fetch: None,
19203 },
19204 );
19205 });
19206 cx.executor().run_until_parked();
19207 assert_eq!(
19208 server_restarts.load(atomic::Ordering::Acquire),
19209 2,
19210 "Should restart LSP server on another related LSP settings change"
19211 );
19212}
19213
19214#[gpui::test]
19215async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
19216 init_test(cx, |_| {});
19217
19218 let mut cx = EditorLspTestContext::new_rust(
19219 lsp::ServerCapabilities {
19220 completion_provider: Some(lsp::CompletionOptions {
19221 trigger_characters: Some(vec![".".to_string()]),
19222 resolve_provider: Some(true),
19223 ..Default::default()
19224 }),
19225 ..Default::default()
19226 },
19227 cx,
19228 )
19229 .await;
19230
19231 cx.set_state("fn main() { let a = 2ˇ; }");
19232 cx.simulate_keystroke(".");
19233 let completion_item = lsp::CompletionItem {
19234 label: "some".into(),
19235 kind: Some(lsp::CompletionItemKind::SNIPPET),
19236 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
19237 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
19238 kind: lsp::MarkupKind::Markdown,
19239 value: "```rust\nSome(2)\n```".to_string(),
19240 })),
19241 deprecated: Some(false),
19242 sort_text: Some("fffffff2".to_string()),
19243 filter_text: Some("some".to_string()),
19244 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
19245 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19246 range: lsp::Range {
19247 start: lsp::Position {
19248 line: 0,
19249 character: 22,
19250 },
19251 end: lsp::Position {
19252 line: 0,
19253 character: 22,
19254 },
19255 },
19256 new_text: "Some(2)".to_string(),
19257 })),
19258 additional_text_edits: Some(vec![lsp::TextEdit {
19259 range: lsp::Range {
19260 start: lsp::Position {
19261 line: 0,
19262 character: 20,
19263 },
19264 end: lsp::Position {
19265 line: 0,
19266 character: 22,
19267 },
19268 },
19269 new_text: "".to_string(),
19270 }]),
19271 ..Default::default()
19272 };
19273
19274 let closure_completion_item = completion_item.clone();
19275 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19276 let task_completion_item = closure_completion_item.clone();
19277 async move {
19278 Ok(Some(lsp::CompletionResponse::Array(vec![
19279 task_completion_item,
19280 ])))
19281 }
19282 });
19283
19284 request.next().await;
19285
19286 cx.condition(|editor, _| editor.context_menu_visible())
19287 .await;
19288 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
19289 editor
19290 .confirm_completion(&ConfirmCompletion::default(), window, cx)
19291 .unwrap()
19292 });
19293 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
19294
19295 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
19296 let task_completion_item = completion_item.clone();
19297 async move { Ok(task_completion_item) }
19298 })
19299 .next()
19300 .await
19301 .unwrap();
19302 apply_additional_edits.await.unwrap();
19303 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
19304}
19305
19306#[gpui::test]
19307async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
19308 init_test(cx, |_| {});
19309
19310 let mut cx = EditorLspTestContext::new_rust(
19311 lsp::ServerCapabilities {
19312 completion_provider: Some(lsp::CompletionOptions {
19313 trigger_characters: Some(vec![".".to_string()]),
19314 resolve_provider: Some(true),
19315 ..Default::default()
19316 }),
19317 ..Default::default()
19318 },
19319 cx,
19320 )
19321 .await;
19322
19323 cx.set_state("fn main() { let a = 2ˇ; }");
19324 cx.simulate_keystroke(".");
19325
19326 let item1 = lsp::CompletionItem {
19327 label: "method id()".to_string(),
19328 filter_text: Some("id".to_string()),
19329 detail: None,
19330 documentation: None,
19331 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19332 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19333 new_text: ".id".to_string(),
19334 })),
19335 ..lsp::CompletionItem::default()
19336 };
19337
19338 let item2 = lsp::CompletionItem {
19339 label: "other".to_string(),
19340 filter_text: Some("other".to_string()),
19341 detail: None,
19342 documentation: None,
19343 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19344 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19345 new_text: ".other".to_string(),
19346 })),
19347 ..lsp::CompletionItem::default()
19348 };
19349
19350 let item1 = item1.clone();
19351 cx.set_request_handler::<lsp::request::Completion, _, _>({
19352 let item1 = item1.clone();
19353 move |_, _, _| {
19354 let item1 = item1.clone();
19355 let item2 = item2.clone();
19356 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
19357 }
19358 })
19359 .next()
19360 .await;
19361
19362 cx.condition(|editor, _| editor.context_menu_visible())
19363 .await;
19364 cx.update_editor(|editor, _, _| {
19365 let context_menu = editor.context_menu.borrow_mut();
19366 let context_menu = context_menu
19367 .as_ref()
19368 .expect("Should have the context menu deployed");
19369 match context_menu {
19370 CodeContextMenu::Completions(completions_menu) => {
19371 let completions = completions_menu.completions.borrow_mut();
19372 assert_eq!(
19373 completions
19374 .iter()
19375 .map(|completion| &completion.label.text)
19376 .collect::<Vec<_>>(),
19377 vec!["method id()", "other"]
19378 )
19379 }
19380 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
19381 }
19382 });
19383
19384 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
19385 let item1 = item1.clone();
19386 move |_, item_to_resolve, _| {
19387 let item1 = item1.clone();
19388 async move {
19389 if item1 == item_to_resolve {
19390 Ok(lsp::CompletionItem {
19391 label: "method id()".to_string(),
19392 filter_text: Some("id".to_string()),
19393 detail: Some("Now resolved!".to_string()),
19394 documentation: Some(lsp::Documentation::String("Docs".to_string())),
19395 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19396 range: lsp::Range::new(
19397 lsp::Position::new(0, 22),
19398 lsp::Position::new(0, 22),
19399 ),
19400 new_text: ".id".to_string(),
19401 })),
19402 ..lsp::CompletionItem::default()
19403 })
19404 } else {
19405 Ok(item_to_resolve)
19406 }
19407 }
19408 }
19409 })
19410 .next()
19411 .await
19412 .unwrap();
19413 cx.run_until_parked();
19414
19415 cx.update_editor(|editor, window, cx| {
19416 editor.context_menu_next(&Default::default(), window, cx);
19417 });
19418 cx.run_until_parked();
19419
19420 cx.update_editor(|editor, _, _| {
19421 let context_menu = editor.context_menu.borrow_mut();
19422 let context_menu = context_menu
19423 .as_ref()
19424 .expect("Should have the context menu deployed");
19425 match context_menu {
19426 CodeContextMenu::Completions(completions_menu) => {
19427 let completions = completions_menu.completions.borrow_mut();
19428 assert_eq!(
19429 completions
19430 .iter()
19431 .map(|completion| &completion.label.text)
19432 .collect::<Vec<_>>(),
19433 vec!["method id() Now resolved!", "other"],
19434 "Should update first completion label, but not second as the filter text did not match."
19435 );
19436 }
19437 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
19438 }
19439 });
19440}
19441
19442#[gpui::test]
19443async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
19444 init_test(cx, |_| {});
19445 let mut cx = EditorLspTestContext::new_rust(
19446 lsp::ServerCapabilities {
19447 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
19448 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
19449 completion_provider: Some(lsp::CompletionOptions {
19450 resolve_provider: Some(true),
19451 ..Default::default()
19452 }),
19453 ..Default::default()
19454 },
19455 cx,
19456 )
19457 .await;
19458 cx.set_state(indoc! {"
19459 struct TestStruct {
19460 field: i32
19461 }
19462
19463 fn mainˇ() {
19464 let unused_var = 42;
19465 let test_struct = TestStruct { field: 42 };
19466 }
19467 "});
19468 let symbol_range = cx.lsp_range(indoc! {"
19469 struct TestStruct {
19470 field: i32
19471 }
19472
19473 «fn main»() {
19474 let unused_var = 42;
19475 let test_struct = TestStruct { field: 42 };
19476 }
19477 "});
19478 let mut hover_requests =
19479 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
19480 Ok(Some(lsp::Hover {
19481 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
19482 kind: lsp::MarkupKind::Markdown,
19483 value: "Function documentation".to_string(),
19484 }),
19485 range: Some(symbol_range),
19486 }))
19487 });
19488
19489 // Case 1: Test that code action menu hide hover popover
19490 cx.dispatch_action(Hover);
19491 hover_requests.next().await;
19492 cx.condition(|editor, _| editor.hover_state.visible()).await;
19493 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
19494 move |_, _, _| async move {
19495 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
19496 lsp::CodeAction {
19497 title: "Remove unused variable".to_string(),
19498 kind: Some(CodeActionKind::QUICKFIX),
19499 edit: Some(lsp::WorkspaceEdit {
19500 changes: Some(
19501 [(
19502 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
19503 vec![lsp::TextEdit {
19504 range: lsp::Range::new(
19505 lsp::Position::new(5, 4),
19506 lsp::Position::new(5, 27),
19507 ),
19508 new_text: "".to_string(),
19509 }],
19510 )]
19511 .into_iter()
19512 .collect(),
19513 ),
19514 ..Default::default()
19515 }),
19516 ..Default::default()
19517 },
19518 )]))
19519 },
19520 );
19521 cx.update_editor(|editor, window, cx| {
19522 editor.toggle_code_actions(
19523 &ToggleCodeActions {
19524 deployed_from: None,
19525 quick_launch: false,
19526 },
19527 window,
19528 cx,
19529 );
19530 });
19531 code_action_requests.next().await;
19532 cx.run_until_parked();
19533 cx.condition(|editor, _| editor.context_menu_visible())
19534 .await;
19535 cx.update_editor(|editor, _, _| {
19536 assert!(
19537 !editor.hover_state.visible(),
19538 "Hover popover should be hidden when code action menu is shown"
19539 );
19540 // Hide code actions
19541 editor.context_menu.take();
19542 });
19543
19544 // Case 2: Test that code completions hide hover popover
19545 cx.dispatch_action(Hover);
19546 hover_requests.next().await;
19547 cx.condition(|editor, _| editor.hover_state.visible()).await;
19548 let counter = Arc::new(AtomicUsize::new(0));
19549 let mut completion_requests =
19550 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19551 let counter = counter.clone();
19552 async move {
19553 counter.fetch_add(1, atomic::Ordering::Release);
19554 Ok(Some(lsp::CompletionResponse::Array(vec![
19555 lsp::CompletionItem {
19556 label: "main".into(),
19557 kind: Some(lsp::CompletionItemKind::FUNCTION),
19558 detail: Some("() -> ()".to_string()),
19559 ..Default::default()
19560 },
19561 lsp::CompletionItem {
19562 label: "TestStruct".into(),
19563 kind: Some(lsp::CompletionItemKind::STRUCT),
19564 detail: Some("struct TestStruct".to_string()),
19565 ..Default::default()
19566 },
19567 ])))
19568 }
19569 });
19570 cx.update_editor(|editor, window, cx| {
19571 editor.show_completions(&ShowCompletions, window, cx);
19572 });
19573 completion_requests.next().await;
19574 cx.condition(|editor, _| editor.context_menu_visible())
19575 .await;
19576 cx.update_editor(|editor, _, _| {
19577 assert!(
19578 !editor.hover_state.visible(),
19579 "Hover popover should be hidden when completion menu is shown"
19580 );
19581 });
19582}
19583
19584#[gpui::test]
19585async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
19586 init_test(cx, |_| {});
19587
19588 let mut cx = EditorLspTestContext::new_rust(
19589 lsp::ServerCapabilities {
19590 completion_provider: Some(lsp::CompletionOptions {
19591 trigger_characters: Some(vec![".".to_string()]),
19592 resolve_provider: Some(true),
19593 ..Default::default()
19594 }),
19595 ..Default::default()
19596 },
19597 cx,
19598 )
19599 .await;
19600
19601 cx.set_state("fn main() { let a = 2ˇ; }");
19602 cx.simulate_keystroke(".");
19603
19604 let unresolved_item_1 = lsp::CompletionItem {
19605 label: "id".to_string(),
19606 filter_text: Some("id".to_string()),
19607 detail: None,
19608 documentation: None,
19609 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19610 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19611 new_text: ".id".to_string(),
19612 })),
19613 ..lsp::CompletionItem::default()
19614 };
19615 let resolved_item_1 = lsp::CompletionItem {
19616 additional_text_edits: Some(vec![lsp::TextEdit {
19617 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
19618 new_text: "!!".to_string(),
19619 }]),
19620 ..unresolved_item_1.clone()
19621 };
19622 let unresolved_item_2 = lsp::CompletionItem {
19623 label: "other".to_string(),
19624 filter_text: Some("other".to_string()),
19625 detail: None,
19626 documentation: None,
19627 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19628 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19629 new_text: ".other".to_string(),
19630 })),
19631 ..lsp::CompletionItem::default()
19632 };
19633 let resolved_item_2 = lsp::CompletionItem {
19634 additional_text_edits: Some(vec![lsp::TextEdit {
19635 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
19636 new_text: "??".to_string(),
19637 }]),
19638 ..unresolved_item_2.clone()
19639 };
19640
19641 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
19642 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
19643 cx.lsp
19644 .server
19645 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19646 let unresolved_item_1 = unresolved_item_1.clone();
19647 let resolved_item_1 = resolved_item_1.clone();
19648 let unresolved_item_2 = unresolved_item_2.clone();
19649 let resolved_item_2 = resolved_item_2.clone();
19650 let resolve_requests_1 = resolve_requests_1.clone();
19651 let resolve_requests_2 = resolve_requests_2.clone();
19652 move |unresolved_request, _| {
19653 let unresolved_item_1 = unresolved_item_1.clone();
19654 let resolved_item_1 = resolved_item_1.clone();
19655 let unresolved_item_2 = unresolved_item_2.clone();
19656 let resolved_item_2 = resolved_item_2.clone();
19657 let resolve_requests_1 = resolve_requests_1.clone();
19658 let resolve_requests_2 = resolve_requests_2.clone();
19659 async move {
19660 if unresolved_request == unresolved_item_1 {
19661 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
19662 Ok(resolved_item_1.clone())
19663 } else if unresolved_request == unresolved_item_2 {
19664 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
19665 Ok(resolved_item_2.clone())
19666 } else {
19667 panic!("Unexpected completion item {unresolved_request:?}")
19668 }
19669 }
19670 }
19671 })
19672 .detach();
19673
19674 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19675 let unresolved_item_1 = unresolved_item_1.clone();
19676 let unresolved_item_2 = unresolved_item_2.clone();
19677 async move {
19678 Ok(Some(lsp::CompletionResponse::Array(vec![
19679 unresolved_item_1,
19680 unresolved_item_2,
19681 ])))
19682 }
19683 })
19684 .next()
19685 .await;
19686
19687 cx.condition(|editor, _| editor.context_menu_visible())
19688 .await;
19689 cx.update_editor(|editor, _, _| {
19690 let context_menu = editor.context_menu.borrow_mut();
19691 let context_menu = context_menu
19692 .as_ref()
19693 .expect("Should have the context menu deployed");
19694 match context_menu {
19695 CodeContextMenu::Completions(completions_menu) => {
19696 let completions = completions_menu.completions.borrow_mut();
19697 assert_eq!(
19698 completions
19699 .iter()
19700 .map(|completion| &completion.label.text)
19701 .collect::<Vec<_>>(),
19702 vec!["id", "other"]
19703 )
19704 }
19705 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
19706 }
19707 });
19708 cx.run_until_parked();
19709
19710 cx.update_editor(|editor, window, cx| {
19711 editor.context_menu_next(&ContextMenuNext, window, cx);
19712 });
19713 cx.run_until_parked();
19714 cx.update_editor(|editor, window, cx| {
19715 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19716 });
19717 cx.run_until_parked();
19718 cx.update_editor(|editor, window, cx| {
19719 editor.context_menu_next(&ContextMenuNext, window, cx);
19720 });
19721 cx.run_until_parked();
19722 cx.update_editor(|editor, window, cx| {
19723 editor
19724 .compose_completion(&ComposeCompletion::default(), window, cx)
19725 .expect("No task returned")
19726 })
19727 .await
19728 .expect("Completion failed");
19729 cx.run_until_parked();
19730
19731 cx.update_editor(|editor, _, cx| {
19732 assert_eq!(
19733 resolve_requests_1.load(atomic::Ordering::Acquire),
19734 1,
19735 "Should always resolve once despite multiple selections"
19736 );
19737 assert_eq!(
19738 resolve_requests_2.load(atomic::Ordering::Acquire),
19739 1,
19740 "Should always resolve once after multiple selections and applying the completion"
19741 );
19742 assert_eq!(
19743 editor.text(cx),
19744 "fn main() { let a = ??.other; }",
19745 "Should use resolved data when applying the completion"
19746 );
19747 });
19748}
19749
19750#[gpui::test]
19751async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
19752 init_test(cx, |_| {});
19753
19754 let item_0 = lsp::CompletionItem {
19755 label: "abs".into(),
19756 insert_text: Some("abs".into()),
19757 data: Some(json!({ "very": "special"})),
19758 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
19759 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19760 lsp::InsertReplaceEdit {
19761 new_text: "abs".to_string(),
19762 insert: lsp::Range::default(),
19763 replace: lsp::Range::default(),
19764 },
19765 )),
19766 ..lsp::CompletionItem::default()
19767 };
19768 let items = iter::once(item_0.clone())
19769 .chain((11..51).map(|i| lsp::CompletionItem {
19770 label: format!("item_{}", i),
19771 insert_text: Some(format!("item_{}", i)),
19772 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
19773 ..lsp::CompletionItem::default()
19774 }))
19775 .collect::<Vec<_>>();
19776
19777 let default_commit_characters = vec!["?".to_string()];
19778 let default_data = json!({ "default": "data"});
19779 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
19780 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
19781 let default_edit_range = lsp::Range {
19782 start: lsp::Position {
19783 line: 0,
19784 character: 5,
19785 },
19786 end: lsp::Position {
19787 line: 0,
19788 character: 5,
19789 },
19790 };
19791
19792 let mut cx = EditorLspTestContext::new_rust(
19793 lsp::ServerCapabilities {
19794 completion_provider: Some(lsp::CompletionOptions {
19795 trigger_characters: Some(vec![".".to_string()]),
19796 resolve_provider: Some(true),
19797 ..Default::default()
19798 }),
19799 ..Default::default()
19800 },
19801 cx,
19802 )
19803 .await;
19804
19805 cx.set_state("fn main() { let a = 2ˇ; }");
19806 cx.simulate_keystroke(".");
19807
19808 let completion_data = default_data.clone();
19809 let completion_characters = default_commit_characters.clone();
19810 let completion_items = items.clone();
19811 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19812 let default_data = completion_data.clone();
19813 let default_commit_characters = completion_characters.clone();
19814 let items = completion_items.clone();
19815 async move {
19816 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
19817 items,
19818 item_defaults: Some(lsp::CompletionListItemDefaults {
19819 data: Some(default_data.clone()),
19820 commit_characters: Some(default_commit_characters.clone()),
19821 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
19822 default_edit_range,
19823 )),
19824 insert_text_format: Some(default_insert_text_format),
19825 insert_text_mode: Some(default_insert_text_mode),
19826 }),
19827 ..lsp::CompletionList::default()
19828 })))
19829 }
19830 })
19831 .next()
19832 .await;
19833
19834 let resolved_items = Arc::new(Mutex::new(Vec::new()));
19835 cx.lsp
19836 .server
19837 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19838 let closure_resolved_items = resolved_items.clone();
19839 move |item_to_resolve, _| {
19840 let closure_resolved_items = closure_resolved_items.clone();
19841 async move {
19842 closure_resolved_items.lock().push(item_to_resolve.clone());
19843 Ok(item_to_resolve)
19844 }
19845 }
19846 })
19847 .detach();
19848
19849 cx.condition(|editor, _| editor.context_menu_visible())
19850 .await;
19851 cx.run_until_parked();
19852 cx.update_editor(|editor, _, _| {
19853 let menu = editor.context_menu.borrow_mut();
19854 match menu.as_ref().expect("should have the completions menu") {
19855 CodeContextMenu::Completions(completions_menu) => {
19856 assert_eq!(
19857 completions_menu
19858 .entries
19859 .borrow()
19860 .iter()
19861 .map(|mat| mat.string.clone())
19862 .collect::<Vec<String>>(),
19863 items
19864 .iter()
19865 .map(|completion| completion.label.clone())
19866 .collect::<Vec<String>>()
19867 );
19868 }
19869 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
19870 }
19871 });
19872 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
19873 // with 4 from the end.
19874 assert_eq!(
19875 *resolved_items.lock(),
19876 [&items[0..16], &items[items.len() - 4..items.len()]]
19877 .concat()
19878 .iter()
19879 .cloned()
19880 .map(|mut item| {
19881 if item.data.is_none() {
19882 item.data = Some(default_data.clone());
19883 }
19884 item
19885 })
19886 .collect::<Vec<lsp::CompletionItem>>(),
19887 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
19888 );
19889 resolved_items.lock().clear();
19890
19891 cx.update_editor(|editor, window, cx| {
19892 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19893 });
19894 cx.run_until_parked();
19895 // Completions that have already been resolved are skipped.
19896 assert_eq!(
19897 *resolved_items.lock(),
19898 items[items.len() - 17..items.len() - 4]
19899 .iter()
19900 .cloned()
19901 .map(|mut item| {
19902 if item.data.is_none() {
19903 item.data = Some(default_data.clone());
19904 }
19905 item
19906 })
19907 .collect::<Vec<lsp::CompletionItem>>()
19908 );
19909 resolved_items.lock().clear();
19910}
19911
19912#[gpui::test]
19913async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
19914 init_test(cx, |_| {});
19915
19916 let mut cx = EditorLspTestContext::new(
19917 Language::new(
19918 LanguageConfig {
19919 matcher: LanguageMatcher {
19920 path_suffixes: vec!["jsx".into()],
19921 ..Default::default()
19922 },
19923 overrides: [(
19924 "element".into(),
19925 LanguageConfigOverride {
19926 completion_query_characters: Override::Set(['-'].into_iter().collect()),
19927 ..Default::default()
19928 },
19929 )]
19930 .into_iter()
19931 .collect(),
19932 ..Default::default()
19933 },
19934 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
19935 )
19936 .with_override_query("(jsx_self_closing_element) @element")
19937 .unwrap(),
19938 lsp::ServerCapabilities {
19939 completion_provider: Some(lsp::CompletionOptions {
19940 trigger_characters: Some(vec![":".to_string()]),
19941 ..Default::default()
19942 }),
19943 ..Default::default()
19944 },
19945 cx,
19946 )
19947 .await;
19948
19949 cx.lsp
19950 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19951 Ok(Some(lsp::CompletionResponse::Array(vec![
19952 lsp::CompletionItem {
19953 label: "bg-blue".into(),
19954 ..Default::default()
19955 },
19956 lsp::CompletionItem {
19957 label: "bg-red".into(),
19958 ..Default::default()
19959 },
19960 lsp::CompletionItem {
19961 label: "bg-yellow".into(),
19962 ..Default::default()
19963 },
19964 ])))
19965 });
19966
19967 cx.set_state(r#"<p class="bgˇ" />"#);
19968
19969 // Trigger completion when typing a dash, because the dash is an extra
19970 // word character in the 'element' scope, which contains the cursor.
19971 cx.simulate_keystroke("-");
19972 cx.executor().run_until_parked();
19973 cx.update_editor(|editor, _, _| {
19974 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19975 {
19976 assert_eq!(
19977 completion_menu_entries(menu),
19978 &["bg-blue", "bg-red", "bg-yellow"]
19979 );
19980 } else {
19981 panic!("expected completion menu to be open");
19982 }
19983 });
19984
19985 cx.simulate_keystroke("l");
19986 cx.executor().run_until_parked();
19987 cx.update_editor(|editor, _, _| {
19988 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19989 {
19990 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
19991 } else {
19992 panic!("expected completion menu to be open");
19993 }
19994 });
19995
19996 // When filtering completions, consider the character after the '-' to
19997 // be the start of a subword.
19998 cx.set_state(r#"<p class="yelˇ" />"#);
19999 cx.simulate_keystroke("l");
20000 cx.executor().run_until_parked();
20001 cx.update_editor(|editor, _, _| {
20002 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
20003 {
20004 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
20005 } else {
20006 panic!("expected completion menu to be open");
20007 }
20008 });
20009}
20010
20011fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
20012 let entries = menu.entries.borrow();
20013 entries.iter().map(|mat| mat.string.clone()).collect()
20014}
20015
20016#[gpui::test]
20017async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
20018 init_test(cx, |settings| {
20019 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
20020 });
20021
20022 let fs = FakeFs::new(cx.executor());
20023 fs.insert_file(path!("/file.ts"), Default::default()).await;
20024
20025 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
20026 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20027
20028 language_registry.add(Arc::new(Language::new(
20029 LanguageConfig {
20030 name: "TypeScript".into(),
20031 matcher: LanguageMatcher {
20032 path_suffixes: vec!["ts".to_string()],
20033 ..Default::default()
20034 },
20035 ..Default::default()
20036 },
20037 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20038 )));
20039 update_test_language_settings(cx, |settings| {
20040 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
20041 });
20042
20043 let test_plugin = "test_plugin";
20044 let _ = language_registry.register_fake_lsp(
20045 "TypeScript",
20046 FakeLspAdapter {
20047 prettier_plugins: vec![test_plugin],
20048 ..Default::default()
20049 },
20050 );
20051
20052 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
20053 let buffer = project
20054 .update(cx, |project, cx| {
20055 project.open_local_buffer(path!("/file.ts"), cx)
20056 })
20057 .await
20058 .unwrap();
20059
20060 let buffer_text = "one\ntwo\nthree\n";
20061 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
20062 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
20063 editor.update_in(cx, |editor, window, cx| {
20064 editor.set_text(buffer_text, window, cx)
20065 });
20066
20067 editor
20068 .update_in(cx, |editor, window, cx| {
20069 editor.perform_format(
20070 project.clone(),
20071 FormatTrigger::Manual,
20072 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
20073 window,
20074 cx,
20075 )
20076 })
20077 .unwrap()
20078 .await;
20079 assert_eq!(
20080 editor.update(cx, |editor, cx| editor.text(cx)),
20081 buffer_text.to_string() + prettier_format_suffix,
20082 "Test prettier formatting was not applied to the original buffer text",
20083 );
20084
20085 update_test_language_settings(cx, |settings| {
20086 settings.defaults.formatter = Some(FormatterList::default())
20087 });
20088 let format = editor.update_in(cx, |editor, window, cx| {
20089 editor.perform_format(
20090 project.clone(),
20091 FormatTrigger::Manual,
20092 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
20093 window,
20094 cx,
20095 )
20096 });
20097 format.await.unwrap();
20098 assert_eq!(
20099 editor.update(cx, |editor, cx| editor.text(cx)),
20100 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
20101 "Autoformatting (via test prettier) was not applied to the original buffer text",
20102 );
20103}
20104
20105#[gpui::test]
20106async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
20107 init_test(cx, |settings| {
20108 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
20109 });
20110
20111 let fs = FakeFs::new(cx.executor());
20112 fs.insert_file(path!("/file.settings"), Default::default())
20113 .await;
20114
20115 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
20116 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20117
20118 let ts_lang = Arc::new(Language::new(
20119 LanguageConfig {
20120 name: "TypeScript".into(),
20121 matcher: LanguageMatcher {
20122 path_suffixes: vec!["ts".to_string()],
20123 ..LanguageMatcher::default()
20124 },
20125 prettier_parser_name: Some("typescript".to_string()),
20126 ..LanguageConfig::default()
20127 },
20128 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20129 ));
20130
20131 language_registry.add(ts_lang.clone());
20132
20133 update_test_language_settings(cx, |settings| {
20134 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
20135 });
20136
20137 let test_plugin = "test_plugin";
20138 let _ = language_registry.register_fake_lsp(
20139 "TypeScript",
20140 FakeLspAdapter {
20141 prettier_plugins: vec![test_plugin],
20142 ..Default::default()
20143 },
20144 );
20145
20146 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
20147 let buffer = project
20148 .update(cx, |project, cx| {
20149 project.open_local_buffer(path!("/file.settings"), cx)
20150 })
20151 .await
20152 .unwrap();
20153
20154 project.update(cx, |project, cx| {
20155 project.set_language_for_buffer(&buffer, ts_lang, cx)
20156 });
20157
20158 let buffer_text = "one\ntwo\nthree\n";
20159 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
20160 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
20161 editor.update_in(cx, |editor, window, cx| {
20162 editor.set_text(buffer_text, window, cx)
20163 });
20164
20165 editor
20166 .update_in(cx, |editor, window, cx| {
20167 editor.perform_format(
20168 project.clone(),
20169 FormatTrigger::Manual,
20170 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
20171 window,
20172 cx,
20173 )
20174 })
20175 .unwrap()
20176 .await;
20177 assert_eq!(
20178 editor.update(cx, |editor, cx| editor.text(cx)),
20179 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
20180 "Test prettier formatting was not applied to the original buffer text",
20181 );
20182
20183 update_test_language_settings(cx, |settings| {
20184 settings.defaults.formatter = Some(FormatterList::default())
20185 });
20186 let format = editor.update_in(cx, |editor, window, cx| {
20187 editor.perform_format(
20188 project.clone(),
20189 FormatTrigger::Manual,
20190 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
20191 window,
20192 cx,
20193 )
20194 });
20195 format.await.unwrap();
20196
20197 assert_eq!(
20198 editor.update(cx, |editor, cx| editor.text(cx)),
20199 buffer_text.to_string()
20200 + prettier_format_suffix
20201 + "\ntypescript\n"
20202 + prettier_format_suffix
20203 + "\ntypescript",
20204 "Autoformatting (via test prettier) was not applied to the original buffer text",
20205 );
20206}
20207
20208#[gpui::test]
20209async fn test_addition_reverts(cx: &mut TestAppContext) {
20210 init_test(cx, |_| {});
20211 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
20212 let base_text = indoc! {r#"
20213 struct Row;
20214 struct Row1;
20215 struct Row2;
20216
20217 struct Row4;
20218 struct Row5;
20219 struct Row6;
20220
20221 struct Row8;
20222 struct Row9;
20223 struct Row10;"#};
20224
20225 // When addition hunks are not adjacent to carets, no hunk revert is performed
20226 assert_hunk_revert(
20227 indoc! {r#"struct Row;
20228 struct Row1;
20229 struct Row1.1;
20230 struct Row1.2;
20231 struct Row2;ˇ
20232
20233 struct Row4;
20234 struct Row5;
20235 struct Row6;
20236
20237 struct Row8;
20238 ˇstruct Row9;
20239 struct Row9.1;
20240 struct Row9.2;
20241 struct Row9.3;
20242 struct Row10;"#},
20243 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
20244 indoc! {r#"struct Row;
20245 struct Row1;
20246 struct Row1.1;
20247 struct Row1.2;
20248 struct Row2;ˇ
20249
20250 struct Row4;
20251 struct Row5;
20252 struct Row6;
20253
20254 struct Row8;
20255 ˇstruct Row9;
20256 struct Row9.1;
20257 struct Row9.2;
20258 struct Row9.3;
20259 struct Row10;"#},
20260 base_text,
20261 &mut cx,
20262 );
20263 // Same for selections
20264 assert_hunk_revert(
20265 indoc! {r#"struct Row;
20266 struct Row1;
20267 struct Row2;
20268 struct Row2.1;
20269 struct Row2.2;
20270 «ˇ
20271 struct Row4;
20272 struct» Row5;
20273 «struct Row6;
20274 ˇ»
20275 struct Row9.1;
20276 struct Row9.2;
20277 struct Row9.3;
20278 struct Row8;
20279 struct Row9;
20280 struct Row10;"#},
20281 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
20282 indoc! {r#"struct Row;
20283 struct Row1;
20284 struct Row2;
20285 struct Row2.1;
20286 struct Row2.2;
20287 «ˇ
20288 struct Row4;
20289 struct» Row5;
20290 «struct Row6;
20291 ˇ»
20292 struct Row9.1;
20293 struct Row9.2;
20294 struct Row9.3;
20295 struct Row8;
20296 struct Row9;
20297 struct Row10;"#},
20298 base_text,
20299 &mut cx,
20300 );
20301
20302 // When carets and selections intersect the addition hunks, those are reverted.
20303 // Adjacent carets got merged.
20304 assert_hunk_revert(
20305 indoc! {r#"struct Row;
20306 ˇ// something on the top
20307 struct Row1;
20308 struct Row2;
20309 struct Roˇw3.1;
20310 struct Row2.2;
20311 struct Row2.3;ˇ
20312
20313 struct Row4;
20314 struct ˇRow5.1;
20315 struct Row5.2;
20316 struct «Rowˇ»5.3;
20317 struct Row5;
20318 struct Row6;
20319 ˇ
20320 struct Row9.1;
20321 struct «Rowˇ»9.2;
20322 struct «ˇRow»9.3;
20323 struct Row8;
20324 struct Row9;
20325 «ˇ// something on bottom»
20326 struct Row10;"#},
20327 vec![
20328 DiffHunkStatusKind::Added,
20329 DiffHunkStatusKind::Added,
20330 DiffHunkStatusKind::Added,
20331 DiffHunkStatusKind::Added,
20332 DiffHunkStatusKind::Added,
20333 ],
20334 indoc! {r#"struct Row;
20335 ˇstruct Row1;
20336 struct Row2;
20337 ˇ
20338 struct Row4;
20339 ˇstruct Row5;
20340 struct Row6;
20341 ˇ
20342 ˇstruct Row8;
20343 struct Row9;
20344 ˇstruct Row10;"#},
20345 base_text,
20346 &mut cx,
20347 );
20348}
20349
20350#[gpui::test]
20351async fn test_modification_reverts(cx: &mut TestAppContext) {
20352 init_test(cx, |_| {});
20353 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
20354 let base_text = indoc! {r#"
20355 struct Row;
20356 struct Row1;
20357 struct Row2;
20358
20359 struct Row4;
20360 struct Row5;
20361 struct Row6;
20362
20363 struct Row8;
20364 struct Row9;
20365 struct Row10;"#};
20366
20367 // Modification hunks behave the same as the addition ones.
20368 assert_hunk_revert(
20369 indoc! {r#"struct Row;
20370 struct Row1;
20371 struct Row33;
20372 ˇ
20373 struct Row4;
20374 struct Row5;
20375 struct Row6;
20376 ˇ
20377 struct Row99;
20378 struct Row9;
20379 struct Row10;"#},
20380 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
20381 indoc! {r#"struct Row;
20382 struct Row1;
20383 struct Row33;
20384 ˇ
20385 struct Row4;
20386 struct Row5;
20387 struct Row6;
20388 ˇ
20389 struct Row99;
20390 struct Row9;
20391 struct Row10;"#},
20392 base_text,
20393 &mut cx,
20394 );
20395 assert_hunk_revert(
20396 indoc! {r#"struct Row;
20397 struct Row1;
20398 struct Row33;
20399 «ˇ
20400 struct Row4;
20401 struct» Row5;
20402 «struct Row6;
20403 ˇ»
20404 struct Row99;
20405 struct Row9;
20406 struct Row10;"#},
20407 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
20408 indoc! {r#"struct Row;
20409 struct Row1;
20410 struct Row33;
20411 «ˇ
20412 struct Row4;
20413 struct» Row5;
20414 «struct Row6;
20415 ˇ»
20416 struct Row99;
20417 struct Row9;
20418 struct Row10;"#},
20419 base_text,
20420 &mut cx,
20421 );
20422
20423 assert_hunk_revert(
20424 indoc! {r#"ˇstruct Row1.1;
20425 struct Row1;
20426 «ˇstr»uct Row22;
20427
20428 struct ˇRow44;
20429 struct Row5;
20430 struct «Rˇ»ow66;ˇ
20431
20432 «struˇ»ct Row88;
20433 struct Row9;
20434 struct Row1011;ˇ"#},
20435 vec![
20436 DiffHunkStatusKind::Modified,
20437 DiffHunkStatusKind::Modified,
20438 DiffHunkStatusKind::Modified,
20439 DiffHunkStatusKind::Modified,
20440 DiffHunkStatusKind::Modified,
20441 DiffHunkStatusKind::Modified,
20442 ],
20443 indoc! {r#"struct Row;
20444 ˇstruct Row1;
20445 struct Row2;
20446 ˇ
20447 struct Row4;
20448 ˇstruct Row5;
20449 struct Row6;
20450 ˇ
20451 struct Row8;
20452 ˇstruct Row9;
20453 struct Row10;ˇ"#},
20454 base_text,
20455 &mut cx,
20456 );
20457}
20458
20459#[gpui::test]
20460async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
20461 init_test(cx, |_| {});
20462 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
20463 let base_text = indoc! {r#"
20464 one
20465
20466 two
20467 three
20468 "#};
20469
20470 cx.set_head_text(base_text);
20471 cx.set_state("\nˇ\n");
20472 cx.executor().run_until_parked();
20473 cx.update_editor(|editor, _window, cx| {
20474 editor.expand_selected_diff_hunks(cx);
20475 });
20476 cx.executor().run_until_parked();
20477 cx.update_editor(|editor, window, cx| {
20478 editor.backspace(&Default::default(), window, cx);
20479 });
20480 cx.run_until_parked();
20481 cx.assert_state_with_diff(
20482 indoc! {r#"
20483
20484 - two
20485 - threeˇ
20486 +
20487 "#}
20488 .to_string(),
20489 );
20490}
20491
20492#[gpui::test]
20493async fn test_deletion_reverts(cx: &mut TestAppContext) {
20494 init_test(cx, |_| {});
20495 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
20496 let base_text = indoc! {r#"struct Row;
20497struct Row1;
20498struct Row2;
20499
20500struct Row4;
20501struct Row5;
20502struct Row6;
20503
20504struct Row8;
20505struct Row9;
20506struct Row10;"#};
20507
20508 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
20509 assert_hunk_revert(
20510 indoc! {r#"struct Row;
20511 struct Row2;
20512
20513 ˇstruct Row4;
20514 struct Row5;
20515 struct Row6;
20516 ˇ
20517 struct Row8;
20518 struct Row10;"#},
20519 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
20520 indoc! {r#"struct Row;
20521 struct Row2;
20522
20523 ˇstruct Row4;
20524 struct Row5;
20525 struct Row6;
20526 ˇ
20527 struct Row8;
20528 struct Row10;"#},
20529 base_text,
20530 &mut cx,
20531 );
20532 assert_hunk_revert(
20533 indoc! {r#"struct Row;
20534 struct Row2;
20535
20536 «ˇstruct Row4;
20537 struct» Row5;
20538 «struct Row6;
20539 ˇ»
20540 struct Row8;
20541 struct Row10;"#},
20542 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
20543 indoc! {r#"struct Row;
20544 struct Row2;
20545
20546 «ˇstruct Row4;
20547 struct» Row5;
20548 «struct Row6;
20549 ˇ»
20550 struct Row8;
20551 struct Row10;"#},
20552 base_text,
20553 &mut cx,
20554 );
20555
20556 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
20557 assert_hunk_revert(
20558 indoc! {r#"struct Row;
20559 ˇstruct Row2;
20560
20561 struct Row4;
20562 struct Row5;
20563 struct Row6;
20564
20565 struct Row8;ˇ
20566 struct Row10;"#},
20567 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
20568 indoc! {r#"struct Row;
20569 struct Row1;
20570 ˇstruct Row2;
20571
20572 struct Row4;
20573 struct Row5;
20574 struct Row6;
20575
20576 struct Row8;ˇ
20577 struct Row9;
20578 struct Row10;"#},
20579 base_text,
20580 &mut cx,
20581 );
20582 assert_hunk_revert(
20583 indoc! {r#"struct Row;
20584 struct Row2«ˇ;
20585 struct Row4;
20586 struct» Row5;
20587 «struct Row6;
20588
20589 struct Row8;ˇ»
20590 struct Row10;"#},
20591 vec![
20592 DiffHunkStatusKind::Deleted,
20593 DiffHunkStatusKind::Deleted,
20594 DiffHunkStatusKind::Deleted,
20595 ],
20596 indoc! {r#"struct Row;
20597 struct Row1;
20598 struct Row2«ˇ;
20599
20600 struct Row4;
20601 struct» Row5;
20602 «struct Row6;
20603
20604 struct Row8;ˇ»
20605 struct Row9;
20606 struct Row10;"#},
20607 base_text,
20608 &mut cx,
20609 );
20610}
20611
20612#[gpui::test]
20613async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
20614 init_test(cx, |_| {});
20615
20616 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
20617 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
20618 let base_text_3 =
20619 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
20620
20621 let text_1 = edit_first_char_of_every_line(base_text_1);
20622 let text_2 = edit_first_char_of_every_line(base_text_2);
20623 let text_3 = edit_first_char_of_every_line(base_text_3);
20624
20625 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
20626 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
20627 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
20628
20629 let multibuffer = cx.new(|cx| {
20630 let mut multibuffer = MultiBuffer::new(ReadWrite);
20631 multibuffer.push_excerpts(
20632 buffer_1.clone(),
20633 [
20634 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20635 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20636 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20637 ],
20638 cx,
20639 );
20640 multibuffer.push_excerpts(
20641 buffer_2.clone(),
20642 [
20643 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20644 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20645 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20646 ],
20647 cx,
20648 );
20649 multibuffer.push_excerpts(
20650 buffer_3.clone(),
20651 [
20652 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20653 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20654 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20655 ],
20656 cx,
20657 );
20658 multibuffer
20659 });
20660
20661 let fs = FakeFs::new(cx.executor());
20662 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20663 let (editor, cx) = cx
20664 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
20665 editor.update_in(cx, |editor, _window, cx| {
20666 for (buffer, diff_base) in [
20667 (buffer_1.clone(), base_text_1),
20668 (buffer_2.clone(), base_text_2),
20669 (buffer_3.clone(), base_text_3),
20670 ] {
20671 let diff = cx.new(|cx| {
20672 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
20673 });
20674 editor
20675 .buffer
20676 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20677 }
20678 });
20679 cx.executor().run_until_parked();
20680
20681 editor.update_in(cx, |editor, window, cx| {
20682 assert_eq!(editor.text(cx), "Xaaa\nXbbb\nXccc\n\nXfff\nXggg\n\nXjjj\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}");
20683 editor.select_all(&SelectAll, window, cx);
20684 editor.git_restore(&Default::default(), window, cx);
20685 });
20686 cx.executor().run_until_parked();
20687
20688 // When all ranges are selected, all buffer hunks are reverted.
20689 editor.update(cx, |editor, cx| {
20690 assert_eq!(editor.text(cx), "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nllll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu\n\n\nvvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}\n\n");
20691 });
20692 buffer_1.update(cx, |buffer, _| {
20693 assert_eq!(buffer.text(), base_text_1);
20694 });
20695 buffer_2.update(cx, |buffer, _| {
20696 assert_eq!(buffer.text(), base_text_2);
20697 });
20698 buffer_3.update(cx, |buffer, _| {
20699 assert_eq!(buffer.text(), base_text_3);
20700 });
20701
20702 editor.update_in(cx, |editor, window, cx| {
20703 editor.undo(&Default::default(), window, cx);
20704 });
20705
20706 editor.update_in(cx, |editor, window, cx| {
20707 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20708 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
20709 });
20710 editor.git_restore(&Default::default(), window, cx);
20711 });
20712
20713 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
20714 // but not affect buffer_2 and its related excerpts.
20715 editor.update(cx, |editor, cx| {
20716 assert_eq!(
20717 editor.text(cx),
20718 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}"
20719 );
20720 });
20721 buffer_1.update(cx, |buffer, _| {
20722 assert_eq!(buffer.text(), base_text_1);
20723 });
20724 buffer_2.update(cx, |buffer, _| {
20725 assert_eq!(
20726 buffer.text(),
20727 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
20728 );
20729 });
20730 buffer_3.update(cx, |buffer, _| {
20731 assert_eq!(
20732 buffer.text(),
20733 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
20734 );
20735 });
20736
20737 fn edit_first_char_of_every_line(text: &str) -> String {
20738 text.split('\n')
20739 .map(|line| format!("X{}", &line[1..]))
20740 .collect::<Vec<_>>()
20741 .join("\n")
20742 }
20743}
20744
20745#[gpui::test]
20746async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
20747 init_test(cx, |_| {});
20748
20749 let cols = 4;
20750 let rows = 10;
20751 let sample_text_1 = sample_text(rows, cols, 'a');
20752 assert_eq!(
20753 sample_text_1,
20754 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
20755 );
20756 let sample_text_2 = sample_text(rows, cols, 'l');
20757 assert_eq!(
20758 sample_text_2,
20759 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
20760 );
20761 let sample_text_3 = sample_text(rows, cols, 'v');
20762 assert_eq!(
20763 sample_text_3,
20764 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
20765 );
20766
20767 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
20768 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
20769 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
20770
20771 let multi_buffer = cx.new(|cx| {
20772 let mut multibuffer = MultiBuffer::new(ReadWrite);
20773 multibuffer.push_excerpts(
20774 buffer_1.clone(),
20775 [
20776 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20777 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20778 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20779 ],
20780 cx,
20781 );
20782 multibuffer.push_excerpts(
20783 buffer_2.clone(),
20784 [
20785 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20786 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20787 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20788 ],
20789 cx,
20790 );
20791 multibuffer.push_excerpts(
20792 buffer_3.clone(),
20793 [
20794 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20795 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20796 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20797 ],
20798 cx,
20799 );
20800 multibuffer
20801 });
20802
20803 let fs = FakeFs::new(cx.executor());
20804 fs.insert_tree(
20805 "/a",
20806 json!({
20807 "main.rs": sample_text_1,
20808 "other.rs": sample_text_2,
20809 "lib.rs": sample_text_3,
20810 }),
20811 )
20812 .await;
20813 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20814 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
20815 let workspace = window
20816 .read_with(cx, |mw, _| mw.workspace().clone())
20817 .unwrap();
20818 let cx = &mut VisualTestContext::from_window(*window, cx);
20819 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20820 Editor::new(
20821 EditorMode::full(),
20822 multi_buffer,
20823 Some(project.clone()),
20824 window,
20825 cx,
20826 )
20827 });
20828 let multibuffer_item_id = workspace.update_in(cx, |workspace, window, cx| {
20829 assert!(
20830 workspace.active_item(cx).is_none(),
20831 "active item should be None before the first item is added"
20832 );
20833 workspace.add_item_to_active_pane(
20834 Box::new(multi_buffer_editor.clone()),
20835 None,
20836 true,
20837 window,
20838 cx,
20839 );
20840 let active_item = workspace
20841 .active_item(cx)
20842 .expect("should have an active item after adding the multi buffer");
20843 assert_eq!(
20844 active_item.buffer_kind(cx),
20845 ItemBufferKind::Multibuffer,
20846 "A multi buffer was expected to active after adding"
20847 );
20848 active_item.item_id()
20849 });
20850
20851 cx.executor().run_until_parked();
20852
20853 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20854 editor.change_selections(
20855 SelectionEffects::scroll(Autoscroll::Next),
20856 window,
20857 cx,
20858 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
20859 );
20860 editor.open_excerpts(&OpenExcerpts, window, cx);
20861 });
20862 cx.executor().run_until_parked();
20863 let first_item_id = workspace.update_in(cx, |workspace, window, cx| {
20864 let active_item = workspace
20865 .active_item(cx)
20866 .expect("should have an active item after navigating into the 1st buffer");
20867 let first_item_id = active_item.item_id();
20868 assert_ne!(
20869 first_item_id, multibuffer_item_id,
20870 "Should navigate into the 1st buffer and activate it"
20871 );
20872 assert_eq!(
20873 active_item.buffer_kind(cx),
20874 ItemBufferKind::Singleton,
20875 "New active item should be a singleton buffer"
20876 );
20877 assert_eq!(
20878 active_item
20879 .act_as::<Editor>(cx)
20880 .expect("should have navigated into an editor for the 1st buffer")
20881 .read(cx)
20882 .text(cx),
20883 sample_text_1
20884 );
20885
20886 workspace
20887 .go_back(workspace.active_pane().downgrade(), window, cx)
20888 .detach_and_log_err(cx);
20889
20890 first_item_id
20891 });
20892
20893 cx.executor().run_until_parked();
20894 workspace.update_in(cx, |workspace, _, cx| {
20895 let active_item = workspace
20896 .active_item(cx)
20897 .expect("should have an active item after navigating back");
20898 assert_eq!(
20899 active_item.item_id(),
20900 multibuffer_item_id,
20901 "Should navigate back to the multi buffer"
20902 );
20903 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20904 });
20905
20906 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20907 editor.change_selections(
20908 SelectionEffects::scroll(Autoscroll::Next),
20909 window,
20910 cx,
20911 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
20912 );
20913 editor.open_excerpts(&OpenExcerpts, window, cx);
20914 });
20915 cx.executor().run_until_parked();
20916 let second_item_id = workspace.update_in(cx, |workspace, window, cx| {
20917 let active_item = workspace
20918 .active_item(cx)
20919 .expect("should have an active item after navigating into the 2nd buffer");
20920 let second_item_id = active_item.item_id();
20921 assert_ne!(
20922 second_item_id, multibuffer_item_id,
20923 "Should navigate away from the multibuffer"
20924 );
20925 assert_ne!(
20926 second_item_id, first_item_id,
20927 "Should navigate into the 2nd buffer and activate it"
20928 );
20929 assert_eq!(
20930 active_item.buffer_kind(cx),
20931 ItemBufferKind::Singleton,
20932 "New active item should be a singleton buffer"
20933 );
20934 assert_eq!(
20935 active_item
20936 .act_as::<Editor>(cx)
20937 .expect("should have navigated into an editor")
20938 .read(cx)
20939 .text(cx),
20940 sample_text_2
20941 );
20942
20943 workspace
20944 .go_back(workspace.active_pane().downgrade(), window, cx)
20945 .detach_and_log_err(cx);
20946
20947 second_item_id
20948 });
20949
20950 cx.executor().run_until_parked();
20951 workspace.update_in(cx, |workspace, _, cx| {
20952 let active_item = workspace
20953 .active_item(cx)
20954 .expect("should have an active item after navigating back from the 2nd buffer");
20955 assert_eq!(
20956 active_item.item_id(),
20957 multibuffer_item_id,
20958 "Should navigate back from the 2nd buffer to the multi buffer"
20959 );
20960 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20961 });
20962
20963 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20964 editor.change_selections(
20965 SelectionEffects::scroll(Autoscroll::Next),
20966 window,
20967 cx,
20968 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
20969 );
20970 editor.open_excerpts(&OpenExcerpts, window, cx);
20971 });
20972 cx.executor().run_until_parked();
20973 workspace.update_in(cx, |workspace, window, cx| {
20974 let active_item = workspace
20975 .active_item(cx)
20976 .expect("should have an active item after navigating into the 3rd buffer");
20977 let third_item_id = active_item.item_id();
20978 assert_ne!(
20979 third_item_id, multibuffer_item_id,
20980 "Should navigate into the 3rd buffer and activate it"
20981 );
20982 assert_ne!(third_item_id, first_item_id);
20983 assert_ne!(third_item_id, second_item_id);
20984 assert_eq!(
20985 active_item.buffer_kind(cx),
20986 ItemBufferKind::Singleton,
20987 "New active item should be a singleton buffer"
20988 );
20989 assert_eq!(
20990 active_item
20991 .act_as::<Editor>(cx)
20992 .expect("should have navigated into an editor")
20993 .read(cx)
20994 .text(cx),
20995 sample_text_3
20996 );
20997
20998 workspace
20999 .go_back(workspace.active_pane().downgrade(), window, cx)
21000 .detach_and_log_err(cx);
21001 });
21002
21003 cx.executor().run_until_parked();
21004 workspace.update_in(cx, |workspace, _, cx| {
21005 let active_item = workspace
21006 .active_item(cx)
21007 .expect("should have an active item after navigating back from the 3rd buffer");
21008 assert_eq!(
21009 active_item.item_id(),
21010 multibuffer_item_id,
21011 "Should navigate back from the 3rd buffer to the multi buffer"
21012 );
21013 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
21014 });
21015}
21016
21017#[gpui::test]
21018async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21019 init_test(cx, |_| {});
21020
21021 let mut cx = EditorTestContext::new(cx).await;
21022
21023 let diff_base = r#"
21024 use some::mod;
21025
21026 const A: u32 = 42;
21027
21028 fn main() {
21029 println!("hello");
21030
21031 println!("world");
21032 }
21033 "#
21034 .unindent();
21035
21036 cx.set_state(
21037 &r#"
21038 use some::modified;
21039
21040 ˇ
21041 fn main() {
21042 println!("hello there");
21043
21044 println!("around the");
21045 println!("world");
21046 }
21047 "#
21048 .unindent(),
21049 );
21050
21051 cx.set_head_text(&diff_base);
21052 executor.run_until_parked();
21053
21054 cx.update_editor(|editor, window, cx| {
21055 editor.go_to_next_hunk(&GoToHunk, window, cx);
21056 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21057 });
21058 executor.run_until_parked();
21059 cx.assert_state_with_diff(
21060 r#"
21061 use some::modified;
21062
21063
21064 fn main() {
21065 - println!("hello");
21066 + ˇ println!("hello there");
21067
21068 println!("around the");
21069 println!("world");
21070 }
21071 "#
21072 .unindent(),
21073 );
21074
21075 cx.update_editor(|editor, window, cx| {
21076 for _ in 0..2 {
21077 editor.go_to_next_hunk(&GoToHunk, window, cx);
21078 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21079 }
21080 });
21081 executor.run_until_parked();
21082 cx.assert_state_with_diff(
21083 r#"
21084 - use some::mod;
21085 + ˇuse some::modified;
21086
21087
21088 fn main() {
21089 - println!("hello");
21090 + println!("hello there");
21091
21092 + println!("around the");
21093 println!("world");
21094 }
21095 "#
21096 .unindent(),
21097 );
21098
21099 cx.update_editor(|editor, window, cx| {
21100 editor.go_to_next_hunk(&GoToHunk, window, cx);
21101 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21102 });
21103 executor.run_until_parked();
21104 cx.assert_state_with_diff(
21105 r#"
21106 - use some::mod;
21107 + use some::modified;
21108
21109 - const A: u32 = 42;
21110 ˇ
21111 fn main() {
21112 - println!("hello");
21113 + println!("hello there");
21114
21115 + println!("around the");
21116 println!("world");
21117 }
21118 "#
21119 .unindent(),
21120 );
21121
21122 cx.update_editor(|editor, window, cx| {
21123 editor.cancel(&Cancel, window, cx);
21124 });
21125
21126 cx.assert_state_with_diff(
21127 r#"
21128 use some::modified;
21129
21130 ˇ
21131 fn main() {
21132 println!("hello there");
21133
21134 println!("around the");
21135 println!("world");
21136 }
21137 "#
21138 .unindent(),
21139 );
21140}
21141
21142#[gpui::test]
21143async fn test_diff_base_change_with_expanded_diff_hunks(
21144 executor: BackgroundExecutor,
21145 cx: &mut TestAppContext,
21146) {
21147 init_test(cx, |_| {});
21148
21149 let mut cx = EditorTestContext::new(cx).await;
21150
21151 let diff_base = r#"
21152 use some::mod1;
21153 use some::mod2;
21154
21155 const A: u32 = 42;
21156 const B: u32 = 42;
21157 const C: u32 = 42;
21158
21159 fn main() {
21160 println!("hello");
21161
21162 println!("world");
21163 }
21164 "#
21165 .unindent();
21166
21167 cx.set_state(
21168 &r#"
21169 use some::mod2;
21170
21171 const A: u32 = 42;
21172 const C: u32 = 42;
21173
21174 fn main(ˇ) {
21175 //println!("hello");
21176
21177 println!("world");
21178 //
21179 //
21180 }
21181 "#
21182 .unindent(),
21183 );
21184
21185 cx.set_head_text(&diff_base);
21186 executor.run_until_parked();
21187
21188 cx.update_editor(|editor, window, cx| {
21189 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21190 });
21191 executor.run_until_parked();
21192 cx.assert_state_with_diff(
21193 r#"
21194 - use some::mod1;
21195 use some::mod2;
21196
21197 const A: u32 = 42;
21198 - const B: u32 = 42;
21199 const C: u32 = 42;
21200
21201 fn main(ˇ) {
21202 - println!("hello");
21203 + //println!("hello");
21204
21205 println!("world");
21206 + //
21207 + //
21208 }
21209 "#
21210 .unindent(),
21211 );
21212
21213 cx.set_head_text("new diff base!");
21214 executor.run_until_parked();
21215 cx.assert_state_with_diff(
21216 r#"
21217 - new diff base!
21218 + use some::mod2;
21219 +
21220 + const A: u32 = 42;
21221 + const C: u32 = 42;
21222 +
21223 + fn main(ˇ) {
21224 + //println!("hello");
21225 +
21226 + println!("world");
21227 + //
21228 + //
21229 + }
21230 "#
21231 .unindent(),
21232 );
21233}
21234
21235#[gpui::test]
21236async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
21237 init_test(cx, |_| {});
21238
21239 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
21240 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
21241 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
21242 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
21243 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
21244 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
21245
21246 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
21247 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
21248 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
21249
21250 let multi_buffer = cx.new(|cx| {
21251 let mut multibuffer = MultiBuffer::new(ReadWrite);
21252 multibuffer.push_excerpts(
21253 buffer_1.clone(),
21254 [
21255 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21256 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21257 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
21258 ],
21259 cx,
21260 );
21261 multibuffer.push_excerpts(
21262 buffer_2.clone(),
21263 [
21264 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21265 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21266 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
21267 ],
21268 cx,
21269 );
21270 multibuffer.push_excerpts(
21271 buffer_3.clone(),
21272 [
21273 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21274 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21275 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
21276 ],
21277 cx,
21278 );
21279 multibuffer
21280 });
21281
21282 let editor =
21283 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
21284 editor
21285 .update(cx, |editor, _window, cx| {
21286 for (buffer, diff_base) in [
21287 (buffer_1.clone(), file_1_old),
21288 (buffer_2.clone(), file_2_old),
21289 (buffer_3.clone(), file_3_old),
21290 ] {
21291 let diff = cx.new(|cx| {
21292 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
21293 });
21294 editor
21295 .buffer
21296 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
21297 }
21298 })
21299 .unwrap();
21300
21301 let mut cx = EditorTestContext::for_editor(editor, cx).await;
21302 cx.run_until_parked();
21303
21304 cx.assert_editor_state(
21305 &"
21306 ˇaaa
21307 ccc
21308 ddd
21309
21310 ggg
21311 hhh
21312
21313
21314 lll
21315 mmm
21316 NNN
21317
21318 qqq
21319 rrr
21320
21321 uuu
21322 111
21323 222
21324 333
21325
21326 666
21327 777
21328
21329 000
21330 !!!"
21331 .unindent(),
21332 );
21333
21334 cx.update_editor(|editor, window, cx| {
21335 editor.select_all(&SelectAll, window, cx);
21336 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21337 });
21338 cx.executor().run_until_parked();
21339
21340 cx.assert_state_with_diff(
21341 "
21342 «aaa
21343 - bbb
21344 ccc
21345 ddd
21346
21347 ggg
21348 hhh
21349
21350
21351 lll
21352 mmm
21353 - nnn
21354 + NNN
21355
21356 qqq
21357 rrr
21358
21359 uuu
21360 111
21361 222
21362 333
21363
21364 + 666
21365 777
21366
21367 000
21368 !!!ˇ»"
21369 .unindent(),
21370 );
21371}
21372
21373#[gpui::test]
21374async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
21375 init_test(cx, |_| {});
21376
21377 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
21378 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
21379
21380 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
21381 let multi_buffer = cx.new(|cx| {
21382 let mut multibuffer = MultiBuffer::new(ReadWrite);
21383 multibuffer.push_excerpts(
21384 buffer.clone(),
21385 [
21386 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
21387 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
21388 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
21389 ],
21390 cx,
21391 );
21392 multibuffer
21393 });
21394
21395 let editor =
21396 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
21397 editor
21398 .update(cx, |editor, _window, cx| {
21399 let diff = cx.new(|cx| {
21400 BufferDiff::new_with_base_text(base, &buffer.read(cx).text_snapshot(), cx)
21401 });
21402 editor
21403 .buffer
21404 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
21405 })
21406 .unwrap();
21407
21408 let mut cx = EditorTestContext::for_editor(editor, cx).await;
21409 cx.run_until_parked();
21410
21411 cx.update_editor(|editor, window, cx| {
21412 editor.expand_all_diff_hunks(&Default::default(), window, cx)
21413 });
21414 cx.executor().run_until_parked();
21415
21416 // When the start of a hunk coincides with the start of its excerpt,
21417 // the hunk is expanded. When the start of a hunk is earlier than
21418 // the start of its excerpt, the hunk is not expanded.
21419 cx.assert_state_with_diff(
21420 "
21421 ˇaaa
21422 - bbb
21423 + BBB
21424
21425 - ddd
21426 - eee
21427 + DDD
21428 + EEE
21429 fff
21430
21431 iii
21432 "
21433 .unindent(),
21434 );
21435}
21436
21437#[gpui::test]
21438async fn test_edits_around_expanded_insertion_hunks(
21439 executor: BackgroundExecutor,
21440 cx: &mut TestAppContext,
21441) {
21442 init_test(cx, |_| {});
21443
21444 let mut cx = EditorTestContext::new(cx).await;
21445
21446 let diff_base = r#"
21447 use some::mod1;
21448 use some::mod2;
21449
21450 const A: u32 = 42;
21451
21452 fn main() {
21453 println!("hello");
21454
21455 println!("world");
21456 }
21457 "#
21458 .unindent();
21459 executor.run_until_parked();
21460 cx.set_state(
21461 &r#"
21462 use some::mod1;
21463 use some::mod2;
21464
21465 const A: u32 = 42;
21466 const B: u32 = 42;
21467 const C: u32 = 42;
21468 ˇ
21469
21470 fn main() {
21471 println!("hello");
21472
21473 println!("world");
21474 }
21475 "#
21476 .unindent(),
21477 );
21478
21479 cx.set_head_text(&diff_base);
21480 executor.run_until_parked();
21481
21482 cx.update_editor(|editor, window, cx| {
21483 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21484 });
21485 executor.run_until_parked();
21486
21487 cx.assert_state_with_diff(
21488 r#"
21489 use some::mod1;
21490 use some::mod2;
21491
21492 const A: u32 = 42;
21493 + const B: u32 = 42;
21494 + const C: u32 = 42;
21495 + ˇ
21496
21497 fn main() {
21498 println!("hello");
21499
21500 println!("world");
21501 }
21502 "#
21503 .unindent(),
21504 );
21505
21506 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
21507 executor.run_until_parked();
21508
21509 cx.assert_state_with_diff(
21510 r#"
21511 use some::mod1;
21512 use some::mod2;
21513
21514 const A: u32 = 42;
21515 + const B: u32 = 42;
21516 + const C: u32 = 42;
21517 + const D: u32 = 42;
21518 + ˇ
21519
21520 fn main() {
21521 println!("hello");
21522
21523 println!("world");
21524 }
21525 "#
21526 .unindent(),
21527 );
21528
21529 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
21530 executor.run_until_parked();
21531
21532 cx.assert_state_with_diff(
21533 r#"
21534 use some::mod1;
21535 use some::mod2;
21536
21537 const A: u32 = 42;
21538 + const B: u32 = 42;
21539 + const C: u32 = 42;
21540 + const D: u32 = 42;
21541 + const E: u32 = 42;
21542 + ˇ
21543
21544 fn main() {
21545 println!("hello");
21546
21547 println!("world");
21548 }
21549 "#
21550 .unindent(),
21551 );
21552
21553 cx.update_editor(|editor, window, cx| {
21554 editor.delete_line(&DeleteLine, window, cx);
21555 });
21556 executor.run_until_parked();
21557
21558 cx.assert_state_with_diff(
21559 r#"
21560 use some::mod1;
21561 use some::mod2;
21562
21563 const A: u32 = 42;
21564 + const B: u32 = 42;
21565 + const C: u32 = 42;
21566 + const D: u32 = 42;
21567 + const E: u32 = 42;
21568 ˇ
21569 fn main() {
21570 println!("hello");
21571
21572 println!("world");
21573 }
21574 "#
21575 .unindent(),
21576 );
21577
21578 cx.update_editor(|editor, window, cx| {
21579 editor.move_up(&MoveUp, window, cx);
21580 editor.delete_line(&DeleteLine, window, cx);
21581 editor.move_up(&MoveUp, window, cx);
21582 editor.delete_line(&DeleteLine, window, cx);
21583 editor.move_up(&MoveUp, window, cx);
21584 editor.delete_line(&DeleteLine, window, cx);
21585 });
21586 executor.run_until_parked();
21587 cx.assert_state_with_diff(
21588 r#"
21589 use some::mod1;
21590 use some::mod2;
21591
21592 const A: u32 = 42;
21593 + const B: u32 = 42;
21594 ˇ
21595 fn main() {
21596 println!("hello");
21597
21598 println!("world");
21599 }
21600 "#
21601 .unindent(),
21602 );
21603
21604 cx.update_editor(|editor, window, cx| {
21605 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
21606 editor.delete_line(&DeleteLine, window, cx);
21607 });
21608 executor.run_until_parked();
21609 cx.assert_state_with_diff(
21610 r#"
21611 ˇ
21612 fn main() {
21613 println!("hello");
21614
21615 println!("world");
21616 }
21617 "#
21618 .unindent(),
21619 );
21620}
21621
21622#[gpui::test]
21623async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
21624 init_test(cx, |_| {});
21625
21626 let mut cx = EditorTestContext::new(cx).await;
21627 cx.set_head_text(indoc! { "
21628 one
21629 two
21630 three
21631 four
21632 five
21633 "
21634 });
21635 cx.set_state(indoc! { "
21636 one
21637 ˇthree
21638 five
21639 "});
21640 cx.run_until_parked();
21641 cx.update_editor(|editor, window, cx| {
21642 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21643 });
21644 cx.assert_state_with_diff(
21645 indoc! { "
21646 one
21647 - two
21648 ˇthree
21649 - four
21650 five
21651 "}
21652 .to_string(),
21653 );
21654 cx.update_editor(|editor, window, cx| {
21655 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21656 });
21657
21658 cx.assert_state_with_diff(
21659 indoc! { "
21660 one
21661 ˇthree
21662 five
21663 "}
21664 .to_string(),
21665 );
21666
21667 cx.update_editor(|editor, window, cx| {
21668 editor.move_up(&MoveUp, window, cx);
21669 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21670 });
21671 cx.assert_state_with_diff(
21672 indoc! { "
21673 ˇone
21674 - two
21675 three
21676 five
21677 "}
21678 .to_string(),
21679 );
21680
21681 cx.update_editor(|editor, window, cx| {
21682 editor.move_down(&MoveDown, window, cx);
21683 editor.move_down(&MoveDown, window, cx);
21684 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21685 });
21686 cx.assert_state_with_diff(
21687 indoc! { "
21688 one
21689 - two
21690 ˇthree
21691 - four
21692 five
21693 "}
21694 .to_string(),
21695 );
21696
21697 cx.set_state(indoc! { "
21698 one
21699 ˇTWO
21700 three
21701 four
21702 five
21703 "});
21704 cx.run_until_parked();
21705 cx.update_editor(|editor, window, cx| {
21706 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21707 });
21708
21709 cx.assert_state_with_diff(
21710 indoc! { "
21711 one
21712 - two
21713 + ˇTWO
21714 three
21715 four
21716 five
21717 "}
21718 .to_string(),
21719 );
21720 cx.update_editor(|editor, window, cx| {
21721 editor.move_up(&Default::default(), window, cx);
21722 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21723 });
21724 cx.assert_state_with_diff(
21725 indoc! { "
21726 one
21727 ˇTWO
21728 three
21729 four
21730 five
21731 "}
21732 .to_string(),
21733 );
21734}
21735
21736#[gpui::test]
21737async fn test_toggling_adjacent_diff_hunks_2(
21738 executor: BackgroundExecutor,
21739 cx: &mut TestAppContext,
21740) {
21741 init_test(cx, |_| {});
21742
21743 let mut cx = EditorTestContext::new(cx).await;
21744
21745 let diff_base = r#"
21746 lineA
21747 lineB
21748 lineC
21749 lineD
21750 "#
21751 .unindent();
21752
21753 cx.set_state(
21754 &r#"
21755 ˇlineA1
21756 lineB
21757 lineD
21758 "#
21759 .unindent(),
21760 );
21761 cx.set_head_text(&diff_base);
21762 executor.run_until_parked();
21763
21764 cx.update_editor(|editor, window, cx| {
21765 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21766 });
21767 executor.run_until_parked();
21768 cx.assert_state_with_diff(
21769 r#"
21770 - lineA
21771 + ˇlineA1
21772 lineB
21773 lineD
21774 "#
21775 .unindent(),
21776 );
21777
21778 cx.update_editor(|editor, window, cx| {
21779 editor.move_down(&MoveDown, window, cx);
21780 editor.move_right(&MoveRight, window, cx);
21781 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21782 });
21783 executor.run_until_parked();
21784 cx.assert_state_with_diff(
21785 r#"
21786 - lineA
21787 + lineA1
21788 lˇineB
21789 - lineC
21790 lineD
21791 "#
21792 .unindent(),
21793 );
21794}
21795
21796#[gpui::test]
21797async fn test_edits_around_expanded_deletion_hunks(
21798 executor: BackgroundExecutor,
21799 cx: &mut TestAppContext,
21800) {
21801 init_test(cx, |_| {});
21802
21803 let mut cx = EditorTestContext::new(cx).await;
21804
21805 let diff_base = r#"
21806 use some::mod1;
21807 use some::mod2;
21808
21809 const A: u32 = 42;
21810 const B: u32 = 42;
21811 const C: u32 = 42;
21812
21813
21814 fn main() {
21815 println!("hello");
21816
21817 println!("world");
21818 }
21819 "#
21820 .unindent();
21821 executor.run_until_parked();
21822 cx.set_state(
21823 &r#"
21824 use some::mod1;
21825 use some::mod2;
21826
21827 ˇconst B: u32 = 42;
21828 const C: u32 = 42;
21829
21830
21831 fn main() {
21832 println!("hello");
21833
21834 println!("world");
21835 }
21836 "#
21837 .unindent(),
21838 );
21839
21840 cx.set_head_text(&diff_base);
21841 executor.run_until_parked();
21842
21843 cx.update_editor(|editor, window, cx| {
21844 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21845 });
21846 executor.run_until_parked();
21847
21848 cx.assert_state_with_diff(
21849 r#"
21850 use some::mod1;
21851 use some::mod2;
21852
21853 - const A: u32 = 42;
21854 ˇconst B: u32 = 42;
21855 const C: u32 = 42;
21856
21857
21858 fn main() {
21859 println!("hello");
21860
21861 println!("world");
21862 }
21863 "#
21864 .unindent(),
21865 );
21866
21867 cx.update_editor(|editor, window, cx| {
21868 editor.delete_line(&DeleteLine, window, cx);
21869 });
21870 executor.run_until_parked();
21871 cx.assert_state_with_diff(
21872 r#"
21873 use some::mod1;
21874 use some::mod2;
21875
21876 - const A: u32 = 42;
21877 - const B: u32 = 42;
21878 ˇconst C: u32 = 42;
21879
21880
21881 fn main() {
21882 println!("hello");
21883
21884 println!("world");
21885 }
21886 "#
21887 .unindent(),
21888 );
21889
21890 cx.update_editor(|editor, window, cx| {
21891 editor.delete_line(&DeleteLine, window, cx);
21892 });
21893 executor.run_until_parked();
21894 cx.assert_state_with_diff(
21895 r#"
21896 use some::mod1;
21897 use some::mod2;
21898
21899 - const A: u32 = 42;
21900 - const B: u32 = 42;
21901 - const C: u32 = 42;
21902 ˇ
21903
21904 fn main() {
21905 println!("hello");
21906
21907 println!("world");
21908 }
21909 "#
21910 .unindent(),
21911 );
21912
21913 cx.update_editor(|editor, window, cx| {
21914 editor.handle_input("replacement", window, cx);
21915 });
21916 executor.run_until_parked();
21917 cx.assert_state_with_diff(
21918 r#"
21919 use some::mod1;
21920 use some::mod2;
21921
21922 - const A: u32 = 42;
21923 - const B: u32 = 42;
21924 - const C: u32 = 42;
21925 -
21926 + replacementˇ
21927
21928 fn main() {
21929 println!("hello");
21930
21931 println!("world");
21932 }
21933 "#
21934 .unindent(),
21935 );
21936}
21937
21938#[gpui::test]
21939async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21940 init_test(cx, |_| {});
21941
21942 let mut cx = EditorTestContext::new(cx).await;
21943
21944 let base_text = r#"
21945 one
21946 two
21947 three
21948 four
21949 five
21950 "#
21951 .unindent();
21952 executor.run_until_parked();
21953 cx.set_state(
21954 &r#"
21955 one
21956 two
21957 fˇour
21958 five
21959 "#
21960 .unindent(),
21961 );
21962
21963 cx.set_head_text(&base_text);
21964 executor.run_until_parked();
21965
21966 cx.update_editor(|editor, window, cx| {
21967 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21968 });
21969 executor.run_until_parked();
21970
21971 cx.assert_state_with_diff(
21972 r#"
21973 one
21974 two
21975 - three
21976 fˇour
21977 five
21978 "#
21979 .unindent(),
21980 );
21981
21982 cx.update_editor(|editor, window, cx| {
21983 editor.backspace(&Backspace, window, cx);
21984 editor.backspace(&Backspace, window, cx);
21985 });
21986 executor.run_until_parked();
21987 cx.assert_state_with_diff(
21988 r#"
21989 one
21990 two
21991 - threeˇ
21992 - four
21993 + our
21994 five
21995 "#
21996 .unindent(),
21997 );
21998}
21999
22000#[gpui::test]
22001async fn test_edit_after_expanded_modification_hunk(
22002 executor: BackgroundExecutor,
22003 cx: &mut TestAppContext,
22004) {
22005 init_test(cx, |_| {});
22006
22007 let mut cx = EditorTestContext::new(cx).await;
22008
22009 let diff_base = r#"
22010 use some::mod1;
22011 use some::mod2;
22012
22013 const A: u32 = 42;
22014 const B: u32 = 42;
22015 const C: u32 = 42;
22016 const D: u32 = 42;
22017
22018
22019 fn main() {
22020 println!("hello");
22021
22022 println!("world");
22023 }"#
22024 .unindent();
22025
22026 cx.set_state(
22027 &r#"
22028 use some::mod1;
22029 use some::mod2;
22030
22031 const A: u32 = 42;
22032 const B: u32 = 42;
22033 const C: u32 = 43ˇ
22034 const D: u32 = 42;
22035
22036
22037 fn main() {
22038 println!("hello");
22039
22040 println!("world");
22041 }"#
22042 .unindent(),
22043 );
22044
22045 cx.set_head_text(&diff_base);
22046 executor.run_until_parked();
22047 cx.update_editor(|editor, window, cx| {
22048 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22049 });
22050 executor.run_until_parked();
22051
22052 cx.assert_state_with_diff(
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 + const C: u32 = 43ˇ
22061 const D: u32 = 42;
22062
22063
22064 fn main() {
22065 println!("hello");
22066
22067 println!("world");
22068 }"#
22069 .unindent(),
22070 );
22071
22072 cx.update_editor(|editor, window, cx| {
22073 editor.handle_input("\nnew_line\n", window, cx);
22074 });
22075 executor.run_until_parked();
22076
22077 cx.assert_state_with_diff(
22078 r#"
22079 use some::mod1;
22080 use some::mod2;
22081
22082 const A: u32 = 42;
22083 const B: u32 = 42;
22084 - const C: u32 = 42;
22085 + const C: u32 = 43
22086 + new_line
22087 + ˇ
22088 const D: u32 = 42;
22089
22090
22091 fn main() {
22092 println!("hello");
22093
22094 println!("world");
22095 }"#
22096 .unindent(),
22097 );
22098}
22099
22100#[gpui::test]
22101async fn test_stage_and_unstage_added_file_hunk(
22102 executor: BackgroundExecutor,
22103 cx: &mut TestAppContext,
22104) {
22105 init_test(cx, |_| {});
22106
22107 let mut cx = EditorTestContext::new(cx).await;
22108 cx.update_editor(|editor, _, cx| {
22109 editor.set_expand_all_diff_hunks(cx);
22110 });
22111
22112 let working_copy = r#"
22113 ˇfn main() {
22114 println!("hello, world!");
22115 }
22116 "#
22117 .unindent();
22118
22119 cx.set_state(&working_copy);
22120 executor.run_until_parked();
22121
22122 cx.assert_state_with_diff(
22123 r#"
22124 + ˇfn main() {
22125 + println!("hello, world!");
22126 + }
22127 "#
22128 .unindent(),
22129 );
22130 cx.assert_index_text(None);
22131
22132 cx.update_editor(|editor, window, cx| {
22133 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22134 });
22135 executor.run_until_parked();
22136 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
22137 cx.assert_state_with_diff(
22138 r#"
22139 + ˇfn main() {
22140 + println!("hello, world!");
22141 + }
22142 "#
22143 .unindent(),
22144 );
22145
22146 cx.update_editor(|editor, window, cx| {
22147 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22148 });
22149 executor.run_until_parked();
22150 cx.assert_index_text(None);
22151}
22152
22153async fn setup_indent_guides_editor(
22154 text: &str,
22155 cx: &mut TestAppContext,
22156) -> (BufferId, EditorTestContext) {
22157 init_test(cx, |_| {});
22158
22159 let mut cx = EditorTestContext::new(cx).await;
22160
22161 let buffer_id = cx.update_editor(|editor, window, cx| {
22162 editor.set_text(text, window, cx);
22163 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
22164
22165 buffer_ids[0]
22166 });
22167
22168 (buffer_id, cx)
22169}
22170
22171fn assert_indent_guides(
22172 range: Range<u32>,
22173 expected: Vec<IndentGuide>,
22174 active_indices: Option<Vec<usize>>,
22175 cx: &mut EditorTestContext,
22176) {
22177 let indent_guides = cx.update_editor(|editor, window, cx| {
22178 let snapshot = editor.snapshot(window, cx).display_snapshot;
22179 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
22180 editor,
22181 MultiBufferRow(range.start)..MultiBufferRow(range.end),
22182 true,
22183 &snapshot,
22184 cx,
22185 );
22186
22187 indent_guides.sort_by(|a, b| {
22188 a.depth.cmp(&b.depth).then(
22189 a.start_row
22190 .cmp(&b.start_row)
22191 .then(a.end_row.cmp(&b.end_row)),
22192 )
22193 });
22194 indent_guides
22195 });
22196
22197 if let Some(expected) = active_indices {
22198 let active_indices = cx.update_editor(|editor, window, cx| {
22199 let snapshot = editor.snapshot(window, cx).display_snapshot;
22200 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
22201 });
22202
22203 assert_eq!(
22204 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
22205 expected,
22206 "Active indent guide indices do not match"
22207 );
22208 }
22209
22210 assert_eq!(indent_guides, expected, "Indent guides do not match");
22211}
22212
22213fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
22214 IndentGuide {
22215 buffer_id,
22216 start_row: MultiBufferRow(start_row),
22217 end_row: MultiBufferRow(end_row),
22218 depth,
22219 tab_size: 4,
22220 settings: IndentGuideSettings {
22221 enabled: true,
22222 line_width: 1,
22223 active_line_width: 1,
22224 coloring: IndentGuideColoring::default(),
22225 background_coloring: IndentGuideBackgroundColoring::default(),
22226 },
22227 }
22228}
22229
22230#[gpui::test]
22231async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
22232 let (buffer_id, mut cx) = setup_indent_guides_editor(
22233 &"
22234 fn main() {
22235 let a = 1;
22236 }"
22237 .unindent(),
22238 cx,
22239 )
22240 .await;
22241
22242 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
22243}
22244
22245#[gpui::test]
22246async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
22247 let (buffer_id, mut cx) = setup_indent_guides_editor(
22248 &"
22249 fn main() {
22250 let a = 1;
22251 let b = 2;
22252 }"
22253 .unindent(),
22254 cx,
22255 )
22256 .await;
22257
22258 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
22259}
22260
22261#[gpui::test]
22262async fn test_indent_guide_nested(cx: &mut TestAppContext) {
22263 let (buffer_id, mut cx) = setup_indent_guides_editor(
22264 &"
22265 fn main() {
22266 let a = 1;
22267 if a == 3 {
22268 let b = 2;
22269 } else {
22270 let c = 3;
22271 }
22272 }"
22273 .unindent(),
22274 cx,
22275 )
22276 .await;
22277
22278 assert_indent_guides(
22279 0..8,
22280 vec![
22281 indent_guide(buffer_id, 1, 6, 0),
22282 indent_guide(buffer_id, 3, 3, 1),
22283 indent_guide(buffer_id, 5, 5, 1),
22284 ],
22285 None,
22286 &mut cx,
22287 );
22288}
22289
22290#[gpui::test]
22291async fn test_indent_guide_tab(cx: &mut TestAppContext) {
22292 let (buffer_id, mut cx) = setup_indent_guides_editor(
22293 &"
22294 fn main() {
22295 let a = 1;
22296 let b = 2;
22297 let c = 3;
22298 }"
22299 .unindent(),
22300 cx,
22301 )
22302 .await;
22303
22304 assert_indent_guides(
22305 0..5,
22306 vec![
22307 indent_guide(buffer_id, 1, 3, 0),
22308 indent_guide(buffer_id, 2, 2, 1),
22309 ],
22310 None,
22311 &mut cx,
22312 );
22313}
22314
22315#[gpui::test]
22316async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
22317 let (buffer_id, mut cx) = setup_indent_guides_editor(
22318 &"
22319 fn main() {
22320 let a = 1;
22321
22322 let c = 3;
22323 }"
22324 .unindent(),
22325 cx,
22326 )
22327 .await;
22328
22329 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
22330}
22331
22332#[gpui::test]
22333async fn test_indent_guide_complex(cx: &mut TestAppContext) {
22334 let (buffer_id, mut cx) = setup_indent_guides_editor(
22335 &"
22336 fn main() {
22337 let a = 1;
22338
22339 let c = 3;
22340
22341 if a == 3 {
22342 let b = 2;
22343 } else {
22344 let c = 3;
22345 }
22346 }"
22347 .unindent(),
22348 cx,
22349 )
22350 .await;
22351
22352 assert_indent_guides(
22353 0..11,
22354 vec![
22355 indent_guide(buffer_id, 1, 9, 0),
22356 indent_guide(buffer_id, 6, 6, 1),
22357 indent_guide(buffer_id, 8, 8, 1),
22358 ],
22359 None,
22360 &mut cx,
22361 );
22362}
22363
22364#[gpui::test]
22365async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
22366 let (buffer_id, mut cx) = setup_indent_guides_editor(
22367 &"
22368 fn main() {
22369 let a = 1;
22370
22371 let c = 3;
22372
22373 if a == 3 {
22374 let b = 2;
22375 } else {
22376 let c = 3;
22377 }
22378 }"
22379 .unindent(),
22380 cx,
22381 )
22382 .await;
22383
22384 assert_indent_guides(
22385 1..11,
22386 vec![
22387 indent_guide(buffer_id, 1, 9, 0),
22388 indent_guide(buffer_id, 6, 6, 1),
22389 indent_guide(buffer_id, 8, 8, 1),
22390 ],
22391 None,
22392 &mut cx,
22393 );
22394}
22395
22396#[gpui::test]
22397async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
22398 let (buffer_id, mut cx) = setup_indent_guides_editor(
22399 &"
22400 fn main() {
22401 let a = 1;
22402
22403 let c = 3;
22404
22405 if a == 3 {
22406 let b = 2;
22407 } else {
22408 let c = 3;
22409 }
22410 }"
22411 .unindent(),
22412 cx,
22413 )
22414 .await;
22415
22416 assert_indent_guides(
22417 1..10,
22418 vec![
22419 indent_guide(buffer_id, 1, 9, 0),
22420 indent_guide(buffer_id, 6, 6, 1),
22421 indent_guide(buffer_id, 8, 8, 1),
22422 ],
22423 None,
22424 &mut cx,
22425 );
22426}
22427
22428#[gpui::test]
22429async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
22430 let (buffer_id, mut cx) = setup_indent_guides_editor(
22431 &"
22432 fn main() {
22433 if a {
22434 b(
22435 c,
22436 d,
22437 )
22438 } else {
22439 e(
22440 f
22441 )
22442 }
22443 }"
22444 .unindent(),
22445 cx,
22446 )
22447 .await;
22448
22449 assert_indent_guides(
22450 0..11,
22451 vec![
22452 indent_guide(buffer_id, 1, 10, 0),
22453 indent_guide(buffer_id, 2, 5, 1),
22454 indent_guide(buffer_id, 7, 9, 1),
22455 indent_guide(buffer_id, 3, 4, 2),
22456 indent_guide(buffer_id, 8, 8, 2),
22457 ],
22458 None,
22459 &mut cx,
22460 );
22461
22462 cx.update_editor(|editor, window, cx| {
22463 editor.fold_at(MultiBufferRow(2), window, cx);
22464 assert_eq!(
22465 editor.display_text(cx),
22466 "
22467 fn main() {
22468 if a {
22469 b(⋯
22470 )
22471 } else {
22472 e(
22473 f
22474 )
22475 }
22476 }"
22477 .unindent()
22478 );
22479 });
22480
22481 assert_indent_guides(
22482 0..11,
22483 vec![
22484 indent_guide(buffer_id, 1, 10, 0),
22485 indent_guide(buffer_id, 2, 5, 1),
22486 indent_guide(buffer_id, 7, 9, 1),
22487 indent_guide(buffer_id, 8, 8, 2),
22488 ],
22489 None,
22490 &mut cx,
22491 );
22492}
22493
22494#[gpui::test]
22495async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
22496 let (buffer_id, mut cx) = setup_indent_guides_editor(
22497 &"
22498 block1
22499 block2
22500 block3
22501 block4
22502 block2
22503 block1
22504 block1"
22505 .unindent(),
22506 cx,
22507 )
22508 .await;
22509
22510 assert_indent_guides(
22511 1..10,
22512 vec![
22513 indent_guide(buffer_id, 1, 4, 0),
22514 indent_guide(buffer_id, 2, 3, 1),
22515 indent_guide(buffer_id, 3, 3, 2),
22516 ],
22517 None,
22518 &mut cx,
22519 );
22520}
22521
22522#[gpui::test]
22523async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
22524 let (buffer_id, mut cx) = setup_indent_guides_editor(
22525 &"
22526 block1
22527 block2
22528 block3
22529
22530 block1
22531 block1"
22532 .unindent(),
22533 cx,
22534 )
22535 .await;
22536
22537 assert_indent_guides(
22538 0..6,
22539 vec![
22540 indent_guide(buffer_id, 1, 2, 0),
22541 indent_guide(buffer_id, 2, 2, 1),
22542 ],
22543 None,
22544 &mut cx,
22545 );
22546}
22547
22548#[gpui::test]
22549async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
22550 let (buffer_id, mut cx) = setup_indent_guides_editor(
22551 &"
22552 function component() {
22553 \treturn (
22554 \t\t\t
22555 \t\t<div>
22556 \t\t\t<abc></abc>
22557 \t\t</div>
22558 \t)
22559 }"
22560 .unindent(),
22561 cx,
22562 )
22563 .await;
22564
22565 assert_indent_guides(
22566 0..8,
22567 vec![
22568 indent_guide(buffer_id, 1, 6, 0),
22569 indent_guide(buffer_id, 2, 5, 1),
22570 indent_guide(buffer_id, 4, 4, 2),
22571 ],
22572 None,
22573 &mut cx,
22574 );
22575}
22576
22577#[gpui::test]
22578async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
22579 let (buffer_id, mut cx) = setup_indent_guides_editor(
22580 &"
22581 function component() {
22582 \treturn (
22583 \t
22584 \t\t<div>
22585 \t\t\t<abc></abc>
22586 \t\t</div>
22587 \t)
22588 }"
22589 .unindent(),
22590 cx,
22591 )
22592 .await;
22593
22594 assert_indent_guides(
22595 0..8,
22596 vec![
22597 indent_guide(buffer_id, 1, 6, 0),
22598 indent_guide(buffer_id, 2, 5, 1),
22599 indent_guide(buffer_id, 4, 4, 2),
22600 ],
22601 None,
22602 &mut cx,
22603 );
22604}
22605
22606#[gpui::test]
22607async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
22608 let (buffer_id, mut cx) = setup_indent_guides_editor(
22609 &"
22610 block1
22611
22612
22613
22614 block2
22615 "
22616 .unindent(),
22617 cx,
22618 )
22619 .await;
22620
22621 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
22622}
22623
22624#[gpui::test]
22625async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
22626 let (buffer_id, mut cx) = setup_indent_guides_editor(
22627 &"
22628 def a:
22629 \tb = 3
22630 \tif True:
22631 \t\tc = 4
22632 \t\td = 5
22633 \tprint(b)
22634 "
22635 .unindent(),
22636 cx,
22637 )
22638 .await;
22639
22640 assert_indent_guides(
22641 0..6,
22642 vec![
22643 indent_guide(buffer_id, 1, 5, 0),
22644 indent_guide(buffer_id, 3, 4, 1),
22645 ],
22646 None,
22647 &mut cx,
22648 );
22649}
22650
22651#[gpui::test]
22652async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
22653 let (buffer_id, mut cx) = setup_indent_guides_editor(
22654 &"
22655 fn main() {
22656 let a = 1;
22657 }"
22658 .unindent(),
22659 cx,
22660 )
22661 .await;
22662
22663 cx.update_editor(|editor, window, cx| {
22664 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22665 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22666 });
22667 });
22668
22669 assert_indent_guides(
22670 0..3,
22671 vec![indent_guide(buffer_id, 1, 1, 0)],
22672 Some(vec![0]),
22673 &mut cx,
22674 );
22675}
22676
22677#[gpui::test]
22678async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
22679 let (buffer_id, mut cx) = setup_indent_guides_editor(
22680 &"
22681 fn main() {
22682 if 1 == 2 {
22683 let a = 1;
22684 }
22685 }"
22686 .unindent(),
22687 cx,
22688 )
22689 .await;
22690
22691 cx.update_editor(|editor, window, cx| {
22692 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22693 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22694 });
22695 });
22696 cx.run_until_parked();
22697
22698 assert_indent_guides(
22699 0..4,
22700 vec![
22701 indent_guide(buffer_id, 1, 3, 0),
22702 indent_guide(buffer_id, 2, 2, 1),
22703 ],
22704 Some(vec![1]),
22705 &mut cx,
22706 );
22707
22708 cx.update_editor(|editor, window, cx| {
22709 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22710 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
22711 });
22712 });
22713 cx.run_until_parked();
22714
22715 assert_indent_guides(
22716 0..4,
22717 vec![
22718 indent_guide(buffer_id, 1, 3, 0),
22719 indent_guide(buffer_id, 2, 2, 1),
22720 ],
22721 Some(vec![1]),
22722 &mut cx,
22723 );
22724
22725 cx.update_editor(|editor, window, cx| {
22726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22727 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
22728 });
22729 });
22730 cx.run_until_parked();
22731
22732 assert_indent_guides(
22733 0..4,
22734 vec![
22735 indent_guide(buffer_id, 1, 3, 0),
22736 indent_guide(buffer_id, 2, 2, 1),
22737 ],
22738 Some(vec![0]),
22739 &mut cx,
22740 );
22741}
22742
22743#[gpui::test]
22744async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
22745 let (buffer_id, mut cx) = setup_indent_guides_editor(
22746 &"
22747 fn main() {
22748 let a = 1;
22749
22750 let b = 2;
22751 }"
22752 .unindent(),
22753 cx,
22754 )
22755 .await;
22756
22757 cx.update_editor(|editor, window, cx| {
22758 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22759 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
22760 });
22761 });
22762
22763 assert_indent_guides(
22764 0..5,
22765 vec![indent_guide(buffer_id, 1, 3, 0)],
22766 Some(vec![0]),
22767 &mut cx,
22768 );
22769}
22770
22771#[gpui::test]
22772async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
22773 let (buffer_id, mut cx) = setup_indent_guides_editor(
22774 &"
22775 def m:
22776 a = 1
22777 pass"
22778 .unindent(),
22779 cx,
22780 )
22781 .await;
22782
22783 cx.update_editor(|editor, window, cx| {
22784 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22785 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22786 });
22787 });
22788
22789 assert_indent_guides(
22790 0..3,
22791 vec![indent_guide(buffer_id, 1, 2, 0)],
22792 Some(vec![0]),
22793 &mut cx,
22794 );
22795}
22796
22797#[gpui::test]
22798async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
22799 init_test(cx, |_| {});
22800 let mut cx = EditorTestContext::new(cx).await;
22801 let text = indoc! {
22802 "
22803 impl A {
22804 fn b() {
22805 0;
22806 3;
22807 5;
22808 6;
22809 7;
22810 }
22811 }
22812 "
22813 };
22814 let base_text = indoc! {
22815 "
22816 impl A {
22817 fn b() {
22818 0;
22819 1;
22820 2;
22821 3;
22822 4;
22823 }
22824 fn c() {
22825 5;
22826 6;
22827 7;
22828 }
22829 }
22830 "
22831 };
22832
22833 cx.update_editor(|editor, window, cx| {
22834 editor.set_text(text, window, cx);
22835
22836 editor.buffer().update(cx, |multibuffer, cx| {
22837 let buffer = multibuffer.as_singleton().unwrap();
22838 let diff = cx.new(|cx| {
22839 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
22840 });
22841
22842 multibuffer.set_all_diff_hunks_expanded(cx);
22843 multibuffer.add_diff(diff, cx);
22844
22845 buffer.read(cx).remote_id()
22846 })
22847 });
22848 cx.run_until_parked();
22849
22850 cx.assert_state_with_diff(
22851 indoc! { "
22852 impl A {
22853 fn b() {
22854 0;
22855 - 1;
22856 - 2;
22857 3;
22858 - 4;
22859 - }
22860 - fn c() {
22861 5;
22862 6;
22863 7;
22864 }
22865 }
22866 ˇ"
22867 }
22868 .to_string(),
22869 );
22870
22871 let mut actual_guides = cx.update_editor(|editor, window, cx| {
22872 editor
22873 .snapshot(window, cx)
22874 .buffer_snapshot()
22875 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
22876 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
22877 .collect::<Vec<_>>()
22878 });
22879 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
22880 assert_eq!(
22881 actual_guides,
22882 vec![
22883 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
22884 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
22885 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
22886 ]
22887 );
22888}
22889
22890#[gpui::test]
22891async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22892 init_test(cx, |_| {});
22893 let mut cx = EditorTestContext::new(cx).await;
22894
22895 let diff_base = r#"
22896 a
22897 b
22898 c
22899 "#
22900 .unindent();
22901
22902 cx.set_state(
22903 &r#"
22904 ˇA
22905 b
22906 C
22907 "#
22908 .unindent(),
22909 );
22910 cx.set_head_text(&diff_base);
22911 cx.update_editor(|editor, window, cx| {
22912 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22913 });
22914 executor.run_until_parked();
22915
22916 let both_hunks_expanded = r#"
22917 - a
22918 + ˇA
22919 b
22920 - c
22921 + C
22922 "#
22923 .unindent();
22924
22925 cx.assert_state_with_diff(both_hunks_expanded.clone());
22926
22927 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22928 let snapshot = editor.snapshot(window, cx);
22929 let hunks = editor
22930 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22931 .collect::<Vec<_>>();
22932 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22933 hunks
22934 .into_iter()
22935 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22936 .collect::<Vec<_>>()
22937 });
22938 assert_eq!(hunk_ranges.len(), 2);
22939
22940 cx.update_editor(|editor, _, cx| {
22941 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22942 });
22943 executor.run_until_parked();
22944
22945 let second_hunk_expanded = r#"
22946 ˇA
22947 b
22948 - c
22949 + C
22950 "#
22951 .unindent();
22952
22953 cx.assert_state_with_diff(second_hunk_expanded);
22954
22955 cx.update_editor(|editor, _, cx| {
22956 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22957 });
22958 executor.run_until_parked();
22959
22960 cx.assert_state_with_diff(both_hunks_expanded.clone());
22961
22962 cx.update_editor(|editor, _, cx| {
22963 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22964 });
22965 executor.run_until_parked();
22966
22967 let first_hunk_expanded = r#"
22968 - a
22969 + ˇA
22970 b
22971 C
22972 "#
22973 .unindent();
22974
22975 cx.assert_state_with_diff(first_hunk_expanded);
22976
22977 cx.update_editor(|editor, _, cx| {
22978 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22979 });
22980 executor.run_until_parked();
22981
22982 cx.assert_state_with_diff(both_hunks_expanded);
22983
22984 cx.set_state(
22985 &r#"
22986 ˇA
22987 b
22988 "#
22989 .unindent(),
22990 );
22991 cx.run_until_parked();
22992
22993 // TODO this cursor position seems bad
22994 cx.assert_state_with_diff(
22995 r#"
22996 - ˇa
22997 + A
22998 b
22999 "#
23000 .unindent(),
23001 );
23002
23003 cx.update_editor(|editor, window, cx| {
23004 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23005 });
23006
23007 cx.assert_state_with_diff(
23008 r#"
23009 - ˇa
23010 + A
23011 b
23012 - c
23013 "#
23014 .unindent(),
23015 );
23016
23017 let hunk_ranges = cx.update_editor(|editor, window, cx| {
23018 let snapshot = editor.snapshot(window, cx);
23019 let hunks = editor
23020 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
23021 .collect::<Vec<_>>();
23022 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
23023 hunks
23024 .into_iter()
23025 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
23026 .collect::<Vec<_>>()
23027 });
23028 assert_eq!(hunk_ranges.len(), 2);
23029
23030 cx.update_editor(|editor, _, cx| {
23031 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
23032 });
23033 executor.run_until_parked();
23034
23035 cx.assert_state_with_diff(
23036 r#"
23037 - ˇa
23038 + A
23039 b
23040 "#
23041 .unindent(),
23042 );
23043}
23044
23045#[gpui::test]
23046async fn test_toggle_deletion_hunk_at_start_of_file(
23047 executor: BackgroundExecutor,
23048 cx: &mut TestAppContext,
23049) {
23050 init_test(cx, |_| {});
23051 let mut cx = EditorTestContext::new(cx).await;
23052
23053 let diff_base = r#"
23054 a
23055 b
23056 c
23057 "#
23058 .unindent();
23059
23060 cx.set_state(
23061 &r#"
23062 ˇb
23063 c
23064 "#
23065 .unindent(),
23066 );
23067 cx.set_head_text(&diff_base);
23068 cx.update_editor(|editor, window, cx| {
23069 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
23070 });
23071 executor.run_until_parked();
23072
23073 let hunk_expanded = r#"
23074 - a
23075 ˇb
23076 c
23077 "#
23078 .unindent();
23079
23080 cx.assert_state_with_diff(hunk_expanded.clone());
23081
23082 let hunk_ranges = cx.update_editor(|editor, window, cx| {
23083 let snapshot = editor.snapshot(window, cx);
23084 let hunks = editor
23085 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
23086 .collect::<Vec<_>>();
23087 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
23088 hunks
23089 .into_iter()
23090 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
23091 .collect::<Vec<_>>()
23092 });
23093 assert_eq!(hunk_ranges.len(), 1);
23094
23095 cx.update_editor(|editor, _, cx| {
23096 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
23097 });
23098 executor.run_until_parked();
23099
23100 let hunk_collapsed = r#"
23101 ˇb
23102 c
23103 "#
23104 .unindent();
23105
23106 cx.assert_state_with_diff(hunk_collapsed);
23107
23108 cx.update_editor(|editor, _, cx| {
23109 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
23110 });
23111 executor.run_until_parked();
23112
23113 cx.assert_state_with_diff(hunk_expanded);
23114}
23115
23116#[gpui::test]
23117async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
23118 executor: BackgroundExecutor,
23119 cx: &mut TestAppContext,
23120) {
23121 init_test(cx, |_| {});
23122 let mut cx = EditorTestContext::new(cx).await;
23123
23124 cx.set_state("ˇnew\nsecond\nthird\n");
23125 cx.set_head_text("old\nsecond\nthird\n");
23126 cx.update_editor(|editor, window, cx| {
23127 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
23128 });
23129 executor.run_until_parked();
23130 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
23131
23132 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
23133 cx.update_editor(|editor, window, cx| {
23134 let snapshot = editor.snapshot(window, cx);
23135 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
23136 let hunks = editor
23137 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
23138 .collect::<Vec<_>>();
23139 assert_eq!(hunks.len(), 1);
23140 let hunk_range = Anchor::range_in_buffer(excerpt_id, hunks[0].buffer_range.clone());
23141 editor.toggle_single_diff_hunk(hunk_range, cx)
23142 });
23143 executor.run_until_parked();
23144 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
23145
23146 // Keep the editor scrolled to the top so the full hunk remains visible.
23147 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
23148}
23149
23150#[gpui::test]
23151async fn test_display_diff_hunks(cx: &mut TestAppContext) {
23152 init_test(cx, |_| {});
23153
23154 let fs = FakeFs::new(cx.executor());
23155 fs.insert_tree(
23156 path!("/test"),
23157 json!({
23158 ".git": {},
23159 "file-1": "ONE\n",
23160 "file-2": "TWO\n",
23161 "file-3": "THREE\n",
23162 }),
23163 )
23164 .await;
23165
23166 fs.set_head_for_repo(
23167 path!("/test/.git").as_ref(),
23168 &[
23169 ("file-1", "one\n".into()),
23170 ("file-2", "two\n".into()),
23171 ("file-3", "three\n".into()),
23172 ],
23173 "deadbeef",
23174 );
23175
23176 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
23177 let mut buffers = vec![];
23178 for i in 1..=3 {
23179 let buffer = project
23180 .update(cx, |project, cx| {
23181 let path = format!(path!("/test/file-{}"), i);
23182 project.open_local_buffer(path, cx)
23183 })
23184 .await
23185 .unwrap();
23186 buffers.push(buffer);
23187 }
23188
23189 let multibuffer = cx.new(|cx| {
23190 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
23191 multibuffer.set_all_diff_hunks_expanded(cx);
23192 for buffer in &buffers {
23193 let snapshot = buffer.read(cx).snapshot();
23194 multibuffer.set_excerpts_for_path(
23195 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
23196 buffer.clone(),
23197 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
23198 2,
23199 cx,
23200 );
23201 }
23202 multibuffer
23203 });
23204
23205 let editor = cx.add_window(|window, cx| {
23206 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
23207 });
23208 cx.run_until_parked();
23209
23210 let snapshot = editor
23211 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
23212 .unwrap();
23213 let hunks = snapshot
23214 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
23215 .map(|hunk| match hunk {
23216 DisplayDiffHunk::Unfolded {
23217 display_row_range, ..
23218 } => display_row_range,
23219 DisplayDiffHunk::Folded { .. } => unreachable!(),
23220 })
23221 .collect::<Vec<_>>();
23222 assert_eq!(
23223 hunks,
23224 [
23225 DisplayRow(2)..DisplayRow(4),
23226 DisplayRow(7)..DisplayRow(9),
23227 DisplayRow(12)..DisplayRow(14),
23228 ]
23229 );
23230}
23231
23232#[gpui::test]
23233async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
23234 init_test(cx, |_| {});
23235
23236 let mut cx = EditorTestContext::new(cx).await;
23237 cx.set_head_text(indoc! { "
23238 one
23239 two
23240 three
23241 four
23242 five
23243 "
23244 });
23245 cx.set_index_text(indoc! { "
23246 one
23247 two
23248 three
23249 four
23250 five
23251 "
23252 });
23253 cx.set_state(indoc! {"
23254 one
23255 TWO
23256 ˇTHREE
23257 FOUR
23258 five
23259 "});
23260 cx.run_until_parked();
23261 cx.update_editor(|editor, window, cx| {
23262 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
23263 });
23264 cx.run_until_parked();
23265 cx.assert_index_text(Some(indoc! {"
23266 one
23267 TWO
23268 THREE
23269 FOUR
23270 five
23271 "}));
23272 cx.set_state(indoc! { "
23273 one
23274 TWO
23275 ˇTHREE-HUNDRED
23276 FOUR
23277 five
23278 "});
23279 cx.run_until_parked();
23280 cx.update_editor(|editor, window, cx| {
23281 let snapshot = editor.snapshot(window, cx);
23282 let hunks = editor
23283 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
23284 .collect::<Vec<_>>();
23285 assert_eq!(hunks.len(), 1);
23286 assert_eq!(
23287 hunks[0].status(),
23288 DiffHunkStatus {
23289 kind: DiffHunkStatusKind::Modified,
23290 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
23291 }
23292 );
23293
23294 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
23295 });
23296 cx.run_until_parked();
23297 cx.assert_index_text(Some(indoc! {"
23298 one
23299 TWO
23300 THREE-HUNDRED
23301 FOUR
23302 five
23303 "}));
23304}
23305
23306#[gpui::test]
23307fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
23308 init_test(cx, |_| {});
23309
23310 let editor = cx.add_window(|window, cx| {
23311 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
23312 build_editor(buffer, window, cx)
23313 });
23314
23315 let render_args = Arc::new(Mutex::new(None));
23316 let snapshot = editor
23317 .update(cx, |editor, window, cx| {
23318 let snapshot = editor.buffer().read(cx).snapshot(cx);
23319 let range =
23320 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
23321
23322 struct RenderArgs {
23323 row: MultiBufferRow,
23324 folded: bool,
23325 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
23326 }
23327
23328 let crease = Crease::inline(
23329 range,
23330 FoldPlaceholder::test(),
23331 {
23332 let toggle_callback = render_args.clone();
23333 move |row, folded, callback, _window, _cx| {
23334 *toggle_callback.lock() = Some(RenderArgs {
23335 row,
23336 folded,
23337 callback,
23338 });
23339 div()
23340 }
23341 },
23342 |_row, _folded, _window, _cx| div(),
23343 );
23344
23345 editor.insert_creases(Some(crease), cx);
23346 let snapshot = editor.snapshot(window, cx);
23347 let _div =
23348 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
23349 snapshot
23350 })
23351 .unwrap();
23352
23353 let render_args = render_args.lock().take().unwrap();
23354 assert_eq!(render_args.row, MultiBufferRow(1));
23355 assert!(!render_args.folded);
23356 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
23357
23358 cx.update_window(*editor, |_, window, cx| {
23359 (render_args.callback)(true, window, cx)
23360 })
23361 .unwrap();
23362 let snapshot = editor
23363 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
23364 .unwrap();
23365 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
23366
23367 cx.update_window(*editor, |_, window, cx| {
23368 (render_args.callback)(false, window, cx)
23369 })
23370 .unwrap();
23371 let snapshot = editor
23372 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
23373 .unwrap();
23374 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
23375}
23376
23377#[gpui::test]
23378async fn test_input_text(cx: &mut TestAppContext) {
23379 init_test(cx, |_| {});
23380 let mut cx = EditorTestContext::new(cx).await;
23381
23382 cx.set_state(
23383 &r#"ˇone
23384 two
23385
23386 three
23387 fourˇ
23388 five
23389
23390 siˇx"#
23391 .unindent(),
23392 );
23393
23394 cx.dispatch_action(HandleInput(String::new()));
23395 cx.assert_editor_state(
23396 &r#"ˇone
23397 two
23398
23399 three
23400 fourˇ
23401 five
23402
23403 siˇx"#
23404 .unindent(),
23405 );
23406
23407 cx.dispatch_action(HandleInput("AAAA".to_string()));
23408 cx.assert_editor_state(
23409 &r#"AAAAˇone
23410 two
23411
23412 three
23413 fourAAAAˇ
23414 five
23415
23416 siAAAAˇx"#
23417 .unindent(),
23418 );
23419}
23420
23421#[gpui::test]
23422async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
23423 init_test(cx, |_| {});
23424
23425 let mut cx = EditorTestContext::new(cx).await;
23426 cx.set_state(
23427 r#"let foo = 1;
23428let foo = 2;
23429let foo = 3;
23430let fooˇ = 4;
23431let foo = 5;
23432let foo = 6;
23433let foo = 7;
23434let foo = 8;
23435let foo = 9;
23436let foo = 10;
23437let foo = 11;
23438let foo = 12;
23439let foo = 13;
23440let foo = 14;
23441let foo = 15;"#,
23442 );
23443
23444 cx.update_editor(|e, window, cx| {
23445 assert_eq!(
23446 e.next_scroll_position,
23447 NextScrollCursorCenterTopBottom::Center,
23448 "Default next scroll direction is center",
23449 );
23450
23451 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
23452 assert_eq!(
23453 e.next_scroll_position,
23454 NextScrollCursorCenterTopBottom::Top,
23455 "After center, next scroll direction should be top",
23456 );
23457
23458 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
23459 assert_eq!(
23460 e.next_scroll_position,
23461 NextScrollCursorCenterTopBottom::Bottom,
23462 "After top, next scroll direction should be bottom",
23463 );
23464
23465 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
23466 assert_eq!(
23467 e.next_scroll_position,
23468 NextScrollCursorCenterTopBottom::Center,
23469 "After bottom, scrolling should start over",
23470 );
23471
23472 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
23473 assert_eq!(
23474 e.next_scroll_position,
23475 NextScrollCursorCenterTopBottom::Top,
23476 "Scrolling continues if retriggered fast enough"
23477 );
23478 });
23479
23480 cx.executor()
23481 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
23482 cx.executor().run_until_parked();
23483 cx.update_editor(|e, _, _| {
23484 assert_eq!(
23485 e.next_scroll_position,
23486 NextScrollCursorCenterTopBottom::Center,
23487 "If scrolling is not triggered fast enough, it should reset"
23488 );
23489 });
23490}
23491
23492#[gpui::test]
23493async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
23494 init_test(cx, |_| {});
23495 let mut cx = EditorLspTestContext::new_rust(
23496 lsp::ServerCapabilities {
23497 definition_provider: Some(lsp::OneOf::Left(true)),
23498 references_provider: Some(lsp::OneOf::Left(true)),
23499 ..lsp::ServerCapabilities::default()
23500 },
23501 cx,
23502 )
23503 .await;
23504
23505 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
23506 let go_to_definition = cx
23507 .lsp
23508 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
23509 move |params, _| async move {
23510 if empty_go_to_definition {
23511 Ok(None)
23512 } else {
23513 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
23514 uri: params.text_document_position_params.text_document.uri,
23515 range: lsp::Range::new(
23516 lsp::Position::new(4, 3),
23517 lsp::Position::new(4, 6),
23518 ),
23519 })))
23520 }
23521 },
23522 );
23523 let references = cx
23524 .lsp
23525 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23526 Ok(Some(vec![lsp::Location {
23527 uri: params.text_document_position.text_document.uri,
23528 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
23529 }]))
23530 });
23531 (go_to_definition, references)
23532 };
23533
23534 cx.set_state(
23535 &r#"fn one() {
23536 let mut a = ˇtwo();
23537 }
23538
23539 fn two() {}"#
23540 .unindent(),
23541 );
23542 set_up_lsp_handlers(false, &mut cx);
23543 let navigated = cx
23544 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
23545 .await
23546 .expect("Failed to navigate to definition");
23547 assert_eq!(
23548 navigated,
23549 Navigated::Yes,
23550 "Should have navigated to definition from the GetDefinition response"
23551 );
23552 cx.assert_editor_state(
23553 &r#"fn one() {
23554 let mut a = two();
23555 }
23556
23557 fn «twoˇ»() {}"#
23558 .unindent(),
23559 );
23560
23561 let editors = cx.update_workspace(|workspace, _, cx| {
23562 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23563 });
23564 cx.update_editor(|_, _, test_editor_cx| {
23565 assert_eq!(
23566 editors.len(),
23567 1,
23568 "Initially, only one, test, editor should be open in the workspace"
23569 );
23570 assert_eq!(
23571 test_editor_cx.entity(),
23572 editors.last().expect("Asserted len is 1").clone()
23573 );
23574 });
23575
23576 set_up_lsp_handlers(true, &mut cx);
23577 let navigated = cx
23578 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
23579 .await
23580 .expect("Failed to navigate to lookup references");
23581 assert_eq!(
23582 navigated,
23583 Navigated::Yes,
23584 "Should have navigated to references as a fallback after empty GoToDefinition response"
23585 );
23586 // We should not change the selections in the existing file,
23587 // if opening another milti buffer with the references
23588 cx.assert_editor_state(
23589 &r#"fn one() {
23590 let mut a = two();
23591 }
23592
23593 fn «twoˇ»() {}"#
23594 .unindent(),
23595 );
23596 let editors = cx.update_workspace(|workspace, _, cx| {
23597 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23598 });
23599 cx.update_editor(|_, _, test_editor_cx| {
23600 assert_eq!(
23601 editors.len(),
23602 2,
23603 "After falling back to references search, we open a new editor with the results"
23604 );
23605 let references_fallback_text = editors
23606 .into_iter()
23607 .find(|new_editor| *new_editor != test_editor_cx.entity())
23608 .expect("Should have one non-test editor now")
23609 .read(test_editor_cx)
23610 .text(test_editor_cx);
23611 assert_eq!(
23612 references_fallback_text, "fn one() {\n let mut a = two();\n}",
23613 "Should use the range from the references response and not the GoToDefinition one"
23614 );
23615 });
23616}
23617
23618#[gpui::test]
23619async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
23620 init_test(cx, |_| {});
23621 cx.update(|cx| {
23622 let mut editor_settings = EditorSettings::get_global(cx).clone();
23623 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
23624 EditorSettings::override_global(editor_settings, cx);
23625 });
23626 let mut cx = EditorLspTestContext::new_rust(
23627 lsp::ServerCapabilities {
23628 definition_provider: Some(lsp::OneOf::Left(true)),
23629 references_provider: Some(lsp::OneOf::Left(true)),
23630 ..lsp::ServerCapabilities::default()
23631 },
23632 cx,
23633 )
23634 .await;
23635 let original_state = r#"fn one() {
23636 let mut a = ˇtwo();
23637 }
23638
23639 fn two() {}"#
23640 .unindent();
23641 cx.set_state(&original_state);
23642
23643 let mut go_to_definition = cx
23644 .lsp
23645 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
23646 move |_, _| async move { Ok(None) },
23647 );
23648 let _references = cx
23649 .lsp
23650 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
23651 panic!("Should not call for references with no go to definition fallback")
23652 });
23653
23654 let navigated = cx
23655 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
23656 .await
23657 .expect("Failed to navigate to lookup references");
23658 go_to_definition
23659 .next()
23660 .await
23661 .expect("Should have called the go_to_definition handler");
23662
23663 assert_eq!(
23664 navigated,
23665 Navigated::No,
23666 "Should have navigated to references as a fallback after empty GoToDefinition response"
23667 );
23668 cx.assert_editor_state(&original_state);
23669 let editors = cx.update_workspace(|workspace, _, cx| {
23670 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23671 });
23672 cx.update_editor(|_, _, _| {
23673 assert_eq!(
23674 editors.len(),
23675 1,
23676 "After unsuccessful fallback, no other editor should have been opened"
23677 );
23678 });
23679}
23680
23681#[gpui::test]
23682async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
23683 init_test(cx, |_| {});
23684 let mut cx = EditorLspTestContext::new_rust(
23685 lsp::ServerCapabilities {
23686 references_provider: Some(lsp::OneOf::Left(true)),
23687 ..lsp::ServerCapabilities::default()
23688 },
23689 cx,
23690 )
23691 .await;
23692
23693 cx.set_state(
23694 &r#"
23695 fn one() {
23696 let mut a = two();
23697 }
23698
23699 fn ˇtwo() {}"#
23700 .unindent(),
23701 );
23702 cx.lsp
23703 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23704 Ok(Some(vec![
23705 lsp::Location {
23706 uri: params.text_document_position.text_document.uri.clone(),
23707 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
23708 },
23709 lsp::Location {
23710 uri: params.text_document_position.text_document.uri,
23711 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
23712 },
23713 ]))
23714 });
23715 let navigated = cx
23716 .update_editor(|editor, window, cx| {
23717 editor.find_all_references(&FindAllReferences::default(), window, cx)
23718 })
23719 .unwrap()
23720 .await
23721 .expect("Failed to navigate to references");
23722 assert_eq!(
23723 navigated,
23724 Navigated::Yes,
23725 "Should have navigated to references from the FindAllReferences response"
23726 );
23727 cx.assert_editor_state(
23728 &r#"fn one() {
23729 let mut a = two();
23730 }
23731
23732 fn ˇtwo() {}"#
23733 .unindent(),
23734 );
23735
23736 let editors = cx.update_workspace(|workspace, _, cx| {
23737 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23738 });
23739 cx.update_editor(|_, _, _| {
23740 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
23741 });
23742
23743 cx.set_state(
23744 &r#"fn one() {
23745 let mut a = ˇtwo();
23746 }
23747
23748 fn two() {}"#
23749 .unindent(),
23750 );
23751 let navigated = cx
23752 .update_editor(|editor, window, cx| {
23753 editor.find_all_references(&FindAllReferences::default(), window, cx)
23754 })
23755 .unwrap()
23756 .await
23757 .expect("Failed to navigate to references");
23758 assert_eq!(
23759 navigated,
23760 Navigated::Yes,
23761 "Should have navigated to references from the FindAllReferences response"
23762 );
23763 cx.assert_editor_state(
23764 &r#"fn one() {
23765 let mut a = ˇtwo();
23766 }
23767
23768 fn two() {}"#
23769 .unindent(),
23770 );
23771 let editors = cx.update_workspace(|workspace, _, cx| {
23772 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23773 });
23774 cx.update_editor(|_, _, _| {
23775 assert_eq!(
23776 editors.len(),
23777 2,
23778 "should have re-used the previous multibuffer"
23779 );
23780 });
23781
23782 cx.set_state(
23783 &r#"fn one() {
23784 let mut a = ˇtwo();
23785 }
23786 fn three() {}
23787 fn two() {}"#
23788 .unindent(),
23789 );
23790 cx.lsp
23791 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23792 Ok(Some(vec![
23793 lsp::Location {
23794 uri: params.text_document_position.text_document.uri.clone(),
23795 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
23796 },
23797 lsp::Location {
23798 uri: params.text_document_position.text_document.uri,
23799 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
23800 },
23801 ]))
23802 });
23803 let navigated = cx
23804 .update_editor(|editor, window, cx| {
23805 editor.find_all_references(&FindAllReferences::default(), window, cx)
23806 })
23807 .unwrap()
23808 .await
23809 .expect("Failed to navigate to references");
23810 assert_eq!(
23811 navigated,
23812 Navigated::Yes,
23813 "Should have navigated to references from the FindAllReferences response"
23814 );
23815 cx.assert_editor_state(
23816 &r#"fn one() {
23817 let mut a = ˇtwo();
23818 }
23819 fn three() {}
23820 fn two() {}"#
23821 .unindent(),
23822 );
23823 let editors = cx.update_workspace(|workspace, _, cx| {
23824 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23825 });
23826 cx.update_editor(|_, _, _| {
23827 assert_eq!(
23828 editors.len(),
23829 3,
23830 "should have used a new multibuffer as offsets changed"
23831 );
23832 });
23833}
23834#[gpui::test]
23835async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
23836 init_test(cx, |_| {});
23837
23838 let language = Arc::new(Language::new(
23839 LanguageConfig::default(),
23840 Some(tree_sitter_rust::LANGUAGE.into()),
23841 ));
23842
23843 let text = r#"
23844 #[cfg(test)]
23845 mod tests() {
23846 #[test]
23847 fn runnable_1() {
23848 let a = 1;
23849 }
23850
23851 #[test]
23852 fn runnable_2() {
23853 let a = 1;
23854 let b = 2;
23855 }
23856 }
23857 "#
23858 .unindent();
23859
23860 let fs = FakeFs::new(cx.executor());
23861 fs.insert_file("/file.rs", Default::default()).await;
23862
23863 let project = Project::test(fs, ["/a".as_ref()], cx).await;
23864 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
23865 let cx = &mut VisualTestContext::from_window(*window, cx);
23866 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
23867 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
23868
23869 let editor = cx.new_window_entity(|window, cx| {
23870 Editor::new(
23871 EditorMode::full(),
23872 multi_buffer,
23873 Some(project.clone()),
23874 window,
23875 cx,
23876 )
23877 });
23878
23879 editor.update_in(cx, |editor, window, cx| {
23880 let snapshot = editor.buffer().read(cx).snapshot(cx);
23881 editor.tasks.insert(
23882 (buffer.read(cx).remote_id(), 3),
23883 RunnableTasks {
23884 templates: vec![],
23885 offset: snapshot.anchor_before(MultiBufferOffset(43)),
23886 column: 0,
23887 extra_variables: HashMap::default(),
23888 context_range: BufferOffset(43)..BufferOffset(85),
23889 },
23890 );
23891 editor.tasks.insert(
23892 (buffer.read(cx).remote_id(), 8),
23893 RunnableTasks {
23894 templates: vec![],
23895 offset: snapshot.anchor_before(MultiBufferOffset(86)),
23896 column: 0,
23897 extra_variables: HashMap::default(),
23898 context_range: BufferOffset(86)..BufferOffset(191),
23899 },
23900 );
23901
23902 // Test finding task when cursor is inside function body
23903 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23904 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
23905 });
23906 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23907 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
23908
23909 // Test finding task when cursor is on function name
23910 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23911 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
23912 });
23913 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23914 assert_eq!(row, 8, "Should find task when cursor is on function name");
23915 });
23916}
23917
23918#[gpui::test]
23919async fn test_folding_buffers(cx: &mut TestAppContext) {
23920 init_test(cx, |_| {});
23921
23922 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23923 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
23924 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
23925
23926 let fs = FakeFs::new(cx.executor());
23927 fs.insert_tree(
23928 path!("/a"),
23929 json!({
23930 "first.rs": sample_text_1,
23931 "second.rs": sample_text_2,
23932 "third.rs": sample_text_3,
23933 }),
23934 )
23935 .await;
23936 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23937 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
23938 let cx = &mut VisualTestContext::from_window(*window, cx);
23939 let worktree = project.update(cx, |project, cx| {
23940 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23941 assert_eq!(worktrees.len(), 1);
23942 worktrees.pop().unwrap()
23943 });
23944 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23945
23946 let buffer_1 = project
23947 .update(cx, |project, cx| {
23948 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23949 })
23950 .await
23951 .unwrap();
23952 let buffer_2 = project
23953 .update(cx, |project, cx| {
23954 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23955 })
23956 .await
23957 .unwrap();
23958 let buffer_3 = project
23959 .update(cx, |project, cx| {
23960 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23961 })
23962 .await
23963 .unwrap();
23964
23965 let multi_buffer = cx.new(|cx| {
23966 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23967 multi_buffer.push_excerpts(
23968 buffer_1.clone(),
23969 [
23970 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23971 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23972 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23973 ],
23974 cx,
23975 );
23976 multi_buffer.push_excerpts(
23977 buffer_2.clone(),
23978 [
23979 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23980 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23981 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23982 ],
23983 cx,
23984 );
23985 multi_buffer.push_excerpts(
23986 buffer_3.clone(),
23987 [
23988 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23989 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23990 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23991 ],
23992 cx,
23993 );
23994 multi_buffer
23995 });
23996 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23997 Editor::new(
23998 EditorMode::full(),
23999 multi_buffer.clone(),
24000 Some(project.clone()),
24001 window,
24002 cx,
24003 )
24004 });
24005
24006 assert_eq!(
24007 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24008 "\n\naaaa\nbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
24009 );
24010
24011 multi_buffer_editor.update(cx, |editor, cx| {
24012 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
24013 });
24014 assert_eq!(
24015 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24016 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
24017 "After folding the first buffer, its text should not be displayed"
24018 );
24019
24020 multi_buffer_editor.update(cx, |editor, cx| {
24021 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
24022 });
24023 assert_eq!(
24024 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24025 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
24026 "After folding the second buffer, its text should not be displayed"
24027 );
24028
24029 multi_buffer_editor.update(cx, |editor, cx| {
24030 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
24031 });
24032 assert_eq!(
24033 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24034 "\n\n\n\n\n",
24035 "After folding the third buffer, its text should not be displayed"
24036 );
24037
24038 // Emulate selection inside the fold logic, that should work
24039 multi_buffer_editor.update_in(cx, |editor, window, cx| {
24040 editor
24041 .snapshot(window, cx)
24042 .next_line_boundary(Point::new(0, 4));
24043 });
24044
24045 multi_buffer_editor.update(cx, |editor, cx| {
24046 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
24047 });
24048 assert_eq!(
24049 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24050 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
24051 "After unfolding the second buffer, its text should be displayed"
24052 );
24053
24054 // Typing inside of buffer 1 causes that buffer to be unfolded.
24055 multi_buffer_editor.update_in(cx, |editor, window, cx| {
24056 assert_eq!(
24057 multi_buffer
24058 .read(cx)
24059 .snapshot(cx)
24060 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
24061 .collect::<String>(),
24062 "bbbb"
24063 );
24064 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24065 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
24066 });
24067 editor.handle_input("B", window, cx);
24068 });
24069
24070 assert_eq!(
24071 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24072 "\n\naaaa\nBbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
24073 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
24074 );
24075
24076 multi_buffer_editor.update(cx, |editor, cx| {
24077 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
24078 });
24079 assert_eq!(
24080 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24081 "\n\naaaa\nBbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
24082 "After unfolding the all buffers, all original text should be displayed"
24083 );
24084}
24085
24086#[gpui::test]
24087async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
24088 init_test(cx, |_| {});
24089
24090 let sample_text_1 = "1111\n2222\n3333".to_string();
24091 let sample_text_2 = "4444\n5555\n6666".to_string();
24092 let sample_text_3 = "7777\n8888\n9999".to_string();
24093
24094 let fs = FakeFs::new(cx.executor());
24095 fs.insert_tree(
24096 path!("/a"),
24097 json!({
24098 "first.rs": sample_text_1,
24099 "second.rs": sample_text_2,
24100 "third.rs": sample_text_3,
24101 }),
24102 )
24103 .await;
24104 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24105 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
24106 let cx = &mut VisualTestContext::from_window(*window, cx);
24107 let worktree = project.update(cx, |project, cx| {
24108 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
24109 assert_eq!(worktrees.len(), 1);
24110 worktrees.pop().unwrap()
24111 });
24112 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
24113
24114 let buffer_1 = project
24115 .update(cx, |project, cx| {
24116 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
24117 })
24118 .await
24119 .unwrap();
24120 let buffer_2 = project
24121 .update(cx, |project, cx| {
24122 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
24123 })
24124 .await
24125 .unwrap();
24126 let buffer_3 = project
24127 .update(cx, |project, cx| {
24128 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
24129 })
24130 .await
24131 .unwrap();
24132
24133 let multi_buffer = cx.new(|cx| {
24134 let mut multi_buffer = MultiBuffer::new(ReadWrite);
24135 multi_buffer.push_excerpts(
24136 buffer_1.clone(),
24137 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
24138 cx,
24139 );
24140 multi_buffer.push_excerpts(
24141 buffer_2.clone(),
24142 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
24143 cx,
24144 );
24145 multi_buffer.push_excerpts(
24146 buffer_3.clone(),
24147 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
24148 cx,
24149 );
24150 multi_buffer
24151 });
24152
24153 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
24154 Editor::new(
24155 EditorMode::full(),
24156 multi_buffer,
24157 Some(project.clone()),
24158 window,
24159 cx,
24160 )
24161 });
24162
24163 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
24164 assert_eq!(
24165 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24166 full_text,
24167 );
24168
24169 multi_buffer_editor.update(cx, |editor, cx| {
24170 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
24171 });
24172 assert_eq!(
24173 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24174 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
24175 "After folding the first buffer, its text should not be displayed"
24176 );
24177
24178 multi_buffer_editor.update(cx, |editor, cx| {
24179 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
24180 });
24181
24182 assert_eq!(
24183 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24184 "\n\n\n\n\n\n7777\n8888\n9999",
24185 "After folding the second buffer, its text should not be displayed"
24186 );
24187
24188 multi_buffer_editor.update(cx, |editor, cx| {
24189 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
24190 });
24191 assert_eq!(
24192 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24193 "\n\n\n\n\n",
24194 "After folding the third buffer, its text should not be displayed"
24195 );
24196
24197 multi_buffer_editor.update(cx, |editor, cx| {
24198 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
24199 });
24200 assert_eq!(
24201 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24202 "\n\n\n\n4444\n5555\n6666\n\n",
24203 "After unfolding the second buffer, its text should be displayed"
24204 );
24205
24206 multi_buffer_editor.update(cx, |editor, cx| {
24207 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
24208 });
24209 assert_eq!(
24210 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24211 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
24212 "After unfolding the first buffer, its text should be displayed"
24213 );
24214
24215 multi_buffer_editor.update(cx, |editor, cx| {
24216 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
24217 });
24218 assert_eq!(
24219 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24220 full_text,
24221 "After unfolding all buffers, all original text should be displayed"
24222 );
24223}
24224
24225#[gpui::test]
24226async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
24227 init_test(cx, |_| {});
24228
24229 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
24230
24231 let fs = FakeFs::new(cx.executor());
24232 fs.insert_tree(
24233 path!("/a"),
24234 json!({
24235 "main.rs": sample_text,
24236 }),
24237 )
24238 .await;
24239 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24240 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
24241 let cx = &mut VisualTestContext::from_window(*window, cx);
24242 let worktree = project.update(cx, |project, cx| {
24243 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
24244 assert_eq!(worktrees.len(), 1);
24245 worktrees.pop().unwrap()
24246 });
24247 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
24248
24249 let buffer_1 = project
24250 .update(cx, |project, cx| {
24251 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24252 })
24253 .await
24254 .unwrap();
24255
24256 let multi_buffer = cx.new(|cx| {
24257 let mut multi_buffer = MultiBuffer::new(ReadWrite);
24258 multi_buffer.push_excerpts(
24259 buffer_1.clone(),
24260 [ExcerptRange::new(
24261 Point::new(0, 0)
24262 ..Point::new(
24263 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
24264 0,
24265 ),
24266 )],
24267 cx,
24268 );
24269 multi_buffer
24270 });
24271 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
24272 Editor::new(
24273 EditorMode::full(),
24274 multi_buffer,
24275 Some(project.clone()),
24276 window,
24277 cx,
24278 )
24279 });
24280
24281 let selection_range = Point::new(1, 0)..Point::new(2, 0);
24282 multi_buffer_editor.update_in(cx, |editor, window, cx| {
24283 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
24284 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
24285 editor.highlight_text(
24286 HighlightKey::Editor,
24287 vec![highlight_range.clone()],
24288 HighlightStyle::color(Hsla::green()),
24289 cx,
24290 );
24291 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24292 s.select_ranges(Some(highlight_range))
24293 });
24294 });
24295
24296 let full_text = format!("\n\n{sample_text}");
24297 assert_eq!(
24298 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
24299 full_text,
24300 );
24301}
24302
24303#[gpui::test]
24304async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
24305 init_test(cx, |_| {});
24306 cx.update(|cx| {
24307 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
24308 "keymaps/default-linux.json",
24309 cx,
24310 )
24311 .unwrap();
24312 cx.bind_keys(default_key_bindings);
24313 });
24314
24315 let (editor, cx) = cx.add_window_view(|window, cx| {
24316 let multi_buffer = MultiBuffer::build_multi(
24317 [
24318 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
24319 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
24320 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
24321 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
24322 ],
24323 cx,
24324 );
24325 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
24326
24327 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
24328 // fold all but the second buffer, so that we test navigating between two
24329 // adjacent folded buffers, as well as folded buffers at the start and
24330 // end the multibuffer
24331 editor.fold_buffer(buffer_ids[0], cx);
24332 editor.fold_buffer(buffer_ids[2], cx);
24333 editor.fold_buffer(buffer_ids[3], cx);
24334
24335 editor
24336 });
24337 cx.simulate_resize(size(px(1000.), px(1000.)));
24338
24339 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
24340 cx.assert_excerpts_with_selections(indoc! {"
24341 [EXCERPT]
24342 ˇ[FOLDED]
24343 [EXCERPT]
24344 a1
24345 b1
24346 [EXCERPT]
24347 [FOLDED]
24348 [EXCERPT]
24349 [FOLDED]
24350 "
24351 });
24352 cx.simulate_keystroke("down");
24353 cx.assert_excerpts_with_selections(indoc! {"
24354 [EXCERPT]
24355 [FOLDED]
24356 [EXCERPT]
24357 ˇa1
24358 b1
24359 [EXCERPT]
24360 [FOLDED]
24361 [EXCERPT]
24362 [FOLDED]
24363 "
24364 });
24365 cx.simulate_keystroke("down");
24366 cx.assert_excerpts_with_selections(indoc! {"
24367 [EXCERPT]
24368 [FOLDED]
24369 [EXCERPT]
24370 a1
24371 ˇb1
24372 [EXCERPT]
24373 [FOLDED]
24374 [EXCERPT]
24375 [FOLDED]
24376 "
24377 });
24378 cx.simulate_keystroke("down");
24379 cx.assert_excerpts_with_selections(indoc! {"
24380 [EXCERPT]
24381 [FOLDED]
24382 [EXCERPT]
24383 a1
24384 b1
24385 ˇ[EXCERPT]
24386 [FOLDED]
24387 [EXCERPT]
24388 [FOLDED]
24389 "
24390 });
24391 cx.simulate_keystroke("down");
24392 cx.assert_excerpts_with_selections(indoc! {"
24393 [EXCERPT]
24394 [FOLDED]
24395 [EXCERPT]
24396 a1
24397 b1
24398 [EXCERPT]
24399 ˇ[FOLDED]
24400 [EXCERPT]
24401 [FOLDED]
24402 "
24403 });
24404 for _ in 0..5 {
24405 cx.simulate_keystroke("down");
24406 cx.assert_excerpts_with_selections(indoc! {"
24407 [EXCERPT]
24408 [FOLDED]
24409 [EXCERPT]
24410 a1
24411 b1
24412 [EXCERPT]
24413 [FOLDED]
24414 [EXCERPT]
24415 ˇ[FOLDED]
24416 "
24417 });
24418 }
24419
24420 cx.simulate_keystroke("up");
24421 cx.assert_excerpts_with_selections(indoc! {"
24422 [EXCERPT]
24423 [FOLDED]
24424 [EXCERPT]
24425 a1
24426 b1
24427 [EXCERPT]
24428 ˇ[FOLDED]
24429 [EXCERPT]
24430 [FOLDED]
24431 "
24432 });
24433 cx.simulate_keystroke("up");
24434 cx.assert_excerpts_with_selections(indoc! {"
24435 [EXCERPT]
24436 [FOLDED]
24437 [EXCERPT]
24438 a1
24439 b1
24440 ˇ[EXCERPT]
24441 [FOLDED]
24442 [EXCERPT]
24443 [FOLDED]
24444 "
24445 });
24446 cx.simulate_keystroke("up");
24447 cx.assert_excerpts_with_selections(indoc! {"
24448 [EXCERPT]
24449 [FOLDED]
24450 [EXCERPT]
24451 a1
24452 ˇb1
24453 [EXCERPT]
24454 [FOLDED]
24455 [EXCERPT]
24456 [FOLDED]
24457 "
24458 });
24459 cx.simulate_keystroke("up");
24460 cx.assert_excerpts_with_selections(indoc! {"
24461 [EXCERPT]
24462 [FOLDED]
24463 [EXCERPT]
24464 ˇa1
24465 b1
24466 [EXCERPT]
24467 [FOLDED]
24468 [EXCERPT]
24469 [FOLDED]
24470 "
24471 });
24472 for _ in 0..5 {
24473 cx.simulate_keystroke("up");
24474 cx.assert_excerpts_with_selections(indoc! {"
24475 [EXCERPT]
24476 ˇ[FOLDED]
24477 [EXCERPT]
24478 a1
24479 b1
24480 [EXCERPT]
24481 [FOLDED]
24482 [EXCERPT]
24483 [FOLDED]
24484 "
24485 });
24486 }
24487}
24488
24489#[gpui::test]
24490async fn test_edit_prediction_text(cx: &mut TestAppContext) {
24491 init_test(cx, |_| {});
24492
24493 // Simple insertion
24494 assert_highlighted_edits(
24495 "Hello, world!",
24496 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
24497 true,
24498 cx,
24499 |highlighted_edits, cx| {
24500 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
24501 assert_eq!(highlighted_edits.highlights.len(), 1);
24502 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
24503 assert_eq!(
24504 highlighted_edits.highlights[0].1.background_color,
24505 Some(cx.theme().status().created_background)
24506 );
24507 },
24508 )
24509 .await;
24510
24511 // Replacement
24512 assert_highlighted_edits(
24513 "This is a test.",
24514 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
24515 false,
24516 cx,
24517 |highlighted_edits, cx| {
24518 assert_eq!(highlighted_edits.text, "That is a test.");
24519 assert_eq!(highlighted_edits.highlights.len(), 1);
24520 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
24521 assert_eq!(
24522 highlighted_edits.highlights[0].1.background_color,
24523 Some(cx.theme().status().created_background)
24524 );
24525 },
24526 )
24527 .await;
24528
24529 // Multiple edits
24530 assert_highlighted_edits(
24531 "Hello, world!",
24532 vec![
24533 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
24534 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
24535 ],
24536 false,
24537 cx,
24538 |highlighted_edits, cx| {
24539 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
24540 assert_eq!(highlighted_edits.highlights.len(), 2);
24541 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
24542 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
24543 assert_eq!(
24544 highlighted_edits.highlights[0].1.background_color,
24545 Some(cx.theme().status().created_background)
24546 );
24547 assert_eq!(
24548 highlighted_edits.highlights[1].1.background_color,
24549 Some(cx.theme().status().created_background)
24550 );
24551 },
24552 )
24553 .await;
24554
24555 // Multiple lines with edits
24556 assert_highlighted_edits(
24557 "First line\nSecond line\nThird line\nFourth line",
24558 vec![
24559 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
24560 (
24561 Point::new(2, 0)..Point::new(2, 10),
24562 "New third line".to_string(),
24563 ),
24564 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
24565 ],
24566 false,
24567 cx,
24568 |highlighted_edits, cx| {
24569 assert_eq!(
24570 highlighted_edits.text,
24571 "Second modified\nNew third line\nFourth updated line"
24572 );
24573 assert_eq!(highlighted_edits.highlights.len(), 3);
24574 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
24575 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
24576 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
24577 for highlight in &highlighted_edits.highlights {
24578 assert_eq!(
24579 highlight.1.background_color,
24580 Some(cx.theme().status().created_background)
24581 );
24582 }
24583 },
24584 )
24585 .await;
24586}
24587
24588#[gpui::test]
24589async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
24590 init_test(cx, |_| {});
24591
24592 // Deletion
24593 assert_highlighted_edits(
24594 "Hello, world!",
24595 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
24596 true,
24597 cx,
24598 |highlighted_edits, cx| {
24599 assert_eq!(highlighted_edits.text, "Hello, world!");
24600 assert_eq!(highlighted_edits.highlights.len(), 1);
24601 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
24602 assert_eq!(
24603 highlighted_edits.highlights[0].1.background_color,
24604 Some(cx.theme().status().deleted_background)
24605 );
24606 },
24607 )
24608 .await;
24609
24610 // Insertion
24611 assert_highlighted_edits(
24612 "Hello, world!",
24613 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
24614 true,
24615 cx,
24616 |highlighted_edits, cx| {
24617 assert_eq!(highlighted_edits.highlights.len(), 1);
24618 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
24619 assert_eq!(
24620 highlighted_edits.highlights[0].1.background_color,
24621 Some(cx.theme().status().created_background)
24622 );
24623 },
24624 )
24625 .await;
24626}
24627
24628async fn assert_highlighted_edits(
24629 text: &str,
24630 edits: Vec<(Range<Point>, String)>,
24631 include_deletions: bool,
24632 cx: &mut TestAppContext,
24633 assertion_fn: impl Fn(HighlightedText, &App),
24634) {
24635 let window = cx.add_window(|window, cx| {
24636 let buffer = MultiBuffer::build_simple(text, cx);
24637 Editor::new(EditorMode::full(), buffer, None, window, cx)
24638 });
24639 let cx = &mut VisualTestContext::from_window(*window, cx);
24640
24641 let (buffer, snapshot) = window
24642 .update(cx, |editor, _window, cx| {
24643 (
24644 editor.buffer().clone(),
24645 editor.buffer().read(cx).snapshot(cx),
24646 )
24647 })
24648 .unwrap();
24649
24650 let edits = edits
24651 .into_iter()
24652 .map(|(range, edit)| {
24653 (
24654 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
24655 edit,
24656 )
24657 })
24658 .collect::<Vec<_>>();
24659
24660 let text_anchor_edits = edits
24661 .clone()
24662 .into_iter()
24663 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
24664 .collect::<Vec<_>>();
24665
24666 let edit_preview = window
24667 .update(cx, |_, _window, cx| {
24668 buffer
24669 .read(cx)
24670 .as_singleton()
24671 .unwrap()
24672 .read(cx)
24673 .preview_edits(text_anchor_edits.into(), cx)
24674 })
24675 .unwrap()
24676 .await;
24677
24678 cx.update(|_window, cx| {
24679 let highlighted_edits = edit_prediction_edit_text(
24680 snapshot.as_singleton().unwrap().2,
24681 &edits,
24682 &edit_preview,
24683 include_deletions,
24684 cx,
24685 );
24686 assertion_fn(highlighted_edits, cx)
24687 });
24688}
24689
24690#[track_caller]
24691fn assert_breakpoint(
24692 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
24693 path: &Arc<Path>,
24694 expected: Vec<(u32, Breakpoint)>,
24695) {
24696 if expected.is_empty() {
24697 assert!(!breakpoints.contains_key(path), "{}", path.display());
24698 } else {
24699 let mut breakpoint = breakpoints
24700 .get(path)
24701 .unwrap()
24702 .iter()
24703 .map(|breakpoint| {
24704 (
24705 breakpoint.row,
24706 Breakpoint {
24707 message: breakpoint.message.clone(),
24708 state: breakpoint.state,
24709 condition: breakpoint.condition.clone(),
24710 hit_condition: breakpoint.hit_condition.clone(),
24711 },
24712 )
24713 })
24714 .collect::<Vec<_>>();
24715
24716 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
24717
24718 assert_eq!(expected, breakpoint);
24719 }
24720}
24721
24722fn add_log_breakpoint_at_cursor(
24723 editor: &mut Editor,
24724 log_message: &str,
24725 window: &mut Window,
24726 cx: &mut Context<Editor>,
24727) {
24728 let (anchor, bp) = editor
24729 .breakpoints_at_cursors(window, cx)
24730 .first()
24731 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
24732 .unwrap_or_else(|| {
24733 let snapshot = editor.snapshot(window, cx);
24734 let cursor_position: Point =
24735 editor.selections.newest(&snapshot.display_snapshot).head();
24736
24737 let breakpoint_position = snapshot
24738 .buffer_snapshot()
24739 .anchor_before(Point::new(cursor_position.row, 0));
24740
24741 (breakpoint_position, Breakpoint::new_log(log_message))
24742 });
24743
24744 editor.edit_breakpoint_at_anchor(
24745 anchor,
24746 bp,
24747 BreakpointEditAction::EditLogMessage(log_message.into()),
24748 cx,
24749 );
24750}
24751
24752#[gpui::test]
24753async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
24754 init_test(cx, |_| {});
24755
24756 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24757 let fs = FakeFs::new(cx.executor());
24758 fs.insert_tree(
24759 path!("/a"),
24760 json!({
24761 "main.rs": sample_text,
24762 }),
24763 )
24764 .await;
24765 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24766 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
24767 let cx = &mut VisualTestContext::from_window(*window, cx);
24768
24769 let fs = FakeFs::new(cx.executor());
24770 fs.insert_tree(
24771 path!("/a"),
24772 json!({
24773 "main.rs": sample_text,
24774 }),
24775 )
24776 .await;
24777 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24778 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
24779 let workspace = window
24780 .read_with(cx, |mw, _| mw.workspace().clone())
24781 .unwrap();
24782 let cx = &mut VisualTestContext::from_window(*window, cx);
24783 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
24784 workspace.project().update(cx, |project, cx| {
24785 project.worktrees(cx).next().unwrap().read(cx).id()
24786 })
24787 });
24788
24789 let buffer = project
24790 .update(cx, |project, cx| {
24791 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24792 })
24793 .await
24794 .unwrap();
24795
24796 let (editor, cx) = cx.add_window_view(|window, cx| {
24797 Editor::new(
24798 EditorMode::full(),
24799 MultiBuffer::build_from_buffer(buffer, cx),
24800 Some(project.clone()),
24801 window,
24802 cx,
24803 )
24804 });
24805
24806 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24807 let abs_path = project.read_with(cx, |project, cx| {
24808 project
24809 .absolute_path(&project_path, cx)
24810 .map(Arc::from)
24811 .unwrap()
24812 });
24813
24814 // assert we can add breakpoint on the first line
24815 editor.update_in(cx, |editor, window, cx| {
24816 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24817 editor.move_to_end(&MoveToEnd, window, cx);
24818 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24819 });
24820
24821 let breakpoints = editor.update(cx, |editor, cx| {
24822 editor
24823 .breakpoint_store()
24824 .as_ref()
24825 .unwrap()
24826 .read(cx)
24827 .all_source_breakpoints(cx)
24828 });
24829
24830 assert_eq!(1, breakpoints.len());
24831 assert_breakpoint(
24832 &breakpoints,
24833 &abs_path,
24834 vec![
24835 (0, Breakpoint::new_standard()),
24836 (3, Breakpoint::new_standard()),
24837 ],
24838 );
24839
24840 editor.update_in(cx, |editor, window, cx| {
24841 editor.move_to_beginning(&MoveToBeginning, window, cx);
24842 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24843 });
24844
24845 let breakpoints = editor.update(cx, |editor, cx| {
24846 editor
24847 .breakpoint_store()
24848 .as_ref()
24849 .unwrap()
24850 .read(cx)
24851 .all_source_breakpoints(cx)
24852 });
24853
24854 assert_eq!(1, breakpoints.len());
24855 assert_breakpoint(
24856 &breakpoints,
24857 &abs_path,
24858 vec![(3, Breakpoint::new_standard())],
24859 );
24860
24861 editor.update_in(cx, |editor, window, cx| {
24862 editor.move_to_end(&MoveToEnd, window, cx);
24863 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24864 });
24865
24866 let breakpoints = editor.update(cx, |editor, cx| {
24867 editor
24868 .breakpoint_store()
24869 .as_ref()
24870 .unwrap()
24871 .read(cx)
24872 .all_source_breakpoints(cx)
24873 });
24874
24875 assert_eq!(0, breakpoints.len());
24876 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24877}
24878
24879#[gpui::test]
24880async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
24881 init_test(cx, |_| {});
24882
24883 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24884
24885 let fs = FakeFs::new(cx.executor());
24886 fs.insert_tree(
24887 path!("/a"),
24888 json!({
24889 "main.rs": sample_text,
24890 }),
24891 )
24892 .await;
24893 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24894 let (multi_workspace, cx) =
24895 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
24896 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
24897
24898 let worktree_id = workspace.update(cx, |workspace, cx| {
24899 workspace.project().update(cx, |project, cx| {
24900 project.worktrees(cx).next().unwrap().read(cx).id()
24901 })
24902 });
24903
24904 let buffer = project
24905 .update(cx, |project, cx| {
24906 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24907 })
24908 .await
24909 .unwrap();
24910
24911 let (editor, cx) = cx.add_window_view(|window, cx| {
24912 Editor::new(
24913 EditorMode::full(),
24914 MultiBuffer::build_from_buffer(buffer, cx),
24915 Some(project.clone()),
24916 window,
24917 cx,
24918 )
24919 });
24920
24921 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24922 let abs_path = project.read_with(cx, |project, cx| {
24923 project
24924 .absolute_path(&project_path, cx)
24925 .map(Arc::from)
24926 .unwrap()
24927 });
24928
24929 editor.update_in(cx, |editor, window, cx| {
24930 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24931 });
24932
24933 let breakpoints = editor.update(cx, |editor, cx| {
24934 editor
24935 .breakpoint_store()
24936 .as_ref()
24937 .unwrap()
24938 .read(cx)
24939 .all_source_breakpoints(cx)
24940 });
24941
24942 assert_breakpoint(
24943 &breakpoints,
24944 &abs_path,
24945 vec![(0, Breakpoint::new_log("hello world"))],
24946 );
24947
24948 // Removing a log message from a log breakpoint should remove it
24949 editor.update_in(cx, |editor, window, cx| {
24950 add_log_breakpoint_at_cursor(editor, "", window, cx);
24951 });
24952
24953 let breakpoints = editor.update(cx, |editor, cx| {
24954 editor
24955 .breakpoint_store()
24956 .as_ref()
24957 .unwrap()
24958 .read(cx)
24959 .all_source_breakpoints(cx)
24960 });
24961
24962 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24963
24964 editor.update_in(cx, |editor, window, cx| {
24965 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24966 editor.move_to_end(&MoveToEnd, window, cx);
24967 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24968 // Not adding a log message to a standard breakpoint shouldn't remove it
24969 add_log_breakpoint_at_cursor(editor, "", window, cx);
24970 });
24971
24972 let breakpoints = editor.update(cx, |editor, cx| {
24973 editor
24974 .breakpoint_store()
24975 .as_ref()
24976 .unwrap()
24977 .read(cx)
24978 .all_source_breakpoints(cx)
24979 });
24980
24981 assert_breakpoint(
24982 &breakpoints,
24983 &abs_path,
24984 vec![
24985 (0, Breakpoint::new_standard()),
24986 (3, Breakpoint::new_standard()),
24987 ],
24988 );
24989
24990 editor.update_in(cx, |editor, window, cx| {
24991 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24992 });
24993
24994 let breakpoints = editor.update(cx, |editor, cx| {
24995 editor
24996 .breakpoint_store()
24997 .as_ref()
24998 .unwrap()
24999 .read(cx)
25000 .all_source_breakpoints(cx)
25001 });
25002
25003 assert_breakpoint(
25004 &breakpoints,
25005 &abs_path,
25006 vec![
25007 (0, Breakpoint::new_standard()),
25008 (3, Breakpoint::new_log("hello world")),
25009 ],
25010 );
25011
25012 editor.update_in(cx, |editor, window, cx| {
25013 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
25014 });
25015
25016 let breakpoints = editor.update(cx, |editor, cx| {
25017 editor
25018 .breakpoint_store()
25019 .as_ref()
25020 .unwrap()
25021 .read(cx)
25022 .all_source_breakpoints(cx)
25023 });
25024
25025 assert_breakpoint(
25026 &breakpoints,
25027 &abs_path,
25028 vec![
25029 (0, Breakpoint::new_standard()),
25030 (3, Breakpoint::new_log("hello Earth!!")),
25031 ],
25032 );
25033}
25034
25035/// This also tests that Editor::breakpoint_at_cursor_head is working properly
25036/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
25037/// or when breakpoints were placed out of order. This tests for a regression too
25038#[gpui::test]
25039async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
25040 init_test(cx, |_| {});
25041
25042 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
25043 let fs = FakeFs::new(cx.executor());
25044 fs.insert_tree(
25045 path!("/a"),
25046 json!({
25047 "main.rs": sample_text,
25048 }),
25049 )
25050 .await;
25051 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25052 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25053 let cx = &mut VisualTestContext::from_window(*window, cx);
25054
25055 let fs = FakeFs::new(cx.executor());
25056 fs.insert_tree(
25057 path!("/a"),
25058 json!({
25059 "main.rs": sample_text,
25060 }),
25061 )
25062 .await;
25063 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25064 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25065 let workspace = window
25066 .read_with(cx, |mw, _| mw.workspace().clone())
25067 .unwrap();
25068 let cx = &mut VisualTestContext::from_window(*window, cx);
25069 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
25070 workspace.project().update(cx, |project, cx| {
25071 project.worktrees(cx).next().unwrap().read(cx).id()
25072 })
25073 });
25074
25075 let buffer = project
25076 .update(cx, |project, cx| {
25077 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
25078 })
25079 .await
25080 .unwrap();
25081
25082 let (editor, cx) = cx.add_window_view(|window, cx| {
25083 Editor::new(
25084 EditorMode::full(),
25085 MultiBuffer::build_from_buffer(buffer, cx),
25086 Some(project.clone()),
25087 window,
25088 cx,
25089 )
25090 });
25091
25092 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
25093 let abs_path = project.read_with(cx, |project, cx| {
25094 project
25095 .absolute_path(&project_path, cx)
25096 .map(Arc::from)
25097 .unwrap()
25098 });
25099
25100 // assert we can add breakpoint on the first line
25101 editor.update_in(cx, |editor, window, cx| {
25102 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25103 editor.move_to_end(&MoveToEnd, window, cx);
25104 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25105 editor.move_up(&MoveUp, window, cx);
25106 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25107 });
25108
25109 let breakpoints = editor.update(cx, |editor, cx| {
25110 editor
25111 .breakpoint_store()
25112 .as_ref()
25113 .unwrap()
25114 .read(cx)
25115 .all_source_breakpoints(cx)
25116 });
25117
25118 assert_eq!(1, breakpoints.len());
25119 assert_breakpoint(
25120 &breakpoints,
25121 &abs_path,
25122 vec![
25123 (0, Breakpoint::new_standard()),
25124 (2, Breakpoint::new_standard()),
25125 (3, Breakpoint::new_standard()),
25126 ],
25127 );
25128
25129 editor.update_in(cx, |editor, window, cx| {
25130 editor.move_to_beginning(&MoveToBeginning, window, cx);
25131 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
25132 editor.move_to_end(&MoveToEnd, window, cx);
25133 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
25134 // Disabling a breakpoint that doesn't exist should do nothing
25135 editor.move_up(&MoveUp, window, cx);
25136 editor.move_up(&MoveUp, window, cx);
25137 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
25138 });
25139
25140 let breakpoints = editor.update(cx, |editor, cx| {
25141 editor
25142 .breakpoint_store()
25143 .as_ref()
25144 .unwrap()
25145 .read(cx)
25146 .all_source_breakpoints(cx)
25147 });
25148
25149 let disable_breakpoint = {
25150 let mut bp = Breakpoint::new_standard();
25151 bp.state = BreakpointState::Disabled;
25152 bp
25153 };
25154
25155 assert_eq!(1, breakpoints.len());
25156 assert_breakpoint(
25157 &breakpoints,
25158 &abs_path,
25159 vec![
25160 (0, disable_breakpoint.clone()),
25161 (2, Breakpoint::new_standard()),
25162 (3, disable_breakpoint.clone()),
25163 ],
25164 );
25165
25166 editor.update_in(cx, |editor, window, cx| {
25167 editor.move_to_beginning(&MoveToBeginning, window, cx);
25168 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
25169 editor.move_to_end(&MoveToEnd, window, cx);
25170 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
25171 editor.move_up(&MoveUp, window, cx);
25172 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
25173 });
25174
25175 let breakpoints = editor.update(cx, |editor, cx| {
25176 editor
25177 .breakpoint_store()
25178 .as_ref()
25179 .unwrap()
25180 .read(cx)
25181 .all_source_breakpoints(cx)
25182 });
25183
25184 assert_eq!(1, breakpoints.len());
25185 assert_breakpoint(
25186 &breakpoints,
25187 &abs_path,
25188 vec![
25189 (0, Breakpoint::new_standard()),
25190 (2, disable_breakpoint),
25191 (3, Breakpoint::new_standard()),
25192 ],
25193 );
25194}
25195
25196#[gpui::test]
25197async fn test_breakpoint_phantom_indicator_collision_on_toggle(cx: &mut TestAppContext) {
25198 init_test(cx, |_| {});
25199
25200 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
25201 let fs = FakeFs::new(cx.executor());
25202 fs.insert_tree(
25203 path!("/a"),
25204 json!({
25205 "main.rs": sample_text,
25206 }),
25207 )
25208 .await;
25209 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25210 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25211 let workspace = window
25212 .read_with(cx, |mw, _| mw.workspace().clone())
25213 .unwrap();
25214 let cx = &mut VisualTestContext::from_window(*window, cx);
25215 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
25216 workspace.project().update(cx, |project, cx| {
25217 project.worktrees(cx).next().unwrap().read(cx).id()
25218 })
25219 });
25220
25221 let buffer = project
25222 .update(cx, |project, cx| {
25223 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
25224 })
25225 .await
25226 .unwrap();
25227
25228 let (editor, cx) = cx.add_window_view(|window, cx| {
25229 Editor::new(
25230 EditorMode::full(),
25231 MultiBuffer::build_from_buffer(buffer, cx),
25232 Some(project.clone()),
25233 window,
25234 cx,
25235 )
25236 });
25237
25238 // Simulate hovering over row 0 with no existing breakpoint.
25239 editor.update(cx, |editor, _cx| {
25240 editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
25241 display_row: DisplayRow(0),
25242 is_active: true,
25243 collides_with_existing_breakpoint: false,
25244 });
25245 });
25246
25247 // Toggle breakpoint on the same row (row 0) — collision should flip to true.
25248 editor.update_in(cx, |editor, window, cx| {
25249 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25250 });
25251 editor.update(cx, |editor, _cx| {
25252 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
25253 assert!(
25254 indicator.collides_with_existing_breakpoint,
25255 "Adding a breakpoint on the hovered row should set collision to true"
25256 );
25257 });
25258
25259 // Toggle again on the same row — breakpoint is removed, collision should flip back to false.
25260 editor.update_in(cx, |editor, window, cx| {
25261 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25262 });
25263 editor.update(cx, |editor, _cx| {
25264 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
25265 assert!(
25266 !indicator.collides_with_existing_breakpoint,
25267 "Removing a breakpoint on the hovered row should set collision to false"
25268 );
25269 });
25270
25271 // Now move cursor to row 2 while phantom indicator stays on row 0.
25272 editor.update_in(cx, |editor, window, cx| {
25273 editor.move_down(&MoveDown, window, cx);
25274 editor.move_down(&MoveDown, window, cx);
25275 });
25276
25277 // Ensure phantom indicator is still on row 0, not colliding.
25278 editor.update(cx, |editor, _cx| {
25279 editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
25280 display_row: DisplayRow(0),
25281 is_active: true,
25282 collides_with_existing_breakpoint: false,
25283 });
25284 });
25285
25286 // Toggle breakpoint on row 2 (cursor row) — phantom on row 0 should NOT be affected.
25287 editor.update_in(cx, |editor, window, cx| {
25288 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
25289 });
25290 editor.update(cx, |editor, _cx| {
25291 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
25292 assert!(
25293 !indicator.collides_with_existing_breakpoint,
25294 "Toggling a breakpoint on a different row should not affect the phantom indicator"
25295 );
25296 });
25297}
25298
25299#[gpui::test]
25300async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
25301 init_test(cx, |_| {});
25302 let capabilities = lsp::ServerCapabilities {
25303 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
25304 prepare_provider: Some(true),
25305 work_done_progress_options: Default::default(),
25306 })),
25307 ..Default::default()
25308 };
25309 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
25310
25311 cx.set_state(indoc! {"
25312 struct Fˇoo {}
25313 "});
25314
25315 cx.update_editor(|editor, _, cx| {
25316 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
25317 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
25318 editor.highlight_background(
25319 HighlightKey::DocumentHighlightRead,
25320 &[highlight_range],
25321 |_, theme| theme.colors().editor_document_highlight_read_background,
25322 cx,
25323 );
25324 });
25325
25326 let mut prepare_rename_handler = cx
25327 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
25328 move |_, _, _| async move {
25329 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
25330 start: lsp::Position {
25331 line: 0,
25332 character: 7,
25333 },
25334 end: lsp::Position {
25335 line: 0,
25336 character: 10,
25337 },
25338 })))
25339 },
25340 );
25341 let prepare_rename_task = cx
25342 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
25343 .expect("Prepare rename was not started");
25344 prepare_rename_handler.next().await.unwrap();
25345 prepare_rename_task.await.expect("Prepare rename failed");
25346
25347 let mut rename_handler =
25348 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
25349 let edit = lsp::TextEdit {
25350 range: lsp::Range {
25351 start: lsp::Position {
25352 line: 0,
25353 character: 7,
25354 },
25355 end: lsp::Position {
25356 line: 0,
25357 character: 10,
25358 },
25359 },
25360 new_text: "FooRenamed".to_string(),
25361 };
25362 Ok(Some(lsp::WorkspaceEdit::new(
25363 // Specify the same edit twice
25364 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
25365 )))
25366 });
25367 let rename_task = cx
25368 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
25369 .expect("Confirm rename was not started");
25370 rename_handler.next().await.unwrap();
25371 rename_task.await.expect("Confirm rename failed");
25372 cx.run_until_parked();
25373
25374 // Despite two edits, only one is actually applied as those are identical
25375 cx.assert_editor_state(indoc! {"
25376 struct FooRenamedˇ {}
25377 "});
25378}
25379
25380#[gpui::test]
25381async fn test_rename_without_prepare(cx: &mut TestAppContext) {
25382 init_test(cx, |_| {});
25383 // These capabilities indicate that the server does not support prepare rename.
25384 let capabilities = lsp::ServerCapabilities {
25385 rename_provider: Some(lsp::OneOf::Left(true)),
25386 ..Default::default()
25387 };
25388 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
25389
25390 cx.set_state(indoc! {"
25391 struct Fˇoo {}
25392 "});
25393
25394 cx.update_editor(|editor, _window, cx| {
25395 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
25396 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
25397 editor.highlight_background(
25398 HighlightKey::DocumentHighlightRead,
25399 &[highlight_range],
25400 |_, theme| theme.colors().editor_document_highlight_read_background,
25401 cx,
25402 );
25403 });
25404
25405 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
25406 .expect("Prepare rename was not started")
25407 .await
25408 .expect("Prepare rename failed");
25409
25410 let mut rename_handler =
25411 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
25412 let edit = lsp::TextEdit {
25413 range: lsp::Range {
25414 start: lsp::Position {
25415 line: 0,
25416 character: 7,
25417 },
25418 end: lsp::Position {
25419 line: 0,
25420 character: 10,
25421 },
25422 },
25423 new_text: "FooRenamed".to_string(),
25424 };
25425 Ok(Some(lsp::WorkspaceEdit::new(
25426 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
25427 )))
25428 });
25429 let rename_task = cx
25430 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
25431 .expect("Confirm rename was not started");
25432 rename_handler.next().await.unwrap();
25433 rename_task.await.expect("Confirm rename failed");
25434 cx.run_until_parked();
25435
25436 // Correct range is renamed, as `surrounding_word` is used to find it.
25437 cx.assert_editor_state(indoc! {"
25438 struct FooRenamedˇ {}
25439 "});
25440}
25441
25442#[gpui::test]
25443async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
25444 init_test(cx, |_| {});
25445 let mut cx = EditorTestContext::new(cx).await;
25446
25447 let language = Arc::new(
25448 Language::new(
25449 LanguageConfig::default(),
25450 Some(tree_sitter_html::LANGUAGE.into()),
25451 )
25452 .with_brackets_query(
25453 r#"
25454 ("<" @open "/>" @close)
25455 ("</" @open ">" @close)
25456 ("<" @open ">" @close)
25457 ("\"" @open "\"" @close)
25458 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
25459 "#,
25460 )
25461 .unwrap(),
25462 );
25463 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25464
25465 cx.set_state(indoc! {"
25466 <span>ˇ</span>
25467 "});
25468 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
25469 cx.assert_editor_state(indoc! {"
25470 <span>
25471 ˇ
25472 </span>
25473 "});
25474
25475 cx.set_state(indoc! {"
25476 <span><span></span>ˇ</span>
25477 "});
25478 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
25479 cx.assert_editor_state(indoc! {"
25480 <span><span></span>
25481 ˇ</span>
25482 "});
25483
25484 cx.set_state(indoc! {"
25485 <span>ˇ
25486 </span>
25487 "});
25488 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
25489 cx.assert_editor_state(indoc! {"
25490 <span>
25491 ˇ
25492 </span>
25493 "});
25494}
25495
25496#[gpui::test(iterations = 10)]
25497async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
25498 init_test(cx, |_| {});
25499
25500 let fs = FakeFs::new(cx.executor());
25501 fs.insert_tree(
25502 path!("/dir"),
25503 json!({
25504 "a.ts": "a",
25505 }),
25506 )
25507 .await;
25508
25509 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
25510 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25511 let workspace = window
25512 .read_with(cx, |mw, _| mw.workspace().clone())
25513 .unwrap();
25514 let cx = &mut VisualTestContext::from_window(*window, cx);
25515
25516 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25517 language_registry.add(Arc::new(Language::new(
25518 LanguageConfig {
25519 name: "TypeScript".into(),
25520 matcher: LanguageMatcher {
25521 path_suffixes: vec!["ts".to_string()],
25522 ..Default::default()
25523 },
25524 ..Default::default()
25525 },
25526 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
25527 )));
25528 let mut fake_language_servers = language_registry.register_fake_lsp(
25529 "TypeScript",
25530 FakeLspAdapter {
25531 capabilities: lsp::ServerCapabilities {
25532 code_lens_provider: Some(lsp::CodeLensOptions {
25533 resolve_provider: Some(true),
25534 }),
25535 execute_command_provider: Some(lsp::ExecuteCommandOptions {
25536 commands: vec!["_the/command".to_string()],
25537 ..lsp::ExecuteCommandOptions::default()
25538 }),
25539 ..lsp::ServerCapabilities::default()
25540 },
25541 ..FakeLspAdapter::default()
25542 },
25543 );
25544
25545 let editor = workspace
25546 .update_in(cx, |workspace, window, cx| {
25547 workspace.open_abs_path(
25548 PathBuf::from(path!("/dir/a.ts")),
25549 OpenOptions::default(),
25550 window,
25551 cx,
25552 )
25553 })
25554 .await
25555 .unwrap()
25556 .downcast::<Editor>()
25557 .unwrap();
25558 cx.executor().run_until_parked();
25559
25560 let fake_server = fake_language_servers.next().await.unwrap();
25561
25562 let buffer = editor.update(cx, |editor, cx| {
25563 editor
25564 .buffer()
25565 .read(cx)
25566 .as_singleton()
25567 .expect("have opened a single file by path")
25568 });
25569
25570 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
25571 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
25572 drop(buffer_snapshot);
25573 let actions = cx
25574 .update_window(*window, |_, window, cx| {
25575 project.code_actions(&buffer, anchor..anchor, window, cx)
25576 })
25577 .unwrap();
25578
25579 fake_server
25580 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
25581 Ok(Some(vec![
25582 lsp::CodeLens {
25583 range: lsp::Range::default(),
25584 command: Some(lsp::Command {
25585 title: "Code lens command".to_owned(),
25586 command: "_the/command".to_owned(),
25587 arguments: None,
25588 }),
25589 data: None,
25590 },
25591 lsp::CodeLens {
25592 range: lsp::Range::default(),
25593 command: Some(lsp::Command {
25594 title: "Command not in capabilities".to_owned(),
25595 command: "not in capabilities".to_owned(),
25596 arguments: None,
25597 }),
25598 data: None,
25599 },
25600 lsp::CodeLens {
25601 range: lsp::Range {
25602 start: lsp::Position {
25603 line: 1,
25604 character: 1,
25605 },
25606 end: lsp::Position {
25607 line: 1,
25608 character: 1,
25609 },
25610 },
25611 command: Some(lsp::Command {
25612 title: "Command not in range".to_owned(),
25613 command: "_the/command".to_owned(),
25614 arguments: None,
25615 }),
25616 data: None,
25617 },
25618 ]))
25619 })
25620 .next()
25621 .await;
25622
25623 let actions = actions.await.unwrap();
25624 assert_eq!(
25625 actions.len(),
25626 1,
25627 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
25628 );
25629 let action = actions[0].clone();
25630 let apply = project.update(cx, |project, cx| {
25631 project.apply_code_action(buffer.clone(), action, true, cx)
25632 });
25633
25634 // Resolving the code action does not populate its edits. In absence of
25635 // edits, we must execute the given command.
25636 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
25637 |mut lens, _| async move {
25638 let lens_command = lens.command.as_mut().expect("should have a command");
25639 assert_eq!(lens_command.title, "Code lens command");
25640 lens_command.arguments = Some(vec![json!("the-argument")]);
25641 Ok(lens)
25642 },
25643 );
25644
25645 // While executing the command, the language server sends the editor
25646 // a `workspaceEdit` request.
25647 fake_server
25648 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
25649 let fake = fake_server.clone();
25650 move |params, _| {
25651 assert_eq!(params.command, "_the/command");
25652 let fake = fake.clone();
25653 async move {
25654 fake.server
25655 .request::<lsp::request::ApplyWorkspaceEdit>(
25656 lsp::ApplyWorkspaceEditParams {
25657 label: None,
25658 edit: lsp::WorkspaceEdit {
25659 changes: Some(
25660 [(
25661 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
25662 vec![lsp::TextEdit {
25663 range: lsp::Range::new(
25664 lsp::Position::new(0, 0),
25665 lsp::Position::new(0, 0),
25666 ),
25667 new_text: "X".into(),
25668 }],
25669 )]
25670 .into_iter()
25671 .collect(),
25672 ),
25673 ..lsp::WorkspaceEdit::default()
25674 },
25675 },
25676 DEFAULT_LSP_REQUEST_TIMEOUT,
25677 )
25678 .await
25679 .into_response()
25680 .unwrap();
25681 Ok(Some(json!(null)))
25682 }
25683 }
25684 })
25685 .next()
25686 .await;
25687
25688 // Applying the code lens command returns a project transaction containing the edits
25689 // sent by the language server in its `workspaceEdit` request.
25690 let transaction = apply.await.unwrap();
25691 assert!(transaction.0.contains_key(&buffer));
25692 buffer.update(cx, |buffer, cx| {
25693 assert_eq!(buffer.text(), "Xa");
25694 buffer.undo(cx);
25695 assert_eq!(buffer.text(), "a");
25696 });
25697
25698 let actions_after_edits = cx
25699 .update(|window, cx| project.code_actions(&buffer, anchor..anchor, window, cx))
25700 .unwrap()
25701 .await;
25702 assert_eq!(
25703 actions, actions_after_edits,
25704 "For the same selection, same code lens actions should be returned"
25705 );
25706
25707 let _responses =
25708 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
25709 panic!("No more code lens requests are expected");
25710 });
25711 editor.update_in(cx, |editor, window, cx| {
25712 editor.select_all(&SelectAll, window, cx);
25713 });
25714 cx.executor().run_until_parked();
25715 let new_actions = cx
25716 .update(|window, cx| project.code_actions(&buffer, anchor..anchor, window, cx))
25717 .unwrap()
25718 .await;
25719 assert_eq!(
25720 actions, new_actions,
25721 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
25722 );
25723}
25724
25725#[gpui::test]
25726async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
25727 init_test(cx, |_| {});
25728
25729 let fs = FakeFs::new(cx.executor());
25730 let main_text = r#"fn main() {
25731println!("1");
25732println!("2");
25733println!("3");
25734println!("4");
25735println!("5");
25736}"#;
25737 let lib_text = "mod foo {}";
25738 fs.insert_tree(
25739 path!("/a"),
25740 json!({
25741 "lib.rs": lib_text,
25742 "main.rs": main_text,
25743 }),
25744 )
25745 .await;
25746
25747 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25748 let (multi_workspace, cx) =
25749 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
25750 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
25751 let worktree_id = workspace.update(cx, |workspace, cx| {
25752 workspace.project().update(cx, |project, cx| {
25753 project.worktrees(cx).next().unwrap().read(cx).id()
25754 })
25755 });
25756
25757 let expected_ranges = vec![
25758 Point::new(0, 0)..Point::new(0, 0),
25759 Point::new(1, 0)..Point::new(1, 1),
25760 Point::new(2, 0)..Point::new(2, 2),
25761 Point::new(3, 0)..Point::new(3, 3),
25762 ];
25763
25764 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25765 let editor_1 = workspace
25766 .update_in(cx, |workspace, window, cx| {
25767 workspace.open_path(
25768 (worktree_id, rel_path("main.rs")),
25769 Some(pane_1.downgrade()),
25770 true,
25771 window,
25772 cx,
25773 )
25774 })
25775 .unwrap()
25776 .await
25777 .downcast::<Editor>()
25778 .unwrap();
25779 pane_1.update(cx, |pane, cx| {
25780 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25781 open_editor.update(cx, |editor, cx| {
25782 assert_eq!(
25783 editor.display_text(cx),
25784 main_text,
25785 "Original main.rs text on initial open",
25786 );
25787 assert_eq!(
25788 editor
25789 .selections
25790 .all::<Point>(&editor.display_snapshot(cx))
25791 .into_iter()
25792 .map(|s| s.range())
25793 .collect::<Vec<_>>(),
25794 vec![Point::zero()..Point::zero()],
25795 "Default selections on initial open",
25796 );
25797 })
25798 });
25799 editor_1.update_in(cx, |editor, window, cx| {
25800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25801 s.select_ranges(expected_ranges.clone());
25802 });
25803 });
25804
25805 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
25806 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
25807 });
25808 let editor_2 = workspace
25809 .update_in(cx, |workspace, window, cx| {
25810 workspace.open_path(
25811 (worktree_id, rel_path("main.rs")),
25812 Some(pane_2.downgrade()),
25813 true,
25814 window,
25815 cx,
25816 )
25817 })
25818 .unwrap()
25819 .await
25820 .downcast::<Editor>()
25821 .unwrap();
25822 pane_2.update(cx, |pane, cx| {
25823 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25824 open_editor.update(cx, |editor, cx| {
25825 assert_eq!(
25826 editor.display_text(cx),
25827 main_text,
25828 "Original main.rs text on initial open in another panel",
25829 );
25830 assert_eq!(
25831 editor
25832 .selections
25833 .all::<Point>(&editor.display_snapshot(cx))
25834 .into_iter()
25835 .map(|s| s.range())
25836 .collect::<Vec<_>>(),
25837 vec![Point::zero()..Point::zero()],
25838 "Default selections on initial open in another panel",
25839 );
25840 })
25841 });
25842
25843 editor_2.update_in(cx, |editor, window, cx| {
25844 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
25845 });
25846
25847 let _other_editor_1 = workspace
25848 .update_in(cx, |workspace, window, cx| {
25849 workspace.open_path(
25850 (worktree_id, rel_path("lib.rs")),
25851 Some(pane_1.downgrade()),
25852 true,
25853 window,
25854 cx,
25855 )
25856 })
25857 .unwrap()
25858 .await
25859 .downcast::<Editor>()
25860 .unwrap();
25861 pane_1
25862 .update_in(cx, |pane, window, cx| {
25863 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
25864 })
25865 .await
25866 .unwrap();
25867 drop(editor_1);
25868 pane_1.update(cx, |pane, cx| {
25869 pane.active_item()
25870 .unwrap()
25871 .downcast::<Editor>()
25872 .unwrap()
25873 .update(cx, |editor, cx| {
25874 assert_eq!(
25875 editor.display_text(cx),
25876 lib_text,
25877 "Other file should be open and active",
25878 );
25879 });
25880 assert_eq!(pane.items().count(), 1, "No other editors should be open");
25881 });
25882
25883 let _other_editor_2 = workspace
25884 .update_in(cx, |workspace, window, cx| {
25885 workspace.open_path(
25886 (worktree_id, rel_path("lib.rs")),
25887 Some(pane_2.downgrade()),
25888 true,
25889 window,
25890 cx,
25891 )
25892 })
25893 .unwrap()
25894 .await
25895 .downcast::<Editor>()
25896 .unwrap();
25897 pane_2
25898 .update_in(cx, |pane, window, cx| {
25899 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
25900 })
25901 .await
25902 .unwrap();
25903 drop(editor_2);
25904 pane_2.update(cx, |pane, cx| {
25905 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25906 open_editor.update(cx, |editor, cx| {
25907 assert_eq!(
25908 editor.display_text(cx),
25909 lib_text,
25910 "Other file should be open and active in another panel too",
25911 );
25912 });
25913 assert_eq!(
25914 pane.items().count(),
25915 1,
25916 "No other editors should be open in another pane",
25917 );
25918 });
25919
25920 let _editor_1_reopened = workspace
25921 .update_in(cx, |workspace, window, cx| {
25922 workspace.open_path(
25923 (worktree_id, rel_path("main.rs")),
25924 Some(pane_1.downgrade()),
25925 true,
25926 window,
25927 cx,
25928 )
25929 })
25930 .unwrap()
25931 .await
25932 .downcast::<Editor>()
25933 .unwrap();
25934 let _editor_2_reopened = workspace
25935 .update_in(cx, |workspace, window, cx| {
25936 workspace.open_path(
25937 (worktree_id, rel_path("main.rs")),
25938 Some(pane_2.downgrade()),
25939 true,
25940 window,
25941 cx,
25942 )
25943 })
25944 .unwrap()
25945 .await
25946 .downcast::<Editor>()
25947 .unwrap();
25948 pane_1.update(cx, |pane, cx| {
25949 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25950 open_editor.update(cx, |editor, cx| {
25951 assert_eq!(
25952 editor.display_text(cx),
25953 main_text,
25954 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
25955 );
25956 assert_eq!(
25957 editor
25958 .selections
25959 .all::<Point>(&editor.display_snapshot(cx))
25960 .into_iter()
25961 .map(|s| s.range())
25962 .collect::<Vec<_>>(),
25963 expected_ranges,
25964 "Previous editor in the 1st panel had selections and should get them restored on reopen",
25965 );
25966 })
25967 });
25968 pane_2.update(cx, |pane, cx| {
25969 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25970 open_editor.update(cx, |editor, cx| {
25971 assert_eq!(
25972 editor.display_text(cx),
25973 r#"fn main() {
25974⋯rintln!("1");
25975⋯intln!("2");
25976⋯ntln!("3");
25977println!("4");
25978println!("5");
25979}"#,
25980 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
25981 );
25982 assert_eq!(
25983 editor
25984 .selections
25985 .all::<Point>(&editor.display_snapshot(cx))
25986 .into_iter()
25987 .map(|s| s.range())
25988 .collect::<Vec<_>>(),
25989 vec![Point::zero()..Point::zero()],
25990 "Previous editor in the 2nd pane had no selections changed hence should restore none",
25991 );
25992 })
25993 });
25994}
25995
25996#[gpui::test]
25997async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
25998 init_test(cx, |_| {});
25999
26000 let fs = FakeFs::new(cx.executor());
26001 let main_text = r#"fn main() {
26002println!("1");
26003println!("2");
26004println!("3");
26005println!("4");
26006println!("5");
26007}"#;
26008 let lib_text = "mod foo {}";
26009 fs.insert_tree(
26010 path!("/a"),
26011 json!({
26012 "lib.rs": lib_text,
26013 "main.rs": main_text,
26014 }),
26015 )
26016 .await;
26017
26018 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26019 let (multi_workspace, cx) =
26020 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26021 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
26022 let worktree_id = workspace.update(cx, |workspace, cx| {
26023 workspace.project().update(cx, |project, cx| {
26024 project.worktrees(cx).next().unwrap().read(cx).id()
26025 })
26026 });
26027
26028 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
26029 let editor = workspace
26030 .update_in(cx, |workspace, window, cx| {
26031 workspace.open_path(
26032 (worktree_id, rel_path("main.rs")),
26033 Some(pane.downgrade()),
26034 true,
26035 window,
26036 cx,
26037 )
26038 })
26039 .unwrap()
26040 .await
26041 .downcast::<Editor>()
26042 .unwrap();
26043 pane.update(cx, |pane, cx| {
26044 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26045 open_editor.update(cx, |editor, cx| {
26046 assert_eq!(
26047 editor.display_text(cx),
26048 main_text,
26049 "Original main.rs text on initial open",
26050 );
26051 })
26052 });
26053 editor.update_in(cx, |editor, window, cx| {
26054 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
26055 });
26056
26057 cx.update_global(|store: &mut SettingsStore, cx| {
26058 store.update_user_settings(cx, |s| {
26059 s.workspace.restore_on_file_reopen = Some(false);
26060 });
26061 });
26062 editor.update_in(cx, |editor, window, cx| {
26063 editor.fold_ranges(
26064 vec![
26065 Point::new(1, 0)..Point::new(1, 1),
26066 Point::new(2, 0)..Point::new(2, 2),
26067 Point::new(3, 0)..Point::new(3, 3),
26068 ],
26069 false,
26070 window,
26071 cx,
26072 );
26073 });
26074 pane.update_in(cx, |pane, window, cx| {
26075 pane.close_all_items(&CloseAllItems::default(), window, cx)
26076 })
26077 .await
26078 .unwrap();
26079 pane.update(cx, |pane, _| {
26080 assert!(pane.active_item().is_none());
26081 });
26082 cx.update_global(|store: &mut SettingsStore, cx| {
26083 store.update_user_settings(cx, |s| {
26084 s.workspace.restore_on_file_reopen = Some(true);
26085 });
26086 });
26087
26088 let _editor_reopened = workspace
26089 .update_in(cx, |workspace, window, cx| {
26090 workspace.open_path(
26091 (worktree_id, rel_path("main.rs")),
26092 Some(pane.downgrade()),
26093 true,
26094 window,
26095 cx,
26096 )
26097 })
26098 .unwrap()
26099 .await
26100 .downcast::<Editor>()
26101 .unwrap();
26102 pane.update(cx, |pane, cx| {
26103 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26104 open_editor.update(cx, |editor, cx| {
26105 assert_eq!(
26106 editor.display_text(cx),
26107 main_text,
26108 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
26109 );
26110 })
26111 });
26112}
26113
26114#[gpui::test]
26115async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
26116 struct EmptyModalView {
26117 focus_handle: gpui::FocusHandle,
26118 }
26119 impl EventEmitter<DismissEvent> for EmptyModalView {}
26120 impl Render for EmptyModalView {
26121 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
26122 div()
26123 }
26124 }
26125 impl Focusable for EmptyModalView {
26126 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
26127 self.focus_handle.clone()
26128 }
26129 }
26130 impl workspace::ModalView for EmptyModalView {}
26131 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
26132 EmptyModalView {
26133 focus_handle: cx.focus_handle(),
26134 }
26135 }
26136
26137 init_test(cx, |_| {});
26138
26139 let fs = FakeFs::new(cx.executor());
26140 let project = Project::test(fs, [], cx).await;
26141 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26142 let workspace = window
26143 .read_with(cx, |mw, _| mw.workspace().clone())
26144 .unwrap();
26145 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
26146 let cx = &mut VisualTestContext::from_window(*window, cx);
26147 let editor = cx.new_window_entity(|window, cx| {
26148 Editor::new(
26149 EditorMode::full(),
26150 buffer,
26151 Some(project.clone()),
26152 window,
26153 cx,
26154 )
26155 });
26156 workspace.update_in(cx, |workspace, window, cx| {
26157 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
26158 });
26159
26160 editor.update_in(cx, |editor, window, cx| {
26161 editor.open_context_menu(&OpenContextMenu, window, cx);
26162 assert!(editor.mouse_context_menu.is_some());
26163 });
26164 workspace.update_in(cx, |workspace, window, cx| {
26165 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
26166 });
26167
26168 cx.read(|cx| {
26169 assert!(editor.read(cx).mouse_context_menu.is_none());
26170 });
26171}
26172
26173fn set_linked_edit_ranges(
26174 opening: (Point, Point),
26175 closing: (Point, Point),
26176 editor: &mut Editor,
26177 cx: &mut Context<Editor>,
26178) {
26179 let Some((buffer, _)) = editor
26180 .buffer
26181 .read(cx)
26182 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
26183 else {
26184 panic!("Failed to get buffer for selection position");
26185 };
26186 let buffer = buffer.read(cx);
26187 let buffer_id = buffer.remote_id();
26188 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
26189 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
26190 let mut linked_ranges = HashMap::default();
26191 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
26192 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
26193}
26194
26195#[gpui::test]
26196async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
26197 init_test(cx, |_| {});
26198
26199 let fs = FakeFs::new(cx.executor());
26200 fs.insert_file(path!("/file.html"), Default::default())
26201 .await;
26202
26203 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
26204
26205 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26206 let html_language = Arc::new(Language::new(
26207 LanguageConfig {
26208 name: "HTML".into(),
26209 matcher: LanguageMatcher {
26210 path_suffixes: vec!["html".to_string()],
26211 ..LanguageMatcher::default()
26212 },
26213 brackets: BracketPairConfig {
26214 pairs: vec![BracketPair {
26215 start: "<".into(),
26216 end: ">".into(),
26217 close: true,
26218 ..Default::default()
26219 }],
26220 ..Default::default()
26221 },
26222 ..Default::default()
26223 },
26224 Some(tree_sitter_html::LANGUAGE.into()),
26225 ));
26226 language_registry.add(html_language);
26227 let mut fake_servers = language_registry.register_fake_lsp(
26228 "HTML",
26229 FakeLspAdapter {
26230 capabilities: lsp::ServerCapabilities {
26231 completion_provider: Some(lsp::CompletionOptions {
26232 resolve_provider: Some(true),
26233 ..Default::default()
26234 }),
26235 ..Default::default()
26236 },
26237 ..Default::default()
26238 },
26239 );
26240
26241 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26242 let workspace = window
26243 .read_with(cx, |mw, _| mw.workspace().clone())
26244 .unwrap();
26245 let cx = &mut VisualTestContext::from_window(*window, cx);
26246
26247 let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
26248 workspace.project().update(cx, |project, cx| {
26249 project.worktrees(cx).next().unwrap().read(cx).id()
26250 })
26251 });
26252
26253 project
26254 .update(cx, |project, cx| {
26255 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
26256 })
26257 .await
26258 .unwrap();
26259 let editor = workspace
26260 .update_in(cx, |workspace, window, cx| {
26261 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
26262 })
26263 .await
26264 .unwrap()
26265 .downcast::<Editor>()
26266 .unwrap();
26267
26268 let fake_server = fake_servers.next().await.unwrap();
26269 cx.run_until_parked();
26270 editor.update_in(cx, |editor, window, cx| {
26271 editor.set_text("<ad></ad>", window, cx);
26272 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
26273 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
26274 });
26275 set_linked_edit_ranges(
26276 (Point::new(0, 1), Point::new(0, 3)),
26277 (Point::new(0, 6), Point::new(0, 8)),
26278 editor,
26279 cx,
26280 );
26281 });
26282 let mut completion_handle =
26283 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
26284 Ok(Some(lsp::CompletionResponse::Array(vec![
26285 lsp::CompletionItem {
26286 label: "head".to_string(),
26287 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
26288 lsp::InsertReplaceEdit {
26289 new_text: "head".to_string(),
26290 insert: lsp::Range::new(
26291 lsp::Position::new(0, 1),
26292 lsp::Position::new(0, 3),
26293 ),
26294 replace: lsp::Range::new(
26295 lsp::Position::new(0, 1),
26296 lsp::Position::new(0, 3),
26297 ),
26298 },
26299 )),
26300 ..Default::default()
26301 },
26302 ])))
26303 });
26304 editor.update_in(cx, |editor, window, cx| {
26305 editor.show_completions(&ShowCompletions, window, cx);
26306 });
26307 cx.run_until_parked();
26308 completion_handle.next().await.unwrap();
26309 editor.update(cx, |editor, _| {
26310 assert!(
26311 editor.context_menu_visible(),
26312 "Completion menu should be visible"
26313 );
26314 });
26315 editor.update_in(cx, |editor, window, cx| {
26316 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
26317 });
26318 cx.executor().run_until_parked();
26319 editor.update(cx, |editor, cx| {
26320 assert_eq!(editor.text(cx), "<head></head>");
26321 });
26322}
26323
26324#[gpui::test]
26325async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
26326 init_test(cx, |_| {});
26327
26328 let mut cx = EditorTestContext::new(cx).await;
26329 let language = Arc::new(Language::new(
26330 LanguageConfig {
26331 name: "TSX".into(),
26332 matcher: LanguageMatcher {
26333 path_suffixes: vec!["tsx".to_string()],
26334 ..LanguageMatcher::default()
26335 },
26336 brackets: BracketPairConfig {
26337 pairs: vec![BracketPair {
26338 start: "<".into(),
26339 end: ">".into(),
26340 close: true,
26341 ..Default::default()
26342 }],
26343 ..Default::default()
26344 },
26345 linked_edit_characters: HashSet::from_iter(['.']),
26346 ..Default::default()
26347 },
26348 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
26349 ));
26350 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26351
26352 // Test typing > does not extend linked pair
26353 cx.set_state("<divˇ<div></div>");
26354 cx.update_editor(|editor, _, cx| {
26355 set_linked_edit_ranges(
26356 (Point::new(0, 1), Point::new(0, 4)),
26357 (Point::new(0, 11), Point::new(0, 14)),
26358 editor,
26359 cx,
26360 );
26361 });
26362 cx.update_editor(|editor, window, cx| {
26363 editor.handle_input(">", window, cx);
26364 });
26365 cx.assert_editor_state("<div>ˇ<div></div>");
26366
26367 // Test typing . do extend linked pair
26368 cx.set_state("<Animatedˇ></Animated>");
26369 cx.update_editor(|editor, _, cx| {
26370 set_linked_edit_ranges(
26371 (Point::new(0, 1), Point::new(0, 9)),
26372 (Point::new(0, 12), Point::new(0, 20)),
26373 editor,
26374 cx,
26375 );
26376 });
26377 cx.update_editor(|editor, window, cx| {
26378 editor.handle_input(".", window, cx);
26379 });
26380 cx.assert_editor_state("<Animated.ˇ></Animated.>");
26381 cx.update_editor(|editor, _, cx| {
26382 set_linked_edit_ranges(
26383 (Point::new(0, 1), Point::new(0, 10)),
26384 (Point::new(0, 13), Point::new(0, 21)),
26385 editor,
26386 cx,
26387 );
26388 });
26389 cx.update_editor(|editor, window, cx| {
26390 editor.handle_input("V", window, cx);
26391 });
26392 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
26393}
26394
26395#[gpui::test]
26396async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
26397 init_test(cx, |_| {});
26398
26399 let fs = FakeFs::new(cx.executor());
26400 fs.insert_tree(
26401 path!("/root"),
26402 json!({
26403 "a": {
26404 "main.rs": "fn main() {}",
26405 },
26406 "foo": {
26407 "bar": {
26408 "external_file.rs": "pub mod external {}",
26409 }
26410 }
26411 }),
26412 )
26413 .await;
26414
26415 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
26416 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26417 language_registry.add(rust_lang());
26418 let _fake_servers = language_registry.register_fake_lsp(
26419 "Rust",
26420 FakeLspAdapter {
26421 ..FakeLspAdapter::default()
26422 },
26423 );
26424 let (multi_workspace, cx) =
26425 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
26426 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
26427 let worktree_id = workspace.update(cx, |workspace, cx| {
26428 workspace.project().update(cx, |project, cx| {
26429 project.worktrees(cx).next().unwrap().read(cx).id()
26430 })
26431 });
26432
26433 let assert_language_servers_count =
26434 |expected: usize, context: &str, cx: &mut VisualTestContext| {
26435 project.update(cx, |project, cx| {
26436 let current = project
26437 .lsp_store()
26438 .read(cx)
26439 .as_local()
26440 .unwrap()
26441 .language_servers
26442 .len();
26443 assert_eq!(expected, current, "{context}");
26444 });
26445 };
26446
26447 assert_language_servers_count(
26448 0,
26449 "No servers should be running before any file is open",
26450 cx,
26451 );
26452 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
26453 let main_editor = workspace
26454 .update_in(cx, |workspace, window, cx| {
26455 workspace.open_path(
26456 (worktree_id, rel_path("main.rs")),
26457 Some(pane.downgrade()),
26458 true,
26459 window,
26460 cx,
26461 )
26462 })
26463 .unwrap()
26464 .await
26465 .downcast::<Editor>()
26466 .unwrap();
26467 pane.update(cx, |pane, cx| {
26468 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26469 open_editor.update(cx, |editor, cx| {
26470 assert_eq!(
26471 editor.display_text(cx),
26472 "fn main() {}",
26473 "Original main.rs text on initial open",
26474 );
26475 });
26476 assert_eq!(open_editor, main_editor);
26477 });
26478 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
26479
26480 let external_editor = workspace
26481 .update_in(cx, |workspace, window, cx| {
26482 workspace.open_abs_path(
26483 PathBuf::from("/root/foo/bar/external_file.rs"),
26484 OpenOptions::default(),
26485 window,
26486 cx,
26487 )
26488 })
26489 .await
26490 .expect("opening external file")
26491 .downcast::<Editor>()
26492 .expect("downcasted external file's open element to editor");
26493 pane.update(cx, |pane, cx| {
26494 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26495 open_editor.update(cx, |editor, cx| {
26496 assert_eq!(
26497 editor.display_text(cx),
26498 "pub mod external {}",
26499 "External file is open now",
26500 );
26501 });
26502 assert_eq!(open_editor, external_editor);
26503 });
26504 assert_language_servers_count(
26505 1,
26506 "Second, external, *.rs file should join the existing server",
26507 cx,
26508 );
26509
26510 pane.update_in(cx, |pane, window, cx| {
26511 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26512 })
26513 .await
26514 .unwrap();
26515 pane.update_in(cx, |pane, window, cx| {
26516 pane.navigate_backward(&Default::default(), window, cx);
26517 });
26518 cx.run_until_parked();
26519 pane.update(cx, |pane, cx| {
26520 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26521 open_editor.update(cx, |editor, cx| {
26522 assert_eq!(
26523 editor.display_text(cx),
26524 "pub mod external {}",
26525 "External file is open now",
26526 );
26527 });
26528 });
26529 assert_language_servers_count(
26530 1,
26531 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
26532 cx,
26533 );
26534
26535 cx.update(|_, cx| {
26536 workspace::reload(cx);
26537 });
26538 assert_language_servers_count(
26539 1,
26540 "After reloading the worktree with local and external files opened, only one project should be started",
26541 cx,
26542 );
26543}
26544
26545#[gpui::test]
26546async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
26547 init_test(cx, |_| {});
26548
26549 let mut cx = EditorTestContext::new(cx).await;
26550 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26551 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26552
26553 // test cursor move to start of each line on tab
26554 // for `if`, `elif`, `else`, `while`, `with` and `for`
26555 cx.set_state(indoc! {"
26556 def main():
26557 ˇ for item in items:
26558 ˇ while item.active:
26559 ˇ if item.value > 10:
26560 ˇ continue
26561 ˇ elif item.value < 0:
26562 ˇ break
26563 ˇ else:
26564 ˇ with item.context() as ctx:
26565 ˇ yield count
26566 ˇ else:
26567 ˇ log('while else')
26568 ˇ else:
26569 ˇ log('for else')
26570 "});
26571 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26572 cx.wait_for_autoindent_applied().await;
26573 cx.assert_editor_state(indoc! {"
26574 def main():
26575 ˇfor item in items:
26576 ˇwhile item.active:
26577 ˇif item.value > 10:
26578 ˇcontinue
26579 ˇelif item.value < 0:
26580 ˇbreak
26581 ˇelse:
26582 ˇwith item.context() as ctx:
26583 ˇyield count
26584 ˇelse:
26585 ˇlog('while else')
26586 ˇelse:
26587 ˇlog('for else')
26588 "});
26589 // test relative indent is preserved when tab
26590 // for `if`, `elif`, `else`, `while`, `with` and `for`
26591 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26592 cx.wait_for_autoindent_applied().await;
26593 cx.assert_editor_state(indoc! {"
26594 def main():
26595 ˇfor item in items:
26596 ˇwhile item.active:
26597 ˇif item.value > 10:
26598 ˇcontinue
26599 ˇelif item.value < 0:
26600 ˇbreak
26601 ˇelse:
26602 ˇwith item.context() as ctx:
26603 ˇyield count
26604 ˇelse:
26605 ˇlog('while else')
26606 ˇelse:
26607 ˇlog('for else')
26608 "});
26609
26610 // test cursor move to start of each line on tab
26611 // for `try`, `except`, `else`, `finally`, `match` and `def`
26612 cx.set_state(indoc! {"
26613 def main():
26614 ˇ try:
26615 ˇ fetch()
26616 ˇ except ValueError:
26617 ˇ handle_error()
26618 ˇ else:
26619 ˇ match value:
26620 ˇ case _:
26621 ˇ finally:
26622 ˇ def status():
26623 ˇ return 0
26624 "});
26625 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26626 cx.wait_for_autoindent_applied().await;
26627 cx.assert_editor_state(indoc! {"
26628 def main():
26629 ˇtry:
26630 ˇfetch()
26631 ˇexcept ValueError:
26632 ˇhandle_error()
26633 ˇelse:
26634 ˇmatch value:
26635 ˇcase _:
26636 ˇfinally:
26637 ˇdef status():
26638 ˇreturn 0
26639 "});
26640 // test relative indent is preserved when tab
26641 // for `try`, `except`, `else`, `finally`, `match` and `def`
26642 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26643 cx.wait_for_autoindent_applied().await;
26644 cx.assert_editor_state(indoc! {"
26645 def main():
26646 ˇtry:
26647 ˇfetch()
26648 ˇexcept ValueError:
26649 ˇhandle_error()
26650 ˇelse:
26651 ˇmatch value:
26652 ˇcase _:
26653 ˇfinally:
26654 ˇdef status():
26655 ˇreturn 0
26656 "});
26657}
26658
26659#[gpui::test]
26660async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
26661 init_test(cx, |_| {});
26662
26663 let mut cx = EditorTestContext::new(cx).await;
26664 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26665 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26666
26667 // test `else` auto outdents when typed inside `if` block
26668 cx.set_state(indoc! {"
26669 def main():
26670 if i == 2:
26671 return
26672 ˇ
26673 "});
26674 cx.update_editor(|editor, window, cx| {
26675 editor.handle_input("else:", window, cx);
26676 });
26677 cx.wait_for_autoindent_applied().await;
26678 cx.assert_editor_state(indoc! {"
26679 def main():
26680 if i == 2:
26681 return
26682 else:ˇ
26683 "});
26684
26685 // test `except` auto outdents when typed inside `try` block
26686 cx.set_state(indoc! {"
26687 def main():
26688 try:
26689 i = 2
26690 ˇ
26691 "});
26692 cx.update_editor(|editor, window, cx| {
26693 editor.handle_input("except:", window, cx);
26694 });
26695 cx.wait_for_autoindent_applied().await;
26696 cx.assert_editor_state(indoc! {"
26697 def main():
26698 try:
26699 i = 2
26700 except:ˇ
26701 "});
26702
26703 // test `else` auto outdents when typed inside `except` block
26704 cx.set_state(indoc! {"
26705 def main():
26706 try:
26707 i = 2
26708 except:
26709 j = 2
26710 ˇ
26711 "});
26712 cx.update_editor(|editor, window, cx| {
26713 editor.handle_input("else:", window, cx);
26714 });
26715 cx.wait_for_autoindent_applied().await;
26716 cx.assert_editor_state(indoc! {"
26717 def main():
26718 try:
26719 i = 2
26720 except:
26721 j = 2
26722 else:ˇ
26723 "});
26724
26725 // test `finally` auto outdents when typed inside `else` block
26726 cx.set_state(indoc! {"
26727 def main():
26728 try:
26729 i = 2
26730 except:
26731 j = 2
26732 else:
26733 k = 2
26734 ˇ
26735 "});
26736 cx.update_editor(|editor, window, cx| {
26737 editor.handle_input("finally:", window, cx);
26738 });
26739 cx.wait_for_autoindent_applied().await;
26740 cx.assert_editor_state(indoc! {"
26741 def main():
26742 try:
26743 i = 2
26744 except:
26745 j = 2
26746 else:
26747 k = 2
26748 finally:ˇ
26749 "});
26750
26751 // test `else` does not outdents when typed inside `except` block right after for block
26752 cx.set_state(indoc! {"
26753 def main():
26754 try:
26755 i = 2
26756 except:
26757 for i in range(n):
26758 pass
26759 ˇ
26760 "});
26761 cx.update_editor(|editor, window, cx| {
26762 editor.handle_input("else:", window, cx);
26763 });
26764 cx.wait_for_autoindent_applied().await;
26765 cx.assert_editor_state(indoc! {"
26766 def main():
26767 try:
26768 i = 2
26769 except:
26770 for i in range(n):
26771 pass
26772 else:ˇ
26773 "});
26774
26775 // test `finally` auto outdents when typed inside `else` block right after for block
26776 cx.set_state(indoc! {"
26777 def main():
26778 try:
26779 i = 2
26780 except:
26781 j = 2
26782 else:
26783 for i in range(n):
26784 pass
26785 ˇ
26786 "});
26787 cx.update_editor(|editor, window, cx| {
26788 editor.handle_input("finally:", window, cx);
26789 });
26790 cx.wait_for_autoindent_applied().await;
26791 cx.assert_editor_state(indoc! {"
26792 def main():
26793 try:
26794 i = 2
26795 except:
26796 j = 2
26797 else:
26798 for i in range(n):
26799 pass
26800 finally:ˇ
26801 "});
26802
26803 // test `except` outdents to inner "try" block
26804 cx.set_state(indoc! {"
26805 def main():
26806 try:
26807 i = 2
26808 if i == 2:
26809 try:
26810 i = 3
26811 ˇ
26812 "});
26813 cx.update_editor(|editor, window, cx| {
26814 editor.handle_input("except:", window, cx);
26815 });
26816 cx.wait_for_autoindent_applied().await;
26817 cx.assert_editor_state(indoc! {"
26818 def main():
26819 try:
26820 i = 2
26821 if i == 2:
26822 try:
26823 i = 3
26824 except:ˇ
26825 "});
26826
26827 // test `except` outdents to outer "try" block
26828 cx.set_state(indoc! {"
26829 def main():
26830 try:
26831 i = 2
26832 if i == 2:
26833 try:
26834 i = 3
26835 ˇ
26836 "});
26837 cx.update_editor(|editor, window, cx| {
26838 editor.handle_input("except:", window, cx);
26839 });
26840 cx.wait_for_autoindent_applied().await;
26841 cx.assert_editor_state(indoc! {"
26842 def main():
26843 try:
26844 i = 2
26845 if i == 2:
26846 try:
26847 i = 3
26848 except:ˇ
26849 "});
26850
26851 // test `else` stays at correct indent when typed after `for` block
26852 cx.set_state(indoc! {"
26853 def main():
26854 for i in range(10):
26855 if i == 3:
26856 break
26857 ˇ
26858 "});
26859 cx.update_editor(|editor, window, cx| {
26860 editor.handle_input("else:", window, cx);
26861 });
26862 cx.wait_for_autoindent_applied().await;
26863 cx.assert_editor_state(indoc! {"
26864 def main():
26865 for i in range(10):
26866 if i == 3:
26867 break
26868 else:ˇ
26869 "});
26870
26871 // test does not outdent on typing after line with square brackets
26872 cx.set_state(indoc! {"
26873 def f() -> list[str]:
26874 ˇ
26875 "});
26876 cx.update_editor(|editor, window, cx| {
26877 editor.handle_input("a", window, cx);
26878 });
26879 cx.wait_for_autoindent_applied().await;
26880 cx.assert_editor_state(indoc! {"
26881 def f() -> list[str]:
26882 aˇ
26883 "});
26884
26885 // test does not outdent on typing : after case keyword
26886 cx.set_state(indoc! {"
26887 match 1:
26888 caseˇ
26889 "});
26890 cx.update_editor(|editor, window, cx| {
26891 editor.handle_input(":", window, cx);
26892 });
26893 cx.wait_for_autoindent_applied().await;
26894 cx.assert_editor_state(indoc! {"
26895 match 1:
26896 case:ˇ
26897 "});
26898}
26899
26900#[gpui::test]
26901async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
26902 init_test(cx, |_| {});
26903 update_test_language_settings(cx, |settings| {
26904 settings.defaults.extend_comment_on_newline = Some(false);
26905 });
26906 let mut cx = EditorTestContext::new(cx).await;
26907 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26908 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26909
26910 // test correct indent after newline on comment
26911 cx.set_state(indoc! {"
26912 # COMMENT:ˇ
26913 "});
26914 cx.update_editor(|editor, window, cx| {
26915 editor.newline(&Newline, window, cx);
26916 });
26917 cx.wait_for_autoindent_applied().await;
26918 cx.assert_editor_state(indoc! {"
26919 # COMMENT:
26920 ˇ
26921 "});
26922
26923 // test correct indent after newline in brackets
26924 cx.set_state(indoc! {"
26925 {ˇ}
26926 "});
26927 cx.update_editor(|editor, window, cx| {
26928 editor.newline(&Newline, window, cx);
26929 });
26930 cx.wait_for_autoindent_applied().await;
26931 cx.assert_editor_state(indoc! {"
26932 {
26933 ˇ
26934 }
26935 "});
26936
26937 cx.set_state(indoc! {"
26938 (ˇ)
26939 "});
26940 cx.update_editor(|editor, window, cx| {
26941 editor.newline(&Newline, window, cx);
26942 });
26943 cx.run_until_parked();
26944 cx.assert_editor_state(indoc! {"
26945 (
26946 ˇ
26947 )
26948 "});
26949
26950 // do not indent after empty lists or dictionaries
26951 cx.set_state(indoc! {"
26952 a = []ˇ
26953 "});
26954 cx.update_editor(|editor, window, cx| {
26955 editor.newline(&Newline, window, cx);
26956 });
26957 cx.run_until_parked();
26958 cx.assert_editor_state(indoc! {"
26959 a = []
26960 ˇ
26961 "});
26962}
26963
26964#[gpui::test]
26965async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
26966 init_test(cx, |_| {});
26967
26968 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
26969 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
26970 language_registry.add(markdown_lang());
26971 language_registry.add(python_lang);
26972
26973 let mut cx = EditorTestContext::new(cx).await;
26974 cx.update_buffer(|buffer, cx| {
26975 buffer.set_language_registry(language_registry);
26976 buffer.set_language(Some(markdown_lang()), cx);
26977 });
26978
26979 // Test that `else:` correctly outdents to match `if:` inside the Python code block
26980 cx.set_state(indoc! {"
26981 # Heading
26982
26983 ```python
26984 def main():
26985 if condition:
26986 pass
26987 ˇ
26988 ```
26989 "});
26990 cx.update_editor(|editor, window, cx| {
26991 editor.handle_input("else:", window, cx);
26992 });
26993 cx.run_until_parked();
26994 cx.assert_editor_state(indoc! {"
26995 # Heading
26996
26997 ```python
26998 def main():
26999 if condition:
27000 pass
27001 else:ˇ
27002 ```
27003 "});
27004}
27005
27006#[gpui::test]
27007async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
27008 init_test(cx, |_| {});
27009
27010 let mut cx = EditorTestContext::new(cx).await;
27011 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
27012 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27013
27014 // test cursor move to start of each line on tab
27015 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
27016 cx.set_state(indoc! {"
27017 function main() {
27018 ˇ for item in $items; do
27019 ˇ while [ -n \"$item\" ]; do
27020 ˇ if [ \"$value\" -gt 10 ]; then
27021 ˇ continue
27022 ˇ elif [ \"$value\" -lt 0 ]; then
27023 ˇ break
27024 ˇ else
27025 ˇ echo \"$item\"
27026 ˇ fi
27027 ˇ done
27028 ˇ done
27029 ˇ}
27030 "});
27031 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
27032 cx.wait_for_autoindent_applied().await;
27033 cx.assert_editor_state(indoc! {"
27034 function main() {
27035 ˇfor item in $items; do
27036 ˇwhile [ -n \"$item\" ]; do
27037 ˇif [ \"$value\" -gt 10 ]; then
27038 ˇcontinue
27039 ˇelif [ \"$value\" -lt 0 ]; then
27040 ˇbreak
27041 ˇelse
27042 ˇecho \"$item\"
27043 ˇfi
27044 ˇdone
27045 ˇdone
27046 ˇ}
27047 "});
27048 // test relative indent is preserved when tab
27049 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
27050 cx.wait_for_autoindent_applied().await;
27051 cx.assert_editor_state(indoc! {"
27052 function main() {
27053 ˇfor item in $items; do
27054 ˇwhile [ -n \"$item\" ]; do
27055 ˇif [ \"$value\" -gt 10 ]; then
27056 ˇcontinue
27057 ˇelif [ \"$value\" -lt 0 ]; then
27058 ˇbreak
27059 ˇelse
27060 ˇecho \"$item\"
27061 ˇfi
27062 ˇdone
27063 ˇdone
27064 ˇ}
27065 "});
27066
27067 // test cursor move to start of each line on tab
27068 // for `case` statement with patterns
27069 cx.set_state(indoc! {"
27070 function handle() {
27071 ˇ case \"$1\" in
27072 ˇ start)
27073 ˇ echo \"a\"
27074 ˇ ;;
27075 ˇ stop)
27076 ˇ echo \"b\"
27077 ˇ ;;
27078 ˇ *)
27079 ˇ echo \"c\"
27080 ˇ ;;
27081 ˇ esac
27082 ˇ}
27083 "});
27084 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
27085 cx.wait_for_autoindent_applied().await;
27086 cx.assert_editor_state(indoc! {"
27087 function handle() {
27088 ˇcase \"$1\" in
27089 ˇstart)
27090 ˇecho \"a\"
27091 ˇ;;
27092 ˇstop)
27093 ˇecho \"b\"
27094 ˇ;;
27095 ˇ*)
27096 ˇecho \"c\"
27097 ˇ;;
27098 ˇesac
27099 ˇ}
27100 "});
27101}
27102
27103#[gpui::test]
27104async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
27105 init_test(cx, |_| {});
27106
27107 let mut cx = EditorTestContext::new(cx).await;
27108 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
27109 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27110
27111 // test indents on comment insert
27112 cx.set_state(indoc! {"
27113 function main() {
27114 ˇ for item in $items; do
27115 ˇ while [ -n \"$item\" ]; do
27116 ˇ if [ \"$value\" -gt 10 ]; then
27117 ˇ continue
27118 ˇ elif [ \"$value\" -lt 0 ]; then
27119 ˇ break
27120 ˇ else
27121 ˇ echo \"$item\"
27122 ˇ fi
27123 ˇ done
27124 ˇ done
27125 ˇ}
27126 "});
27127 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
27128 cx.wait_for_autoindent_applied().await;
27129 cx.assert_editor_state(indoc! {"
27130 function main() {
27131 #ˇ for item in $items; do
27132 #ˇ while [ -n \"$item\" ]; do
27133 #ˇ if [ \"$value\" -gt 10 ]; then
27134 #ˇ continue
27135 #ˇ elif [ \"$value\" -lt 0 ]; then
27136 #ˇ break
27137 #ˇ else
27138 #ˇ echo \"$item\"
27139 #ˇ fi
27140 #ˇ done
27141 #ˇ done
27142 #ˇ}
27143 "});
27144}
27145
27146#[gpui::test]
27147async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
27148 init_test(cx, |_| {});
27149
27150 let mut cx = EditorTestContext::new(cx).await;
27151 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
27152 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27153
27154 // test `else` auto outdents when typed inside `if` block
27155 cx.set_state(indoc! {"
27156 if [ \"$1\" = \"test\" ]; then
27157 echo \"foo bar\"
27158 ˇ
27159 "});
27160 cx.update_editor(|editor, window, cx| {
27161 editor.handle_input("else", window, cx);
27162 });
27163 cx.wait_for_autoindent_applied().await;
27164 cx.assert_editor_state(indoc! {"
27165 if [ \"$1\" = \"test\" ]; then
27166 echo \"foo bar\"
27167 elseˇ
27168 "});
27169
27170 // test `elif` auto outdents when typed inside `if` block
27171 cx.set_state(indoc! {"
27172 if [ \"$1\" = \"test\" ]; then
27173 echo \"foo bar\"
27174 ˇ
27175 "});
27176 cx.update_editor(|editor, window, cx| {
27177 editor.handle_input("elif", window, cx);
27178 });
27179 cx.wait_for_autoindent_applied().await;
27180 cx.assert_editor_state(indoc! {"
27181 if [ \"$1\" = \"test\" ]; then
27182 echo \"foo bar\"
27183 elifˇ
27184 "});
27185
27186 // test `fi` auto outdents when typed inside `else` block
27187 cx.set_state(indoc! {"
27188 if [ \"$1\" = \"test\" ]; then
27189 echo \"foo bar\"
27190 else
27191 echo \"bar baz\"
27192 ˇ
27193 "});
27194 cx.update_editor(|editor, window, cx| {
27195 editor.handle_input("fi", window, cx);
27196 });
27197 cx.wait_for_autoindent_applied().await;
27198 cx.assert_editor_state(indoc! {"
27199 if [ \"$1\" = \"test\" ]; then
27200 echo \"foo bar\"
27201 else
27202 echo \"bar baz\"
27203 fiˇ
27204 "});
27205
27206 // test `done` auto outdents when typed inside `while` block
27207 cx.set_state(indoc! {"
27208 while read line; do
27209 echo \"$line\"
27210 ˇ
27211 "});
27212 cx.update_editor(|editor, window, cx| {
27213 editor.handle_input("done", window, cx);
27214 });
27215 cx.wait_for_autoindent_applied().await;
27216 cx.assert_editor_state(indoc! {"
27217 while read line; do
27218 echo \"$line\"
27219 doneˇ
27220 "});
27221
27222 // test `done` auto outdents when typed inside `for` block
27223 cx.set_state(indoc! {"
27224 for file in *.txt; do
27225 cat \"$file\"
27226 ˇ
27227 "});
27228 cx.update_editor(|editor, window, cx| {
27229 editor.handle_input("done", window, cx);
27230 });
27231 cx.wait_for_autoindent_applied().await;
27232 cx.assert_editor_state(indoc! {"
27233 for file in *.txt; do
27234 cat \"$file\"
27235 doneˇ
27236 "});
27237
27238 // test `esac` auto outdents when typed inside `case` block
27239 cx.set_state(indoc! {"
27240 case \"$1\" in
27241 start)
27242 echo \"foo bar\"
27243 ;;
27244 stop)
27245 echo \"bar baz\"
27246 ;;
27247 ˇ
27248 "});
27249 cx.update_editor(|editor, window, cx| {
27250 editor.handle_input("esac", window, cx);
27251 });
27252 cx.wait_for_autoindent_applied().await;
27253 cx.assert_editor_state(indoc! {"
27254 case \"$1\" in
27255 start)
27256 echo \"foo bar\"
27257 ;;
27258 stop)
27259 echo \"bar baz\"
27260 ;;
27261 esacˇ
27262 "});
27263
27264 // test `*)` auto outdents when typed inside `case` block
27265 cx.set_state(indoc! {"
27266 case \"$1\" in
27267 start)
27268 echo \"foo bar\"
27269 ;;
27270 ˇ
27271 "});
27272 cx.update_editor(|editor, window, cx| {
27273 editor.handle_input("*)", window, cx);
27274 });
27275 cx.wait_for_autoindent_applied().await;
27276 cx.assert_editor_state(indoc! {"
27277 case \"$1\" in
27278 start)
27279 echo \"foo bar\"
27280 ;;
27281 *)ˇ
27282 "});
27283
27284 // test `fi` outdents to correct level with nested if blocks
27285 cx.set_state(indoc! {"
27286 if [ \"$1\" = \"test\" ]; then
27287 echo \"outer if\"
27288 if [ \"$2\" = \"debug\" ]; then
27289 echo \"inner if\"
27290 ˇ
27291 "});
27292 cx.update_editor(|editor, window, cx| {
27293 editor.handle_input("fi", window, cx);
27294 });
27295 cx.wait_for_autoindent_applied().await;
27296 cx.assert_editor_state(indoc! {"
27297 if [ \"$1\" = \"test\" ]; then
27298 echo \"outer if\"
27299 if [ \"$2\" = \"debug\" ]; then
27300 echo \"inner if\"
27301 fiˇ
27302 "});
27303}
27304
27305#[gpui::test]
27306async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
27307 init_test(cx, |_| {});
27308 update_test_language_settings(cx, |settings| {
27309 settings.defaults.extend_comment_on_newline = Some(false);
27310 });
27311 let mut cx = EditorTestContext::new(cx).await;
27312 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
27313 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
27314
27315 // test correct indent after newline on comment
27316 cx.set_state(indoc! {"
27317 # COMMENT:ˇ
27318 "});
27319 cx.update_editor(|editor, window, cx| {
27320 editor.newline(&Newline, window, cx);
27321 });
27322 cx.wait_for_autoindent_applied().await;
27323 cx.assert_editor_state(indoc! {"
27324 # COMMENT:
27325 ˇ
27326 "});
27327
27328 // test correct indent after newline after `then`
27329 cx.set_state(indoc! {"
27330
27331 if [ \"$1\" = \"test\" ]; thenˇ
27332 "});
27333 cx.update_editor(|editor, window, cx| {
27334 editor.newline(&Newline, window, cx);
27335 });
27336 cx.wait_for_autoindent_applied().await;
27337 cx.assert_editor_state(indoc! {"
27338
27339 if [ \"$1\" = \"test\" ]; then
27340 ˇ
27341 "});
27342
27343 // test correct indent after newline after `else`
27344 cx.set_state(indoc! {"
27345 if [ \"$1\" = \"test\" ]; then
27346 elseˇ
27347 "});
27348 cx.update_editor(|editor, window, cx| {
27349 editor.newline(&Newline, window, cx);
27350 });
27351 cx.wait_for_autoindent_applied().await;
27352 cx.assert_editor_state(indoc! {"
27353 if [ \"$1\" = \"test\" ]; then
27354 else
27355 ˇ
27356 "});
27357
27358 // test correct indent after newline after `elif`
27359 cx.set_state(indoc! {"
27360 if [ \"$1\" = \"test\" ]; then
27361 elifˇ
27362 "});
27363 cx.update_editor(|editor, window, cx| {
27364 editor.newline(&Newline, window, cx);
27365 });
27366 cx.wait_for_autoindent_applied().await;
27367 cx.assert_editor_state(indoc! {"
27368 if [ \"$1\" = \"test\" ]; then
27369 elif
27370 ˇ
27371 "});
27372
27373 // test correct indent after newline after `do`
27374 cx.set_state(indoc! {"
27375 for file in *.txt; doˇ
27376 "});
27377 cx.update_editor(|editor, window, cx| {
27378 editor.newline(&Newline, window, cx);
27379 });
27380 cx.wait_for_autoindent_applied().await;
27381 cx.assert_editor_state(indoc! {"
27382 for file in *.txt; do
27383 ˇ
27384 "});
27385
27386 // test correct indent after newline after case pattern
27387 cx.set_state(indoc! {"
27388 case \"$1\" in
27389 start)ˇ
27390 "});
27391 cx.update_editor(|editor, window, cx| {
27392 editor.newline(&Newline, window, cx);
27393 });
27394 cx.wait_for_autoindent_applied().await;
27395 cx.assert_editor_state(indoc! {"
27396 case \"$1\" in
27397 start)
27398 ˇ
27399 "});
27400
27401 // test correct indent after newline after case pattern
27402 cx.set_state(indoc! {"
27403 case \"$1\" in
27404 start)
27405 ;;
27406 *)ˇ
27407 "});
27408 cx.update_editor(|editor, window, cx| {
27409 editor.newline(&Newline, window, cx);
27410 });
27411 cx.wait_for_autoindent_applied().await;
27412 cx.assert_editor_state(indoc! {"
27413 case \"$1\" in
27414 start)
27415 ;;
27416 *)
27417 ˇ
27418 "});
27419
27420 // test correct indent after newline after function opening brace
27421 cx.set_state(indoc! {"
27422 function test() {ˇ}
27423 "});
27424 cx.update_editor(|editor, window, cx| {
27425 editor.newline(&Newline, window, cx);
27426 });
27427 cx.wait_for_autoindent_applied().await;
27428 cx.assert_editor_state(indoc! {"
27429 function test() {
27430 ˇ
27431 }
27432 "});
27433
27434 // test no extra indent after semicolon on same line
27435 cx.set_state(indoc! {"
27436 echo \"test\";ˇ
27437 "});
27438 cx.update_editor(|editor, window, cx| {
27439 editor.newline(&Newline, window, cx);
27440 });
27441 cx.wait_for_autoindent_applied().await;
27442 cx.assert_editor_state(indoc! {"
27443 echo \"test\";
27444 ˇ
27445 "});
27446}
27447
27448fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
27449 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
27450 point..point
27451}
27452
27453#[track_caller]
27454fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
27455 let (text, ranges) = marked_text_ranges(marked_text, true);
27456 assert_eq!(editor.text(cx), text);
27457 assert_eq!(
27458 editor.selections.ranges(&editor.display_snapshot(cx)),
27459 ranges
27460 .iter()
27461 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
27462 .collect::<Vec<_>>(),
27463 "Assert selections are {}",
27464 marked_text
27465 );
27466}
27467
27468pub fn handle_signature_help_request(
27469 cx: &mut EditorLspTestContext,
27470 mocked_response: lsp::SignatureHelp,
27471) -> impl Future<Output = ()> + use<> {
27472 let mut request =
27473 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
27474 let mocked_response = mocked_response.clone();
27475 async move { Ok(Some(mocked_response)) }
27476 });
27477
27478 async move {
27479 request.next().await;
27480 }
27481}
27482
27483#[track_caller]
27484pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
27485 cx.update_editor(|editor, _, _| {
27486 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
27487 let entries = menu.entries.borrow();
27488 let entries = entries
27489 .iter()
27490 .map(|entry| entry.string.as_str())
27491 .collect::<Vec<_>>();
27492 assert_eq!(entries, expected);
27493 } else {
27494 panic!("Expected completions menu");
27495 }
27496 });
27497}
27498
27499#[gpui::test]
27500async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
27501 init_test(cx, |_| {});
27502 let mut cx = EditorLspTestContext::new_rust(
27503 lsp::ServerCapabilities {
27504 completion_provider: Some(lsp::CompletionOptions {
27505 ..Default::default()
27506 }),
27507 ..Default::default()
27508 },
27509 cx,
27510 )
27511 .await;
27512 cx.lsp
27513 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
27514 Ok(Some(lsp::CompletionResponse::Array(vec![
27515 lsp::CompletionItem {
27516 label: "unsafe".into(),
27517 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
27518 range: lsp::Range {
27519 start: lsp::Position {
27520 line: 0,
27521 character: 9,
27522 },
27523 end: lsp::Position {
27524 line: 0,
27525 character: 11,
27526 },
27527 },
27528 new_text: "unsafe".to_string(),
27529 })),
27530 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
27531 ..Default::default()
27532 },
27533 ])))
27534 });
27535
27536 cx.update_editor(|editor, _, cx| {
27537 editor.project().unwrap().update(cx, |project, cx| {
27538 project.snippets().update(cx, |snippets, _cx| {
27539 snippets.add_snippet_for_test(
27540 None,
27541 PathBuf::from("test_snippets.json"),
27542 vec![
27543 Arc::new(project::snippet_provider::Snippet {
27544 prefix: vec![
27545 "unlimited word count".to_string(),
27546 "unlimit word count".to_string(),
27547 "unlimited unknown".to_string(),
27548 ],
27549 body: "this is many words".to_string(),
27550 description: Some("description".to_string()),
27551 name: "multi-word snippet test".to_string(),
27552 }),
27553 Arc::new(project::snippet_provider::Snippet {
27554 prefix: vec!["unsnip".to_string(), "@few".to_string()],
27555 body: "fewer words".to_string(),
27556 description: Some("alt description".to_string()),
27557 name: "other name".to_string(),
27558 }),
27559 Arc::new(project::snippet_provider::Snippet {
27560 prefix: vec!["ab aa".to_string()],
27561 body: "abcd".to_string(),
27562 description: None,
27563 name: "alphabet".to_string(),
27564 }),
27565 ],
27566 );
27567 });
27568 })
27569 });
27570
27571 let get_completions = |cx: &mut EditorLspTestContext| {
27572 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
27573 Some(CodeContextMenu::Completions(context_menu)) => {
27574 let entries = context_menu.entries.borrow();
27575 entries
27576 .iter()
27577 .map(|entry| entry.string.clone())
27578 .collect_vec()
27579 }
27580 _ => vec![],
27581 })
27582 };
27583
27584 // snippets:
27585 // @foo
27586 // foo bar
27587 //
27588 // when typing:
27589 //
27590 // when typing:
27591 // - if I type a symbol "open the completions with snippets only"
27592 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
27593 //
27594 // stuff we need:
27595 // - filtering logic change?
27596 // - remember how far back the completion started.
27597
27598 let test_cases: &[(&str, &[&str])] = &[
27599 (
27600 "un",
27601 &[
27602 "unsafe",
27603 "unlimit word count",
27604 "unlimited unknown",
27605 "unlimited word count",
27606 "unsnip",
27607 ],
27608 ),
27609 (
27610 "u ",
27611 &[
27612 "unlimit word count",
27613 "unlimited unknown",
27614 "unlimited word count",
27615 ],
27616 ),
27617 ("u a", &["ab aa", "unsafe"]), // unsAfe
27618 (
27619 "u u",
27620 &[
27621 "unsafe",
27622 "unlimit word count",
27623 "unlimited unknown", // ranked highest among snippets
27624 "unlimited word count",
27625 "unsnip",
27626 ],
27627 ),
27628 ("uw c", &["unlimit word count", "unlimited word count"]),
27629 (
27630 "u w",
27631 &[
27632 "unlimit word count",
27633 "unlimited word count",
27634 "unlimited unknown",
27635 ],
27636 ),
27637 ("u w ", &["unlimit word count", "unlimited word count"]),
27638 (
27639 "u ",
27640 &[
27641 "unlimit word count",
27642 "unlimited unknown",
27643 "unlimited word count",
27644 ],
27645 ),
27646 ("wor", &[]),
27647 ("uf", &["unsafe"]),
27648 ("af", &["unsafe"]),
27649 ("afu", &[]),
27650 (
27651 "ue",
27652 &["unsafe", "unlimited unknown", "unlimited word count"],
27653 ),
27654 ("@", &["@few"]),
27655 ("@few", &["@few"]),
27656 ("@ ", &[]),
27657 ("a@", &["@few"]),
27658 ("a@f", &["@few", "unsafe"]),
27659 ("a@fw", &["@few"]),
27660 ("a", &["ab aa", "unsafe"]),
27661 ("aa", &["ab aa"]),
27662 ("aaa", &["ab aa"]),
27663 ("ab", &["ab aa"]),
27664 ("ab ", &["ab aa"]),
27665 ("ab a", &["ab aa", "unsafe"]),
27666 ("ab ab", &["ab aa"]),
27667 ("ab ab aa", &["ab aa"]),
27668 ];
27669
27670 for &(input_to_simulate, expected_completions) in test_cases {
27671 cx.set_state("fn a() { ˇ }\n");
27672 for c in input_to_simulate.split("") {
27673 cx.simulate_input(c);
27674 cx.run_until_parked();
27675 }
27676 let expected_completions = expected_completions
27677 .iter()
27678 .map(|s| s.to_string())
27679 .collect_vec();
27680 assert_eq!(
27681 get_completions(&mut cx),
27682 expected_completions,
27683 "< actual / expected >, input = {input_to_simulate:?}",
27684 );
27685 }
27686}
27687
27688/// Handle completion request passing a marked string specifying where the completion
27689/// should be triggered from using '|' character, what range should be replaced, and what completions
27690/// should be returned using '<' and '>' to delimit the range.
27691///
27692/// Also see `handle_completion_request_with_insert_and_replace`.
27693#[track_caller]
27694pub fn handle_completion_request(
27695 marked_string: &str,
27696 completions: Vec<&'static str>,
27697 is_incomplete: bool,
27698 counter: Arc<AtomicUsize>,
27699 cx: &mut EditorLspTestContext,
27700) -> impl Future<Output = ()> {
27701 let complete_from_marker: TextRangeMarker = '|'.into();
27702 let replace_range_marker: TextRangeMarker = ('<', '>').into();
27703 let (_, mut marked_ranges) = marked_text_ranges_by(
27704 marked_string,
27705 vec![complete_from_marker.clone(), replace_range_marker.clone()],
27706 );
27707
27708 let complete_from_position = cx.to_lsp(MultiBufferOffset(
27709 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
27710 ));
27711 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
27712 let replace_range =
27713 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
27714
27715 let mut request =
27716 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
27717 let completions = completions.clone();
27718 counter.fetch_add(1, atomic::Ordering::Release);
27719 async move {
27720 assert_eq!(params.text_document_position.text_document.uri, url.clone());
27721 assert_eq!(
27722 params.text_document_position.position,
27723 complete_from_position
27724 );
27725 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
27726 is_incomplete,
27727 item_defaults: None,
27728 items: completions
27729 .iter()
27730 .map(|completion_text| lsp::CompletionItem {
27731 label: completion_text.to_string(),
27732 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
27733 range: replace_range,
27734 new_text: completion_text.to_string(),
27735 })),
27736 ..Default::default()
27737 })
27738 .collect(),
27739 })))
27740 }
27741 });
27742
27743 async move {
27744 request.next().await;
27745 }
27746}
27747
27748/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
27749/// given instead, which also contains an `insert` range.
27750///
27751/// This function uses markers to define ranges:
27752/// - `|` marks the cursor position
27753/// - `<>` marks the replace range
27754/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
27755pub fn handle_completion_request_with_insert_and_replace(
27756 cx: &mut EditorLspTestContext,
27757 marked_string: &str,
27758 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
27759 counter: Arc<AtomicUsize>,
27760) -> impl Future<Output = ()> {
27761 let complete_from_marker: TextRangeMarker = '|'.into();
27762 let replace_range_marker: TextRangeMarker = ('<', '>').into();
27763 let insert_range_marker: TextRangeMarker = ('{', '}').into();
27764
27765 let (_, mut marked_ranges) = marked_text_ranges_by(
27766 marked_string,
27767 vec![
27768 complete_from_marker.clone(),
27769 replace_range_marker.clone(),
27770 insert_range_marker.clone(),
27771 ],
27772 );
27773
27774 let complete_from_position = cx.to_lsp(MultiBufferOffset(
27775 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
27776 ));
27777 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
27778 let replace_range =
27779 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
27780
27781 let insert_range = match marked_ranges.remove(&insert_range_marker) {
27782 Some(ranges) if !ranges.is_empty() => {
27783 let range1 = ranges[0].clone();
27784 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
27785 }
27786 _ => lsp::Range {
27787 start: replace_range.start,
27788 end: complete_from_position,
27789 },
27790 };
27791
27792 let mut request =
27793 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
27794 let completions = completions.clone();
27795 counter.fetch_add(1, atomic::Ordering::Release);
27796 async move {
27797 assert_eq!(params.text_document_position.text_document.uri, url.clone());
27798 assert_eq!(
27799 params.text_document_position.position, complete_from_position,
27800 "marker `|` position doesn't match",
27801 );
27802 Ok(Some(lsp::CompletionResponse::Array(
27803 completions
27804 .iter()
27805 .map(|(label, new_text)| lsp::CompletionItem {
27806 label: label.to_string(),
27807 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
27808 lsp::InsertReplaceEdit {
27809 insert: insert_range,
27810 replace: replace_range,
27811 new_text: new_text.to_string(),
27812 },
27813 )),
27814 ..Default::default()
27815 })
27816 .collect(),
27817 )))
27818 }
27819 });
27820
27821 async move {
27822 request.next().await;
27823 }
27824}
27825
27826fn handle_resolve_completion_request(
27827 cx: &mut EditorLspTestContext,
27828 edits: Option<Vec<(&'static str, &'static str)>>,
27829) -> impl Future<Output = ()> {
27830 let edits = edits.map(|edits| {
27831 edits
27832 .iter()
27833 .map(|(marked_string, new_text)| {
27834 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
27835 let replace_range = cx.to_lsp_range(
27836 MultiBufferOffset(marked_ranges[0].start)
27837 ..MultiBufferOffset(marked_ranges[0].end),
27838 );
27839 lsp::TextEdit::new(replace_range, new_text.to_string())
27840 })
27841 .collect::<Vec<_>>()
27842 });
27843
27844 let mut request =
27845 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
27846 let edits = edits.clone();
27847 async move {
27848 Ok(lsp::CompletionItem {
27849 additional_text_edits: edits,
27850 ..Default::default()
27851 })
27852 }
27853 });
27854
27855 async move {
27856 request.next().await;
27857 }
27858}
27859
27860pub(crate) fn update_test_language_settings(
27861 cx: &mut TestAppContext,
27862 f: impl Fn(&mut AllLanguageSettingsContent),
27863) {
27864 cx.update(|cx| {
27865 SettingsStore::update_global(cx, |store, cx| {
27866 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
27867 });
27868 });
27869}
27870
27871pub(crate) fn update_test_project_settings(
27872 cx: &mut TestAppContext,
27873 f: impl Fn(&mut ProjectSettingsContent),
27874) {
27875 cx.update(|cx| {
27876 SettingsStore::update_global(cx, |store, cx| {
27877 store.update_user_settings(cx, |settings| f(&mut settings.project));
27878 });
27879 });
27880}
27881
27882pub(crate) fn update_test_editor_settings(
27883 cx: &mut TestAppContext,
27884 f: impl Fn(&mut EditorSettingsContent),
27885) {
27886 cx.update(|cx| {
27887 SettingsStore::update_global(cx, |store, cx| {
27888 store.update_user_settings(cx, |settings| f(&mut settings.editor));
27889 })
27890 })
27891}
27892
27893pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
27894 cx.update(|cx| {
27895 assets::Assets.load_test_fonts(cx);
27896 let store = SettingsStore::test(cx);
27897 cx.set_global(store);
27898 theme::init(theme::LoadThemes::JustBase, cx);
27899 release_channel::init(semver::Version::new(0, 0, 0), cx);
27900 crate::init(cx);
27901 });
27902 zlog::init_test();
27903 update_test_language_settings(cx, f);
27904}
27905
27906#[track_caller]
27907fn assert_hunk_revert(
27908 not_reverted_text_with_selections: &str,
27909 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
27910 expected_reverted_text_with_selections: &str,
27911 base_text: &str,
27912 cx: &mut EditorLspTestContext,
27913) {
27914 cx.set_state(not_reverted_text_with_selections);
27915 cx.set_head_text(base_text);
27916 cx.executor().run_until_parked();
27917
27918 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
27919 let snapshot = editor.snapshot(window, cx);
27920 let reverted_hunk_statuses = snapshot
27921 .buffer_snapshot()
27922 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
27923 .map(|hunk| hunk.status().kind)
27924 .collect::<Vec<_>>();
27925
27926 editor.git_restore(&Default::default(), window, cx);
27927 reverted_hunk_statuses
27928 });
27929 cx.executor().run_until_parked();
27930 cx.assert_editor_state(expected_reverted_text_with_selections);
27931 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
27932}
27933
27934#[gpui::test(iterations = 10)]
27935async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
27936 init_test(cx, |_| {});
27937
27938 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
27939 let counter = diagnostic_requests.clone();
27940
27941 let fs = FakeFs::new(cx.executor());
27942 fs.insert_tree(
27943 path!("/a"),
27944 json!({
27945 "first.rs": "fn main() { let a = 5; }",
27946 "second.rs": "// Test file",
27947 }),
27948 )
27949 .await;
27950
27951 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27952 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
27953 let workspace = window
27954 .read_with(cx, |mw, _| mw.workspace().clone())
27955 .unwrap();
27956 let cx = &mut VisualTestContext::from_window(*window, cx);
27957
27958 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27959 language_registry.add(rust_lang());
27960 let mut fake_servers = language_registry.register_fake_lsp(
27961 "Rust",
27962 FakeLspAdapter {
27963 capabilities: lsp::ServerCapabilities {
27964 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
27965 lsp::DiagnosticOptions {
27966 identifier: None,
27967 inter_file_dependencies: true,
27968 workspace_diagnostics: true,
27969 work_done_progress_options: Default::default(),
27970 },
27971 )),
27972 ..Default::default()
27973 },
27974 ..Default::default()
27975 },
27976 );
27977
27978 let editor = workspace
27979 .update_in(cx, |workspace, window, cx| {
27980 workspace.open_abs_path(
27981 PathBuf::from(path!("/a/first.rs")),
27982 OpenOptions::default(),
27983 window,
27984 cx,
27985 )
27986 })
27987 .await
27988 .unwrap()
27989 .downcast::<Editor>()
27990 .unwrap();
27991 let fake_server = fake_servers.next().await.unwrap();
27992 let server_id = fake_server.server.server_id();
27993 let mut first_request = fake_server
27994 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
27995 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
27996 let result_id = Some(new_result_id.to_string());
27997 assert_eq!(
27998 params.text_document.uri,
27999 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
28000 );
28001 async move {
28002 Ok(lsp::DocumentDiagnosticReportResult::Report(
28003 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
28004 related_documents: None,
28005 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
28006 items: Vec::new(),
28007 result_id,
28008 },
28009 }),
28010 ))
28011 }
28012 });
28013
28014 let ensure_result_id = |expected_result_id: Option<SharedString>, cx: &mut TestAppContext| {
28015 project.update(cx, |project, cx| {
28016 let buffer_id = editor
28017 .read(cx)
28018 .buffer()
28019 .read(cx)
28020 .as_singleton()
28021 .expect("created a singleton buffer")
28022 .read(cx)
28023 .remote_id();
28024 let buffer_result_id = project
28025 .lsp_store()
28026 .read(cx)
28027 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
28028 assert_eq!(expected_result_id, buffer_result_id);
28029 });
28030 };
28031
28032 ensure_result_id(None, cx);
28033 cx.executor().advance_clock(Duration::from_millis(60));
28034 cx.executor().run_until_parked();
28035 assert_eq!(
28036 diagnostic_requests.load(atomic::Ordering::Acquire),
28037 1,
28038 "Opening file should trigger diagnostic request"
28039 );
28040 first_request
28041 .next()
28042 .await
28043 .expect("should have sent the first diagnostics pull request");
28044 ensure_result_id(Some(SharedString::new_static("1")), cx);
28045
28046 // Editing should trigger diagnostics
28047 editor.update_in(cx, |editor, window, cx| {
28048 editor.handle_input("2", window, cx)
28049 });
28050 cx.executor().advance_clock(Duration::from_millis(60));
28051 cx.executor().run_until_parked();
28052 assert_eq!(
28053 diagnostic_requests.load(atomic::Ordering::Acquire),
28054 2,
28055 "Editing should trigger diagnostic request"
28056 );
28057 ensure_result_id(Some(SharedString::new_static("2")), cx);
28058
28059 // Moving cursor should not trigger diagnostic request
28060 editor.update_in(cx, |editor, window, cx| {
28061 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28062 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
28063 });
28064 });
28065 cx.executor().advance_clock(Duration::from_millis(60));
28066 cx.executor().run_until_parked();
28067 assert_eq!(
28068 diagnostic_requests.load(atomic::Ordering::Acquire),
28069 2,
28070 "Cursor movement should not trigger diagnostic request"
28071 );
28072 ensure_result_id(Some(SharedString::new_static("2")), cx);
28073 // Multiple rapid edits should be debounced
28074 for _ in 0..5 {
28075 editor.update_in(cx, |editor, window, cx| {
28076 editor.handle_input("x", window, cx)
28077 });
28078 }
28079 cx.executor().advance_clock(Duration::from_millis(60));
28080 cx.executor().run_until_parked();
28081
28082 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
28083 assert!(
28084 final_requests <= 4,
28085 "Multiple rapid edits should be debounced (got {final_requests} requests)",
28086 );
28087 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
28088}
28089
28090#[gpui::test]
28091async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
28092 // Regression test for issue #11671
28093 // Previously, adding a cursor after moving multiple cursors would reset
28094 // the cursor count instead of adding to the existing cursors.
28095 init_test(cx, |_| {});
28096 let mut cx = EditorTestContext::new(cx).await;
28097
28098 // Create a simple buffer with cursor at start
28099 cx.set_state(indoc! {"
28100 ˇaaaa
28101 bbbb
28102 cccc
28103 dddd
28104 eeee
28105 ffff
28106 gggg
28107 hhhh"});
28108
28109 // Add 2 cursors below (so we have 3 total)
28110 cx.update_editor(|editor, window, cx| {
28111 editor.add_selection_below(&Default::default(), window, cx);
28112 editor.add_selection_below(&Default::default(), window, cx);
28113 });
28114
28115 // Verify we have 3 cursors
28116 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
28117 assert_eq!(
28118 initial_count, 3,
28119 "Should have 3 cursors after adding 2 below"
28120 );
28121
28122 // Move down one line
28123 cx.update_editor(|editor, window, cx| {
28124 editor.move_down(&MoveDown, window, cx);
28125 });
28126
28127 // Add another cursor below
28128 cx.update_editor(|editor, window, cx| {
28129 editor.add_selection_below(&Default::default(), window, cx);
28130 });
28131
28132 // Should now have 4 cursors (3 original + 1 new)
28133 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
28134 assert_eq!(
28135 final_count, 4,
28136 "Should have 4 cursors after moving and adding another"
28137 );
28138}
28139
28140#[gpui::test]
28141async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
28142 init_test(cx, |_| {});
28143
28144 let mut cx = EditorTestContext::new(cx).await;
28145
28146 cx.set_state(indoc!(
28147 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
28148 Second line here"#
28149 ));
28150
28151 cx.update_editor(|editor, window, cx| {
28152 // Enable soft wrapping with a narrow width to force soft wrapping and
28153 // confirm that more than 2 rows are being displayed.
28154 editor.set_wrap_width(Some(100.0.into()), cx);
28155 assert!(editor.display_text(cx).lines().count() > 2);
28156
28157 editor.add_selection_below(
28158 &AddSelectionBelow {
28159 skip_soft_wrap: true,
28160 },
28161 window,
28162 cx,
28163 );
28164
28165 assert_eq!(
28166 display_ranges(editor, cx),
28167 &[
28168 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
28169 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
28170 ]
28171 );
28172
28173 editor.add_selection_above(
28174 &AddSelectionAbove {
28175 skip_soft_wrap: true,
28176 },
28177 window,
28178 cx,
28179 );
28180
28181 assert_eq!(
28182 display_ranges(editor, cx),
28183 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
28184 );
28185
28186 editor.add_selection_below(
28187 &AddSelectionBelow {
28188 skip_soft_wrap: false,
28189 },
28190 window,
28191 cx,
28192 );
28193
28194 assert_eq!(
28195 display_ranges(editor, cx),
28196 &[
28197 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
28198 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
28199 ]
28200 );
28201
28202 editor.add_selection_above(
28203 &AddSelectionAbove {
28204 skip_soft_wrap: false,
28205 },
28206 window,
28207 cx,
28208 );
28209
28210 assert_eq!(
28211 display_ranges(editor, cx),
28212 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
28213 );
28214 });
28215
28216 // Set up text where selections are in the middle of a soft-wrapped line.
28217 // When adding selection below with `skip_soft_wrap` set to `true`, the new
28218 // selection should be at the same buffer column, not the same pixel
28219 // position.
28220 cx.set_state(indoc!(
28221 r#"1. Very long line to show «howˇ» a wrapped line would look
28222 2. Very long line to show how a wrapped line would look"#
28223 ));
28224
28225 cx.update_editor(|editor, window, cx| {
28226 // Enable soft wrapping with a narrow width to force soft wrapping and
28227 // confirm that more than 2 rows are being displayed.
28228 editor.set_wrap_width(Some(100.0.into()), cx);
28229 assert!(editor.display_text(cx).lines().count() > 2);
28230
28231 editor.add_selection_below(
28232 &AddSelectionBelow {
28233 skip_soft_wrap: true,
28234 },
28235 window,
28236 cx,
28237 );
28238
28239 // Assert that there's now 2 selections, both selecting the same column
28240 // range in the buffer row.
28241 let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
28242 let selections = editor.selections.all::<Point>(&display_map);
28243 assert_eq!(selections.len(), 2);
28244 assert_eq!(selections[0].start.column, selections[1].start.column);
28245 assert_eq!(selections[0].end.column, selections[1].end.column);
28246 });
28247}
28248
28249#[gpui::test]
28250async fn test_insert_snippet(cx: &mut TestAppContext) {
28251 init_test(cx, |_| {});
28252 let mut cx = EditorTestContext::new(cx).await;
28253
28254 cx.update_editor(|editor, _, cx| {
28255 editor.project().unwrap().update(cx, |project, cx| {
28256 project.snippets().update(cx, |snippets, _cx| {
28257 let snippet = project::snippet_provider::Snippet {
28258 prefix: vec![], // no prefix needed!
28259 body: "an Unspecified".to_string(),
28260 description: Some("shhhh it's a secret".to_string()),
28261 name: "super secret snippet".to_string(),
28262 };
28263 snippets.add_snippet_for_test(
28264 None,
28265 PathBuf::from("test_snippets.json"),
28266 vec![Arc::new(snippet)],
28267 );
28268
28269 let snippet = project::snippet_provider::Snippet {
28270 prefix: vec![], // no prefix needed!
28271 body: " Location".to_string(),
28272 description: Some("the word 'location'".to_string()),
28273 name: "location word".to_string(),
28274 };
28275 snippets.add_snippet_for_test(
28276 Some("Markdown".to_string()),
28277 PathBuf::from("test_snippets.json"),
28278 vec![Arc::new(snippet)],
28279 );
28280 });
28281 })
28282 });
28283
28284 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
28285
28286 cx.update_editor(|editor, window, cx| {
28287 editor.insert_snippet_at_selections(
28288 &InsertSnippet {
28289 language: None,
28290 name: Some("super secret snippet".to_string()),
28291 snippet: None,
28292 },
28293 window,
28294 cx,
28295 );
28296
28297 // Language is specified in the action,
28298 // so the buffer language does not need to match
28299 editor.insert_snippet_at_selections(
28300 &InsertSnippet {
28301 language: Some("Markdown".to_string()),
28302 name: Some("location word".to_string()),
28303 snippet: None,
28304 },
28305 window,
28306 cx,
28307 );
28308
28309 editor.insert_snippet_at_selections(
28310 &InsertSnippet {
28311 language: None,
28312 name: None,
28313 snippet: Some("$0 after".to_string()),
28314 },
28315 window,
28316 cx,
28317 );
28318 });
28319
28320 cx.assert_editor_state(
28321 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
28322 );
28323}
28324
28325#[gpui::test]
28326async fn test_inlay_hints_request_timeout(cx: &mut TestAppContext) {
28327 use crate::inlays::inlay_hints::InlayHintRefreshReason;
28328 use crate::inlays::inlay_hints::tests::{cached_hint_labels, init_test, visible_hint_labels};
28329 use settings::InlayHintSettingsContent;
28330 use std::sync::atomic::AtomicU32;
28331 use std::time::Duration;
28332
28333 const BASE_TIMEOUT_SECS: u64 = 1;
28334
28335 let request_count = Arc::new(AtomicU32::new(0));
28336 let closure_request_count = request_count.clone();
28337
28338 init_test(cx, |settings| {
28339 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
28340 enabled: Some(true),
28341 ..InlayHintSettingsContent::default()
28342 })
28343 });
28344 cx.update(|cx| {
28345 SettingsStore::update_global(cx, |store, cx| {
28346 store.update_user_settings(cx, |settings| {
28347 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
28348 request_timeout: Some(BASE_TIMEOUT_SECS),
28349 button: Some(true),
28350 notifications: None,
28351 semantic_token_rules: None,
28352 });
28353 });
28354 });
28355 });
28356
28357 let fs = FakeFs::new(cx.executor());
28358 fs.insert_tree(
28359 path!("/a"),
28360 json!({
28361 "main.rs": "fn main() { let a = 5; }",
28362 }),
28363 )
28364 .await;
28365
28366 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
28367 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28368 language_registry.add(rust_lang());
28369 let mut fake_servers = language_registry.register_fake_lsp(
28370 "Rust",
28371 FakeLspAdapter {
28372 capabilities: lsp::ServerCapabilities {
28373 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
28374 ..lsp::ServerCapabilities::default()
28375 },
28376 initializer: Some(Box::new(move |fake_server| {
28377 let request_count = closure_request_count.clone();
28378 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
28379 move |params, cx| {
28380 let request_count = request_count.clone();
28381 async move {
28382 cx.background_executor()
28383 .timer(Duration::from_secs(BASE_TIMEOUT_SECS * 2))
28384 .await;
28385 let count = request_count.fetch_add(1, atomic::Ordering::Release) + 1;
28386 assert_eq!(
28387 params.text_document.uri,
28388 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
28389 );
28390 Ok(Some(vec![lsp::InlayHint {
28391 position: lsp::Position::new(0, 1),
28392 label: lsp::InlayHintLabel::String(count.to_string()),
28393 kind: None,
28394 text_edits: None,
28395 tooltip: None,
28396 padding_left: None,
28397 padding_right: None,
28398 data: None,
28399 }]))
28400 }
28401 },
28402 );
28403 })),
28404 ..FakeLspAdapter::default()
28405 },
28406 );
28407
28408 let buffer = project
28409 .update(cx, |project, cx| {
28410 project.open_local_buffer(path!("/a/main.rs"), cx)
28411 })
28412 .await
28413 .unwrap();
28414 let editor = cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
28415
28416 cx.executor().run_until_parked();
28417 let fake_server = fake_servers.next().await.unwrap();
28418
28419 cx.executor()
28420 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS) + Duration::from_millis(100));
28421 cx.executor().run_until_parked();
28422 editor
28423 .update(cx, |editor, _window, cx| {
28424 assert!(
28425 cached_hint_labels(editor, cx).is_empty(),
28426 "First request should time out, no hints cached"
28427 );
28428 })
28429 .unwrap();
28430
28431 editor
28432 .update(cx, |editor, _window, cx| {
28433 editor.refresh_inlay_hints(
28434 InlayHintRefreshReason::RefreshRequested {
28435 server_id: fake_server.server.server_id(),
28436 request_id: Some(1),
28437 },
28438 cx,
28439 );
28440 })
28441 .unwrap();
28442 cx.executor()
28443 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS) + Duration::from_millis(100));
28444 cx.executor().run_until_parked();
28445 editor
28446 .update(cx, |editor, _window, cx| {
28447 assert!(
28448 cached_hint_labels(editor, cx).is_empty(),
28449 "Second request should also time out with BASE_TIMEOUT, no hints cached"
28450 );
28451 })
28452 .unwrap();
28453
28454 cx.update(|cx| {
28455 SettingsStore::update_global(cx, |store, cx| {
28456 store.update_user_settings(cx, |settings| {
28457 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
28458 request_timeout: Some(BASE_TIMEOUT_SECS * 4),
28459 button: Some(true),
28460 notifications: None,
28461 semantic_token_rules: None,
28462 });
28463 });
28464 });
28465 });
28466 editor
28467 .update(cx, |editor, _window, cx| {
28468 editor.refresh_inlay_hints(
28469 InlayHintRefreshReason::RefreshRequested {
28470 server_id: fake_server.server.server_id(),
28471 request_id: Some(2),
28472 },
28473 cx,
28474 );
28475 })
28476 .unwrap();
28477 cx.executor()
28478 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS * 4) + Duration::from_millis(100));
28479 cx.executor().run_until_parked();
28480 editor
28481 .update(cx, |editor, _window, cx| {
28482 assert_eq!(
28483 vec!["1".to_string()],
28484 cached_hint_labels(editor, cx),
28485 "With extended timeout (BASE * 4), hints should arrive successfully"
28486 );
28487 assert_eq!(vec!["1".to_string()], visible_hint_labels(editor, cx));
28488 })
28489 .unwrap();
28490}
28491
28492#[gpui::test]
28493async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
28494 init_test(cx, |_| {});
28495 let (editor, cx) = cx.add_window_view(Editor::single_line);
28496 editor.update_in(cx, |editor, window, cx| {
28497 editor.set_text("oops\n\nwow\n", window, cx)
28498 });
28499 cx.run_until_parked();
28500 editor.update(cx, |editor, cx| {
28501 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
28502 });
28503 editor.update(cx, |editor, cx| {
28504 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
28505 });
28506 cx.run_until_parked();
28507 editor.update(cx, |editor, cx| {
28508 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
28509 });
28510}
28511
28512#[gpui::test]
28513async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
28514 init_test(cx, |_| {});
28515
28516 cx.update(|cx| {
28517 register_project_item::<Editor>(cx);
28518 });
28519
28520 let fs = FakeFs::new(cx.executor());
28521 fs.insert_tree("/root1", json!({})).await;
28522 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
28523 .await;
28524
28525 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
28526 let (multi_workspace, cx) =
28527 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
28528 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
28529
28530 let worktree_id = project.update(cx, |project, cx| {
28531 project.worktrees(cx).next().unwrap().read(cx).id()
28532 });
28533
28534 let handle = workspace
28535 .update_in(cx, |workspace, window, cx| {
28536 let project_path = (worktree_id, rel_path("one.pdf"));
28537 workspace.open_path(project_path, None, true, window, cx)
28538 })
28539 .await
28540 .unwrap();
28541 // The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
28542 // Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
28543 // With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
28544 assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
28545}
28546
28547#[gpui::test]
28548async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
28549 init_test(cx, |_| {});
28550
28551 let language = Arc::new(Language::new(
28552 LanguageConfig::default(),
28553 Some(tree_sitter_rust::LANGUAGE.into()),
28554 ));
28555
28556 // Test hierarchical sibling navigation
28557 let text = r#"
28558 fn outer() {
28559 if condition {
28560 let a = 1;
28561 }
28562 let b = 2;
28563 }
28564
28565 fn another() {
28566 let c = 3;
28567 }
28568 "#;
28569
28570 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
28571 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
28572 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
28573
28574 // Wait for parsing to complete
28575 editor
28576 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
28577 .await;
28578
28579 editor.update_in(cx, |editor, window, cx| {
28580 // Start by selecting "let a = 1;" inside the if block
28581 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28582 s.select_display_ranges([
28583 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
28584 ]);
28585 });
28586
28587 let initial_selection = editor
28588 .selections
28589 .display_ranges(&editor.display_snapshot(cx));
28590 assert_eq!(initial_selection.len(), 1, "Should have one selection");
28591
28592 // Test select next sibling - should move up levels to find the next sibling
28593 // Since "let a = 1;" has no siblings in the if block, it should move up
28594 // to find "let b = 2;" which is a sibling of the if block
28595 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
28596 let next_selection = editor
28597 .selections
28598 .display_ranges(&editor.display_snapshot(cx));
28599
28600 // Should have a selection and it should be different from the initial
28601 assert_eq!(
28602 next_selection.len(),
28603 1,
28604 "Should have one selection after next"
28605 );
28606 assert_ne!(
28607 next_selection[0], initial_selection[0],
28608 "Next sibling selection should be different"
28609 );
28610
28611 // Test hierarchical navigation by going to the end of the current function
28612 // and trying to navigate to the next function
28613 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28614 s.select_display_ranges([
28615 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
28616 ]);
28617 });
28618
28619 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
28620 let function_next_selection = editor
28621 .selections
28622 .display_ranges(&editor.display_snapshot(cx));
28623
28624 // Should move to the next function
28625 assert_eq!(
28626 function_next_selection.len(),
28627 1,
28628 "Should have one selection after function next"
28629 );
28630
28631 // Test select previous sibling navigation
28632 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
28633 let prev_selection = editor
28634 .selections
28635 .display_ranges(&editor.display_snapshot(cx));
28636
28637 // Should have a selection and it should be different
28638 assert_eq!(
28639 prev_selection.len(),
28640 1,
28641 "Should have one selection after prev"
28642 );
28643 assert_ne!(
28644 prev_selection[0], function_next_selection[0],
28645 "Previous sibling selection should be different from next"
28646 );
28647 });
28648}
28649
28650#[gpui::test]
28651async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
28652 init_test(cx, |_| {});
28653
28654 let mut cx = EditorTestContext::new(cx).await;
28655 cx.set_state(
28656 "let ˇvariable = 42;
28657let another = variable + 1;
28658let result = variable * 2;",
28659 );
28660
28661 // Set up document highlights manually (simulating LSP response)
28662 cx.update_editor(|editor, _window, cx| {
28663 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
28664
28665 // Create highlights for "variable" occurrences
28666 let highlight_ranges = [
28667 Point::new(0, 4)..Point::new(0, 12), // First "variable"
28668 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
28669 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
28670 ];
28671
28672 let anchor_ranges: Vec<_> = highlight_ranges
28673 .iter()
28674 .map(|range| range.clone().to_anchors(&buffer_snapshot))
28675 .collect();
28676
28677 editor.highlight_background(
28678 HighlightKey::DocumentHighlightRead,
28679 &anchor_ranges,
28680 |_, theme| theme.colors().editor_document_highlight_read_background,
28681 cx,
28682 );
28683 });
28684
28685 // Go to next highlight - should move to second "variable"
28686 cx.update_editor(|editor, window, cx| {
28687 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28688 });
28689 cx.assert_editor_state(
28690 "let variable = 42;
28691let another = ˇvariable + 1;
28692let result = variable * 2;",
28693 );
28694
28695 // Go to next highlight - should move to third "variable"
28696 cx.update_editor(|editor, window, cx| {
28697 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28698 });
28699 cx.assert_editor_state(
28700 "let variable = 42;
28701let another = variable + 1;
28702let result = ˇvariable * 2;",
28703 );
28704
28705 // Go to next highlight - should stay at third "variable" (no wrap-around)
28706 cx.update_editor(|editor, window, cx| {
28707 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28708 });
28709 cx.assert_editor_state(
28710 "let variable = 42;
28711let another = variable + 1;
28712let result = ˇvariable * 2;",
28713 );
28714
28715 // Now test going backwards from third position
28716 cx.update_editor(|editor, window, cx| {
28717 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28718 });
28719 cx.assert_editor_state(
28720 "let variable = 42;
28721let another = ˇvariable + 1;
28722let result = variable * 2;",
28723 );
28724
28725 // Go to previous highlight - should move to first "variable"
28726 cx.update_editor(|editor, window, cx| {
28727 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28728 });
28729 cx.assert_editor_state(
28730 "let ˇvariable = 42;
28731let another = variable + 1;
28732let result = variable * 2;",
28733 );
28734
28735 // Go to previous highlight - should stay on first "variable"
28736 cx.update_editor(|editor, window, cx| {
28737 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28738 });
28739 cx.assert_editor_state(
28740 "let ˇvariable = 42;
28741let another = variable + 1;
28742let result = variable * 2;",
28743 );
28744}
28745
28746#[gpui::test]
28747async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
28748 cx: &mut gpui::TestAppContext,
28749) {
28750 init_test(cx, |_| {});
28751
28752 let url = "https://zed.dev";
28753
28754 let markdown_language = Arc::new(Language::new(
28755 LanguageConfig {
28756 name: "Markdown".into(),
28757 ..LanguageConfig::default()
28758 },
28759 None,
28760 ));
28761
28762 let mut cx = EditorTestContext::new(cx).await;
28763 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28764 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
28765
28766 cx.update_editor(|editor, window, cx| {
28767 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28768 editor.paste(&Paste, window, cx);
28769 });
28770
28771 cx.assert_editor_state(&format!(
28772 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
28773 ));
28774}
28775
28776#[gpui::test]
28777async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
28778 init_test(cx, |_| {});
28779
28780 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
28781 let mut cx = EditorTestContext::new(cx).await;
28782
28783 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28784
28785 // Case 1: Test if adding a character with multi cursors preserves nested list indents
28786 cx.set_state(&indoc! {"
28787 - [ ] Item 1
28788 - [ ] Item 1.a
28789 - [ˇ] Item 2
28790 - [ˇ] Item 2.a
28791 - [ˇ] Item 2.b
28792 "
28793 });
28794 cx.update_editor(|editor, window, cx| {
28795 editor.handle_input("x", window, cx);
28796 });
28797 cx.run_until_parked();
28798 cx.assert_editor_state(indoc! {"
28799 - [ ] Item 1
28800 - [ ] Item 1.a
28801 - [xˇ] Item 2
28802 - [xˇ] Item 2.a
28803 - [xˇ] Item 2.b
28804 "
28805 });
28806
28807 // Case 2: Test adding new line after nested list continues the list with unchecked task
28808 cx.set_state(&indoc! {"
28809 - [ ] Item 1
28810 - [ ] Item 1.a
28811 - [x] Item 2
28812 - [x] Item 2.a
28813 - [x] Item 2.bˇ"
28814 });
28815 cx.update_editor(|editor, window, cx| {
28816 editor.newline(&Newline, window, cx);
28817 });
28818 cx.assert_editor_state(indoc! {"
28819 - [ ] Item 1
28820 - [ ] Item 1.a
28821 - [x] Item 2
28822 - [x] Item 2.a
28823 - [x] Item 2.b
28824 - [ ] ˇ"
28825 });
28826
28827 // Case 3: Test adding content to continued list item
28828 cx.update_editor(|editor, window, cx| {
28829 editor.handle_input("Item 2.c", window, cx);
28830 });
28831 cx.run_until_parked();
28832 cx.assert_editor_state(indoc! {"
28833 - [ ] Item 1
28834 - [ ] Item 1.a
28835 - [x] Item 2
28836 - [x] Item 2.a
28837 - [x] Item 2.b
28838 - [ ] Item 2.cˇ"
28839 });
28840
28841 // Case 4: Test adding new line after nested ordered list continues with next number
28842 cx.set_state(indoc! {"
28843 1. Item 1
28844 1. Item 1.a
28845 2. Item 2
28846 1. Item 2.a
28847 2. Item 2.bˇ"
28848 });
28849 cx.update_editor(|editor, window, cx| {
28850 editor.newline(&Newline, window, cx);
28851 });
28852 cx.assert_editor_state(indoc! {"
28853 1. Item 1
28854 1. Item 1.a
28855 2. Item 2
28856 1. Item 2.a
28857 2. Item 2.b
28858 3. ˇ"
28859 });
28860
28861 // Case 5: Adding content to continued ordered list item
28862 cx.update_editor(|editor, window, cx| {
28863 editor.handle_input("Item 2.c", window, cx);
28864 });
28865 cx.run_until_parked();
28866 cx.assert_editor_state(indoc! {"
28867 1. Item 1
28868 1. Item 1.a
28869 2. Item 2
28870 1. Item 2.a
28871 2. Item 2.b
28872 3. Item 2.cˇ"
28873 });
28874
28875 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
28876 cx.set_state(indoc! {"
28877 - Item 1
28878 - Item 1.a
28879 - Item 1.a
28880 ˇ"});
28881 cx.update_editor(|editor, window, cx| {
28882 editor.handle_input("-", window, cx);
28883 });
28884 cx.run_until_parked();
28885 cx.assert_editor_state(indoc! {"
28886 - Item 1
28887 - Item 1.a
28888 - Item 1.a
28889 -ˇ"});
28890
28891 // Case 7: Test blockquote newline preserves something
28892 cx.set_state(indoc! {"
28893 > Item 1ˇ"
28894 });
28895 cx.update_editor(|editor, window, cx| {
28896 editor.newline(&Newline, window, cx);
28897 });
28898 cx.assert_editor_state(indoc! {"
28899 > Item 1
28900 ˇ"
28901 });
28902}
28903
28904#[gpui::test]
28905async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
28906 cx: &mut gpui::TestAppContext,
28907) {
28908 init_test(cx, |_| {});
28909
28910 let url = "https://zed.dev";
28911
28912 let markdown_language = Arc::new(Language::new(
28913 LanguageConfig {
28914 name: "Markdown".into(),
28915 ..LanguageConfig::default()
28916 },
28917 None,
28918 ));
28919
28920 let mut cx = EditorTestContext::new(cx).await;
28921 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28922 cx.set_state(&format!(
28923 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
28924 ));
28925
28926 cx.update_editor(|editor, window, cx| {
28927 editor.copy(&Copy, window, cx);
28928 });
28929
28930 cx.set_state(&format!(
28931 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
28932 ));
28933
28934 cx.update_editor(|editor, window, cx| {
28935 editor.paste(&Paste, window, cx);
28936 });
28937
28938 cx.assert_editor_state(&format!(
28939 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
28940 ));
28941}
28942
28943#[gpui::test]
28944async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
28945 cx: &mut gpui::TestAppContext,
28946) {
28947 init_test(cx, |_| {});
28948
28949 let url = "https://zed.dev";
28950
28951 let markdown_language = Arc::new(Language::new(
28952 LanguageConfig {
28953 name: "Markdown".into(),
28954 ..LanguageConfig::default()
28955 },
28956 None,
28957 ));
28958
28959 let mut cx = EditorTestContext::new(cx).await;
28960 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28961 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
28962
28963 cx.update_editor(|editor, window, cx| {
28964 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28965 editor.paste(&Paste, window, cx);
28966 });
28967
28968 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
28969}
28970
28971#[gpui::test]
28972async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
28973 cx: &mut gpui::TestAppContext,
28974) {
28975 init_test(cx, |_| {});
28976
28977 let text = "Awesome";
28978
28979 let markdown_language = Arc::new(Language::new(
28980 LanguageConfig {
28981 name: "Markdown".into(),
28982 ..LanguageConfig::default()
28983 },
28984 None,
28985 ));
28986
28987 let mut cx = EditorTestContext::new(cx).await;
28988 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28989 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
28990
28991 cx.update_editor(|editor, window, cx| {
28992 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
28993 editor.paste(&Paste, window, cx);
28994 });
28995
28996 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
28997}
28998
28999#[gpui::test]
29000async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
29001 cx: &mut gpui::TestAppContext,
29002) {
29003 init_test(cx, |_| {});
29004
29005 let url = "https://zed.dev";
29006
29007 let markdown_language = Arc::new(Language::new(
29008 LanguageConfig {
29009 name: "Rust".into(),
29010 ..LanguageConfig::default()
29011 },
29012 None,
29013 ));
29014
29015 let mut cx = EditorTestContext::new(cx).await;
29016 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29017 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
29018
29019 cx.update_editor(|editor, window, cx| {
29020 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
29021 editor.paste(&Paste, window, cx);
29022 });
29023
29024 cx.assert_editor_state(&format!(
29025 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
29026 ));
29027}
29028
29029#[gpui::test]
29030async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
29031 cx: &mut TestAppContext,
29032) {
29033 init_test(cx, |_| {});
29034
29035 let url = "https://zed.dev";
29036
29037 let markdown_language = Arc::new(Language::new(
29038 LanguageConfig {
29039 name: "Markdown".into(),
29040 ..LanguageConfig::default()
29041 },
29042 None,
29043 ));
29044
29045 let (editor, cx) = cx.add_window_view(|window, cx| {
29046 let multi_buffer = MultiBuffer::build_multi(
29047 [
29048 ("this will embed -> link", vec![Point::row_range(0..1)]),
29049 ("this will replace -> link", vec![Point::row_range(0..1)]),
29050 ],
29051 cx,
29052 );
29053 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
29054 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
29055 s.select_ranges(vec![
29056 Point::new(0, 19)..Point::new(0, 23),
29057 Point::new(1, 21)..Point::new(1, 25),
29058 ])
29059 });
29060 let first_buffer_id = multi_buffer
29061 .read(cx)
29062 .excerpt_buffer_ids()
29063 .into_iter()
29064 .next()
29065 .unwrap();
29066 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
29067 first_buffer.update(cx, |buffer, cx| {
29068 buffer.set_language(Some(markdown_language.clone()), cx);
29069 });
29070
29071 editor
29072 });
29073 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29074
29075 cx.update_editor(|editor, window, cx| {
29076 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
29077 editor.paste(&Paste, window, cx);
29078 });
29079
29080 cx.assert_editor_state(&format!(
29081 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
29082 ));
29083}
29084
29085#[gpui::test]
29086async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
29087 init_test(cx, |_| {});
29088
29089 let fs = FakeFs::new(cx.executor());
29090 fs.insert_tree(
29091 path!("/project"),
29092 json!({
29093 "first.rs": "# First Document\nSome content here.",
29094 "second.rs": "Plain text content for second file.",
29095 }),
29096 )
29097 .await;
29098
29099 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
29100 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
29101 let cx = &mut VisualTestContext::from_window(*window, cx);
29102
29103 let language = rust_lang();
29104 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
29105 language_registry.add(language.clone());
29106 let mut fake_servers = language_registry.register_fake_lsp(
29107 "Rust",
29108 FakeLspAdapter {
29109 ..FakeLspAdapter::default()
29110 },
29111 );
29112
29113 let buffer1 = project
29114 .update(cx, |project, cx| {
29115 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
29116 })
29117 .await
29118 .unwrap();
29119 let buffer2 = project
29120 .update(cx, |project, cx| {
29121 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
29122 })
29123 .await
29124 .unwrap();
29125
29126 let multi_buffer = cx.new(|cx| {
29127 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
29128 multi_buffer.set_excerpts_for_path(
29129 PathKey::for_buffer(&buffer1, cx),
29130 buffer1.clone(),
29131 [Point::zero()..buffer1.read(cx).max_point()],
29132 3,
29133 cx,
29134 );
29135 multi_buffer.set_excerpts_for_path(
29136 PathKey::for_buffer(&buffer2, cx),
29137 buffer2.clone(),
29138 [Point::zero()..buffer1.read(cx).max_point()],
29139 3,
29140 cx,
29141 );
29142 multi_buffer
29143 });
29144
29145 let (editor, cx) = cx.add_window_view(|window, cx| {
29146 Editor::new(
29147 EditorMode::full(),
29148 multi_buffer,
29149 Some(project.clone()),
29150 window,
29151 cx,
29152 )
29153 });
29154
29155 let fake_language_server = fake_servers.next().await.unwrap();
29156
29157 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
29158
29159 let save = editor.update_in(cx, |editor, window, cx| {
29160 assert!(editor.is_dirty(cx));
29161
29162 editor.save(
29163 SaveOptions {
29164 format: true,
29165 autosave: true,
29166 },
29167 project,
29168 window,
29169 cx,
29170 )
29171 });
29172 let (start_edit_tx, start_edit_rx) = oneshot::channel();
29173 let (done_edit_tx, done_edit_rx) = oneshot::channel();
29174 let mut done_edit_rx = Some(done_edit_rx);
29175 let mut start_edit_tx = Some(start_edit_tx);
29176
29177 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
29178 start_edit_tx.take().unwrap().send(()).unwrap();
29179 let done_edit_rx = done_edit_rx.take().unwrap();
29180 async move {
29181 done_edit_rx.await.unwrap();
29182 Ok(None)
29183 }
29184 });
29185
29186 start_edit_rx.await.unwrap();
29187 buffer2
29188 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
29189 .unwrap();
29190
29191 done_edit_tx.send(()).unwrap();
29192
29193 save.await.unwrap();
29194 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
29195}
29196
29197#[gpui::test]
29198fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
29199 init_test(cx, |_| {});
29200
29201 let editor = cx.add_window(|window, cx| {
29202 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
29203 build_editor(buffer, window, cx)
29204 });
29205
29206 editor
29207 .update(cx, |editor, window, cx| {
29208 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
29209 s.select_display_ranges([
29210 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
29211 ])
29212 });
29213
29214 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
29215
29216 assert_eq!(
29217 editor.display_text(cx),
29218 "line1\nline2\nline2",
29219 "Duplicating last line upward should create duplicate above, not on same line"
29220 );
29221
29222 assert_eq!(
29223 editor
29224 .selections
29225 .display_ranges(&editor.display_snapshot(cx)),
29226 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
29227 "Selection should move to the duplicated line"
29228 );
29229 })
29230 .unwrap();
29231}
29232
29233#[gpui::test]
29234async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
29235 init_test(cx, |_| {});
29236
29237 let mut cx = EditorTestContext::new(cx).await;
29238
29239 cx.set_state("line1\nline2ˇ");
29240
29241 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
29242
29243 let clipboard_text = cx
29244 .read_from_clipboard()
29245 .and_then(|item| item.text().as_deref().map(str::to_string));
29246
29247 assert_eq!(
29248 clipboard_text,
29249 Some("line2\n".to_string()),
29250 "Copying a line without trailing newline should include a newline"
29251 );
29252
29253 cx.set_state("line1\nˇ");
29254
29255 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
29256
29257 cx.assert_editor_state("line1\nline2\nˇ");
29258}
29259
29260#[gpui::test]
29261async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
29262 init_test(cx, |_| {});
29263
29264 let mut cx = EditorTestContext::new(cx).await;
29265
29266 cx.set_state("ˇline1\nˇline2\nˇline3\n");
29267
29268 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
29269
29270 let clipboard_text = cx
29271 .read_from_clipboard()
29272 .and_then(|item| item.text().as_deref().map(str::to_string));
29273
29274 assert_eq!(
29275 clipboard_text,
29276 Some("line1\nline2\nline3\n".to_string()),
29277 "Copying multiple lines should include a single newline between lines"
29278 );
29279
29280 cx.set_state("lineA\nˇ");
29281
29282 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
29283
29284 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
29285}
29286
29287#[gpui::test]
29288async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
29289 init_test(cx, |_| {});
29290
29291 let mut cx = EditorTestContext::new(cx).await;
29292
29293 cx.set_state("ˇline1\nˇline2\nˇline3\n");
29294
29295 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
29296
29297 let clipboard_text = cx
29298 .read_from_clipboard()
29299 .and_then(|item| item.text().as_deref().map(str::to_string));
29300
29301 assert_eq!(
29302 clipboard_text,
29303 Some("line1\nline2\nline3\n".to_string()),
29304 "Copying multiple lines should include a single newline between lines"
29305 );
29306
29307 cx.set_state("lineA\nˇ");
29308
29309 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
29310
29311 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
29312}
29313
29314#[gpui::test]
29315async fn test_end_of_editor_context(cx: &mut TestAppContext) {
29316 init_test(cx, |_| {});
29317
29318 let mut cx = EditorTestContext::new(cx).await;
29319
29320 cx.set_state("line1\nline2ˇ");
29321 cx.update_editor(|e, window, cx| {
29322 e.set_mode(EditorMode::SingleLine);
29323 assert!(e.key_context(window, cx).contains("end_of_input"));
29324 });
29325 cx.set_state("ˇline1\nline2");
29326 cx.update_editor(|e, window, cx| {
29327 assert!(!e.key_context(window, cx).contains("end_of_input"));
29328 });
29329 cx.set_state("line1ˇ\nline2");
29330 cx.update_editor(|e, window, cx| {
29331 assert!(!e.key_context(window, cx).contains("end_of_input"));
29332 });
29333}
29334
29335#[gpui::test]
29336async fn test_sticky_scroll(cx: &mut TestAppContext) {
29337 init_test(cx, |_| {});
29338 let mut cx = EditorTestContext::new(cx).await;
29339
29340 let buffer = indoc! {"
29341 ˇfn foo() {
29342 let abc = 123;
29343 }
29344 struct Bar;
29345 impl Bar {
29346 fn new() -> Self {
29347 Self
29348 }
29349 }
29350 fn baz() {
29351 }
29352 "};
29353 cx.set_state(&buffer);
29354
29355 cx.update_editor(|e, _, cx| {
29356 e.buffer()
29357 .read(cx)
29358 .as_singleton()
29359 .unwrap()
29360 .update(cx, |buffer, cx| {
29361 buffer.set_language(Some(rust_lang()), cx);
29362 })
29363 });
29364
29365 let mut sticky_headers = |offset: ScrollOffset| {
29366 cx.update_editor(|e, window, cx| {
29367 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
29368 });
29369 cx.run_until_parked();
29370 cx.update_editor(|e, window, cx| {
29371 EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
29372 .into_iter()
29373 .map(
29374 |StickyHeader {
29375 start_point,
29376 offset,
29377 ..
29378 }| { (start_point, offset) },
29379 )
29380 .collect::<Vec<_>>()
29381 })
29382 };
29383
29384 let fn_foo = Point { row: 0, column: 0 };
29385 let impl_bar = Point { row: 4, column: 0 };
29386 let fn_new = Point { row: 5, column: 4 };
29387
29388 assert_eq!(sticky_headers(0.0), vec![]);
29389 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
29390 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
29391 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
29392 assert_eq!(sticky_headers(2.0), vec![]);
29393 assert_eq!(sticky_headers(2.5), vec![]);
29394 assert_eq!(sticky_headers(3.0), vec![]);
29395 assert_eq!(sticky_headers(3.5), vec![]);
29396 assert_eq!(sticky_headers(4.0), vec![]);
29397 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
29398 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
29399 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
29400 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
29401 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
29402 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
29403 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
29404 assert_eq!(sticky_headers(8.0), vec![]);
29405 assert_eq!(sticky_headers(8.5), vec![]);
29406 assert_eq!(sticky_headers(9.0), vec![]);
29407 assert_eq!(sticky_headers(9.5), vec![]);
29408 assert_eq!(sticky_headers(10.0), vec![]);
29409}
29410
29411#[gpui::test]
29412async fn test_sticky_scroll_with_expanded_deleted_diff_hunks(
29413 executor: BackgroundExecutor,
29414 cx: &mut TestAppContext,
29415) {
29416 init_test(cx, |_| {});
29417 let mut cx = EditorTestContext::new(cx).await;
29418
29419 let diff_base = indoc! {"
29420 fn foo() {
29421 let a = 1;
29422 let b = 2;
29423 let c = 3;
29424 let d = 4;
29425 let e = 5;
29426 }
29427 "};
29428
29429 let buffer = indoc! {"
29430 ˇfn foo() {
29431 }
29432 "};
29433
29434 cx.set_state(&buffer);
29435
29436 cx.update_editor(|e, _, cx| {
29437 e.buffer()
29438 .read(cx)
29439 .as_singleton()
29440 .unwrap()
29441 .update(cx, |buffer, cx| {
29442 buffer.set_language(Some(rust_lang()), cx);
29443 })
29444 });
29445
29446 cx.set_head_text(diff_base);
29447 executor.run_until_parked();
29448
29449 cx.update_editor(|editor, window, cx| {
29450 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
29451 });
29452 executor.run_until_parked();
29453
29454 // After expanding, the display should look like:
29455 // row 0: fn foo() {
29456 // row 1: - let a = 1; (deleted)
29457 // row 2: - let b = 2; (deleted)
29458 // row 3: - let c = 3; (deleted)
29459 // row 4: - let d = 4; (deleted)
29460 // row 5: - let e = 5; (deleted)
29461 // row 6: }
29462 //
29463 // fn foo() spans display rows 0-6. Scrolling into the deleted region
29464 // (rows 1-5) should still show fn foo() as a sticky header.
29465
29466 let fn_foo = Point { row: 0, column: 0 };
29467
29468 let mut sticky_headers = |offset: ScrollOffset| {
29469 cx.update_editor(|e, window, cx| {
29470 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
29471 });
29472 cx.run_until_parked();
29473 cx.update_editor(|e, window, cx| {
29474 EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
29475 .into_iter()
29476 .map(
29477 |StickyHeader {
29478 start_point,
29479 offset,
29480 ..
29481 }| { (start_point, offset) },
29482 )
29483 .collect::<Vec<_>>()
29484 })
29485 };
29486
29487 assert_eq!(sticky_headers(0.0), vec![]);
29488 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
29489 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
29490 // Scrolling into deleted lines: fn foo() should still be a sticky header.
29491 assert_eq!(sticky_headers(2.0), vec![(fn_foo, 0.0)]);
29492 assert_eq!(sticky_headers(3.0), vec![(fn_foo, 0.0)]);
29493 assert_eq!(sticky_headers(4.0), vec![(fn_foo, 0.0)]);
29494 assert_eq!(sticky_headers(5.0), vec![(fn_foo, 0.0)]);
29495 assert_eq!(sticky_headers(5.5), vec![(fn_foo, -0.5)]);
29496 // Past the closing brace: no more sticky header.
29497 assert_eq!(sticky_headers(6.0), vec![]);
29498}
29499
29500#[gpui::test]
29501fn test_relative_line_numbers(cx: &mut TestAppContext) {
29502 init_test(cx, |_| {});
29503
29504 let buffer_1 = cx.new(|cx| Buffer::local("aaaaaaaaaa\nbbb\n", cx));
29505 let buffer_2 = cx.new(|cx| Buffer::local("cccccccccc\nddd\n", cx));
29506 let buffer_3 = cx.new(|cx| Buffer::local("eee\nffffffffff\n", cx));
29507
29508 let multibuffer = cx.new(|cx| {
29509 let mut multibuffer = MultiBuffer::new(ReadWrite);
29510 multibuffer.push_excerpts(
29511 buffer_1.clone(),
29512 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
29513 cx,
29514 );
29515 multibuffer.push_excerpts(
29516 buffer_2.clone(),
29517 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
29518 cx,
29519 );
29520 multibuffer.push_excerpts(
29521 buffer_3.clone(),
29522 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
29523 cx,
29524 );
29525 multibuffer
29526 });
29527
29528 // wrapped contents of multibuffer:
29529 // aaa
29530 // aaa
29531 // aaa
29532 // a
29533 // bbb
29534 //
29535 // ccc
29536 // ccc
29537 // ccc
29538 // c
29539 // ddd
29540 //
29541 // eee
29542 // fff
29543 // fff
29544 // fff
29545 // f
29546
29547 let editor = cx.add_window(|window, cx| build_editor(multibuffer, window, cx));
29548 _ = editor.update(cx, |editor, window, cx| {
29549 editor.set_wrap_width(Some(30.0.into()), cx); // every 3 characters
29550
29551 // includes trailing newlines.
29552 let expected_line_numbers = [2, 6, 7, 10, 14, 15, 18, 19, 23];
29553 let expected_wrapped_line_numbers = [
29554 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23,
29555 ];
29556
29557 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
29558 s.select_ranges([
29559 Point::new(7, 0)..Point::new(7, 1), // second row of `ccc`
29560 ]);
29561 });
29562
29563 let snapshot = editor.snapshot(window, cx);
29564
29565 // these are all 0-indexed
29566 let base_display_row = DisplayRow(11);
29567 let base_row = 3;
29568 let wrapped_base_row = 7;
29569
29570 // test not counting wrapped lines
29571 let expected_relative_numbers = expected_line_numbers
29572 .into_iter()
29573 .enumerate()
29574 .map(|(i, row)| (DisplayRow(row), i.abs_diff(base_row) as u32))
29575 .filter(|(_, relative_line_number)| *relative_line_number != 0)
29576 .collect_vec();
29577 let actual_relative_numbers = snapshot
29578 .calculate_relative_line_numbers(
29579 &(DisplayRow(0)..DisplayRow(24)),
29580 base_display_row,
29581 false,
29582 )
29583 .into_iter()
29584 .sorted()
29585 .collect_vec();
29586 assert_eq!(expected_relative_numbers, actual_relative_numbers);
29587 // check `calculate_relative_line_numbers()` against `relative_line_delta()` for each line
29588 for (display_row, relative_number) in expected_relative_numbers {
29589 assert_eq!(
29590 relative_number,
29591 snapshot
29592 .relative_line_delta(display_row, base_display_row, false)
29593 .unsigned_abs() as u32,
29594 );
29595 }
29596
29597 // test counting wrapped lines
29598 let expected_wrapped_relative_numbers = expected_wrapped_line_numbers
29599 .into_iter()
29600 .enumerate()
29601 .map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32))
29602 .filter(|(row, _)| *row != base_display_row)
29603 .collect_vec();
29604 let actual_relative_numbers = snapshot
29605 .calculate_relative_line_numbers(
29606 &(DisplayRow(0)..DisplayRow(24)),
29607 base_display_row,
29608 true,
29609 )
29610 .into_iter()
29611 .sorted()
29612 .collect_vec();
29613 assert_eq!(expected_wrapped_relative_numbers, actual_relative_numbers);
29614 // check `calculate_relative_line_numbers()` against `relative_wrapped_line_delta()` for each line
29615 for (display_row, relative_number) in expected_wrapped_relative_numbers {
29616 assert_eq!(
29617 relative_number,
29618 snapshot
29619 .relative_line_delta(display_row, base_display_row, true)
29620 .unsigned_abs() as u32,
29621 );
29622 }
29623 });
29624}
29625
29626#[gpui::test]
29627async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
29628 init_test(cx, |_| {});
29629 cx.update(|cx| {
29630 SettingsStore::update_global(cx, |store, cx| {
29631 store.update_user_settings(cx, |settings| {
29632 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
29633 enabled: Some(true),
29634 })
29635 });
29636 });
29637 });
29638 let mut cx = EditorTestContext::new(cx).await;
29639
29640 let line_height = cx.update_editor(|editor, window, cx| {
29641 editor
29642 .style(cx)
29643 .text
29644 .line_height_in_pixels(window.rem_size())
29645 });
29646
29647 let buffer = indoc! {"
29648 ˇfn foo() {
29649 let abc = 123;
29650 }
29651 struct Bar;
29652 impl Bar {
29653 fn new() -> Self {
29654 Self
29655 }
29656 }
29657 fn baz() {
29658 }
29659 "};
29660 cx.set_state(&buffer);
29661
29662 cx.update_editor(|e, _, cx| {
29663 e.buffer()
29664 .read(cx)
29665 .as_singleton()
29666 .unwrap()
29667 .update(cx, |buffer, cx| {
29668 buffer.set_language(Some(rust_lang()), cx);
29669 })
29670 });
29671
29672 let fn_foo = || empty_range(0, 0);
29673 let impl_bar = || empty_range(4, 0);
29674 let fn_new = || empty_range(5, 4);
29675
29676 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
29677 cx.update_editor(|e, window, cx| {
29678 e.scroll(
29679 gpui::Point {
29680 x: 0.,
29681 y: scroll_offset,
29682 },
29683 None,
29684 window,
29685 cx,
29686 );
29687 });
29688 cx.run_until_parked();
29689 cx.simulate_click(
29690 gpui::Point {
29691 x: px(0.),
29692 y: click_offset as f32 * line_height,
29693 },
29694 Modifiers::none(),
29695 );
29696 cx.run_until_parked();
29697 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
29698 };
29699 assert_eq!(
29700 scroll_and_click(
29701 4.5, // impl Bar is halfway off the screen
29702 0.0 // click top of screen
29703 ),
29704 // scrolled to impl Bar
29705 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29706 );
29707
29708 assert_eq!(
29709 scroll_and_click(
29710 4.5, // impl Bar is halfway off the screen
29711 0.25 // click middle of impl Bar
29712 ),
29713 // scrolled to impl Bar
29714 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29715 );
29716
29717 assert_eq!(
29718 scroll_and_click(
29719 4.5, // impl Bar is halfway off the screen
29720 1.5 // click below impl Bar (e.g. fn new())
29721 ),
29722 // scrolled to fn new() - this is below the impl Bar header which has persisted
29723 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
29724 );
29725
29726 assert_eq!(
29727 scroll_and_click(
29728 5.5, // fn new is halfway underneath impl Bar
29729 0.75 // click on the overlap of impl Bar and fn new()
29730 ),
29731 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29732 );
29733
29734 assert_eq!(
29735 scroll_and_click(
29736 5.5, // fn new is halfway underneath impl Bar
29737 1.25 // click on the visible part of fn new()
29738 ),
29739 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
29740 );
29741
29742 assert_eq!(
29743 scroll_and_click(
29744 1.5, // fn foo is halfway off the screen
29745 0.0 // click top of screen
29746 ),
29747 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
29748 );
29749
29750 assert_eq!(
29751 scroll_and_click(
29752 1.5, // fn foo is halfway off the screen
29753 0.75 // click visible part of let abc...
29754 )
29755 .0,
29756 // no change in scroll
29757 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
29758 (gpui::Point { x: 0., y: 1.5 })
29759 );
29760}
29761
29762#[gpui::test]
29763async fn test_next_prev_reference(cx: &mut TestAppContext) {
29764 const CYCLE_POSITIONS: &[&'static str] = &[
29765 indoc! {"
29766 fn foo() {
29767 let ˇabc = 123;
29768 let x = abc + 1;
29769 let y = abc + 2;
29770 let z = abc + 2;
29771 }
29772 "},
29773 indoc! {"
29774 fn foo() {
29775 let abc = 123;
29776 let x = ˇabc + 1;
29777 let y = abc + 2;
29778 let z = abc + 2;
29779 }
29780 "},
29781 indoc! {"
29782 fn foo() {
29783 let abc = 123;
29784 let x = abc + 1;
29785 let y = ˇabc + 2;
29786 let z = abc + 2;
29787 }
29788 "},
29789 indoc! {"
29790 fn foo() {
29791 let abc = 123;
29792 let x = abc + 1;
29793 let y = abc + 2;
29794 let z = ˇabc + 2;
29795 }
29796 "},
29797 ];
29798
29799 init_test(cx, |_| {});
29800
29801 let mut cx = EditorLspTestContext::new_rust(
29802 lsp::ServerCapabilities {
29803 references_provider: Some(lsp::OneOf::Left(true)),
29804 ..Default::default()
29805 },
29806 cx,
29807 )
29808 .await;
29809
29810 // importantly, the cursor is in the middle
29811 cx.set_state(indoc! {"
29812 fn foo() {
29813 let aˇbc = 123;
29814 let x = abc + 1;
29815 let y = abc + 2;
29816 let z = abc + 2;
29817 }
29818 "});
29819
29820 let reference_ranges = [
29821 lsp::Position::new(1, 8),
29822 lsp::Position::new(2, 12),
29823 lsp::Position::new(3, 12),
29824 lsp::Position::new(4, 12),
29825 ]
29826 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
29827
29828 cx.lsp
29829 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
29830 Ok(Some(
29831 reference_ranges
29832 .map(|range| lsp::Location {
29833 uri: params.text_document_position.text_document.uri.clone(),
29834 range,
29835 })
29836 .to_vec(),
29837 ))
29838 });
29839
29840 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
29841 cx.update_editor(|editor, window, cx| {
29842 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
29843 })
29844 .unwrap()
29845 .await
29846 .unwrap()
29847 };
29848
29849 _move(Direction::Next, 1, &mut cx).await;
29850 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29851
29852 _move(Direction::Next, 1, &mut cx).await;
29853 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29854
29855 _move(Direction::Next, 1, &mut cx).await;
29856 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29857
29858 // loops back to the start
29859 _move(Direction::Next, 1, &mut cx).await;
29860 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29861
29862 // loops back to the end
29863 _move(Direction::Prev, 1, &mut cx).await;
29864 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29865
29866 _move(Direction::Prev, 1, &mut cx).await;
29867 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29868
29869 _move(Direction::Prev, 1, &mut cx).await;
29870 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29871
29872 _move(Direction::Prev, 1, &mut cx).await;
29873 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29874
29875 _move(Direction::Next, 3, &mut cx).await;
29876 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29877
29878 _move(Direction::Prev, 2, &mut cx).await;
29879 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29880}
29881
29882#[gpui::test]
29883async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
29884 init_test(cx, |_| {});
29885
29886 let (editor, cx) = cx.add_window_view(|window, cx| {
29887 let multi_buffer = MultiBuffer::build_multi(
29888 [
29889 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29890 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29891 ],
29892 cx,
29893 );
29894 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29895 });
29896
29897 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29898 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
29899
29900 cx.assert_excerpts_with_selections(indoc! {"
29901 [EXCERPT]
29902 ˇ1
29903 2
29904 3
29905 [EXCERPT]
29906 1
29907 2
29908 3
29909 "});
29910
29911 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
29912 cx.update_editor(|editor, window, cx| {
29913 editor.change_selections(None.into(), window, cx, |s| {
29914 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29915 });
29916 });
29917 cx.assert_excerpts_with_selections(indoc! {"
29918 [EXCERPT]
29919 1
29920 2ˇ
29921 3
29922 [EXCERPT]
29923 1
29924 2
29925 3
29926 "});
29927
29928 cx.update_editor(|editor, window, cx| {
29929 editor
29930 .select_all_matches(&SelectAllMatches, window, cx)
29931 .unwrap();
29932 });
29933 cx.assert_excerpts_with_selections(indoc! {"
29934 [EXCERPT]
29935 1
29936 2ˇ
29937 3
29938 [EXCERPT]
29939 1
29940 2ˇ
29941 3
29942 "});
29943
29944 cx.update_editor(|editor, window, cx| {
29945 editor.handle_input("X", window, cx);
29946 });
29947 cx.assert_excerpts_with_selections(indoc! {"
29948 [EXCERPT]
29949 1
29950 Xˇ
29951 3
29952 [EXCERPT]
29953 1
29954 Xˇ
29955 3
29956 "});
29957
29958 // Scenario 2: Select "2", then fold second buffer before insertion
29959 cx.update_multibuffer(|mb, cx| {
29960 for buffer_id in buffer_ids.iter() {
29961 let buffer = mb.buffer(*buffer_id).unwrap();
29962 buffer.update(cx, |buffer, cx| {
29963 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29964 });
29965 }
29966 });
29967
29968 // Select "2" and select all matches
29969 cx.update_editor(|editor, window, cx| {
29970 editor.change_selections(None.into(), window, cx, |s| {
29971 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29972 });
29973 editor
29974 .select_all_matches(&SelectAllMatches, window, cx)
29975 .unwrap();
29976 });
29977
29978 // Fold second buffer - should remove selections from folded buffer
29979 cx.update_editor(|editor, _, cx| {
29980 editor.fold_buffer(buffer_ids[1], cx);
29981 });
29982 cx.assert_excerpts_with_selections(indoc! {"
29983 [EXCERPT]
29984 1
29985 2ˇ
29986 3
29987 [EXCERPT]
29988 [FOLDED]
29989 "});
29990
29991 // Insert text - should only affect first buffer
29992 cx.update_editor(|editor, window, cx| {
29993 editor.handle_input("Y", window, cx);
29994 });
29995 cx.update_editor(|editor, _, cx| {
29996 editor.unfold_buffer(buffer_ids[1], cx);
29997 });
29998 cx.assert_excerpts_with_selections(indoc! {"
29999 [EXCERPT]
30000 1
30001 Yˇ
30002 3
30003 [EXCERPT]
30004 1
30005 2
30006 3
30007 "});
30008
30009 // Scenario 3: Select "2", then fold first buffer before insertion
30010 cx.update_multibuffer(|mb, cx| {
30011 for buffer_id in buffer_ids.iter() {
30012 let buffer = mb.buffer(*buffer_id).unwrap();
30013 buffer.update(cx, |buffer, cx| {
30014 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
30015 });
30016 }
30017 });
30018
30019 // Select "2" and select all matches
30020 cx.update_editor(|editor, window, cx| {
30021 editor.change_selections(None.into(), window, cx, |s| {
30022 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
30023 });
30024 editor
30025 .select_all_matches(&SelectAllMatches, window, cx)
30026 .unwrap();
30027 });
30028
30029 // Fold first buffer - should remove selections from folded buffer
30030 cx.update_editor(|editor, _, cx| {
30031 editor.fold_buffer(buffer_ids[0], cx);
30032 });
30033 cx.assert_excerpts_with_selections(indoc! {"
30034 [EXCERPT]
30035 [FOLDED]
30036 [EXCERPT]
30037 1
30038 2ˇ
30039 3
30040 "});
30041
30042 // Insert text - should only affect second buffer
30043 cx.update_editor(|editor, window, cx| {
30044 editor.handle_input("Z", window, cx);
30045 });
30046 cx.update_editor(|editor, _, cx| {
30047 editor.unfold_buffer(buffer_ids[0], cx);
30048 });
30049 cx.assert_excerpts_with_selections(indoc! {"
30050 [EXCERPT]
30051 1
30052 2
30053 3
30054 [EXCERPT]
30055 1
30056 Zˇ
30057 3
30058 "});
30059
30060 // Test correct folded header is selected upon fold
30061 cx.update_editor(|editor, _, cx| {
30062 editor.fold_buffer(buffer_ids[0], cx);
30063 editor.fold_buffer(buffer_ids[1], cx);
30064 });
30065 cx.assert_excerpts_with_selections(indoc! {"
30066 [EXCERPT]
30067 [FOLDED]
30068 [EXCERPT]
30069 ˇ[FOLDED]
30070 "});
30071
30072 // Test selection inside folded buffer unfolds it on type
30073 cx.update_editor(|editor, window, cx| {
30074 editor.handle_input("W", window, cx);
30075 });
30076 cx.update_editor(|editor, _, cx| {
30077 editor.unfold_buffer(buffer_ids[0], cx);
30078 });
30079 cx.assert_excerpts_with_selections(indoc! {"
30080 [EXCERPT]
30081 1
30082 2
30083 3
30084 [EXCERPT]
30085 Wˇ1
30086 Z
30087 3
30088 "});
30089}
30090
30091#[gpui::test]
30092async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
30093 init_test(cx, |_| {});
30094
30095 let (editor, cx) = cx.add_window_view(|window, cx| {
30096 let multi_buffer = MultiBuffer::build_multi(
30097 [
30098 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
30099 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
30100 ],
30101 cx,
30102 );
30103 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
30104 });
30105
30106 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
30107
30108 cx.assert_excerpts_with_selections(indoc! {"
30109 [EXCERPT]
30110 ˇ1
30111 2
30112 3
30113 [EXCERPT]
30114 1
30115 2
30116 3
30117 4
30118 5
30119 6
30120 7
30121 8
30122 9
30123 "});
30124
30125 cx.update_editor(|editor, window, cx| {
30126 editor.change_selections(None.into(), window, cx, |s| {
30127 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
30128 });
30129 });
30130
30131 cx.assert_excerpts_with_selections(indoc! {"
30132 [EXCERPT]
30133 1
30134 2
30135 3
30136 [EXCERPT]
30137 1
30138 2
30139 3
30140 4
30141 5
30142 6
30143 ˇ7
30144 8
30145 9
30146 "});
30147
30148 cx.update_editor(|editor, _window, cx| {
30149 editor.set_vertical_scroll_margin(0, cx);
30150 });
30151
30152 cx.update_editor(|editor, window, cx| {
30153 assert_eq!(editor.vertical_scroll_margin(), 0);
30154 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
30155 assert_eq!(
30156 editor.snapshot(window, cx).scroll_position(),
30157 gpui::Point::new(0., 12.0)
30158 );
30159 });
30160
30161 cx.update_editor(|editor, _window, cx| {
30162 editor.set_vertical_scroll_margin(3, cx);
30163 });
30164
30165 cx.update_editor(|editor, window, cx| {
30166 assert_eq!(editor.vertical_scroll_margin(), 3);
30167 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
30168 assert_eq!(
30169 editor.snapshot(window, cx).scroll_position(),
30170 gpui::Point::new(0., 9.0)
30171 );
30172 });
30173}
30174
30175#[gpui::test]
30176async fn test_find_references_single_case(cx: &mut TestAppContext) {
30177 init_test(cx, |_| {});
30178 let mut cx = EditorLspTestContext::new_rust(
30179 lsp::ServerCapabilities {
30180 references_provider: Some(lsp::OneOf::Left(true)),
30181 ..lsp::ServerCapabilities::default()
30182 },
30183 cx,
30184 )
30185 .await;
30186
30187 let before = indoc!(
30188 r#"
30189 fn main() {
30190 let aˇbc = 123;
30191 let xyz = abc;
30192 }
30193 "#
30194 );
30195 let after = indoc!(
30196 r#"
30197 fn main() {
30198 let abc = 123;
30199 let xyz = ˇabc;
30200 }
30201 "#
30202 );
30203
30204 cx.lsp
30205 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
30206 Ok(Some(vec![
30207 lsp::Location {
30208 uri: params.text_document_position.text_document.uri.clone(),
30209 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
30210 },
30211 lsp::Location {
30212 uri: params.text_document_position.text_document.uri,
30213 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
30214 },
30215 ]))
30216 });
30217
30218 cx.set_state(before);
30219
30220 let action = FindAllReferences {
30221 always_open_multibuffer: false,
30222 };
30223
30224 let navigated = cx
30225 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
30226 .expect("should have spawned a task")
30227 .await
30228 .unwrap();
30229
30230 assert_eq!(navigated, Navigated::No);
30231
30232 cx.run_until_parked();
30233
30234 cx.assert_editor_state(after);
30235}
30236
30237#[gpui::test]
30238async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
30239 init_test(cx, |settings| {
30240 settings.defaults.tab_size = Some(2.try_into().unwrap());
30241 });
30242
30243 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30244 let mut cx = EditorTestContext::new(cx).await;
30245 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30246
30247 // Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
30248 cx.set_state(indoc! {"
30249 - [ ] taskˇ
30250 "});
30251 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30252 cx.wait_for_autoindent_applied().await;
30253 cx.assert_editor_state(indoc! {"
30254 - [ ] task
30255 - [ ] ˇ
30256 "});
30257
30258 // Case 2: Works with checked task items too
30259 cx.set_state(indoc! {"
30260 - [x] completed taskˇ
30261 "});
30262 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30263 cx.wait_for_autoindent_applied().await;
30264 cx.assert_editor_state(indoc! {"
30265 - [x] completed task
30266 - [ ] ˇ
30267 "});
30268
30269 // Case 2.1: Works with uppercase checked marker too
30270 cx.set_state(indoc! {"
30271 - [X] completed taskˇ
30272 "});
30273 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30274 cx.wait_for_autoindent_applied().await;
30275 cx.assert_editor_state(indoc! {"
30276 - [X] completed task
30277 - [ ] ˇ
30278 "});
30279
30280 // Case 3: Cursor position doesn't matter - content after marker is what counts
30281 cx.set_state(indoc! {"
30282 - [ ] taˇsk
30283 "});
30284 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30285 cx.wait_for_autoindent_applied().await;
30286 cx.assert_editor_state(indoc! {"
30287 - [ ] ta
30288 - [ ] ˇsk
30289 "});
30290
30291 // Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
30292 cx.set_state(indoc! {"
30293 - [ ] ˇ
30294 "});
30295 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30296 cx.wait_for_autoindent_applied().await;
30297 cx.assert_editor_state(
30298 indoc! {"
30299 - [ ]$$
30300 ˇ
30301 "}
30302 .replace("$", " ")
30303 .as_str(),
30304 );
30305
30306 // Case 5: Adding newline with content adds marker preserving indentation
30307 cx.set_state(indoc! {"
30308 - [ ] task
30309 - [ ] indentedˇ
30310 "});
30311 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30312 cx.wait_for_autoindent_applied().await;
30313 cx.assert_editor_state(indoc! {"
30314 - [ ] task
30315 - [ ] indented
30316 - [ ] ˇ
30317 "});
30318
30319 // Case 6: Adding newline with cursor right after prefix, unindents
30320 cx.set_state(indoc! {"
30321 - [ ] task
30322 - [ ] sub task
30323 - [ ] ˇ
30324 "});
30325 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30326 cx.wait_for_autoindent_applied().await;
30327 cx.assert_editor_state(indoc! {"
30328 - [ ] task
30329 - [ ] sub task
30330 - [ ] ˇ
30331 "});
30332 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30333 cx.wait_for_autoindent_applied().await;
30334
30335 // Case 7: Adding newline with cursor right after prefix, removes marker
30336 cx.assert_editor_state(indoc! {"
30337 - [ ] task
30338 - [ ] sub task
30339 - [ ] ˇ
30340 "});
30341 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30342 cx.wait_for_autoindent_applied().await;
30343 cx.assert_editor_state(indoc! {"
30344 - [ ] task
30345 - [ ] sub task
30346 ˇ
30347 "});
30348
30349 // Case 8: Cursor before or inside prefix does not add marker
30350 cx.set_state(indoc! {"
30351 ˇ- [ ] task
30352 "});
30353 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30354 cx.wait_for_autoindent_applied().await;
30355 cx.assert_editor_state(indoc! {"
30356
30357 ˇ- [ ] task
30358 "});
30359
30360 cx.set_state(indoc! {"
30361 - [ˇ ] task
30362 "});
30363 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30364 cx.wait_for_autoindent_applied().await;
30365 cx.assert_editor_state(indoc! {"
30366 - [
30367 ˇ
30368 ] task
30369 "});
30370}
30371
30372#[gpui::test]
30373async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
30374 init_test(cx, |settings| {
30375 settings.defaults.tab_size = Some(2.try_into().unwrap());
30376 });
30377
30378 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30379 let mut cx = EditorTestContext::new(cx).await;
30380 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30381
30382 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
30383 cx.set_state(indoc! {"
30384 - itemˇ
30385 "});
30386 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30387 cx.wait_for_autoindent_applied().await;
30388 cx.assert_editor_state(indoc! {"
30389 - item
30390 - ˇ
30391 "});
30392
30393 // Case 2: Works with different markers
30394 cx.set_state(indoc! {"
30395 * starred itemˇ
30396 "});
30397 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30398 cx.wait_for_autoindent_applied().await;
30399 cx.assert_editor_state(indoc! {"
30400 * starred item
30401 * ˇ
30402 "});
30403
30404 cx.set_state(indoc! {"
30405 + plus itemˇ
30406 "});
30407 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30408 cx.wait_for_autoindent_applied().await;
30409 cx.assert_editor_state(indoc! {"
30410 + plus item
30411 + ˇ
30412 "});
30413
30414 // Case 3: Cursor position doesn't matter - content after marker is what counts
30415 cx.set_state(indoc! {"
30416 - itˇem
30417 "});
30418 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30419 cx.wait_for_autoindent_applied().await;
30420 cx.assert_editor_state(indoc! {"
30421 - it
30422 - ˇem
30423 "});
30424
30425 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
30426 cx.set_state(indoc! {"
30427 - ˇ
30428 "});
30429 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30430 cx.wait_for_autoindent_applied().await;
30431 cx.assert_editor_state(
30432 indoc! {"
30433 - $
30434 ˇ
30435 "}
30436 .replace("$", " ")
30437 .as_str(),
30438 );
30439
30440 // Case 5: Adding newline with content adds marker preserving indentation
30441 cx.set_state(indoc! {"
30442 - item
30443 - indentedˇ
30444 "});
30445 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30446 cx.wait_for_autoindent_applied().await;
30447 cx.assert_editor_state(indoc! {"
30448 - item
30449 - indented
30450 - ˇ
30451 "});
30452
30453 // Case 6: Adding newline with cursor right after marker, unindents
30454 cx.set_state(indoc! {"
30455 - item
30456 - sub item
30457 - ˇ
30458 "});
30459 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30460 cx.wait_for_autoindent_applied().await;
30461 cx.assert_editor_state(indoc! {"
30462 - item
30463 - sub item
30464 - ˇ
30465 "});
30466 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30467 cx.wait_for_autoindent_applied().await;
30468
30469 // Case 7: Adding newline with cursor right after marker, removes marker
30470 cx.assert_editor_state(indoc! {"
30471 - item
30472 - sub item
30473 - ˇ
30474 "});
30475 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30476 cx.wait_for_autoindent_applied().await;
30477 cx.assert_editor_state(indoc! {"
30478 - item
30479 - sub item
30480 ˇ
30481 "});
30482
30483 // Case 8: Cursor before or inside prefix does not add marker
30484 cx.set_state(indoc! {"
30485 ˇ- item
30486 "});
30487 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30488 cx.wait_for_autoindent_applied().await;
30489 cx.assert_editor_state(indoc! {"
30490
30491 ˇ- item
30492 "});
30493
30494 cx.set_state(indoc! {"
30495 -ˇ item
30496 "});
30497 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30498 cx.wait_for_autoindent_applied().await;
30499 cx.assert_editor_state(indoc! {"
30500 -
30501 ˇitem
30502 "});
30503}
30504
30505#[gpui::test]
30506async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
30507 init_test(cx, |settings| {
30508 settings.defaults.tab_size = Some(2.try_into().unwrap());
30509 });
30510
30511 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30512 let mut cx = EditorTestContext::new(cx).await;
30513 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30514
30515 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
30516 cx.set_state(indoc! {"
30517 1. first itemˇ
30518 "});
30519 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30520 cx.wait_for_autoindent_applied().await;
30521 cx.assert_editor_state(indoc! {"
30522 1. first item
30523 2. ˇ
30524 "});
30525
30526 // Case 2: Works with larger numbers
30527 cx.set_state(indoc! {"
30528 10. tenth itemˇ
30529 "});
30530 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30531 cx.wait_for_autoindent_applied().await;
30532 cx.assert_editor_state(indoc! {"
30533 10. tenth item
30534 11. ˇ
30535 "});
30536
30537 // Case 3: Cursor position doesn't matter - content after marker is what counts
30538 cx.set_state(indoc! {"
30539 1. itˇem
30540 "});
30541 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30542 cx.wait_for_autoindent_applied().await;
30543 cx.assert_editor_state(indoc! {"
30544 1. it
30545 2. ˇem
30546 "});
30547
30548 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
30549 cx.set_state(indoc! {"
30550 1. ˇ
30551 "});
30552 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30553 cx.wait_for_autoindent_applied().await;
30554 cx.assert_editor_state(
30555 indoc! {"
30556 1. $
30557 ˇ
30558 "}
30559 .replace("$", " ")
30560 .as_str(),
30561 );
30562
30563 // Case 5: Adding newline with content adds marker preserving indentation
30564 cx.set_state(indoc! {"
30565 1. item
30566 2. indentedˇ
30567 "});
30568 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30569 cx.wait_for_autoindent_applied().await;
30570 cx.assert_editor_state(indoc! {"
30571 1. item
30572 2. indented
30573 3. ˇ
30574 "});
30575
30576 // Case 6: Adding newline with cursor right after marker, unindents
30577 cx.set_state(indoc! {"
30578 1. item
30579 2. sub item
30580 3. ˇ
30581 "});
30582 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30583 cx.wait_for_autoindent_applied().await;
30584 cx.assert_editor_state(indoc! {"
30585 1. item
30586 2. sub item
30587 1. ˇ
30588 "});
30589 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30590 cx.wait_for_autoindent_applied().await;
30591
30592 // Case 7: Adding newline with cursor right after marker, removes marker
30593 cx.assert_editor_state(indoc! {"
30594 1. item
30595 2. sub item
30596 1. ˇ
30597 "});
30598 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30599 cx.wait_for_autoindent_applied().await;
30600 cx.assert_editor_state(indoc! {"
30601 1. item
30602 2. sub item
30603 ˇ
30604 "});
30605
30606 // Case 8: Cursor before or inside prefix does not add marker
30607 cx.set_state(indoc! {"
30608 ˇ1. item
30609 "});
30610 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30611 cx.wait_for_autoindent_applied().await;
30612 cx.assert_editor_state(indoc! {"
30613
30614 ˇ1. item
30615 "});
30616
30617 cx.set_state(indoc! {"
30618 1ˇ. item
30619 "});
30620 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30621 cx.wait_for_autoindent_applied().await;
30622 cx.assert_editor_state(indoc! {"
30623 1
30624 ˇ. item
30625 "});
30626}
30627
30628#[gpui::test]
30629async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
30630 init_test(cx, |settings| {
30631 settings.defaults.tab_size = Some(2.try_into().unwrap());
30632 });
30633
30634 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30635 let mut cx = EditorTestContext::new(cx).await;
30636 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30637
30638 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
30639 cx.set_state(indoc! {"
30640 1. first item
30641 1. sub first item
30642 2. sub second item
30643 3. ˇ
30644 "});
30645 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30646 cx.wait_for_autoindent_applied().await;
30647 cx.assert_editor_state(indoc! {"
30648 1. first item
30649 1. sub first item
30650 2. sub second item
30651 1. ˇ
30652 "});
30653}
30654
30655#[gpui::test]
30656async fn test_tab_list_indent(cx: &mut TestAppContext) {
30657 init_test(cx, |settings| {
30658 settings.defaults.tab_size = Some(2.try_into().unwrap());
30659 });
30660
30661 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30662 let mut cx = EditorTestContext::new(cx).await;
30663 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30664
30665 // Case 1: Unordered list - cursor after prefix, adds indent before prefix
30666 cx.set_state(indoc! {"
30667 - ˇitem
30668 "});
30669 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30670 cx.wait_for_autoindent_applied().await;
30671 let expected = indoc! {"
30672 $$- ˇitem
30673 "};
30674 cx.assert_editor_state(expected.replace("$", " ").as_str());
30675
30676 // Case 2: Task list - cursor after prefix
30677 cx.set_state(indoc! {"
30678 - [ ] ˇtask
30679 "});
30680 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30681 cx.wait_for_autoindent_applied().await;
30682 let expected = indoc! {"
30683 $$- [ ] ˇtask
30684 "};
30685 cx.assert_editor_state(expected.replace("$", " ").as_str());
30686
30687 // Case 3: Ordered list - cursor after prefix
30688 cx.set_state(indoc! {"
30689 1. ˇfirst
30690 "});
30691 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30692 cx.wait_for_autoindent_applied().await;
30693 let expected = indoc! {"
30694 $$1. ˇfirst
30695 "};
30696 cx.assert_editor_state(expected.replace("$", " ").as_str());
30697
30698 // Case 4: With existing indentation - adds more indent
30699 let initial = indoc! {"
30700 $$- ˇitem
30701 "};
30702 cx.set_state(initial.replace("$", " ").as_str());
30703 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30704 cx.wait_for_autoindent_applied().await;
30705 let expected = indoc! {"
30706 $$$$- ˇitem
30707 "};
30708 cx.assert_editor_state(expected.replace("$", " ").as_str());
30709
30710 // Case 5: Empty list item
30711 cx.set_state(indoc! {"
30712 - ˇ
30713 "});
30714 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30715 cx.wait_for_autoindent_applied().await;
30716 let expected = indoc! {"
30717 $$- ˇ
30718 "};
30719 cx.assert_editor_state(expected.replace("$", " ").as_str());
30720
30721 // Case 6: Cursor at end of line with content
30722 cx.set_state(indoc! {"
30723 - itemˇ
30724 "});
30725 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30726 cx.wait_for_autoindent_applied().await;
30727 let expected = indoc! {"
30728 $$- itemˇ
30729 "};
30730 cx.assert_editor_state(expected.replace("$", " ").as_str());
30731
30732 // Case 7: Cursor at start of list item, indents it
30733 cx.set_state(indoc! {"
30734 - item
30735 ˇ - sub item
30736 "});
30737 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30738 cx.wait_for_autoindent_applied().await;
30739 let expected = indoc! {"
30740 - item
30741 ˇ - sub item
30742 "};
30743 cx.assert_editor_state(expected);
30744
30745 // Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
30746 cx.update_editor(|_, _, cx| {
30747 SettingsStore::update_global(cx, |store, cx| {
30748 store.update_user_settings(cx, |settings| {
30749 settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
30750 });
30751 });
30752 });
30753 cx.set_state(indoc! {"
30754 - item
30755 ˇ - sub item
30756 "});
30757 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30758 cx.wait_for_autoindent_applied().await;
30759 let expected = indoc! {"
30760 - item
30761 ˇ- sub item
30762 "};
30763 cx.assert_editor_state(expected);
30764}
30765
30766#[gpui::test]
30767async fn test_local_worktree_trust(cx: &mut TestAppContext) {
30768 init_test(cx, |_| {});
30769 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), cx));
30770
30771 cx.update(|cx| {
30772 SettingsStore::update_global(cx, |store, cx| {
30773 store.update_user_settings(cx, |settings| {
30774 settings.project.all_languages.defaults.inlay_hints =
30775 Some(InlayHintSettingsContent {
30776 enabled: Some(true),
30777 ..InlayHintSettingsContent::default()
30778 });
30779 });
30780 });
30781 });
30782
30783 let fs = FakeFs::new(cx.executor());
30784 fs.insert_tree(
30785 path!("/project"),
30786 json!({
30787 ".zed": {
30788 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
30789 },
30790 "main.rs": "fn main() {}"
30791 }),
30792 )
30793 .await;
30794
30795 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
30796 let server_name = "override-rust-analyzer";
30797 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
30798
30799 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
30800 language_registry.add(rust_lang());
30801
30802 let capabilities = lsp::ServerCapabilities {
30803 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
30804 ..lsp::ServerCapabilities::default()
30805 };
30806 let mut fake_language_servers = language_registry.register_fake_lsp(
30807 "Rust",
30808 FakeLspAdapter {
30809 name: server_name,
30810 capabilities,
30811 initializer: Some(Box::new({
30812 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30813 move |fake_server| {
30814 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30815 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
30816 move |_params, _| {
30817 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
30818 async move {
30819 Ok(Some(vec![lsp::InlayHint {
30820 position: lsp::Position::new(0, 0),
30821 label: lsp::InlayHintLabel::String("hint".to_string()),
30822 kind: None,
30823 text_edits: None,
30824 tooltip: None,
30825 padding_left: None,
30826 padding_right: None,
30827 data: None,
30828 }]))
30829 }
30830 },
30831 );
30832 }
30833 })),
30834 ..FakeLspAdapter::default()
30835 },
30836 );
30837
30838 cx.run_until_parked();
30839
30840 let worktree_id = project.read_with(cx, |project, cx| {
30841 project
30842 .worktrees(cx)
30843 .next()
30844 .map(|wt| wt.read(cx).id())
30845 .expect("should have a worktree")
30846 });
30847 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
30848
30849 let trusted_worktrees =
30850 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
30851
30852 let can_trust = trusted_worktrees.update(cx, |store, cx| {
30853 store.can_trust(&worktree_store, worktree_id, cx)
30854 });
30855 assert!(!can_trust, "worktree should be restricted initially");
30856
30857 let buffer_before_approval = project
30858 .update(cx, |project, cx| {
30859 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
30860 })
30861 .await
30862 .unwrap();
30863
30864 let (editor, cx) = cx.add_window_view(|window, cx| {
30865 Editor::new(
30866 EditorMode::full(),
30867 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
30868 Some(project.clone()),
30869 window,
30870 cx,
30871 )
30872 });
30873 cx.run_until_parked();
30874 let fake_language_server = fake_language_servers.next();
30875
30876 cx.read(|cx| {
30877 let file = buffer_before_approval.read(cx).file();
30878 assert_eq!(
30879 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30880 .language_servers,
30881 ["...".to_string()],
30882 "local .zed/settings.json must not apply before trust approval"
30883 )
30884 });
30885
30886 editor.update_in(cx, |editor, window, cx| {
30887 editor.handle_input("1", window, cx);
30888 });
30889 cx.run_until_parked();
30890 cx.executor()
30891 .advance_clock(std::time::Duration::from_secs(1));
30892 assert_eq!(
30893 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
30894 0,
30895 "inlay hints must not be queried before trust approval"
30896 );
30897
30898 trusted_worktrees.update(cx, |store, cx| {
30899 store.trust(
30900 &worktree_store,
30901 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
30902 cx,
30903 );
30904 });
30905 cx.run_until_parked();
30906
30907 cx.read(|cx| {
30908 let file = buffer_before_approval.read(cx).file();
30909 assert_eq!(
30910 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30911 .language_servers,
30912 ["override-rust-analyzer".to_string()],
30913 "local .zed/settings.json should apply after trust approval"
30914 )
30915 });
30916 let _fake_language_server = fake_language_server.await.unwrap();
30917 editor.update_in(cx, |editor, window, cx| {
30918 editor.handle_input("1", window, cx);
30919 });
30920 cx.run_until_parked();
30921 cx.executor()
30922 .advance_clock(std::time::Duration::from_secs(1));
30923 assert!(
30924 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
30925 "inlay hints should be queried after trust approval"
30926 );
30927
30928 let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
30929 store.can_trust(&worktree_store, worktree_id, cx)
30930 });
30931 assert!(can_trust_after, "worktree should be trusted after trust()");
30932}
30933
30934#[gpui::test]
30935fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
30936 // This test reproduces a bug where drawing an editor at a position above the viewport
30937 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
30938 // causes an infinite loop in blocks_in_range.
30939 //
30940 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
30941 // the content mask intersection produces visible_bounds with origin at the viewport top.
30942 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
30943 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
30944 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
30945 init_test(cx, |_| {});
30946
30947 let window = cx.add_window(|_, _| gpui::Empty);
30948 let mut cx = VisualTestContext::from_window(*window, cx);
30949
30950 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
30951 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
30952
30953 // Simulate a small viewport (500x500 pixels at origin 0,0)
30954 cx.simulate_resize(gpui::size(px(500.), px(500.)));
30955
30956 // Draw the editor at a very negative Y position, simulating an editor that's been
30957 // scrolled way above the visible viewport (like in a List that has scrolled past it).
30958 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
30959 // This should NOT hang - it should just render nothing.
30960 cx.draw(
30961 gpui::point(px(0.), px(-10000.)),
30962 gpui::size(px(500.), px(3000.)),
30963 |_, _| editor.clone().into_any_element(),
30964 );
30965
30966 // If we get here without hanging, the test passes
30967}
30968
30969#[gpui::test]
30970async fn test_diff_review_indicator_created_on_gutter_hover(cx: &mut TestAppContext) {
30971 init_test(cx, |_| {});
30972
30973 let fs = FakeFs::new(cx.executor());
30974 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30975 .await;
30976
30977 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30978 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
30979 let workspace = window
30980 .read_with(cx, |mw, _| mw.workspace().clone())
30981 .unwrap();
30982 let cx = &mut VisualTestContext::from_window(*window, cx);
30983
30984 let editor = workspace
30985 .update_in(cx, |workspace, window, cx| {
30986 workspace.open_abs_path(
30987 PathBuf::from(path!("/root/file.txt")),
30988 OpenOptions::default(),
30989 window,
30990 cx,
30991 )
30992 })
30993 .await
30994 .unwrap()
30995 .downcast::<Editor>()
30996 .unwrap();
30997
30998 // Enable diff review button mode
30999 editor.update(cx, |editor, cx| {
31000 editor.set_show_diff_review_button(true, cx);
31001 });
31002
31003 // Initially, no indicator should be present
31004 editor.update(cx, |editor, _cx| {
31005 assert!(
31006 editor.gutter_diff_review_indicator.0.is_none(),
31007 "Indicator should be None initially"
31008 );
31009 });
31010}
31011
31012#[gpui::test]
31013async fn test_diff_review_button_hidden_when_ai_disabled(cx: &mut TestAppContext) {
31014 init_test(cx, |_| {});
31015
31016 // Register DisableAiSettings and set disable_ai to true
31017 cx.update(|cx| {
31018 project::DisableAiSettings::register(cx);
31019 project::DisableAiSettings::override_global(
31020 project::DisableAiSettings { disable_ai: true },
31021 cx,
31022 );
31023 });
31024
31025 let fs = FakeFs::new(cx.executor());
31026 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
31027 .await;
31028
31029 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
31030 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
31031 let workspace = window
31032 .read_with(cx, |mw, _| mw.workspace().clone())
31033 .unwrap();
31034 let cx = &mut VisualTestContext::from_window(*window, cx);
31035
31036 let editor = workspace
31037 .update_in(cx, |workspace, window, cx| {
31038 workspace.open_abs_path(
31039 PathBuf::from(path!("/root/file.txt")),
31040 OpenOptions::default(),
31041 window,
31042 cx,
31043 )
31044 })
31045 .await
31046 .unwrap()
31047 .downcast::<Editor>()
31048 .unwrap();
31049
31050 // Enable diff review button mode
31051 editor.update(cx, |editor, cx| {
31052 editor.set_show_diff_review_button(true, cx);
31053 });
31054
31055 // Verify AI is disabled
31056 cx.read(|cx| {
31057 assert!(
31058 project::DisableAiSettings::get_global(cx).disable_ai,
31059 "AI should be disabled"
31060 );
31061 });
31062
31063 // The indicator should not be created when AI is disabled
31064 // (The mouse_moved handler checks DisableAiSettings before creating the indicator)
31065 editor.update(cx, |editor, _cx| {
31066 assert!(
31067 editor.gutter_diff_review_indicator.0.is_none(),
31068 "Indicator should be None when AI is disabled"
31069 );
31070 });
31071}
31072
31073#[gpui::test]
31074async fn test_diff_review_button_shown_when_ai_enabled(cx: &mut TestAppContext) {
31075 init_test(cx, |_| {});
31076
31077 // Register DisableAiSettings and set disable_ai to false
31078 cx.update(|cx| {
31079 project::DisableAiSettings::register(cx);
31080 project::DisableAiSettings::override_global(
31081 project::DisableAiSettings { disable_ai: false },
31082 cx,
31083 );
31084 });
31085
31086 let fs = FakeFs::new(cx.executor());
31087 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
31088 .await;
31089
31090 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
31091 let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
31092 let workspace = window
31093 .read_with(cx, |mw, _| mw.workspace().clone())
31094 .unwrap();
31095 let cx = &mut VisualTestContext::from_window(*window, cx);
31096
31097 let editor = workspace
31098 .update_in(cx, |workspace, window, cx| {
31099 workspace.open_abs_path(
31100 PathBuf::from(path!("/root/file.txt")),
31101 OpenOptions::default(),
31102 window,
31103 cx,
31104 )
31105 })
31106 .await
31107 .unwrap()
31108 .downcast::<Editor>()
31109 .unwrap();
31110
31111 // Enable diff review button mode
31112 editor.update(cx, |editor, cx| {
31113 editor.set_show_diff_review_button(true, cx);
31114 });
31115
31116 // Verify AI is enabled
31117 cx.read(|cx| {
31118 assert!(
31119 !project::DisableAiSettings::get_global(cx).disable_ai,
31120 "AI should be enabled"
31121 );
31122 });
31123
31124 // The show_diff_review_button flag should be true
31125 editor.update(cx, |editor, _cx| {
31126 assert!(
31127 editor.show_diff_review_button(),
31128 "show_diff_review_button should be true"
31129 );
31130 });
31131}
31132
31133/// Helper function to create a DiffHunkKey for testing.
31134/// Uses Anchor::min() as a placeholder anchor since these tests don't need
31135/// real buffer positioning.
31136fn test_hunk_key(file_path: &str) -> DiffHunkKey {
31137 DiffHunkKey {
31138 file_path: if file_path.is_empty() {
31139 Arc::from(util::rel_path::RelPath::empty())
31140 } else {
31141 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
31142 },
31143 hunk_start_anchor: Anchor::min(),
31144 }
31145}
31146
31147/// Helper function to create a DiffHunkKey with a specific anchor for testing.
31148fn test_hunk_key_with_anchor(file_path: &str, anchor: Anchor) -> DiffHunkKey {
31149 DiffHunkKey {
31150 file_path: if file_path.is_empty() {
31151 Arc::from(util::rel_path::RelPath::empty())
31152 } else {
31153 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
31154 },
31155 hunk_start_anchor: anchor,
31156 }
31157}
31158
31159/// Helper function to add a review comment with default anchors for testing.
31160fn add_test_comment(
31161 editor: &mut Editor,
31162 key: DiffHunkKey,
31163 comment: &str,
31164 cx: &mut Context<Editor>,
31165) -> usize {
31166 editor.add_review_comment(key, comment.to_string(), Anchor::min()..Anchor::max(), cx)
31167}
31168
31169#[gpui::test]
31170fn test_review_comment_add_to_hunk(cx: &mut TestAppContext) {
31171 init_test(cx, |_| {});
31172
31173 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31174
31175 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
31176 let key = test_hunk_key("");
31177
31178 let id = add_test_comment(editor, key.clone(), "Test comment", cx);
31179
31180 let snapshot = editor.buffer().read(cx).snapshot(cx);
31181 assert_eq!(editor.total_review_comment_count(), 1);
31182 assert_eq!(editor.hunk_comment_count(&key, &snapshot), 1);
31183
31184 let comments = editor.comments_for_hunk(&key, &snapshot);
31185 assert_eq!(comments.len(), 1);
31186 assert_eq!(comments[0].comment, "Test comment");
31187 assert_eq!(comments[0].id, id);
31188 });
31189}
31190
31191#[gpui::test]
31192fn test_review_comments_are_per_hunk(cx: &mut TestAppContext) {
31193 init_test(cx, |_| {});
31194
31195 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31196
31197 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
31198 let snapshot = editor.buffer().read(cx).snapshot(cx);
31199 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
31200 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
31201 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
31202 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
31203
31204 add_test_comment(editor, key1.clone(), "Comment for file1", cx);
31205 add_test_comment(editor, key2.clone(), "Comment for file2", cx);
31206
31207 let snapshot = editor.buffer().read(cx).snapshot(cx);
31208 assert_eq!(editor.total_review_comment_count(), 2);
31209 assert_eq!(editor.hunk_comment_count(&key1, &snapshot), 1);
31210 assert_eq!(editor.hunk_comment_count(&key2, &snapshot), 1);
31211
31212 assert_eq!(
31213 editor.comments_for_hunk(&key1, &snapshot)[0].comment,
31214 "Comment for file1"
31215 );
31216 assert_eq!(
31217 editor.comments_for_hunk(&key2, &snapshot)[0].comment,
31218 "Comment for file2"
31219 );
31220 });
31221}
31222
31223#[gpui::test]
31224fn test_review_comment_remove(cx: &mut TestAppContext) {
31225 init_test(cx, |_| {});
31226
31227 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31228
31229 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
31230 let key = test_hunk_key("");
31231
31232 let id = add_test_comment(editor, key, "To be removed", cx);
31233
31234 assert_eq!(editor.total_review_comment_count(), 1);
31235
31236 let removed = editor.remove_review_comment(id, cx);
31237 assert!(removed);
31238 assert_eq!(editor.total_review_comment_count(), 0);
31239
31240 // Try to remove again
31241 let removed_again = editor.remove_review_comment(id, cx);
31242 assert!(!removed_again);
31243 });
31244}
31245
31246#[gpui::test]
31247fn test_review_comment_update(cx: &mut TestAppContext) {
31248 init_test(cx, |_| {});
31249
31250 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31251
31252 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
31253 let key = test_hunk_key("");
31254
31255 let id = add_test_comment(editor, key.clone(), "Original text", cx);
31256
31257 let updated = editor.update_review_comment(id, "Updated text".to_string(), cx);
31258 assert!(updated);
31259
31260 let snapshot = editor.buffer().read(cx).snapshot(cx);
31261 let comments = editor.comments_for_hunk(&key, &snapshot);
31262 assert_eq!(comments[0].comment, "Updated text");
31263 assert!(!comments[0].is_editing); // Should clear editing flag
31264 });
31265}
31266
31267#[gpui::test]
31268fn test_review_comment_take_all(cx: &mut TestAppContext) {
31269 init_test(cx, |_| {});
31270
31271 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31272
31273 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
31274 let snapshot = editor.buffer().read(cx).snapshot(cx);
31275 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
31276 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
31277 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
31278 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
31279
31280 let id1 = add_test_comment(editor, key1.clone(), "Comment 1", cx);
31281 let id2 = add_test_comment(editor, key1.clone(), "Comment 2", cx);
31282 let id3 = add_test_comment(editor, key2.clone(), "Comment 3", cx);
31283
31284 // IDs should be sequential starting from 0
31285 assert_eq!(id1, 0);
31286 assert_eq!(id2, 1);
31287 assert_eq!(id3, 2);
31288
31289 assert_eq!(editor.total_review_comment_count(), 3);
31290
31291 let taken = editor.take_all_review_comments(cx);
31292
31293 // Should have 2 entries (one per hunk)
31294 assert_eq!(taken.len(), 2);
31295
31296 // Total comments should be 3
31297 let total: usize = taken
31298 .iter()
31299 .map(|(_, comments): &(DiffHunkKey, Vec<StoredReviewComment>)| comments.len())
31300 .sum();
31301 assert_eq!(total, 3);
31302
31303 // Storage should be empty
31304 assert_eq!(editor.total_review_comment_count(), 0);
31305
31306 // After taking all comments, ID counter should reset
31307 // New comments should get IDs starting from 0 again
31308 let new_id1 = add_test_comment(editor, key1, "New Comment 1", cx);
31309 let new_id2 = add_test_comment(editor, key2, "New Comment 2", cx);
31310
31311 assert_eq!(new_id1, 0, "ID counter should reset after take_all");
31312 assert_eq!(new_id2, 1, "IDs should be sequential after reset");
31313 });
31314}
31315
31316#[gpui::test]
31317fn test_diff_review_overlay_show_and_dismiss(cx: &mut TestAppContext) {
31318 init_test(cx, |_| {});
31319
31320 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31321
31322 // Show overlay
31323 editor
31324 .update(cx, |editor, window, cx| {
31325 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
31326 })
31327 .unwrap();
31328
31329 // Verify overlay is shown
31330 editor
31331 .update(cx, |editor, _window, cx| {
31332 assert!(!editor.diff_review_overlays.is_empty());
31333 assert_eq!(editor.diff_review_line_range(cx), Some((0, 0)));
31334 assert!(editor.diff_review_prompt_editor().is_some());
31335 })
31336 .unwrap();
31337
31338 // Dismiss overlay
31339 editor
31340 .update(cx, |editor, _window, cx| {
31341 editor.dismiss_all_diff_review_overlays(cx);
31342 })
31343 .unwrap();
31344
31345 // Verify overlay is dismissed
31346 editor
31347 .update(cx, |editor, _window, cx| {
31348 assert!(editor.diff_review_overlays.is_empty());
31349 assert_eq!(editor.diff_review_line_range(cx), None);
31350 assert!(editor.diff_review_prompt_editor().is_none());
31351 })
31352 .unwrap();
31353}
31354
31355#[gpui::test]
31356fn test_diff_review_overlay_dismiss_via_cancel(cx: &mut TestAppContext) {
31357 init_test(cx, |_| {});
31358
31359 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31360
31361 // Show overlay
31362 editor
31363 .update(cx, |editor, window, cx| {
31364 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
31365 })
31366 .unwrap();
31367
31368 // Verify overlay is shown
31369 editor
31370 .update(cx, |editor, _window, _cx| {
31371 assert!(!editor.diff_review_overlays.is_empty());
31372 })
31373 .unwrap();
31374
31375 // Dismiss via dismiss_menus_and_popups (which is called by cancel action)
31376 editor
31377 .update(cx, |editor, window, cx| {
31378 editor.dismiss_menus_and_popups(true, window, cx);
31379 })
31380 .unwrap();
31381
31382 // Verify overlay is dismissed
31383 editor
31384 .update(cx, |editor, _window, _cx| {
31385 assert!(editor.diff_review_overlays.is_empty());
31386 })
31387 .unwrap();
31388}
31389
31390#[gpui::test]
31391fn test_diff_review_empty_comment_not_submitted(cx: &mut TestAppContext) {
31392 init_test(cx, |_| {});
31393
31394 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31395
31396 // Show overlay
31397 editor
31398 .update(cx, |editor, window, cx| {
31399 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
31400 })
31401 .unwrap();
31402
31403 // Try to submit without typing anything (empty comment)
31404 editor
31405 .update(cx, |editor, window, cx| {
31406 editor.submit_diff_review_comment(window, cx);
31407 })
31408 .unwrap();
31409
31410 // Verify no comment was added
31411 editor
31412 .update(cx, |editor, _window, _cx| {
31413 assert_eq!(editor.total_review_comment_count(), 0);
31414 })
31415 .unwrap();
31416
31417 // Try to submit with whitespace-only comment
31418 editor
31419 .update(cx, |editor, window, cx| {
31420 if let Some(prompt_editor) = editor.diff_review_prompt_editor().cloned() {
31421 prompt_editor.update(cx, |pe, cx| {
31422 pe.insert(" \n\t ", window, cx);
31423 });
31424 }
31425 editor.submit_diff_review_comment(window, cx);
31426 })
31427 .unwrap();
31428
31429 // Verify still no comment was added
31430 editor
31431 .update(cx, |editor, _window, _cx| {
31432 assert_eq!(editor.total_review_comment_count(), 0);
31433 })
31434 .unwrap();
31435}
31436
31437#[gpui::test]
31438fn test_diff_review_inline_edit_flow(cx: &mut TestAppContext) {
31439 init_test(cx, |_| {});
31440
31441 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31442
31443 // Add a comment directly
31444 let comment_id = editor
31445 .update(cx, |editor, _window, cx| {
31446 let key = test_hunk_key("");
31447 add_test_comment(editor, key, "Original comment", cx)
31448 })
31449 .unwrap();
31450
31451 // Set comment to editing mode
31452 editor
31453 .update(cx, |editor, _window, cx| {
31454 editor.set_comment_editing(comment_id, true, cx);
31455 })
31456 .unwrap();
31457
31458 // Verify editing flag is set
31459 editor
31460 .update(cx, |editor, _window, cx| {
31461 let key = test_hunk_key("");
31462 let snapshot = editor.buffer().read(cx).snapshot(cx);
31463 let comments = editor.comments_for_hunk(&key, &snapshot);
31464 assert_eq!(comments.len(), 1);
31465 assert!(comments[0].is_editing);
31466 })
31467 .unwrap();
31468
31469 // Update the comment
31470 editor
31471 .update(cx, |editor, _window, cx| {
31472 let updated =
31473 editor.update_review_comment(comment_id, "Updated comment".to_string(), cx);
31474 assert!(updated);
31475 })
31476 .unwrap();
31477
31478 // Verify comment was updated and editing flag is cleared
31479 editor
31480 .update(cx, |editor, _window, cx| {
31481 let key = test_hunk_key("");
31482 let snapshot = editor.buffer().read(cx).snapshot(cx);
31483 let comments = editor.comments_for_hunk(&key, &snapshot);
31484 assert_eq!(comments[0].comment, "Updated comment");
31485 assert!(!comments[0].is_editing);
31486 })
31487 .unwrap();
31488}
31489
31490#[gpui::test]
31491fn test_orphaned_comments_are_cleaned_up(cx: &mut TestAppContext) {
31492 init_test(cx, |_| {});
31493
31494 // Create an editor with some text
31495 let editor = cx.add_window(|window, cx| {
31496 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
31497 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31498 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31499 });
31500
31501 // Add a comment with an anchor on line 2
31502 editor
31503 .update(cx, |editor, _window, cx| {
31504 let snapshot = editor.buffer().read(cx).snapshot(cx);
31505 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
31506 let key = DiffHunkKey {
31507 file_path: Arc::from(util::rel_path::RelPath::empty()),
31508 hunk_start_anchor: anchor,
31509 };
31510 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
31511 assert_eq!(editor.total_review_comment_count(), 1);
31512 })
31513 .unwrap();
31514
31515 // Delete all content (this should orphan the comment's anchor)
31516 editor
31517 .update(cx, |editor, window, cx| {
31518 editor.select_all(&SelectAll, window, cx);
31519 editor.insert("completely new content", window, cx);
31520 })
31521 .unwrap();
31522
31523 // Trigger cleanup
31524 editor
31525 .update(cx, |editor, _window, cx| {
31526 editor.cleanup_orphaned_review_comments(cx);
31527 // Comment should be removed because its anchor is invalid
31528 assert_eq!(editor.total_review_comment_count(), 0);
31529 })
31530 .unwrap();
31531}
31532
31533#[gpui::test]
31534fn test_orphaned_comments_cleanup_called_on_buffer_edit(cx: &mut TestAppContext) {
31535 init_test(cx, |_| {});
31536
31537 // Create an editor with some text
31538 let editor = cx.add_window(|window, cx| {
31539 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
31540 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31541 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31542 });
31543
31544 // Add a comment with an anchor on line 2
31545 editor
31546 .update(cx, |editor, _window, cx| {
31547 let snapshot = editor.buffer().read(cx).snapshot(cx);
31548 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
31549 let key = DiffHunkKey {
31550 file_path: Arc::from(util::rel_path::RelPath::empty()),
31551 hunk_start_anchor: anchor,
31552 };
31553 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
31554 assert_eq!(editor.total_review_comment_count(), 1);
31555 })
31556 .unwrap();
31557
31558 // Edit the buffer - this should trigger cleanup via on_buffer_event
31559 // Delete all content which orphans the anchor
31560 editor
31561 .update(cx, |editor, window, cx| {
31562 editor.select_all(&SelectAll, window, cx);
31563 editor.insert("completely new content", window, cx);
31564 // The cleanup is called automatically in on_buffer_event when Edited fires
31565 })
31566 .unwrap();
31567
31568 // Verify cleanup happened automatically (not manually triggered)
31569 editor
31570 .update(cx, |editor, _window, _cx| {
31571 // Comment should be removed because its anchor became invalid
31572 // and cleanup was called automatically on buffer edit
31573 assert_eq!(editor.total_review_comment_count(), 0);
31574 })
31575 .unwrap();
31576}
31577
31578#[gpui::test]
31579fn test_comments_stored_for_multiple_hunks(cx: &mut TestAppContext) {
31580 init_test(cx, |_| {});
31581
31582 // This test verifies that comments can be stored for multiple different hunks
31583 // and that hunk_comment_count correctly identifies comments per hunk.
31584 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31585
31586 _ = editor.update(cx, |editor, _window, cx| {
31587 let snapshot = editor.buffer().read(cx).snapshot(cx);
31588
31589 // Create two different hunk keys (simulating two different files)
31590 let anchor = snapshot.anchor_before(Point::new(0, 0));
31591 let key1 = DiffHunkKey {
31592 file_path: Arc::from(util::rel_path::RelPath::unix("file1.rs").unwrap()),
31593 hunk_start_anchor: anchor,
31594 };
31595 let key2 = DiffHunkKey {
31596 file_path: Arc::from(util::rel_path::RelPath::unix("file2.rs").unwrap()),
31597 hunk_start_anchor: anchor,
31598 };
31599
31600 // Add comments to first hunk
31601 editor.add_review_comment(
31602 key1.clone(),
31603 "Comment 1 for file1".to_string(),
31604 anchor..anchor,
31605 cx,
31606 );
31607 editor.add_review_comment(
31608 key1.clone(),
31609 "Comment 2 for file1".to_string(),
31610 anchor..anchor,
31611 cx,
31612 );
31613
31614 // Add comment to second hunk
31615 editor.add_review_comment(
31616 key2.clone(),
31617 "Comment for file2".to_string(),
31618 anchor..anchor,
31619 cx,
31620 );
31621
31622 // Verify total count
31623 assert_eq!(editor.total_review_comment_count(), 3);
31624
31625 // Verify per-hunk counts
31626 let snapshot = editor.buffer().read(cx).snapshot(cx);
31627 assert_eq!(
31628 editor.hunk_comment_count(&key1, &snapshot),
31629 2,
31630 "file1 should have 2 comments"
31631 );
31632 assert_eq!(
31633 editor.hunk_comment_count(&key2, &snapshot),
31634 1,
31635 "file2 should have 1 comment"
31636 );
31637
31638 // Verify comments_for_hunk returns correct comments
31639 let file1_comments = editor.comments_for_hunk(&key1, &snapshot);
31640 assert_eq!(file1_comments.len(), 2);
31641 assert_eq!(file1_comments[0].comment, "Comment 1 for file1");
31642 assert_eq!(file1_comments[1].comment, "Comment 2 for file1");
31643
31644 let file2_comments = editor.comments_for_hunk(&key2, &snapshot);
31645 assert_eq!(file2_comments.len(), 1);
31646 assert_eq!(file2_comments[0].comment, "Comment for file2");
31647 });
31648}
31649
31650#[gpui::test]
31651fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
31652 init_test(cx, |_| {});
31653
31654 // This test verifies that hunk_keys_match correctly identifies when two
31655 // DiffHunkKeys refer to the same hunk (same file path and anchor point).
31656 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31657
31658 _ = editor.update(cx, |editor, _window, cx| {
31659 let snapshot = editor.buffer().read(cx).snapshot(cx);
31660 let anchor = snapshot.anchor_before(Point::new(0, 0));
31661
31662 // Create two keys with the same file path and anchor
31663 let key1 = DiffHunkKey {
31664 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
31665 hunk_start_anchor: anchor,
31666 };
31667 let key2 = DiffHunkKey {
31668 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
31669 hunk_start_anchor: anchor,
31670 };
31671
31672 // Add comment to first key
31673 editor.add_review_comment(key1, "Test comment".to_string(), anchor..anchor, cx);
31674
31675 // Verify second key (same hunk) finds the comment
31676 let snapshot = editor.buffer().read(cx).snapshot(cx);
31677 assert_eq!(
31678 editor.hunk_comment_count(&key2, &snapshot),
31679 1,
31680 "Same hunk should find the comment"
31681 );
31682
31683 // Create a key with different file path
31684 let different_file_key = DiffHunkKey {
31685 file_path: Arc::from(util::rel_path::RelPath::unix("other.rs").unwrap()),
31686 hunk_start_anchor: anchor,
31687 };
31688
31689 // Different file should not find the comment
31690 assert_eq!(
31691 editor.hunk_comment_count(&different_file_key, &snapshot),
31692 0,
31693 "Different file should not find the comment"
31694 );
31695 });
31696}
31697
31698#[gpui::test]
31699fn test_overlay_comments_expanded_state(cx: &mut TestAppContext) {
31700 init_test(cx, |_| {});
31701
31702 // This test verifies that set_diff_review_comments_expanded correctly
31703 // updates the expanded state of overlays.
31704 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31705
31706 // Show overlay
31707 editor
31708 .update(cx, |editor, window, cx| {
31709 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
31710 })
31711 .unwrap();
31712
31713 // Verify initially expanded (default)
31714 editor
31715 .update(cx, |editor, _window, _cx| {
31716 assert!(
31717 editor.diff_review_overlays[0].comments_expanded,
31718 "Should be expanded by default"
31719 );
31720 })
31721 .unwrap();
31722
31723 // Set to collapsed using the public method
31724 editor
31725 .update(cx, |editor, _window, cx| {
31726 editor.set_diff_review_comments_expanded(false, cx);
31727 })
31728 .unwrap();
31729
31730 // Verify collapsed
31731 editor
31732 .update(cx, |editor, _window, _cx| {
31733 assert!(
31734 !editor.diff_review_overlays[0].comments_expanded,
31735 "Should be collapsed after setting to false"
31736 );
31737 })
31738 .unwrap();
31739
31740 // Set back to expanded
31741 editor
31742 .update(cx, |editor, _window, cx| {
31743 editor.set_diff_review_comments_expanded(true, cx);
31744 })
31745 .unwrap();
31746
31747 // Verify expanded again
31748 editor
31749 .update(cx, |editor, _window, _cx| {
31750 assert!(
31751 editor.diff_review_overlays[0].comments_expanded,
31752 "Should be expanded after setting to true"
31753 );
31754 })
31755 .unwrap();
31756}
31757
31758#[gpui::test]
31759fn test_diff_review_multiline_selection(cx: &mut TestAppContext) {
31760 init_test(cx, |_| {});
31761
31762 // Create an editor with multiple lines of text
31763 let editor = cx.add_window(|window, cx| {
31764 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\nline 4\nline 5\n", cx));
31765 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31766 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31767 });
31768
31769 // Test showing overlay with a multi-line selection (lines 1-3, which are rows 0-2)
31770 editor
31771 .update(cx, |editor, window, cx| {
31772 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(2), window, cx);
31773 })
31774 .unwrap();
31775
31776 // Verify line range
31777 editor
31778 .update(cx, |editor, _window, cx| {
31779 assert!(!editor.diff_review_overlays.is_empty());
31780 assert_eq!(editor.diff_review_line_range(cx), Some((0, 2)));
31781 })
31782 .unwrap();
31783
31784 // Dismiss and test with reversed range (end < start)
31785 editor
31786 .update(cx, |editor, _window, cx| {
31787 editor.dismiss_all_diff_review_overlays(cx);
31788 })
31789 .unwrap();
31790
31791 // Show overlay with reversed range - should normalize it
31792 editor
31793 .update(cx, |editor, window, cx| {
31794 editor.show_diff_review_overlay(DisplayRow(3)..DisplayRow(1), window, cx);
31795 })
31796 .unwrap();
31797
31798 // Verify range is normalized (start <= end)
31799 editor
31800 .update(cx, |editor, _window, cx| {
31801 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
31802 })
31803 .unwrap();
31804}
31805
31806#[gpui::test]
31807fn test_diff_review_drag_state(cx: &mut TestAppContext) {
31808 init_test(cx, |_| {});
31809
31810 let editor = cx.add_window(|window, cx| {
31811 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
31812 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31813 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31814 });
31815
31816 // Initially no drag state
31817 editor
31818 .update(cx, |editor, _window, _cx| {
31819 assert!(editor.diff_review_drag_state.is_none());
31820 })
31821 .unwrap();
31822
31823 // Start drag at row 1
31824 editor
31825 .update(cx, |editor, window, cx| {
31826 editor.start_diff_review_drag(DisplayRow(1), window, cx);
31827 })
31828 .unwrap();
31829
31830 // Verify drag state is set
31831 editor
31832 .update(cx, |editor, window, cx| {
31833 assert!(editor.diff_review_drag_state.is_some());
31834 let snapshot = editor.snapshot(window, cx);
31835 let range = editor
31836 .diff_review_drag_state
31837 .as_ref()
31838 .unwrap()
31839 .row_range(&snapshot.display_snapshot);
31840 assert_eq!(*range.start(), DisplayRow(1));
31841 assert_eq!(*range.end(), DisplayRow(1));
31842 })
31843 .unwrap();
31844
31845 // Update drag to row 3
31846 editor
31847 .update(cx, |editor, window, cx| {
31848 editor.update_diff_review_drag(DisplayRow(3), window, cx);
31849 })
31850 .unwrap();
31851
31852 // Verify drag state is updated
31853 editor
31854 .update(cx, |editor, window, cx| {
31855 assert!(editor.diff_review_drag_state.is_some());
31856 let snapshot = editor.snapshot(window, cx);
31857 let range = editor
31858 .diff_review_drag_state
31859 .as_ref()
31860 .unwrap()
31861 .row_range(&snapshot.display_snapshot);
31862 assert_eq!(*range.start(), DisplayRow(1));
31863 assert_eq!(*range.end(), DisplayRow(3));
31864 })
31865 .unwrap();
31866
31867 // End drag - should show overlay
31868 editor
31869 .update(cx, |editor, window, cx| {
31870 editor.end_diff_review_drag(window, cx);
31871 })
31872 .unwrap();
31873
31874 // Verify drag state is cleared and overlay is shown
31875 editor
31876 .update(cx, |editor, _window, cx| {
31877 assert!(editor.diff_review_drag_state.is_none());
31878 assert!(!editor.diff_review_overlays.is_empty());
31879 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
31880 })
31881 .unwrap();
31882}
31883
31884#[gpui::test]
31885fn test_diff_review_drag_cancel(cx: &mut TestAppContext) {
31886 init_test(cx, |_| {});
31887
31888 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31889
31890 // Start drag
31891 editor
31892 .update(cx, |editor, window, cx| {
31893 editor.start_diff_review_drag(DisplayRow(0), window, cx);
31894 })
31895 .unwrap();
31896
31897 // Verify drag state is set
31898 editor
31899 .update(cx, |editor, _window, _cx| {
31900 assert!(editor.diff_review_drag_state.is_some());
31901 })
31902 .unwrap();
31903
31904 // Cancel drag
31905 editor
31906 .update(cx, |editor, _window, cx| {
31907 editor.cancel_diff_review_drag(cx);
31908 })
31909 .unwrap();
31910
31911 // Verify drag state is cleared and no overlay was created
31912 editor
31913 .update(cx, |editor, _window, _cx| {
31914 assert!(editor.diff_review_drag_state.is_none());
31915 assert!(editor.diff_review_overlays.is_empty());
31916 })
31917 .unwrap();
31918}
31919
31920#[gpui::test]
31921fn test_calculate_overlay_height(cx: &mut TestAppContext) {
31922 init_test(cx, |_| {});
31923
31924 // This test verifies that calculate_overlay_height returns correct heights
31925 // based on comment count and expanded state.
31926 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31927
31928 _ = editor.update(cx, |editor, _window, cx| {
31929 let snapshot = editor.buffer().read(cx).snapshot(cx);
31930 let anchor = snapshot.anchor_before(Point::new(0, 0));
31931 let key = DiffHunkKey {
31932 file_path: Arc::from(util::rel_path::RelPath::empty()),
31933 hunk_start_anchor: anchor,
31934 };
31935
31936 // No comments: base height of 2
31937 let height_no_comments = editor.calculate_overlay_height(&key, true, &snapshot);
31938 assert_eq!(
31939 height_no_comments, 2,
31940 "Base height should be 2 with no comments"
31941 );
31942
31943 // Add one comment
31944 editor.add_review_comment(key.clone(), "Comment 1".to_string(), anchor..anchor, cx);
31945
31946 let snapshot = editor.buffer().read(cx).snapshot(cx);
31947
31948 // With comments expanded: base (2) + header (1) + 2 per comment
31949 let height_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
31950 assert_eq!(
31951 height_expanded,
31952 2 + 1 + 2, // base + header + 1 comment * 2
31953 "Height with 1 comment expanded"
31954 );
31955
31956 // With comments collapsed: base (2) + header (1)
31957 let height_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
31958 assert_eq!(
31959 height_collapsed,
31960 2 + 1, // base + header only
31961 "Height with comments collapsed"
31962 );
31963
31964 // Add more comments
31965 editor.add_review_comment(key.clone(), "Comment 2".to_string(), anchor..anchor, cx);
31966 editor.add_review_comment(key.clone(), "Comment 3".to_string(), anchor..anchor, cx);
31967
31968 let snapshot = editor.buffer().read(cx).snapshot(cx);
31969
31970 // With 3 comments expanded
31971 let height_3_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
31972 assert_eq!(
31973 height_3_expanded,
31974 2 + 1 + (3 * 2), // base + header + 3 comments * 2
31975 "Height with 3 comments expanded"
31976 );
31977
31978 // Collapsed height stays the same regardless of comment count
31979 let height_3_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
31980 assert_eq!(
31981 height_3_collapsed,
31982 2 + 1, // base + header only
31983 "Height with 3 comments collapsed should be same as 1 comment collapsed"
31984 );
31985 });
31986}
31987
31988#[gpui::test]
31989async fn test_move_to_start_end_of_larger_syntax_node_single_cursor(cx: &mut TestAppContext) {
31990 init_test(cx, |_| {});
31991
31992 let language = Arc::new(Language::new(
31993 LanguageConfig::default(),
31994 Some(tree_sitter_rust::LANGUAGE.into()),
31995 ));
31996
31997 let text = r#"
31998 fn main() {
31999 let x = foo(1, 2);
32000 }
32001 "#
32002 .unindent();
32003
32004 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
32005 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32006 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32007
32008 editor
32009 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32010 .await;
32011
32012 // Test case 1: Move to end of syntax nodes
32013 editor.update_in(cx, |editor, window, cx| {
32014 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32015 s.select_display_ranges([
32016 DisplayPoint::new(DisplayRow(1), 16)..DisplayPoint::new(DisplayRow(1), 16)
32017 ]);
32018 });
32019 });
32020 editor.update(cx, |editor, cx| {
32021 assert_text_with_selections(
32022 editor,
32023 indoc! {r#"
32024 fn main() {
32025 let x = foo(ˇ1, 2);
32026 }
32027 "#},
32028 cx,
32029 );
32030 });
32031 editor.update_in(cx, |editor, window, cx| {
32032 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
32033 });
32034 editor.update(cx, |editor, cx| {
32035 assert_text_with_selections(
32036 editor,
32037 indoc! {r#"
32038 fn main() {
32039 let x = foo(1ˇ, 2);
32040 }
32041 "#},
32042 cx,
32043 );
32044 });
32045 editor.update_in(cx, |editor, window, cx| {
32046 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
32047 });
32048 editor.update(cx, |editor, cx| {
32049 assert_text_with_selections(
32050 editor,
32051 indoc! {r#"
32052 fn main() {
32053 let x = foo(1, 2)ˇ;
32054 }
32055 "#},
32056 cx,
32057 );
32058 });
32059 editor.update_in(cx, |editor, window, cx| {
32060 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
32061 });
32062 editor.update(cx, |editor, cx| {
32063 assert_text_with_selections(
32064 editor,
32065 indoc! {r#"
32066 fn main() {
32067 let x = foo(1, 2);ˇ
32068 }
32069 "#},
32070 cx,
32071 );
32072 });
32073 editor.update_in(cx, |editor, window, cx| {
32074 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
32075 });
32076 editor.update(cx, |editor, cx| {
32077 assert_text_with_selections(
32078 editor,
32079 indoc! {r#"
32080 fn main() {
32081 let x = foo(1, 2);
32082 }ˇ
32083 "#},
32084 cx,
32085 );
32086 });
32087
32088 // Test case 2: Move to start of syntax nodes
32089 editor.update_in(cx, |editor, window, cx| {
32090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32091 s.select_display_ranges([
32092 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20)
32093 ]);
32094 });
32095 });
32096 editor.update(cx, |editor, cx| {
32097 assert_text_with_selections(
32098 editor,
32099 indoc! {r#"
32100 fn main() {
32101 let x = foo(1, 2ˇ);
32102 }
32103 "#},
32104 cx,
32105 );
32106 });
32107 editor.update_in(cx, |editor, window, cx| {
32108 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
32109 });
32110 editor.update(cx, |editor, cx| {
32111 assert_text_with_selections(
32112 editor,
32113 indoc! {r#"
32114 fn main() {
32115 let x = fooˇ(1, 2);
32116 }
32117 "#},
32118 cx,
32119 );
32120 });
32121 editor.update_in(cx, |editor, window, cx| {
32122 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
32123 });
32124 editor.update(cx, |editor, cx| {
32125 assert_text_with_selections(
32126 editor,
32127 indoc! {r#"
32128 fn main() {
32129 let x = ˇfoo(1, 2);
32130 }
32131 "#},
32132 cx,
32133 );
32134 });
32135 editor.update_in(cx, |editor, window, cx| {
32136 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
32137 });
32138 editor.update(cx, |editor, cx| {
32139 assert_text_with_selections(
32140 editor,
32141 indoc! {r#"
32142 fn main() {
32143 ˇlet x = foo(1, 2);
32144 }
32145 "#},
32146 cx,
32147 );
32148 });
32149 editor.update_in(cx, |editor, window, cx| {
32150 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
32151 });
32152 editor.update(cx, |editor, cx| {
32153 assert_text_with_selections(
32154 editor,
32155 indoc! {r#"
32156 fn main() ˇ{
32157 let x = foo(1, 2);
32158 }
32159 "#},
32160 cx,
32161 );
32162 });
32163 editor.update_in(cx, |editor, window, cx| {
32164 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
32165 });
32166 editor.update(cx, |editor, cx| {
32167 assert_text_with_selections(
32168 editor,
32169 indoc! {r#"
32170 ˇfn main() {
32171 let x = foo(1, 2);
32172 }
32173 "#},
32174 cx,
32175 );
32176 });
32177}
32178
32179#[gpui::test]
32180async fn test_move_to_start_end_of_larger_syntax_node_two_cursors(cx: &mut TestAppContext) {
32181 init_test(cx, |_| {});
32182
32183 let language = Arc::new(Language::new(
32184 LanguageConfig::default(),
32185 Some(tree_sitter_rust::LANGUAGE.into()),
32186 ));
32187
32188 let text = r#"
32189 fn main() {
32190 let x = foo(1, 2);
32191 let y = bar(3, 4);
32192 }
32193 "#
32194 .unindent();
32195
32196 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
32197 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32198 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32199
32200 editor
32201 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32202 .await;
32203
32204 // Test case 1: Move to end of syntax nodes with two cursors
32205 editor.update_in(cx, |editor, window, cx| {
32206 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32207 s.select_display_ranges([
32208 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20),
32209 DisplayPoint::new(DisplayRow(2), 20)..DisplayPoint::new(DisplayRow(2), 20),
32210 ]);
32211 });
32212 });
32213 editor.update(cx, |editor, cx| {
32214 assert_text_with_selections(
32215 editor,
32216 indoc! {r#"
32217 fn main() {
32218 let x = foo(1, 2ˇ);
32219 let y = bar(3, 4ˇ);
32220 }
32221 "#},
32222 cx,
32223 );
32224 });
32225 editor.update_in(cx, |editor, window, cx| {
32226 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
32227 });
32228 editor.update(cx, |editor, cx| {
32229 assert_text_with_selections(
32230 editor,
32231 indoc! {r#"
32232 fn main() {
32233 let x = foo(1, 2)ˇ;
32234 let y = bar(3, 4)ˇ;
32235 }
32236 "#},
32237 cx,
32238 );
32239 });
32240 editor.update_in(cx, |editor, window, cx| {
32241 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
32242 });
32243 editor.update(cx, |editor, cx| {
32244 assert_text_with_selections(
32245 editor,
32246 indoc! {r#"
32247 fn main() {
32248 let x = foo(1, 2);ˇ
32249 let y = bar(3, 4);ˇ
32250 }
32251 "#},
32252 cx,
32253 );
32254 });
32255
32256 // Test case 2: Move to start of syntax nodes with two cursors
32257 editor.update_in(cx, |editor, window, cx| {
32258 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32259 s.select_display_ranges([
32260 DisplayPoint::new(DisplayRow(1), 19)..DisplayPoint::new(DisplayRow(1), 19),
32261 DisplayPoint::new(DisplayRow(2), 19)..DisplayPoint::new(DisplayRow(2), 19),
32262 ]);
32263 });
32264 });
32265 editor.update(cx, |editor, cx| {
32266 assert_text_with_selections(
32267 editor,
32268 indoc! {r#"
32269 fn main() {
32270 let x = foo(1, ˇ2);
32271 let y = bar(3, ˇ4);
32272 }
32273 "#},
32274 cx,
32275 );
32276 });
32277 editor.update_in(cx, |editor, window, cx| {
32278 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
32279 });
32280 editor.update(cx, |editor, cx| {
32281 assert_text_with_selections(
32282 editor,
32283 indoc! {r#"
32284 fn main() {
32285 let x = fooˇ(1, 2);
32286 let y = barˇ(3, 4);
32287 }
32288 "#},
32289 cx,
32290 );
32291 });
32292 editor.update_in(cx, |editor, window, cx| {
32293 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
32294 });
32295 editor.update(cx, |editor, cx| {
32296 assert_text_with_selections(
32297 editor,
32298 indoc! {r#"
32299 fn main() {
32300 let x = ˇfoo(1, 2);
32301 let y = ˇbar(3, 4);
32302 }
32303 "#},
32304 cx,
32305 );
32306 });
32307 editor.update_in(cx, |editor, window, cx| {
32308 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
32309 });
32310 editor.update(cx, |editor, cx| {
32311 assert_text_with_selections(
32312 editor,
32313 indoc! {r#"
32314 fn main() {
32315 ˇlet x = foo(1, 2);
32316 ˇlet y = bar(3, 4);
32317 }
32318 "#},
32319 cx,
32320 );
32321 });
32322}
32323
32324#[gpui::test]
32325async fn test_move_to_start_end_of_larger_syntax_node_with_selections_and_strings(
32326 cx: &mut TestAppContext,
32327) {
32328 init_test(cx, |_| {});
32329
32330 let language = Arc::new(Language::new(
32331 LanguageConfig::default(),
32332 Some(tree_sitter_rust::LANGUAGE.into()),
32333 ));
32334
32335 let text = r#"
32336 fn main() {
32337 let x = foo(1, 2);
32338 let msg = "hello world";
32339 }
32340 "#
32341 .unindent();
32342
32343 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
32344 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32345 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32346
32347 editor
32348 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32349 .await;
32350
32351 // Test case 1: With existing selection, move_to_end keeps selection
32352 editor.update_in(cx, |editor, window, cx| {
32353 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32354 s.select_display_ranges([
32355 DisplayPoint::new(DisplayRow(1), 12)..DisplayPoint::new(DisplayRow(1), 21)
32356 ]);
32357 });
32358 });
32359 editor.update(cx, |editor, cx| {
32360 assert_text_with_selections(
32361 editor,
32362 indoc! {r#"
32363 fn main() {
32364 let x = «foo(1, 2)ˇ»;
32365 let msg = "hello world";
32366 }
32367 "#},
32368 cx,
32369 );
32370 });
32371 editor.update_in(cx, |editor, window, cx| {
32372 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
32373 });
32374 editor.update(cx, |editor, cx| {
32375 assert_text_with_selections(
32376 editor,
32377 indoc! {r#"
32378 fn main() {
32379 let x = «foo(1, 2)ˇ»;
32380 let msg = "hello world";
32381 }
32382 "#},
32383 cx,
32384 );
32385 });
32386
32387 // Test case 2: Move to end within a string
32388 editor.update_in(cx, |editor, window, cx| {
32389 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32390 s.select_display_ranges([
32391 DisplayPoint::new(DisplayRow(2), 15)..DisplayPoint::new(DisplayRow(2), 15)
32392 ]);
32393 });
32394 });
32395 editor.update(cx, |editor, cx| {
32396 assert_text_with_selections(
32397 editor,
32398 indoc! {r#"
32399 fn main() {
32400 let x = foo(1, 2);
32401 let msg = "ˇhello world";
32402 }
32403 "#},
32404 cx,
32405 );
32406 });
32407 editor.update_in(cx, |editor, window, cx| {
32408 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
32409 });
32410 editor.update(cx, |editor, cx| {
32411 assert_text_with_selections(
32412 editor,
32413 indoc! {r#"
32414 fn main() {
32415 let x = foo(1, 2);
32416 let msg = "hello worldˇ";
32417 }
32418 "#},
32419 cx,
32420 );
32421 });
32422
32423 // Test case 3: Move to start within a string
32424 editor.update_in(cx, |editor, window, cx| {
32425 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32426 s.select_display_ranges([
32427 DisplayPoint::new(DisplayRow(2), 21)..DisplayPoint::new(DisplayRow(2), 21)
32428 ]);
32429 });
32430 });
32431 editor.update(cx, |editor, cx| {
32432 assert_text_with_selections(
32433 editor,
32434 indoc! {r#"
32435 fn main() {
32436 let x = foo(1, 2);
32437 let msg = "hello ˇworld";
32438 }
32439 "#},
32440 cx,
32441 );
32442 });
32443 editor.update_in(cx, |editor, window, cx| {
32444 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
32445 });
32446 editor.update(cx, |editor, cx| {
32447 assert_text_with_selections(
32448 editor,
32449 indoc! {r#"
32450 fn main() {
32451 let x = foo(1, 2);
32452 let msg = "ˇhello world";
32453 }
32454 "#},
32455 cx,
32456 );
32457 });
32458}
32459
32460#[gpui::test]
32461async fn test_select_to_start_end_of_larger_syntax_node(cx: &mut TestAppContext) {
32462 init_test(cx, |_| {});
32463
32464 let language = Arc::new(Language::new(
32465 LanguageConfig::default(),
32466 Some(tree_sitter_rust::LANGUAGE.into()),
32467 ));
32468
32469 // Test Group 1.1: Cursor in String - First Jump (Select to End)
32470 let text = r#"let msg = "foo bar baz";"#.unindent();
32471
32472 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32473 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32474 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32475
32476 editor
32477 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32478 .await;
32479
32480 editor.update_in(cx, |editor, window, cx| {
32481 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32482 s.select_display_ranges([
32483 DisplayPoint::new(DisplayRow(0), 14)..DisplayPoint::new(DisplayRow(0), 14)
32484 ]);
32485 });
32486 });
32487 editor.update(cx, |editor, cx| {
32488 assert_text_with_selections(editor, indoc! {r#"let msg = "fooˇ bar baz";"#}, cx);
32489 });
32490 editor.update_in(cx, |editor, window, cx| {
32491 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32492 });
32493 editor.update(cx, |editor, cx| {
32494 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar bazˇ»";"#}, cx);
32495 });
32496
32497 // Test Group 1.2: Cursor in String - Second Jump (Select to End)
32498 editor.update_in(cx, |editor, window, cx| {
32499 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32500 });
32501 editor.update(cx, |editor, cx| {
32502 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz"ˇ»;"#}, cx);
32503 });
32504
32505 // Test Group 1.3: Cursor in String - Third Jump (Select to End)
32506 editor.update_in(cx, |editor, window, cx| {
32507 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32508 });
32509 editor.update(cx, |editor, cx| {
32510 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz";ˇ»"#}, cx);
32511 });
32512
32513 // Test Group 1.4: Cursor in String - First Jump (Select to Start)
32514 editor.update_in(cx, |editor, window, cx| {
32515 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32516 s.select_display_ranges([
32517 DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18)
32518 ]);
32519 });
32520 });
32521 editor.update(cx, |editor, cx| {
32522 assert_text_with_selections(editor, indoc! {r#"let msg = "foo barˇ baz";"#}, cx);
32523 });
32524 editor.update_in(cx, |editor, window, cx| {
32525 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
32526 });
32527 editor.update(cx, |editor, cx| {
32528 assert_text_with_selections(editor, indoc! {r#"let msg = "«ˇfoo bar» baz";"#}, cx);
32529 });
32530
32531 // Test Group 1.5: Cursor in String - Second Jump (Select to Start)
32532 editor.update_in(cx, |editor, window, cx| {
32533 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
32534 });
32535 editor.update(cx, |editor, cx| {
32536 assert_text_with_selections(editor, indoc! {r#"let msg = «ˇ"foo bar» baz";"#}, cx);
32537 });
32538
32539 // Test Group 1.6: Cursor in String - Third Jump (Select to Start)
32540 editor.update_in(cx, |editor, window, cx| {
32541 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
32542 });
32543 editor.update(cx, |editor, cx| {
32544 assert_text_with_selections(editor, indoc! {r#"«ˇlet msg = "foo bar» baz";"#}, cx);
32545 });
32546
32547 // Test Group 2.1: Let Statement Progression (Select to End)
32548 let text = r#"
32549fn main() {
32550 let x = "hello";
32551}
32552"#
32553 .unindent();
32554
32555 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32556 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32557 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32558
32559 editor
32560 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32561 .await;
32562
32563 editor.update_in(cx, |editor, window, cx| {
32564 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32565 s.select_display_ranges([
32566 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)
32567 ]);
32568 });
32569 });
32570 editor.update(cx, |editor, cx| {
32571 assert_text_with_selections(
32572 editor,
32573 indoc! {r#"
32574 fn main() {
32575 let xˇ = "hello";
32576 }
32577 "#},
32578 cx,
32579 );
32580 });
32581 editor.update_in(cx, |editor, window, cx| {
32582 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32583 });
32584 editor.update(cx, |editor, cx| {
32585 assert_text_with_selections(
32586 editor,
32587 indoc! {r##"
32588 fn main() {
32589 let x« = "hello";ˇ»
32590 }
32591 "##},
32592 cx,
32593 );
32594 });
32595 editor.update_in(cx, |editor, window, cx| {
32596 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32597 });
32598 editor.update(cx, |editor, cx| {
32599 assert_text_with_selections(
32600 editor,
32601 indoc! {r#"
32602 fn main() {
32603 let x« = "hello";
32604 }ˇ»
32605 "#},
32606 cx,
32607 );
32608 });
32609
32610 // Test Group 2.2a: From Inside String Content Node To String Content Boundary
32611 let text = r#"let x = "hello";"#.unindent();
32612
32613 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32614 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32615 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32616
32617 editor
32618 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32619 .await;
32620
32621 editor.update_in(cx, |editor, window, cx| {
32622 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32623 s.select_display_ranges([
32624 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12)
32625 ]);
32626 });
32627 });
32628 editor.update(cx, |editor, cx| {
32629 assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo";"#}, cx);
32630 });
32631 editor.update_in(cx, |editor, window, cx| {
32632 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
32633 });
32634 editor.update(cx, |editor, cx| {
32635 assert_text_with_selections(editor, indoc! {r#"let x = "«ˇhel»lo";"#}, cx);
32636 });
32637
32638 // Test Group 2.2b: From Edge of String Content Node To String Literal Boundary
32639 editor.update_in(cx, |editor, window, cx| {
32640 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32641 s.select_display_ranges([
32642 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
32643 ]);
32644 });
32645 });
32646 editor.update(cx, |editor, cx| {
32647 assert_text_with_selections(editor, indoc! {r#"let x = "ˇhello";"#}, cx);
32648 });
32649 editor.update_in(cx, |editor, window, cx| {
32650 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
32651 });
32652 editor.update(cx, |editor, cx| {
32653 assert_text_with_selections(editor, indoc! {r#"let x = «ˇ"»hello";"#}, cx);
32654 });
32655
32656 // Test Group 3.1: Create Selection from Cursor (Select to End)
32657 let text = r#"let x = "hello world";"#.unindent();
32658
32659 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32660 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32661 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32662
32663 editor
32664 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32665 .await;
32666
32667 editor.update_in(cx, |editor, window, cx| {
32668 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32669 s.select_display_ranges([
32670 DisplayPoint::new(DisplayRow(0), 14)..DisplayPoint::new(DisplayRow(0), 14)
32671 ]);
32672 });
32673 });
32674 editor.update(cx, |editor, cx| {
32675 assert_text_with_selections(editor, indoc! {r#"let x = "helloˇ world";"#}, cx);
32676 });
32677 editor.update_in(cx, |editor, window, cx| {
32678 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32679 });
32680 editor.update(cx, |editor, cx| {
32681 assert_text_with_selections(editor, indoc! {r#"let x = "hello« worldˇ»";"#}, cx);
32682 });
32683
32684 // Test Group 3.2: Extend Existing Selection (Select to End)
32685 editor.update_in(cx, |editor, window, cx| {
32686 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32687 s.select_display_ranges([
32688 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 17)
32689 ]);
32690 });
32691 });
32692 editor.update(cx, |editor, cx| {
32693 assert_text_with_selections(editor, indoc! {r#"let x = "he«llo woˇ»rld";"#}, cx);
32694 });
32695 editor.update_in(cx, |editor, window, cx| {
32696 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32697 });
32698 editor.update(cx, |editor, cx| {
32699 assert_text_with_selections(editor, indoc! {r#"let x = "he«llo worldˇ»";"#}, cx);
32700 });
32701
32702 // Test Group 4.1: Multiple Cursors - All Expand to Different Syntax Nodes
32703 let text = r#"let x = "hello"; let y = 42;"#.unindent();
32704
32705 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32706 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32707 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32708
32709 editor
32710 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32711 .await;
32712
32713 editor.update_in(cx, |editor, window, cx| {
32714 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32715 s.select_display_ranges([
32716 // Cursor inside string content
32717 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12),
32718 // Cursor at let statement semicolon
32719 DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18),
32720 // Cursor inside integer literal
32721 DisplayPoint::new(DisplayRow(0), 26)..DisplayPoint::new(DisplayRow(0), 26),
32722 ]);
32723 });
32724 });
32725 editor.update(cx, |editor, cx| {
32726 assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo"; lˇet y = 4ˇ2;"#}, cx);
32727 });
32728 editor.update_in(cx, |editor, window, cx| {
32729 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32730 });
32731 editor.update(cx, |editor, cx| {
32732 assert_text_with_selections(editor, indoc! {r#"let x = "hel«loˇ»"; l«et y = 42;ˇ»"#}, cx);
32733 });
32734
32735 // Test Group 4.2: Multiple Cursors on Separate Lines
32736 let text = r#"
32737let x = "hello";
32738let y = 42;
32739"#
32740 .unindent();
32741
32742 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32743 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32744 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32745
32746 editor
32747 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32748 .await;
32749
32750 editor.update_in(cx, |editor, window, cx| {
32751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32752 s.select_display_ranges([
32753 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12),
32754 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9),
32755 ]);
32756 });
32757 });
32758
32759 editor.update(cx, |editor, cx| {
32760 assert_text_with_selections(
32761 editor,
32762 indoc! {r#"
32763 let x = "helˇlo";
32764 let y = 4ˇ2;
32765 "#},
32766 cx,
32767 );
32768 });
32769 editor.update_in(cx, |editor, window, cx| {
32770 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32771 });
32772 editor.update(cx, |editor, cx| {
32773 assert_text_with_selections(
32774 editor,
32775 indoc! {r#"
32776 let x = "hel«loˇ»";
32777 let y = 4«2ˇ»;
32778 "#},
32779 cx,
32780 );
32781 });
32782
32783 // Test Group 5.1: Nested Function Calls
32784 let text = r#"let result = foo(bar("arg"));"#.unindent();
32785
32786 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32787 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32788 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32789
32790 editor
32791 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32792 .await;
32793
32794 editor.update_in(cx, |editor, window, cx| {
32795 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32796 s.select_display_ranges([
32797 DisplayPoint::new(DisplayRow(0), 22)..DisplayPoint::new(DisplayRow(0), 22)
32798 ]);
32799 });
32800 });
32801 editor.update(cx, |editor, cx| {
32802 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("ˇarg"));"#}, cx);
32803 });
32804 editor.update_in(cx, |editor, window, cx| {
32805 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32806 });
32807 editor.update(cx, |editor, cx| {
32808 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«argˇ»"));"#}, cx);
32809 });
32810 editor.update_in(cx, |editor, window, cx| {
32811 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32812 });
32813 editor.update(cx, |editor, cx| {
32814 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg"ˇ»));"#}, cx);
32815 });
32816 editor.update_in(cx, |editor, window, cx| {
32817 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32818 });
32819 editor.update(cx, |editor, cx| {
32820 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg")ˇ»);"#}, cx);
32821 });
32822
32823 // Test Group 6.1: Block Comments
32824 let text = r#"let x = /* multi
32825 line
32826 comment */;"#
32827 .unindent();
32828
32829 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32830 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32831 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32832
32833 editor
32834 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32835 .await;
32836
32837 editor.update_in(cx, |editor, window, cx| {
32838 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32839 s.select_display_ranges([
32840 DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16)
32841 ]);
32842 });
32843 });
32844 editor.update(cx, |editor, cx| {
32845 assert_text_with_selections(
32846 editor,
32847 indoc! {r#"
32848let x = /* multiˇ
32849line
32850comment */;"#},
32851 cx,
32852 );
32853 });
32854 editor.update_in(cx, |editor, window, cx| {
32855 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32856 });
32857 editor.update(cx, |editor, cx| {
32858 assert_text_with_selections(
32859 editor,
32860 indoc! {r#"
32861let x = /* multi«
32862line
32863comment */ˇ»;"#},
32864 cx,
32865 );
32866 });
32867
32868 // Test Group 6.2: Array/Vector Literals
32869 let text = r#"let arr = [1, 2, 3];"#.unindent();
32870
32871 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32872 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32873 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32874
32875 editor
32876 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32877 .await;
32878
32879 editor.update_in(cx, |editor, window, cx| {
32880 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32881 s.select_display_ranges([
32882 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
32883 ]);
32884 });
32885 });
32886 editor.update(cx, |editor, cx| {
32887 assert_text_with_selections(editor, indoc! {r#"let arr = [ˇ1, 2, 3];"#}, cx);
32888 });
32889 editor.update_in(cx, |editor, window, cx| {
32890 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32891 });
32892 editor.update(cx, |editor, cx| {
32893 assert_text_with_selections(editor, indoc! {r#"let arr = [«1ˇ», 2, 3];"#}, cx);
32894 });
32895 editor.update_in(cx, |editor, window, cx| {
32896 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32897 });
32898 editor.update(cx, |editor, cx| {
32899 assert_text_with_selections(editor, indoc! {r#"let arr = [«1, 2, 3]ˇ»;"#}, cx);
32900 });
32901}